def test_from_dict(appointment_data): # The appointment should be build if we don't miss any field appointment = Appointment.from_dict(appointment_data) assert isinstance(appointment, Appointment) # Otherwise it should fail for key in appointment_data.keys(): prev_val = appointment_data[key] appointment_data[key] = None with pytest.raises(ValueError, match="Wrong appointment data"): Appointment.from_dict(appointment_data) appointment_data[key] = prev_val
def from_dict(cls, appointment_data): """ Builds an appointment from a dictionary. This method is useful to load data from a database. Args: appointment_data (:obj:`dict`): a dictionary containing the following keys: ``{locator, to_self_delay, encrypted_blob, user_id}`` Returns: :obj:`ExtendedAppointment <teos.extended_appointment.ExtendedAppointment>`: An appointment initialized using the provided data. Raises: ValueError: If one of the mandatory keys is missing in ``appointment_data``. """ appointment = Appointment.from_dict(appointment_data) user_id = appointment_data.get("user_id") user_signature = appointment_data.get("user_signature") start_block = appointment_data.get("start_block") if any(v is None for v in [user_id, user_signature, start_block]): raise ValueError("Wrong appointment data, some fields are missing") return cls( appointment.locator, appointment.encrypted_blob, appointment.to_self_delay, user_id, user_signature, start_block, )
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 send_appointment(tower_id, tower, appointment_dict, signature): data = {"appointment": appointment_dict, "signature": signature} add_appointment_endpoint = f"{tower.netaddr}/add_appointment" response = process_post_response( post_request(data, add_appointment_endpoint, tower_id)) tower_signature = response.get("signature") # Check that the server signed the appointment as it should. if not tower_signature: raise SignatureError( "The response does not contain the signature of the appointment", signature=None) rpk = Cryptographer.recover_pk( Appointment.from_dict(appointment_dict).serialize(), tower_signature) recovered_id = Cryptographer.get_compressed_pk(rpk) if tower_id != recovered_id: raise SignatureError( "The returned appointment's signature is invalid", tower_id=tower_id, recovered_id=recovered_id, signature=tower_signature, ) return response
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_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_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(): # 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 from_dict(cls, appointment_data): """ Builds an appointment from a dictionary. This method is useful to load data from a database. Args: appointment_data (:obj:`dict`): a dictionary containing the following keys: ``{locator, to_self_delay, encrypted_blob, user_id}`` Returns: :obj:`ExtendedAppointment <teos.extended_appointment.ExtendedAppointment>`: An appointment initialized using the provided data. Raises: ValueError: If one of the mandatory keys is missing in ``appointment_data``. """ appointment = Appointment.from_dict(appointment_data) user_id = appointment_data.get("user_id") if not user_id: raise ValueError("Wrong appointment data, user_id is missing") else: appointment = cls(appointment.locator, appointment.to_self_delay, appointment.encrypted_blob, user_id) return appointment
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_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_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_from_dict(appointment_data): # The appointment should be build if we don't miss any field appointment = Appointment.from_dict(appointment_data) assert isinstance(appointment, Appointment) # Otherwise it should fail for key in appointment_data.keys(): prev_val = appointment_data[key] appointment_data[key] = None try: Appointment.from_dict(appointment_data) assert False except ValueError: appointment_data[key] = prev_val assert True
def filter_valid_breaches(self, breaches): """ Filters what of the found breaches contain valid transaction data. The :obj:`Watcher` cannot if a given :obj:`EncryptedBlob <common.encrypted_blob.EncryptedBlob>` contains a valid transaction until a breach if seen. Blobs that contain arbitrary data are dropped and not sent to the :obj:`Responder <teos.responder.Responder>`. Args: breaches (:obj:`dict`): a dictionary containing channel breaches (``locator:txid``). Returns: :obj:`dict`: A dictionary containing all the breaches flagged either as valid or invalid. The structure is as follows: ``{locator, dispute_txid, penalty_txid, penalty_rawtx, valid_breach}`` """ valid_breaches = {} invalid_breaches = [] # A cache of the already decrypted blobs so replicate decryption can be avoided decrypted_blobs = {} for locator, dispute_txid in breaches.items(): for uuid in self.locator_uuid_map[locator]: appointment = Appointment.from_dict(self.db_manager.load_watcher_appointment(uuid)) if appointment.encrypted_blob.data in decrypted_blobs: penalty_tx, penalty_rawtx = decrypted_blobs[appointment.encrypted_blob.data] else: try: penalty_rawtx = Cryptographer.decrypt(appointment.encrypted_blob, dispute_txid) except ValueError: penalty_rawtx = None penalty_tx = self.block_processor.decode_raw_transaction(penalty_rawtx) decrypted_blobs[appointment.encrypted_blob.data] = (penalty_tx, penalty_rawtx) if penalty_tx is not None: valid_breaches[uuid] = { "locator": locator, "dispute_txid": dispute_txid, "penalty_txid": penalty_tx.get("txid"), "penalty_rawtx": penalty_rawtx, } logger.info( "Breach found for locator", locator=locator, uuid=uuid, penalty_txid=penalty_tx.get("txid") ) else: invalid_breaches.append(uuid) return valid_breaches, invalid_breaches
def inspect(self, appointment_data, signature, public_key): """ Inspects whether the data provided by the user is correct. Args: appointment_data (:obj:`dict`): a dictionary containing the appointment data. signature (:obj:`str`): the appointment signature provided by the user (hex encoded). public_key (:obj:`str`): the user's public key (hex encoded). Returns: :obj:`Appointment <teos.appointment.Appointment>` or :obj:`tuple`: An appointment initialized with the provided data if it is correct. Returns a tuple ``(return code, message)`` describing the error otherwise. Errors are defined in :mod:`Errors <teos.errors>`. """ block_height = self.block_processor.get_block_count() if block_height is not None: rcode, message = self.check_locator( appointment_data.get("locator")) if rcode == 0: rcode, message = self.check_start_time( appointment_data.get("start_time"), block_height) if rcode == 0: rcode, message = self.check_end_time( appointment_data.get("end_time"), appointment_data.get("start_time"), block_height) if rcode == 0: rcode, message = self.check_to_self_delay( appointment_data.get("to_self_delay")) if rcode == 0: rcode, message = self.check_blob( appointment_data.get("encrypted_blob")) if rcode == 0: rcode, message = self.check_appointment_signature( appointment_data, signature, public_key) if rcode == 0: r = Appointment.from_dict(appointment_data) else: r = (rcode, message) else: # In case of an unknown exception, assign a special rcode and reason. r = (errors.UNKNOWN_JSON_RPC_EXCEPTION, "Unexpected error occurred") return r
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 check_appointment_signature(appointment_data, signature, pk): """ Checks if the provided user signature is correct. Args: appointment_data (:obj:`dict`): the appointment that was signed by the user. signature (:obj:`str`): the user's signature (hex encoded). pk (:obj:`str`): the user's public key (hex encoded). Returns: :obj:`tuple`: A tuple (return code, message) as follows: - ``(0, None)`` if the ``signature`` is correct. - ``!= (0, None)`` otherwise. The possible return errors are: ``APPOINTMENT_EMPTY_FIELD``, ``APPOINTMENT_WRONG_FIELD_TYPE``, and ``APPOINTMENT_WRONG_FIELD_FORMAT``. """ message = None rcode = 0 if signature is None: rcode = errors.APPOINTMENT_EMPTY_FIELD message = "empty signature received" elif pk is None: rcode = errors.APPOINTMENT_EMPTY_FIELD message = "empty public key received" elif re.match(r"^[0-9A-Fa-f]{66}$", pk) is None: rcode = errors.APPOINTMENT_WRONG_FIELD message = "public key must be a hex encoded 33-byte long value" else: appointment = Appointment.from_dict(appointment_data) rpk = Cryptographer.recover_pk(appointment.serialize(), signature) pk = PublicKey(unhexlify(pk)) valid_sig = Cryptographer.verify_rpk(pk, rpk) if not valid_sig: rcode = errors.APPOINTMENT_INVALID_SIGNATURE message = "invalid signature" return rcode, message
def test_serialize(appointment_data): # From the tower end, appointments are only created if they pass the inspector tests, so not covering weird formats. # Serialize may fail if, from the user end, the user tries to do it with an weird appointment. Not critical. appointment = Appointment.from_dict(appointment_data) serialized_appointment = appointment.serialize() # Size must be 16 + len(encrypted_blob) + 4 assert len(serialized_appointment) >= 20 assert isinstance(serialized_appointment, bytes) locator = serialized_appointment[:16] encrypted_blob = serialized_appointment[16:-4] to_self_delay = serialized_appointment[-4:] assert locator.hex() == appointment.locator assert encrypted_blob.hex() == appointment.encrypted_blob assert int.from_bytes(to_self_delay, "big") == appointment.to_self_delay
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 test_serialize(appointment_data): # From the tower end, appointments are only created if they pass the inspector tests, so not covering weird formats. # Serialize may fail if, from the user end, the user tries to do it with an weird appointment. Not critical. appointment = Appointment.from_dict(appointment_data) serialized_appointment = appointment.serialize() # Size must be 16 + 4 + 4 + 4 + len(encrypted_blob) assert len(serialized_appointment) >= 28 assert isinstance(serialized_appointment, bytes) locator = serialized_appointment[:16] to_self_delay = serialized_appointment[16:20] encrypted_blob = serialized_appointment[20:] assert binascii.hexlify(locator).decode() == appointment.locator assert struct.unpack(">I", to_self_delay)[0] == appointment.to_self_delay assert binascii.hexlify( encrypted_blob).decode() == appointment.encrypted_blob
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_check_appointment_signature(): # The inspector receives the public key as hex client_sk, client_pk = generate_keypair() client_pk_hex = client_pk.format().hex() dummy_appointment_data, _ = generate_dummy_appointment_data( real_height=False) assert Inspector.check_appointment_signature( dummy_appointment_data["appointment"], dummy_appointment_data["signature"], dummy_appointment_data["public_key"]) fake_sk, _ = generate_keypair() # Create a bad signature to make sure inspector rejects it bad_signature = Cryptographer.sign( Appointment.from_dict( dummy_appointment_data["appointment"]).serialize(), fake_sk) assert (Inspector.check_appointment_signature( dummy_appointment_data["appointment"], bad_signature, client_pk_hex)[0] == APPOINTMENT_INVALID_SIGNATURE)
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 add_appointment(self): appointment = Appointment.from_dict( request.get_json().get("appointment")) signature = request.get_json().get("signature") user_id = Cryptographer.get_compressed_pk( Cryptographer.recover_pk(appointment.serialize(), signature)) if mocked_return == "success": response, rtype = add_appointment_success(appointment, signature, self.users[user_id], self.sk) elif mocked_return == "reject_no_slots": response, rtype = add_appointment_reject_no_slots() elif mocked_return == "reject_invalid": response, rtype = add_appointment_reject_invalid() elif mocked_return == "misbehaving_tower": response, rtype = add_appointment_misbehaving_tower( appointment, signature, self.users[user_id], self.sk) else: response, rtype = add_appointment_service_unavailable() return jsonify(response), rtype
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 generate_dummy_appointment(real_height=True, start_time_offset=5, end_time_offset=30): appointment_data, dispute_tx = generate_dummy_appointment_data( real_height=real_height, start_time_offset=start_time_offset, end_time_offset=end_time_offset ) return Appointment.from_dict(appointment_data["appointment"]), dispute_tx
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)
"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(): # generate a private/public key pair, and an empty file, and return their names