def test_encrypt_odd_length_data(): blob = Blob(get_random_value_hex(64)[-1]) key = get_random_value_hex(32) try: Cryptographer.encrypt(blob, key) assert False except ValueError: assert True
def test_encrypt_wrong_key_size(): blob = Blob(get_random_value_hex(64)) key = get_random_value_hex(31) try: Cryptographer.encrypt(blob, key) assert False except ValueError: assert True
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_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 create_appointment(appointment_data): """ Creates an appointment object from an appointment data dictionary provided by the user. Performs all the required sanity checks on the input data: - Check that the given commitment_txid is correct (proper format and not missing) - Check that the transaction is correct (not missing) Args: appointment_data (:obj:`dict`): a dictionary containing the appointment data. Returns: :obj:`common.appointment.Appointment`: An appointment built from the appointment data provided by the user. """ tx_id = appointment_data.get("tx_id") tx = appointment_data.get("tx") if not tx_id: raise InvalidParameter("Missing tx_id, locator cannot be computed") elif not is_256b_hex_str(tx_id): raise InvalidParameter("Wrong tx_id, locator cannot be computed") elif not tx: raise InvalidParameter("The tx field is missing in the provided data") elif not isinstance(tx, str): raise InvalidParameter("The provided tx field is not a string") appointment_data["locator"] = compute_locator(tx_id) appointment_data["encrypted_blob"] = Cryptographer.encrypt(tx, tx_id) return Appointment.from_dict(appointment_data)
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_create_appointment(): # Tests that an appointment is properly created provided the input data is correct appointment = teos_client.create_appointment(dummy_appointment_data) assert isinstance(appointment, Appointment) assert appointment.locator == dummy_appointment_data.get( "locator") and appointment.to_self_delay == dummy_appointment_data.get( "to_self_delay") assert appointment.encrypted_blob == Cryptographer.encrypt( dummy_appointment_data.get("tx"), dummy_appointment_data.get("tx_id"))
def test_check_breach(watcher): # A breach will be flagged as valid only if the encrypted blob can be properly decrypted and the resulting data # matches a transaction format. uuid = uuid4().hex appointment, dispute_tx = generate_dummy_appointment() dispute_txid = watcher.block_processor.decode_raw_transaction(dispute_tx).get("txid") penalty_txid, penalty_rawtx = watcher.check_breach(uuid, appointment, dispute_txid) assert Cryptographer.encrypt(penalty_rawtx, dispute_txid) == appointment.encrypted_blob
def test_check_breach_invalid_transaction(watcher): # If the breach triggers an appointment with data that can be decrypted but does not match a transaction, it should # fail uuid = uuid4().hex appointment, dispute_tx = generate_dummy_appointment() dispute_txid = watcher.block_processor.decode_raw_transaction(dispute_tx).get("txid") # Set the blob to something "random" appointment.encrypted_blob = Cryptographer.encrypt(get_random_value_hex(200), dispute_txid) with pytest.raises(InvalidTransactionFormat): watcher.check_breach(uuid, appointment, dispute_txid)
def _generate_dummy_appointment(): commitment_txid = get_random_value_hex(32) penalty_tx = get_random_value_hex(150) appointment_data = { "locator": compute_locator(commitment_txid), "to_self_delay": 20, "encrypted_blob": Cryptographer.encrypt(penalty_tx, commitment_txid), "user_id": get_random_value_hex(16), "user_signature": get_random_value_hex(50), "start_block": 200, } return ExtendedAppointment.from_dict(appointment_data), commitment_txid
def test_add_appointment_in_cache_invalid_blob(watcher): # Generate an appointment with an invalid transaction 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=watcher.block_processor.get_block_count() + 1) # We need to create the appointment manually commitment_tx, commitment_txid, penalty_tx = create_txs() locator = compute_locator(commitment_tx) dummy_appointment_data = { "tx": penalty_tx, "tx_id": commitment_txid, "to_self_delay": 20 } encrypted_blob = Cryptographer.encrypt(penalty_tx[::-1], commitment_txid) 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 = Appointment.from_dict(appointment_data) watcher.locator_cache.cache[appointment.locator] = commitment_txid # Try to add the appointment user_signature = Cryptographer.sign(appointment.serialize(), user_sk) response = watcher.add_appointment(appointment, user_signature) appointment_receipt = receipts.create_appointment_receipt( user_signature, response.get("start_block")) # The appointment is accepted but dropped (same as an invalid appointment that gets triggered) 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_receipt, response.get("signature")))) assert not watcher.locator_uuid_map.get(appointment.locator) assert appointment.locator not in [ tracker.get("locator") for tracker in watcher.responder.trackers.values() ]
def test_appointment_wrong_key(bitcoin_cli, create_txs): # 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 # The appointment data is built using a random 32-byte value. appointment_data = build_appointment_data(bitcoin_cli, get_random_value_hex(32), penalty_tx) # We can't 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(Blob(penalty_tx), get_random_value_hex(32)) appointment = Appointment.from_dict(appointment_data) teos_pk, cli_sk, cli_pk_der = teos_cli.load_keys( cli_config.get("TEOS_PUBLIC_KEY"), cli_config.get("CLI_PRIVATE_KEY"), cli_config.get("CLI_PUBLIC_KEY") ) hex_pk_der = binascii.hexlify(cli_pk_der) signature = Cryptographer.sign(appointment.serialize(), cli_sk) data = {"appointment": appointment.to_dict(), "signature": signature, "public_key": hex_pk_der.decode("utf-8")} # Send appointment to the server. response = teos_cli.post_appointment(data, teos_add_appointment_endpoint) response_json = teos_cli.process_post_appointment_response(response) # Check that the server has accepted the appointment signature = response_json.get("signature") assert signature is not None rpk = Cryptographer.recover_pk(appointment.serialize(), signature) assert Cryptographer.verify_rpk(teos_pk, rpk) is True 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. sleep(1) appointment_info = get_appointment_info(appointment.locator) assert appointment_info is not None assert len(appointment_info) == 1 assert appointment_info[0].get("status") == "not_found"
def generate_dummy_appointment_data(real_height=True, start_time_offset=5, end_time_offset=30): if real_height: current_height = bitcoin_cli(bitcoind_connect_params).getblockcount() else: current_height = 10 dispute_tx = create_dummy_transaction() dispute_txid = dispute_tx.tx_id.hex() penalty_tx = create_dummy_transaction(dispute_txid) dummy_appointment_data = { "tx": penalty_tx.hex(), "tx_id": dispute_txid, "start_time": current_height + start_time_offset, "end_time": current_height + end_time_offset, "to_self_delay": 20, } # dummy keys for this test client_sk, client_pk = generate_keypair() client_pk_hex = client_pk.format().hex() locator = compute_locator(dispute_txid) blob = Blob(dummy_appointment_data.get("tx")) encrypted_blob = Cryptographer.encrypt(blob, dummy_appointment_data.get("tx_id")) appointment_data = { "locator": locator, "start_time": dummy_appointment_data.get("start_time"), "end_time": dummy_appointment_data.get("end_time"), "to_self_delay": dummy_appointment_data.get("to_self_delay"), "encrypted_blob": encrypted_blob, } signature = Cryptographer.sign(Appointment.from_dict(appointment_data).serialize(), client_sk) data = {"appointment": appointment_data, "signature": signature, "public_key": client_pk_hex} return data, dispute_tx.hex()
def test_add_appointment_in_cache_invalid_transaction(internal_api, client, block_processor): internal_api.watcher.gatekeeper.registered_users[user_id] = UserInfo( available_slots=1, subscription_expiry=block_processor.get_block_count() + 1) # We need to create the appointment manually commitment_tx, commitment_txid, penalty_tx = create_txs() locator = compute_locator(commitment_tx) dummy_appointment_data = { "tx": penalty_tx, "tx_id": commitment_txid, "to_self_delay": 20 } encrypted_blob = Cryptographer.encrypt(penalty_tx[::-1], commitment_txid) appointment_data = { "locator": locator, "to_self_delay": dummy_appointment_data.get("to_self_delay"), "encrypted_blob": encrypted_blob, } appointment = Appointment.from_dict(appointment_data) internal_api.watcher.locator_cache.cache[ appointment.locator] = commitment_txid appointment_signature = Cryptographer.sign(appointment.serialize(), user_sk) # Add the data to the cache internal_api.watcher.locator_cache.cache[ commitment_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") == block_processor.get_block_count())
def generate_dummy_appointment(): 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"), 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), } return ExtendedAppointment.from_dict(appointment_data), dispute_tx.hex()
def add_appointment(appointment_data, user_sk, teos_id, teos_url): """ Manages the add_appointment command. The life cycle of the function is as follows: - Check that the given commitment_txid is correct (proper format and not missing) - Check that the transaction is correct (not missing) - Create the appointment locator and encrypted blob from the commitment_txid and the penalty_tx - Sign the appointment - Send the appointment to the tower - Wait for the response - Check the tower's response and signature Args: appointment_data (:obj:`dict`): a dictionary containing the appointment data. 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:`tuple`: A tuple (`:obj:Appointment <common.appointment.Appointment>`, :obj:`str`) containing the appointment and the tower's signature. Raises: :obj:`InvalidParameter <cli.exceptions.InvalidParameter>`: if `appointment_data` or any of its fields is invalid. :obj:`ValueError`: if the appointment cannot be signed. :obj:`ConnectionError`: if the client cannot connect to the tower. :obj:`TowerResponseError <cli.exceptions.TowerResponseError>`: if the tower responded with an error, or the response was invalid. """ if not appointment_data: raise InvalidParameter("The provided appointment JSON is empty") tx_id = appointment_data.get("tx_id") tx = appointment_data.get("tx") if not is_256b_hex_str(tx_id): raise InvalidParameter("The provided locator is wrong or missing") if not tx: raise InvalidParameter("The provided data is missing the transaction") appointment_data["locator"] = compute_locator(tx_id) appointment_data["encrypted_blob"] = Cryptographer.encrypt(tx, tx_id) 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. logger.info("Sending appointment to the Eye of Satoshi") add_appointment_endpoint = "{}/add_appointment".format(teos_url) response = process_post_response( post_request(data, add_appointment_endpoint)) tower_signature = response.get("signature") # Check that the server signed the appointment as it should. if not tower_signature: raise TowerResponseError( "The response does not contain the signature of the appointment") rpk = Cryptographer.recover_pk(appointment.serialize(), tower_signature) if teos_id != Cryptographer.get_compressed_pk(rpk): raise TowerResponseError( "The returned appointment's signature is invalid") logger.info("Appointment accepted and signed by the Eye of Satoshi") logger.info("Remaining slots: {}".format(response.get("available_slots"))) logger.info("Start block: {}".format(response.get("start_block"))) return appointment, tower_signature
def on_commitment_revocation(plugin, **kwargs): """ Sends an appointment to all registered towers for every net commitment transaction. kwargs should contain the commitment identifier (commitment_txid) and the penalty transaction (penalty_tx) Args: plugin (:obj:`Plugin`): this plugin. """ try: commitment_txid, penalty_tx = arg_parser.parse_add_appointment_arguments(kwargs) appointment = Appointment( locator=compute_locator(commitment_txid), encrypted_blob=Cryptographer.encrypt(penalty_tx, commitment_txid), to_self_delay=20, # does not matter for now, any value 20-2^32-1 would do ) signature = Cryptographer.sign(appointment.serialize(), plugin.wt_client.sk) except (InvalidParameter, EncryptionError, SignatureError) as e: plugin.log(str(e), level="warn") return {"result": "continue"} # Send appointment to the towers. # FIXME: sending the appointment to all registered towers atm. Some management would be nice. for tower_id, tower in plugin.wt_client.towers.items(): tower_update = {} if tower.status == "misbehaving": continue try: if tower.status == "reachable": tower_signature, available_slots = add_appointment( plugin, tower_id, tower, appointment.to_dict(), signature ) tower_update["appointment"] = (appointment.locator, tower_signature) tower_update["available_slots"] = available_slots else: if tower.status in ["temporarily unreachable", "unreachable"]: plugin.log(f"{tower_id} is {tower.status}. Adding {appointment.locator} to pending") elif tower.status == "subscription error": plugin.log(f"There is a subscription issue with {tower_id}. Adding appointment to pending") tower_update["pending_appointment"] = (appointment.to_dict(), signature), "add" except SignatureError as e: tower_update["status"] = "misbehaving" tower_update["misbehaving_proof"] = { "appointment": appointment.to_dict(), "signature": e.kwargs.get("signature"), "recovered_id": e.kwargs.get("recovered_id"), "receipt": e.kwargs.get("receipt"), } except TowerConnectionError: # All TowerConnectionError are transitory. Connections are tried on register so URLs cannot be malformed. # Flag appointment for retry tower_update["status"] = "temporarily unreachable" plugin.log(f"Adding {appointment.locator} to pending") tower_update["pending_appointment"] = (appointment.to_dict(), signature), "add" tower_update["retry"] = True except TowerResponseError as e: tower_update["status"] = e.kwargs.get("status") if tower_update["status"] in ["temporarily unreachable", "subscription error"]: plugin.log(f"Adding {appointment.locator} to pending") tower_update["pending_appointment"] = (appointment.to_dict(), signature), "add" if tower_update["status"] == "temporarily unreachable": tower_update["retry"] = True if e.kwargs.get("invalid_appointment"): tower_update["invalid_appointment"] = (appointment.to_dict(), signature) finally: # Update memory and TowersDB plugin.wt_client.update_tower_state(tower_id, tower_update) if tower_update.get("retry"): plugin.wt_client.retrier.temp_unreachable_towers.put(tower_id) return {"result": "continue"}
dummy_appointment_request = { "tx": get_random_value_hex(192), "tx_id": get_random_value_hex(32), "start_time": 1500, "end_time": 50000, "to_self_delay": 200, } # This is the format appointment turns into once it hits "add_appointment" dummy_appointment_full = { "locator": compute_locator(dummy_appointment_request.get("tx_id")), "start_time": dummy_appointment_request.get("start_time"), "end_time": dummy_appointment_request.get("end_time"), "to_self_delay": dummy_appointment_request.get("to_self_delay"), "encrypted_blob": Cryptographer.encrypt( Blob(dummy_appointment_request.get("tx")), dummy_appointment_request.get("tx_id") ), } dummy_appointment = Appointment.from_dict(dummy_appointment_full) def load_dummy_keys(*args): return dummy_pk, dummy_sk, dummy_pk.format(compressed=True) def get_dummy_signature(*args): return Cryptographer.sign(dummy_appointment.serialize(), dummy_sk) def get_bad_signature(*args):
def test_encrypt(): blob = Blob(data) assert Cryptographer.encrypt(blob, key) == encrypted_data
get_subscription_info_endpoint = "{}/get_subscription_info".format(teos_url) dummy_appointment_data = { "tx": get_random_value_hex(192), "tx_id": get_random_value_hex(32), "to_self_delay": 200 } # This is the format appointment turns into once it hits "add_appointment" dummy_appointment_dict = { "locator": compute_locator(dummy_appointment_data.get("tx_id")), "to_self_delay": dummy_appointment_data.get("to_self_delay"), "encrypted_blob": Cryptographer.encrypt(dummy_appointment_data.get("tx"), dummy_appointment_data.get("tx_id")), } dummy_appointment = Appointment.from_dict(dummy_appointment_dict) dummy_user_data = { "appointments": [], "available_slots": 100, "subscription_expiry": 7000 } # The height is never checked in the tests, so we can make it up CURRENT_HEIGHT = 300 @pytest.fixture def keyfiles():
def test_encrypt(): assert Cryptographer.encrypt(data, key) == encrypted_data
def test_encrypt_wrong_key_size(): blob = get_random_value_hex(64) key = get_random_value_hex(31) with pytest.raises(InvalidParameter, match="32-byte hex value"): Cryptographer.encrypt(blob, key)
def test_encrypt_odd_length_data(): blob = get_random_value_hex(64)[-1] key = get_random_value_hex(32) with pytest.raises(InvalidParameter, match="Odd-length"): Cryptographer.encrypt(blob, key)
def add_appointment(args, teos_url, config): """ Manages the add_appointment command, from argument parsing, trough sending the appointment to the tower, until saving the appointment receipt. The life cycle of the function is as follows: - Load the add_appointment arguments - Check that the given commitment_txid is correct (proper format and not missing) - Check that the transaction is correct (not missing) - Create the appointment locator and encrypted blob from the commitment_txid and the penalty_tx - Load the client private key and sign the appointment - Send the appointment to the tower - Wait for the response - Check the tower's response and signature - Store the receipt (appointment + signature) on disk If any of the above-mentioned steps fails, the method returns false, otherwise it returns true. Args: args (:obj:`list`): a list of arguments to pass to ``parse_add_appointment_args``. Must contain a json encoded appointment, or the file option and the path to a file containing a json encoded appointment. teos_url (:obj:`str`): the teos base url. config (:obj:`dict`): a config dictionary following the format of :func:`create_config_dict <common.config_loader.ConfigLoader.create_config_dict>`. Returns: :obj:`bool`: True if the appointment is accepted by the tower and the receipt is properly stored, false if any error occurs during the process. """ # Currently the base_url is the same as the add_appointment_endpoint add_appointment_endpoint = teos_url teos_pk, cli_sk, cli_pk_der = load_keys(config.get("TEOS_PUBLIC_KEY"), config.get("CLI_PRIVATE_KEY"), config.get("CLI_PUBLIC_KEY")) try: hex_pk_der = binascii.hexlify(cli_pk_der) except binascii.Error as e: logger.error("Could not successfully encode public key as hex", error=str(e)) return False if teos_pk is None: return False # Get appointment data from user. appointment_data = parse_add_appointment_args(args) if appointment_data is None: logger.error("The provided appointment JSON is empty") return False valid_txid = check_sha256_hex_format(appointment_data.get("tx_id")) if not valid_txid: logger.error("The provided txid is not valid") return False tx_id = appointment_data.get("tx_id") tx = appointment_data.get("tx") if None not in [tx_id, tx]: appointment_data["locator"] = compute_locator(tx_id) appointment_data["encrypted_blob"] = Cryptographer.encrypt( Blob(tx), tx_id) else: logger.error("Appointment data is missing some fields") return False appointment = Appointment.from_dict(appointment_data) signature = Cryptographer.sign(appointment.serialize(), cli_sk) if not (appointment and signature): return False data = { "appointment": appointment.to_dict(), "signature": signature, "public_key": hex_pk_der.decode("utf-8") } # Send appointment to the server. server_response = post_appointment(data, add_appointment_endpoint) if server_response is None: return False response_json = process_post_appointment_response(server_response) if response_json is None: return False signature = response_json.get("signature") # Check that the server signed the appointment as it should. if signature is None: logger.error( "The response does not contain the signature of the appointment") return False rpk = Cryptographer.recover_pk(appointment.serialize(), signature) if not Cryptographer.verify_rpk(teos_pk, rpk): logger.error("The returned appointment's signature is invalid") return False logger.info("Appointment accepted and signed by the Eye of Satoshi") # All good, store appointment and signature return save_appointment_receipt(appointment.to_dict(), signature, config)