def test_add_appointment(): # Simulate a request to add_appointment for dummy_appointment, make sure that the right endpoint is requested # and the return value is True appointment = teos_client.create_appointment(dummy_appointment_data) user_signature = Cryptographer.sign(appointment.serialize(), dummy_user_sk) appointment_receipt = receipts.create_appointment_receipt( user_signature, CURRENT_HEIGHT) response = { "locator": dummy_appointment.locator, "signature": Cryptographer.sign(appointment_receipt, dummy_teos_sk), "available_slots": 100, "start_block": CURRENT_HEIGHT, "subscription_expiry": CURRENT_HEIGHT + 4320, } responses.add(responses.POST, add_appointment_endpoint, json=response, status=200) result = teos_client.add_appointment( Appointment.from_dict(dummy_appointment_data), dummy_user_sk, dummy_teos_id, teos_url) assert len(responses.calls) == 1 assert responses.calls[0].request.url == add_appointment_endpoint assert result
def test_get_appointment_non_registered(internal_api, stub, generate_dummy_appointment): # If the user is not registered or the appointment does not belong to him the response should be NOT_FOUND stub.register(RegisterRequest(user_id=user_id)) another_user_sk, another_user_pk = generate_keypair() # Send the appointment first appointment, _ = generate_dummy_appointment() appointment_signature = Cryptographer.sign(appointment.serialize(), user_sk) send_appointment(stub, appointment, appointment_signature) # Request it back message = f"get appointment {appointment.locator}" request_signature = Cryptographer.sign(message.encode("utf-8"), another_user_sk) with pytest.raises(grpc.RpcError) as e: stub.get_appointment( GetAppointmentRequest(locator=appointment.locator, signature=request_signature)) assert e.value.code() == grpc.StatusCode.NOT_FOUND assert "Appointment not found" in e.value.details() # Notice how the request will succeed if `user` (user_id) requests it request_signature = Cryptographer.sign(message.encode("utf-8"), user_sk) response = stub.get_appointment( GetAppointmentRequest(locator=appointment.locator, signature=request_signature)) assert isinstance(response, GetAppointmentResponse)
def test_add_appointment_in_cache(watcher): # Generate an appointment and add the dispute txid to the cache user_sk, user_pk = generate_keypair() user_id = Cryptographer.get_compressed_pk(user_pk) watcher.gatekeeper.registered_users[user_id] = UserInfo(available_slots=1, subscription_expiry=10) appointment, dispute_tx = generate_dummy_appointment() dispute_txid = watcher.block_processor.decode_raw_transaction(dispute_tx).get("txid") watcher.locator_cache.cache[appointment.locator] = dispute_txid # Try to add the appointment response = watcher.add_appointment(appointment, Cryptographer.sign(appointment.serialize(), user_sk)) # The appointment is accepted but it's not in the Watcher assert ( response and response.get("locator") == appointment.locator and Cryptographer.get_compressed_pk(watcher.signing_key.public_key) == Cryptographer.get_compressed_pk(Cryptographer.recover_pk(appointment.serialize(), response.get("signature"))) ) assert not watcher.locator_uuid_map.get(appointment.locator) # It went to the Responder straightaway assert appointment.locator in [tracker.get("locator") for tracker in watcher.responder.trackers.values()] # Trying to send it again should fail since it is already in the Responder with pytest.raises(AppointmentAlreadyTriggered): watcher.add_appointment(appointment, Cryptographer.sign(appointment.serialize(), user_sk))
def test_get_appointment_subscription_expired(internal_api, stub, generate_dummy_appointment): # UNAUTHENTICATED is returned if the subscription has expired stub.register(RegisterRequest(user_id=user_id)) # Send the appointment first appointment, _ = generate_dummy_appointment() appointment_signature = Cryptographer.sign(appointment.serialize(), user_sk) send_appointment(stub, appointment, appointment_signature) # Modify the user data so the subscription has already ended expiry = internal_api.watcher.block_processor.get_block_count( ) - internal_api.watcher.gatekeeper.expiry_delta - 1 internal_api.watcher.gatekeeper.registered_users[ user_id].subscription_expiry = expiry # Request it back message = f"get appointment {appointment.locator}" request_signature = Cryptographer.sign(message.encode("utf-8"), user_sk) with pytest.raises(grpc.RpcError) as e: stub.get_appointment( GetAppointmentRequest(locator=appointment.locator, signature=request_signature)) assert e.value.code() == grpc.StatusCode.UNAUTHENTICATED assert "Your subscription expired at" in e.value.details()
def test_add_too_many_appointments(watcher): # Simulate the user is registered user_sk, user_pk = generate_keypair() available_slots = 100 user_id = Cryptographer.get_compressed_pk(user_pk) watcher.gatekeeper.registered_users[user_id] = UserInfo(available_slots=available_slots, subscription_expiry=10) # Appointments on top of the limit should be rejected watcher.appointments = dict() for i in range(MAX_APPOINTMENTS): appointment, dispute_tx = generate_dummy_appointment() appointment_signature = Cryptographer.sign(appointment.serialize(), user_sk) response = watcher.add_appointment(appointment, appointment_signature) assert response.get("locator") == appointment.locator assert Cryptographer.get_compressed_pk(watcher.signing_key.public_key) == Cryptographer.get_compressed_pk( Cryptographer.recover_pk(appointment.serialize(), response.get("signature")) ) assert response.get("available_slots") == available_slots - (i + 1) with pytest.raises(AppointmentLimitReached): appointment, dispute_tx = generate_dummy_appointment() appointment_signature = Cryptographer.sign(appointment.serialize(), user_sk) watcher.add_appointment(appointment, appointment_signature)
def test_add_appointment_update_smaller(internal_api, client, appointment, block_processor): # Update an appointment by one bigger, and check slots are freed internal_api.watcher.gatekeeper.registered_users[user_id] = UserInfo( available_slots=2, subscription_expiry=block_processor.get_block_count() + 1) # This should take 2 slots appointment.encrypted_blob = TWO_SLOTS_BLOTS appointment_signature = Cryptographer.sign(appointment.serialize(), user_sk) r = add_appointment(client, { "appointment": appointment.to_dict(), "signature": appointment_signature }, user_id) assert (r.status_code == HTTP_OK and r.json.get("available_slots") == 0 and r.json.get("start_block") == block_processor.get_block_count()) # Let's update with one just small enough appointment.encrypted_blob = "A" * (ENCRYPTED_BLOB_MAX_SIZE_HEX - 2) appointment_signature = Cryptographer.sign(appointment.serialize(), user_sk) r = add_appointment(client, { "appointment": appointment.to_dict(), "signature": appointment_signature }, user_id) assert (r.status_code == HTTP_OK and r.json.get("available_slots") == 1 and r.json.get("start_block") == block_processor.get_block_count())
def test_add_appointment_update_same_size(internal_api, client, appointment, block_processor): # Update an appointment by one of the same size and check that no additional slots are filled internal_api.watcher.gatekeeper.registered_users[user_id] = UserInfo( available_slots=1, subscription_expiry=block_processor.get_block_count() + 1) appointment_signature = Cryptographer.sign(appointment.serialize(), user_sk) r = add_appointment(client, { "appointment": appointment.to_dict(), "signature": appointment_signature }, user_id) assert (r.status_code == HTTP_OK and r.json.get("available_slots") == 0 and r.json.get("start_block") == block_processor.get_block_count()) # The user has no additional slots, but it should be able to update # Let's just reverse the encrypted blob for example appointment.encrypted_blob = appointment.encrypted_blob[::-1] appointment_signature = Cryptographer.sign(appointment.serialize(), user_sk) r = add_appointment(client, { "appointment": appointment.to_dict(), "signature": appointment_signature }, user_id) assert (r.status_code == HTTP_OK and r.json.get("available_slots") == 0 and r.json.get("start_block") == block_processor.get_block_count())
def test_sign(): # Otherwise we should get a signature sk, _ = generate_keypair() message = b"" assert Cryptographer.sign(message, sk) is not None assert isinstance(Cryptographer.sign(message, sk), str)
def test_add_appointment_with_invalid_signature(): # Simulate a request to add_appointment for dummy_appointment, but sign with a different key, # make sure that the right endpoint is requested, but the return value is False appointment = teos_client.create_appointment(dummy_appointment_data) user_signature = Cryptographer.sign(appointment.serialize(), dummy_user_sk) appointment_receipt = receipts.create_appointment_receipt( user_signature, CURRENT_HEIGHT) # Sign with a bad key response = { "locator": dummy_appointment.locator, "signature": Cryptographer.sign(appointment_receipt, another_sk), "available_slots": 100, "start_block": CURRENT_HEIGHT, "subscription_expiry": CURRENT_HEIGHT + 4320, } responses.add(responses.POST, add_appointment_endpoint, json=response, status=200) with pytest.raises(TowerResponseError): teos_client.add_appointment( Appointment.from_dict(dummy_appointment_data), dummy_user_sk, dummy_teos_id, teos_url) # should have performed exactly 1 network request assert len(responses.calls) == 1
def test_add_appointment(watcher, generate_dummy_appointment): # Simulate the user is registered user_sk, user_pk = generate_keypair() available_slots = 100 user_id = Cryptographer.get_compressed_pk(user_pk) watcher.gatekeeper.registered_users[user_id] = UserInfo( available_slots=available_slots, subscription_expiry=watcher.block_processor.get_block_count() + 1) appointment, dispute_tx = generate_dummy_appointment() appointment_signature = Cryptographer.sign(appointment.serialize(), user_sk) response = watcher.add_appointment(appointment, appointment_signature) assert response.get("locator") == appointment.locator assert Cryptographer.get_compressed_pk( watcher.signing_key.public_key) == Cryptographer.get_compressed_pk( Cryptographer.recover_pk( receipts.create_appointment_receipt( appointment_signature, response.get("start_block")), response.get("signature"), )) assert response.get("available_slots") == available_slots - 1 # Check that we can also add an already added appointment (same locator) response = watcher.add_appointment(appointment, appointment_signature) assert response.get("locator") == appointment.locator assert Cryptographer.get_compressed_pk( watcher.signing_key.public_key) == Cryptographer.get_compressed_pk( Cryptographer.recover_pk( receipts.create_appointment_receipt( appointment_signature, response.get("start_block")), response.get("signature"), )) # The slot count should not have been reduced and only one copy is kept. assert response.get("available_slots") == available_slots - 1 assert len(watcher.locator_uuid_map[appointment.locator]) == 1 # If two appointments with the same locator come from different users, they are kept. another_user_sk, another_user_pk = generate_keypair() another_user_id = Cryptographer.get_compressed_pk(another_user_pk) watcher.gatekeeper.registered_users[another_user_id] = UserInfo( available_slots=available_slots, subscription_expiry=watcher.block_processor.get_block_count() + 1) appointment_signature = Cryptographer.sign(appointment.serialize(), another_user_sk) response = watcher.add_appointment(appointment, appointment_signature) assert response.get("locator") == appointment.locator assert Cryptographer.get_compressed_pk( watcher.signing_key.public_key) == Cryptographer.get_compressed_pk( Cryptographer.recover_pk( receipts.create_appointment_receipt( appointment_signature, response.get("start_block")), response.get("signature"), )) assert response.get("available_slots") == available_slots - 1 assert len(watcher.locator_uuid_map[appointment.locator]) == 2
def test_add_appointment_trigger_on_cache_cannot_decrypt(bitcoin_cli): commitment_tx, penalty_tx = create_txs(bitcoin_cli) # Let's send the commitment to the network and mine a block broadcast_transaction_and_mine_block(bitcoin_cli, commitment_tx, bitcoin_cli.getnewaddress()) sleep(1) # The appointment data is built using a random 32-byte value. appointment_data = build_appointment_data(get_random_value_hex(32), penalty_tx) # We cannot use teos_cli.add_appointment here since it computes the locator internally, so let's do it manually. appointment_data["locator"] = compute_locator(bitcoin_cli.decoderawtransaction(commitment_tx).get("txid")) appointment_data["encrypted_blob"] = Cryptographer.encrypt(penalty_tx, get_random_value_hex(32)) appointment = Appointment.from_dict(appointment_data) signature = Cryptographer.sign(appointment.serialize(), user_sk) data = {"appointment": appointment.to_dict(), "signature": signature} # Send appointment to the server. response = teos_cli.post_request(data, teos_add_appointment_endpoint) response_json = teos_cli.process_post_response(response) # Check that the server has accepted the appointment signature = response_json.get("signature") rpk = Cryptographer.recover_pk(appointment.serialize(), signature) assert teos_id == Cryptographer.get_compressed_pk(rpk) assert response_json.get("locator") == appointment.locator # The appointment should should have been inmediately dropped with pytest.raises(TowerResponseError): get_appointment_info(appointment_data["locator"])
def test_add_too_many_appointment(internal_api, client, block_processor, generate_dummy_appointment): # Give slots to the user internal_api.watcher.gatekeeper.registered_users[user_id] = UserInfo( available_slots=200, subscription_expiry=block_processor.get_block_count() + 1) free_appointment_slots = MAX_APPOINTMENTS - len( internal_api.watcher.appointments) for i in range(free_appointment_slots + 1): appointment, dispute_tx = generate_dummy_appointment() locator_dispute_tx_map[appointment.locator] = dispute_tx appointment_signature = Cryptographer.sign(appointment.serialize(), user_sk) r = add_appointment( client, { "appointment": appointment.to_dict(), "signature": appointment_signature }, user_id) if i < free_appointment_slots: assert r.status_code == HTTP_OK and r.json.get( "start_block") == block_processor.get_block_count() else: assert r.status_code == HTTP_SERVICE_UNAVAILABLE
def test_appointment_wrong_decryption_key(bitcoin_cli): # This tests an appointment encrypted with a key that has not been derived from the same source as the locator. # Therefore the tower won't be able to decrypt the blob once the appointment is triggered. commitment_tx, penalty_tx = create_txs(bitcoin_cli) # The appointment data is built using a random 32-byte value. appointment_data = build_appointment_data(get_random_value_hex(32), penalty_tx) # We cannot use teos_cli.add_appointment here since it computes the locator internally, so let's do it manually. # We will encrypt the blob using the random value and derive the locator from the commitment tx. appointment_data["locator"] = compute_locator(bitcoin_cli.decoderawtransaction(commitment_tx).get("txid")) appointment_data["encrypted_blob"] = Cryptographer.encrypt(penalty_tx, get_random_value_hex(32)) appointment = Appointment.from_dict(appointment_data) signature = Cryptographer.sign(appointment.serialize(), user_sk) data = {"appointment": appointment.to_dict(), "signature": signature} # Send appointment to the server. response = teos_cli.post_request(data, teos_add_appointment_endpoint) response_json = teos_cli.process_post_response(response) # Check that the server has accepted the appointment signature = response_json.get("signature") rpk = Cryptographer.recover_pk(appointment.serialize(), signature) assert teos_id == Cryptographer.get_compressed_pk(rpk) assert response_json.get("locator") == appointment.locator # Trigger the appointment new_addr = bitcoin_cli.getnewaddress() broadcast_transaction_and_mine_block(bitcoin_cli, commitment_tx, new_addr) # The appointment should have been removed since the decryption failed. with pytest.raises(TowerResponseError): get_appointment_info(appointment.locator)
def test_inspect(run_bitcoind): # At this point every single check function has been already tested, let's test inspect with an invalid and a valid # appointments. client_sk, client_pk = generate_keypair() client_pk_hex = client_pk.format().hex() # Valid appointment locator = get_random_value_hex(LOCATOR_LEN_BYTES) start_time = block_processor.get_block_count() + 5 end_time = start_time + 20 to_self_delay = MIN_TO_SELF_DELAY encrypted_blob = get_random_value_hex(64) appointment_data = { "locator": locator, "start_time": start_time, "end_time": end_time, "to_self_delay": to_self_delay, "encrypted_blob": encrypted_blob, } signature = Cryptographer.sign( Appointment.from_dict(appointment_data).serialize(), client_sk) appointment = inspector.inspect(appointment_data, signature, client_pk_hex) assert (type(appointment) == Appointment and appointment.locator == locator and appointment.start_time == start_time and appointment.end_time == end_time and appointment.to_self_delay == to_self_delay and appointment.encrypted_blob.data == encrypted_blob)
def test_get_appointment_in_watcher(internal_api, client, appointment, monkeypatch): # Mock the appointment in the Watcher uuid = hash_160("{}{}".format(appointment.locator, user_id)) extended_appointment_summary = { "locator": appointment.locator, "user_id": user_id } internal_api.watcher.appointments[uuid] = extended_appointment_summary internal_api.watcher.db_manager.store_watcher_appointment( uuid, appointment.to_dict()) # mock the gatekeeper (user won't be registered if the previous tests weren't ran) monkeypatch.setattr(internal_api.watcher.gatekeeper, "authenticate_user", mock_authenticate_user) # Next we can request it message = "get appointment {}".format(appointment.locator) signature = Cryptographer.sign(message.encode("utf-8"), user_sk) data = {"locator": appointment.locator, "signature": signature} r = client.post(get_appointment_endpoint, json=data) assert r.status_code == HTTP_OK # Check that the appointment is on the Watcher assert r.json.get("status") == AppointmentStatus.BEING_WATCHED # Cast the extended appointment (used by the tower) to a regular appointment (used by the user) appointment = Appointment.from_dict(appointment.to_dict()) # Check the the sent appointment matches the received one assert r.json.get("locator") == appointment.locator assert appointment.to_dict() == r.json.get("appointment")
def test_add_appointment_in_cache_invalid_transaction(api, client): api.watcher.gatekeeper.registered_users[user_id] = UserInfo(available_slots=1, subscription_expiry=0) # We need to create the appointment manually dispute_tx = create_dummy_transaction() dispute_txid = dispute_tx.tx_id.hex() penalty_tx = create_dummy_transaction(dispute_txid) locator = compute_locator(dispute_txid) dummy_appointment_data = {"tx": penalty_tx.hex(), "tx_id": dispute_txid, "to_self_delay": 20} encrypted_blob = Cryptographer.encrypt(dummy_appointment_data.get("tx")[::-1], dummy_appointment_data.get("tx_id")) appointment_data = { "locator": locator, "to_self_delay": dummy_appointment_data.get("to_self_delay"), "encrypted_blob": encrypted_blob, "user_id": get_random_value_hex(16), } appointment = ExtendedAppointment.from_dict(appointment_data) api.watcher.locator_cache.cache[appointment.locator] = dispute_tx.tx_id.hex() appointment_signature = Cryptographer.sign(appointment.serialize(), user_sk) # Add the data to the cache api.watcher.locator_cache.cache[dispute_txid] = appointment.locator # The appointment should be accepted r = add_appointment(client, {"appointment": appointment.to_dict(), "signature": appointment_signature}, user_id) assert ( r.status_code == HTTP_OK and r.json.get("available_slots") == 0 and r.json.get("start_block") == api.watcher.last_known_block )
def test_get_appointment_in_responder(api, client, appointment): # Mock the appointment in the Responder tracker_data = { "locator": appointment.locator, "dispute_txid": get_random_value_hex(32), "penalty_txid": get_random_value_hex(32), "penalty_rawtx": get_random_value_hex(250), "user_id": get_random_value_hex(16), } tx_tracker = TransactionTracker.from_dict(tracker_data) uuid = hash_160("{}{}".format(appointment.locator, user_id)) api.watcher.db_manager.create_triggered_appointment_flag(uuid) api.watcher.responder.db_manager.store_responder_tracker(uuid, tx_tracker.to_dict()) # Request back the data message = "get appointment {}".format(appointment.locator) signature = Cryptographer.sign(message.encode("utf-8"), user_sk) data = {"locator": appointment.locator, "signature": signature} # Next we can request it r = client.post(get_appointment_endpoint, json=data) assert r.status_code == HTTP_OK # Check that the appointment is on the Responder assert r.json.get("status") == "dispute_responded" # Check the the sent appointment matches the received one assert tx_tracker.locator == r.json.get("locator") assert tx_tracker.dispute_txid == r.json.get("appointment").get("dispute_txid") assert tx_tracker.penalty_txid == r.json.get("appointment").get("penalty_txid") assert tx_tracker.penalty_rawtx == r.json.get("appointment").get("penalty_rawtx")
def test_get_appointment_in_responder(internal_api, client, generate_dummy_tracker, monkeypatch): tx_tracker = generate_dummy_tracker() # Mock the appointment in the Responder uuid = hash_160("{}{}".format(tx_tracker.locator, user_id)) internal_api.watcher.responder.trackers[uuid] = tx_tracker.get_summary() internal_api.watcher.responder.db_manager.store_responder_tracker( uuid, tx_tracker.to_dict()) # mock the gatekeeper (user won't be registered if the previous tests weren't ran) monkeypatch.setattr(internal_api.watcher.gatekeeper, "authenticate_user", mock_authenticate_user) # Request back the data message = "get appointment {}".format(tx_tracker.locator) signature = Cryptographer.sign(message.encode("utf-8"), user_sk) data = {"locator": tx_tracker.locator, "signature": signature} # Next we can request it r = client.post(get_appointment_endpoint, json=data) assert r.status_code == HTTP_OK # Check that the appointment is on the Responder assert r.json.get("status") == AppointmentStatus.DISPUTE_RESPONDED # Check the the sent appointment matches the received one assert tx_tracker.locator == r.json.get("locator") assert tx_tracker.dispute_txid == r.json.get("appointment").get( "dispute_txid") assert tx_tracker.penalty_txid == r.json.get("appointment").get( "penalty_txid") assert tx_tracker.penalty_rawtx == r.json.get("appointment").get( "penalty_rawtx")
def test_register(api, client, monkeypatch): # Tests registering a user within the tower # Monkeypatch the response from the InternalAPI so the user is accepted slots = config.get("SUBSCRIPTION_SLOTS") expiry = config.get("SUBSCRIPTION_DURATION") receipt = receipts.create_registration_receipt(user_id, slots, expiry) signature = Cryptographer.sign(receipt, teos_sk) response = RegisterResponse( user_id=user_id, available_slots=slots, subscription_expiry=expiry, subscription_signature=signature, ) monkeypatch.setattr(api.stub, "register", lambda x: response) # Send the register request data = {"public_key": user_id} r = client.post(register_endpoint, json=data) # Check the reply assert r.status_code == HTTP_OK assert r.json.get("public_key") == user_id assert r.json.get("available_slots") == config.get("SUBSCRIPTION_SLOTS") assert r.json.get("subscription_expiry") == config.get( "SUBSCRIPTION_DURATION") rpk = Cryptographer.recover_pk(receipt, r.json.get("subscription_signature")) assert Cryptographer.get_compressed_pk(rpk) == teos_id
def test_add_appointment_multiple_times_different_users( internal_api, client, appointment, block_processor, n=MULTIPLE_APPOINTMENTS): # If the same appointment comes from different users, all are kept # Create user keys and appointment signatures user_keys = [generate_keypair() for _ in range(n)] signatures = [ Cryptographer.sign(appointment.serialize(), key[0]) for key in user_keys ] tmp_user_ids = [Cryptographer.get_compressed_pk(pk) for _, pk in user_keys] # Add one slot per public key for pair in user_keys: user_id = Cryptographer.get_compressed_pk(pair[1]) internal_api.watcher.gatekeeper.registered_users[user_id] = UserInfo( available_slots=1, subscription_expiry=block_processor.get_block_count() + 1) # Send the appointments for compressed_pk, signature in zip(tmp_user_ids, signatures): r = add_appointment(client, { "appointment": appointment.to_dict(), "signature": signature }, compressed_pk) assert r.status_code == HTTP_OK assert r.json.get("available_slots") == 0 assert r.json.get("start_block") == block_processor.get_block_count() # Check that all the appointments have been added and that there are no duplicates assert len(set( internal_api.watcher.locator_uuid_map[appointment.locator])) == n
def test_add_too_many_appointments(watcher, generate_dummy_appointment, monkeypatch): # Adding appointment beyond the user limit should fail # Mock the user being registered expiry = 100 user_info = UserInfo(MAX_APPOINTMENTS, expiry) monkeypatch.setattr(watcher.gatekeeper, "authenticate_user", lambda x, y: user_id) monkeypatch.setattr(watcher.gatekeeper, "has_subscription_expired", lambda x: (False, expiry)) monkeypatch.setattr(watcher.gatekeeper, "get_user_info", lambda x: user_info) for i in range(user_info.available_slots): appointment = generate_dummy_appointment() response = watcher.add_appointment(appointment, appointment.user_signature) appointment_receipt = receipts.create_appointment_receipt( appointment.user_signature, response.get("start_block")) assert response.get("locator") == appointment.locator assert Cryptographer.get_compressed_pk( watcher.signing_key.public_key) == Cryptographer.get_compressed_pk( Cryptographer.recover_pk(appointment_receipt, response.get("signature"))) with pytest.raises(AppointmentLimitReached): appointment = generate_dummy_appointment() appointment_signature = Cryptographer.sign(appointment.serialize(), user_sk) watcher.add_appointment(appointment, appointment_signature)
def test_add_appointment_multiple_times_same_user(api, client, generate_dummy_appointment, monkeypatch): # Multiple appointments with the same locator should be valid. At the API level we only need to test that # the request goes trough. appointment = generate_dummy_appointment() appointment_signature = Cryptographer.sign(appointment.serialize(), user_sk) # Monkeypatch the InternalAPI return slots = 10 subscription_expiry = 100 appointment = generate_dummy_appointment() response = { "locator": appointment.locator, "start_block": appointment.start_block, "signature": get_random_value_hex(70), "available_slots": slots, "subscription_expiry": subscription_expiry, } monkeypatch.setattr(api.stub, "add_appointment", lambda x: AddAppointmentResponse(**response)) for _ in range(MULTIPLE_APPOINTMENTS): r = client.post(add_appointment_endpoint, json={ "appointment": appointment.to_dict(), "signature": appointment_signature }) assert r.status_code == HTTP_OK assert r.json.get("available_slots") == slots assert r.json.get("start_block") == appointment.start_block
def test_add_appointment_not_registered_no_enough_slots( api, client, generate_dummy_appointment, monkeypatch): # A properly formatted add appointment request: # - from a non-registered user # - from a user with no free slots # - from a user with no enough free slots # should fail. To prevent probing, they all fail as UNAUTHENTICATED. Further testing can be done in the Watcher # but it's transparent from the API POV. # Mock the non-registered user (gRPC UNAUTHENTICATED error) e_code = grpc.StatusCode.UNAUTHENTICATED monkeypatch.setattr(api.stub, "add_appointment", raise_grpc_error) monkeypatch.setattr(rpc_error, "code", lambda: e_code) monkeypatch.setattr(rpc_error, "details", lambda: "") # Send the data appointment = generate_dummy_appointment() appointment_signature = Cryptographer.sign(appointment.serialize(), user_sk) r = client.post(add_appointment_endpoint, json={ "appointment": appointment.to_dict(), "signature": appointment_signature }) assert r.status_code == HTTP_BAD_REQUEST assert errors.APPOINTMENT_INVALID_SIGNATURE_OR_SUBSCRIPTION_ERROR == r.json.get( "error_code")
def test_add_appointment(api, client, generate_dummy_appointment, monkeypatch): # Test adding a properly formatted appointment # Monkeypatch the InternalAPI return slots = 10 subscription_expiry = 100 appointment = generate_dummy_appointment() response = { "locator": appointment.locator, "start_block": appointment.start_block, "signature": get_random_value_hex(70), "available_slots": slots, "subscription_expiry": subscription_expiry, } monkeypatch.setattr(api.stub, "add_appointment", lambda x: AddAppointmentResponse(**response)) # Properly formatted appointment appointment_signature = Cryptographer.sign(appointment.serialize(), user_sk) r = client.post(add_appointment_endpoint, json={ "appointment": appointment.to_dict(), "signature": appointment_signature }) assert r.status_code == HTTP_OK assert r.json == response
def test_get_appointment_watcher(api, client, generate_dummy_appointment, monkeypatch): # Mock the appointment in the Watcher appointment = generate_dummy_appointment() app_data = AppointmentData(appointment=AppointmentProto( locator=appointment.locator, encrypted_blob=appointment.encrypted_blob, to_self_delay=appointment.to_self_delay, )) status = AppointmentStatus.BEING_WATCHED monkeypatch.setattr( api.stub, "get_appointment", lambda x: GetAppointmentResponse(appointment_data=app_data, status=status)) # Request it message = "get appointment {}".format(appointment.locator) signature = Cryptographer.sign(message.encode("utf-8"), user_sk) data = {"locator": appointment.locator, "signature": signature} r = client.post(get_appointment_endpoint, json=data) assert r.status_code == HTTP_OK # Check that requested appointment data matches the mocked one # Cast the extended appointment (used by the tower) to a regular appointment (used by the user) local_appointment = Appointment.from_dict(appointment.to_dict()) assert r.json.get("status") == AppointmentStatus.BEING_WATCHED assert r.json.get("appointment") == local_appointment.to_dict()
def register(self): user_id = request.get_json().get("public_key") available_slots = 100 if user_id not in self.users else self.users[ user_id]["available_slots"] + 100 subscription_expiry = CURRENT_HEIGHT + 4320 self.users[user_id] = { "available_slots": available_slots, "subscription_expiry": subscription_expiry } registration_receipt = receipts.create_registration_receipt( user_id, available_slots, subscription_expiry) rcode = constants.HTTP_OK response = { "public_key": user_id, "available_slots": self.users[user_id].get("available_slots"), "subscription_expiry": self.users[user_id].get("subscription_expiry"), "subscription_signature": Cryptographer.sign(registration_receipt, self.sk), } return response, rcode
def get_subscription_info(user_sk, teos_id, teos_url): """ Gets information about a user's subscription status from the tower. Args: user_sk (:obj:`PrivateKey`): the user's private key. teos_id (:obj:`str`): the tower's compressed public key. teos_url (:obj:`str`): the teos base url. Returns: :obj:`dict`: A dictionary containing the subscription data, including number of remaining slots and the subscription's expiry time. Raises: :obj:`ConnectionError`: if the client cannot connect to the tower. :obj:`TowerResponseError`: if the tower responded with an error, or the response was invalid. """ message = "get subscription info" signature = Cryptographer.sign(message.encode("utf-8"), user_sk) data = {"signature": signature} # Send request to the server. get_subscription_info_endpoint = "{}/get_subscription_info".format( teos_url) logger.info("Requesting subscription information from the Eye of Satoshi") response = process_post_response( post_request(data, get_subscription_info_endpoint)) return response
def test_get_appointment_in_responder(api, client, generate_dummy_tracker, monkeypatch): # Mock the appointment in the Responder tracker = generate_dummy_tracker() track_data = AppointmentData(tracker=TrackerProto( locator=tracker.locator, dispute_txid=tracker.dispute_txid, penalty_txid=tracker.penalty_txid, penalty_rawtx=tracker.penalty_rawtx, )) status = AppointmentStatus.DISPUTE_RESPONDED monkeypatch.setattr( api.stub, "get_appointment", lambda x: GetAppointmentResponse(appointment_data=track_data, status=status)) # Request it message = "get appointment {}".format(tracker.locator) signature = Cryptographer.sign(message.encode("utf-8"), user_sk) data = {"locator": tracker.locator, "signature": signature} r = client.post(get_appointment_endpoint, json=data) assert r.status_code == HTTP_OK # Check the received tracker data matches the mocked one assert tracker.locator == r.json.get("locator") assert tracker.dispute_txid == r.json.get("appointment").get( "dispute_txid") assert tracker.penalty_txid == r.json.get("appointment").get( "penalty_txid") assert tracker.penalty_rawtx == r.json.get("appointment").get( "penalty_rawtx")
def get_appointment(plugin, tower_id, locator): """ Gets information about an appointment from the tower. Args: plugin (:obj:`Plugin`): this plugin. tower_id (:obj:`str`): the identifier of the tower to query. locator (:obj:`str`): the appointment locator. Returns: :obj:`dict`: a dictionary containing the appointment data. """ # FIXME: All responses from the tower should be signed. try: tower_id, locator = arg_parser.parse_get_appointment_arguments(tower_id, locator) if tower_id not in plugin.wt_client.towers: raise InvalidParameter("tower_id is not within the registered towers", tower_id=tower_id) message = f"get appointment {locator}" signature = Cryptographer.sign(message.encode("utf-8"), plugin.wt_client.sk) data = {"locator": locator, "signature": signature} # Send request to the server. tower_netaddr = plugin.wt_client.towers[tower_id].netaddr get_appointment_endpoint = f"{tower_netaddr}/get_appointment" plugin.log(f"Requesting appointment from {tower_id}") response = process_post_response(post_request(data, get_appointment_endpoint, tower_id)) return response except (InvalidParameter, TowerConnectionError, TowerResponseError) as e: plugin.log(str(e), level="warn") return e.to_json()
def test_get_subscription_info(api, client, monkeypatch): # MonkeyPatch the InternalAPI response (user being in the tower) available_slots = 42 subscription_expiry = 1234 appointments = [get_random_value_hex(32)] user_struct = Struct() user_struct.update({ "subscription_expiry": subscription_expiry, "available_slots": available_slots, "appointments": appointments }) monkeypatch.setattr(api.stub, "get_subscription_info", lambda x: GetUserResponse(user=user_struct)) # Request the data message = "get subscription info" signature = Cryptographer.sign(message.encode("utf-8"), user_sk) data = {"signature": signature} r = client.post(get_subscription_info_endpoint, json=data) # Check the data matches assert r.status_code == HTTP_OK assert r.get_json().get("available_slots") == available_slots assert r.get_json().get("subscription_expiry") == subscription_expiry assert r.get_json().get("appointments") == appointments