def test_from_dict(ext_appointment_data): # The appointment should be build if we don't miss any field ext_appointment = ExtendedAppointment.from_dict(ext_appointment_data) assert isinstance(ext_appointment, ExtendedAppointment) # Otherwise it should fail for key in ext_appointment_data.keys(): prev_val = ext_appointment_data[key] ext_appointment_data[key] = None with pytest.raises(ValueError, match="Wrong appointment data"): ExtendedAppointment.from_dict(ext_appointment_data) ext_appointment_data[key] = prev_val
def build_appointments(appointments_data): """ Builds an appointments dictionary (``uuid:ExtendedAppointment``) and a locator_uuid_map (``locator:uuid``) given a dictionary of appointments from the database. Args: appointments_data (:obj:`dict`): a dictionary of dictionaries representing all the :obj:`Watcher <teos.watcher.Watcher>` appointments stored in the database. The structure is as follows: ``{uuid: {locator: str, ...}, uuid: {locator:...}}`` Returns: :obj:`tuple`: A tuple with two dictionaries. ``appointments`` containing the appointment information in :obj:`ExtendedAppointment <teos.extended_appointment.ExtendedAppointment>` objects and ``locator_uuid_map`` containing a map of appointment (``uuid:locator``). """ appointments = {} locator_uuid_map = {} for uuid, data in appointments_data.items(): appointment = ExtendedAppointment.from_dict(data) appointments[uuid] = appointment.get_summary() if appointment.locator in locator_uuid_map: locator_uuid_map[appointment.locator].append(uuid) else: locator_uuid_map[appointment.locator] = [uuid] return appointments, locator_uuid_map
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 filter_breaches(self, breaches): """ Filters the valid from the invalid channel breaches. The :obj:`Watcher` cannot know if an ``encrypted_blob`` contains a valid transaction until a breach is 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:`tuple`: A dictionary and a list. The former contains the valid breaches, while the latter contain the invalid ones. The valid breaches dictionary has the following structure: ``{locator, dispute_txid, penalty_txid, penalty_rawtx}`` """ 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 = ExtendedAppointment.from_dict( self.db_manager.load_watcher_appointment(uuid)) if appointment.encrypted_blob in decrypted_blobs: penalty_txid, penalty_rawtx = decrypted_blobs[ appointment.encrypted_blob] valid_breaches[uuid] = { "locator": appointment.locator, "dispute_txid": dispute_txid, "penalty_txid": penalty_txid, "penalty_rawtx": penalty_rawtx, } else: try: penalty_txid, penalty_rawtx = self.check_breach( uuid, appointment, dispute_txid) valid_breaches[uuid] = { "locator": appointment.locator, "dispute_txid": dispute_txid, "penalty_txid": penalty_txid, "penalty_rawtx": penalty_rawtx, } decrypted_blobs[appointment.encrypted_blob] = ( penalty_txid, penalty_rawtx) except (EncryptionError, InvalidTransactionFormat): invalid_breaches.append(uuid) return valid_breaches, invalid_breaches
def _generate_dummy_appointment(): appointment_data = { "locator": get_random_value_hex(16), "to_self_delay": 20, "encrypted_blob": get_random_value_hex(150), "user_id": get_random_value_hex(16), "user_signature": get_random_value_hex(50), "start_block": 200, } return ExtendedAppointment.from_dict(appointment_data)
def test_init_appointment(appointment_data): # The appointment has no checks whatsoever, since the inspector is the one taking care or that, and the only one # creating appointments. appointment = ExtendedAppointment( appointment_data["locator"], appointment_data["to_self_delay"], appointment_data["encrypted_blob"], appointment_data["user_id"], ) assert (appointment_data["locator"] == appointment.locator and appointment_data["to_self_delay"] == appointment.to_self_delay and appointment_data["encrypted_blob"] == appointment.encrypted_blob and appointment_data["user_id"] == appointment.user_id)
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 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 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=10) # 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) watcher.locator_cache.cache[appointment.locator] = dispute_tx.tx_id.hex() # Try to add the appointment response = watcher.add_appointment(appointment, Cryptographer.sign(appointment.serialize(), user_sk)) # 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.serialize(), 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 inspect(self, appointment_data): """ Inspects whether the data provided by the user is correct. Args: appointment_data (:obj:`dict`): a dictionary containing the appointment data. Returns: :obj:`Extended <teos.extended_appointment.ExtendedAppointment>`: An appointment initialized with the provided data. Raises: :obj:`InspectionFailed`: if any of the fields is wrong. """ if appointment_data is None: raise InspectionFailed(errors.APPOINTMENT_EMPTY_FIELD, "empty appointment received") elif not isinstance(appointment_data, dict): raise InspectionFailed(errors.APPOINTMENT_WRONG_FIELD, "wrong appointment format") block_height = self.block_processor.get_block_count() if block_height is None: raise InspectionFailed(errors.UNKNOWN_JSON_RPC_EXCEPTION, "unexpected error occurred") self.check_locator(appointment_data.get("locator")) self.check_to_self_delay(appointment_data.get("to_self_delay")) self.check_blob(appointment_data.get("encrypted_blob")) # Set user_id to None since we still don't know it, it'll be set by the API after querying the gatekeeper return ExtendedAppointment( appointment_data.get("locator"), appointment_data.get("to_self_delay"), appointment_data.get("encrypted_blob"), user_id=None, )
def add_appointment(self, appointment, user_signature): """ Adds a new appointment to the ``appointments`` dictionary if ``max_appointments`` has not been reached. ``add_appointment`` is the entry point of the :obj:`Watcher`. Upon receiving a new appointment it will start monitoring the blockchain (``do_watch``) until ``appointments`` is empty. Once a breach is seen on the blockchain, the :obj:`Watcher` will decrypt the corresponding ``encrypted_blob`` and pass the information to the :obj:`Responder <teos.responder.Responder>`. The tower may store multiple appointments with the same ``locator`` to avoid DoS attacks based on data rewriting. `locators`` should be derived from the ``dispute_txid``, but that task is performed by the user, and the tower has no way of verifying whether or not they have been properly derived. Therefore, appointments are identified by ``uuid`` and stored in ``appointments`` and ``locator_uuid_map``. Args: appointment (:obj:`Appointment <common.appointment.Appointment>`): the appointment to be added to the :obj:`Watcher`. user_signature (:obj:`str`): the user's appointment signature (hex-encoded). Returns: :obj:`dict`: The tower response as a dict, containing: ``locator``, ``signature``, ``available_slots`` and ``subscription_expiry``. Raises: :obj:`AppointmentLimitReached`: If the tower cannot hold more appointments (cap reached). :obj:`AuthenticationFailure`: If the user cannot be authenticated. :obj:`NotEnoughSlots`: If the user does not have enough available slots, so the appointment is rejected. :obj:`SubscriptionExpired`: If the user subscription has expired. """ with self.rw_lock.gen_wlock(): if len(self.appointments) >= self.max_appointments: message = "Maximum appointments reached, appointment rejected" self.logger.info(message, locator=appointment.locator) raise AppointmentLimitReached(message) user_id = self.gatekeeper.authenticate_user( appointment.serialize(), user_signature) has_subscription_expired, expiry = self.gatekeeper.has_subscription_expired( user_id) if has_subscription_expired: raise SubscriptionExpired( f"Your subscription expired at block {expiry}") start_block = self.block_processor.get_block( self.last_known_block).get("height") extended_appointment = ExtendedAppointment( appointment.locator, appointment.encrypted_blob, appointment.to_self_delay, user_id, user_signature, start_block, ) # The uuids are generated as the RIPEMD160(locator||user_pubkey). # If an appointment is requested by the user the uuid can be recomputed and queried straightaway (no maps). uuid = hash_160("{}{}".format(extended_appointment.locator, user_id)) # If this is a copy of an appointment we've already reacted to, the new appointment is rejected. if self.responder.has_tracker(uuid): message = "Appointment already in Responder" self.logger.info(message) raise AppointmentAlreadyTriggered(message) # Add the appointment to the Gatekeeper available_slots = self.gatekeeper.add_update_appointment( user_id, uuid, extended_appointment) # Appointments that were triggered in blocks held in the cache dispute_txid = self.locator_cache.get_txid( extended_appointment.locator) if dispute_txid: try: penalty_txid, penalty_rawtx = self.check_breach( uuid, extended_appointment, dispute_txid) receipt = self.responder.handle_breach( uuid, extended_appointment.locator, dispute_txid, penalty_txid, penalty_rawtx, user_id, self.last_known_block, ) # At this point the appointment is accepted but data is only kept if it goes through the Responder. # Otherwise it is dropped. if receipt.delivered: self.db_manager.store_watcher_appointment( uuid, extended_appointment.to_dict()) self.db_manager.create_append_locator_map( extended_appointment.locator, uuid) self.db_manager.create_triggered_appointment_flag(uuid) except (EncryptionError, InvalidTransactionFormat): # If data inside the encrypted blob is invalid, the appointment is accepted but the data is dropped. # (same as with data that bounces in the Responder). This reduces the appointment slot count so it # could be used to discourage user misbehaviour. pass # Regular appointments that have not been triggered (or, at least, not recently) else: self.appointments[uuid] = extended_appointment.get_summary() if extended_appointment.locator in self.locator_uuid_map: # If the uuid is already in the map it means this is an update. if uuid not in self.locator_uuid_map[ extended_appointment.locator]: self.locator_uuid_map[ extended_appointment.locator].append(uuid) else: # Otherwise two users have sent an appointment with the same locator, so we need to store both. self.locator_uuid_map[extended_appointment.locator] = [ uuid ] self.db_manager.store_watcher_appointment( uuid, extended_appointment.to_dict()) self.db_manager.create_append_locator_map( extended_appointment.locator, uuid) try: signature = Cryptographer.sign( receipts.create_appointment_receipt( user_signature, start_block), self.signing_key) except (InvalidParameter, SignatureError): # This should never happen since data is sanitized, just in case to avoid a crash self.logger.error("Data couldn't be signed", appointment=extended_appointment.to_dict()) signature = None self.logger.info("New appointment accepted", locator=extended_appointment.locator) return { "locator": extended_appointment.locator, "start_block": extended_appointment.start_block, "signature": signature, "available_slots": available_slots, "subscription_expiry": self.gatekeeper.registered_users[user_id].subscription_expiry, }
def test_get_summary(ext_appointment_data): assert ExtendedAppointment.from_dict( ext_appointment_data).get_summary() == { "locator": ext_appointment_data["locator"], "user_id": ext_appointment_data["user_id"], }