diff --git a/teos/api.py b/teos/api.py index e08e2e7..d78b3ad 100644 --- a/teos/api.py +++ b/teos/api.py @@ -10,6 +10,7 @@ from teos.gatekeeper import NotEnoughSlots, AuthenticationFailure from common.logger import Logger from common.cryptographer import hash_160 +from common.exceptions import InvalidParameter from common.constants import HTTP_OK, HTTP_BAD_REQUEST, HTTP_SERVICE_UNAVAILABLE, HTTP_NOT_FOUND @@ -48,7 +49,7 @@ def get_request_data_json(request): :obj:`dict`: the dictionary parsed from the json request. Raises: - :obj:`TypeError`: if the request is not json encoded or it does not decodes to a dictionary. + :obj:`InvalidParameter`: if the request is not json encoded or it does not decodes to a dictionary. """ if request.is_json: @@ -56,9 +57,9 @@ def get_request_data_json(request): if isinstance(request_data, dict): return request_data else: - raise TypeError("Invalid request content") + raise InvalidParameter("Invalid request content") else: - raise TypeError("Request is not json encoded") + raise InvalidParameter("Request is not json encoded") class API: @@ -112,7 +113,7 @@ class API: try: request_data = get_request_data_json(request) - except TypeError as e: + except InvalidParameter as e: logger.info("Received invalid register request", from_addr="{}".format(remote_addr)) return abort(HTTP_BAD_REQUEST, e) @@ -128,7 +129,7 @@ class API: "subscription_expiry": subscription_expiry, } - except ValueError as e: + except InvalidParameter as e: rcode = HTTP_BAD_REQUEST error = "Error {}: {}".format(errors.REGISTRATION_MISSING_FIELD, str(e)) response = {"error": error} @@ -164,7 +165,7 @@ class API: try: request_data = get_request_data_json(request) - except TypeError as e: + except InvalidParameter as e: return abort(HTTP_BAD_REQUEST, e) try: @@ -218,7 +219,7 @@ class API: try: request_data = get_request_data_json(request) - except TypeError as e: + except InvalidParameter as e: logger.info("Received invalid get_appointment request", from_addr="{}".format(remote_addr)) return abort(HTTP_BAD_REQUEST, e) diff --git a/teos/gatekeeper.py b/teos/gatekeeper.py index 52811ff..b67a17f 100644 --- a/teos/gatekeeper.py +++ b/teos/gatekeeper.py @@ -53,7 +53,15 @@ class Gatekeeper: perform actions. Attributes: + default_slots (:obj:`int`): the number of slots assigned to a user subscription. + default_subscription_duration (:obj:`int`): the expiry assigned to a user subscription. + expiry_delta (:obj:`int`): the grace period given to the user to renew their subscription. + block_processor (:obj:`BlockProcessor `): a ``BlockProcessor`` instance to + get block from bitcoind. + user_db (:obj:`UserDBM `): a ``UserDBM`` instance to interact with the database. registered_users (:obj:`dict`): a map of user_pk:UserInfo. + lock (:obj:`Lock`): a Threading.Lock object to lock access to the Gatekeeper on updates. + """ def __init__(self, user_db, block_processor, default_slots, default_subscription_duration, expiry_delta): @@ -75,12 +83,15 @@ class Gatekeeper: user_pk(:obj:`str`): the public key that identifies the user (33-bytes hex str). Returns: - :obj:`tuple`: a tuple with the number of available slots in the user subscription and the subscription end - time (in absolute block height). + :obj:`tuple`: a tuple with the number of available slots in the user subscription and the subscription + expiry (in absolute block height). + + Raises: + :obj:`InvalidParameter`: if the user_pk does not match the expected format. """ if not is_compressed_pk(user_pk): - raise ValueError("Provided public key does not match expected format (33-byte hex string)") + raise InvalidParameter("Provided public key does not match expected format (33-byte hex string)") if user_pk not in self.registered_users: self.registered_users[user_pk] = UserInfo( @@ -125,11 +136,33 @@ class Gatekeeper: raise AuthenticationFailure("Wrong message or signature.") def update_available_slots(self, user_id, new_appointment, old_appointment=None): + """ + Updates (add/removes) slots from a user subscription. + + Slots are removed if a new subscription is given, or an update is given with a new subscription bigger than the + old one. + + Slots are added if an update is given but the new appointment is smaller than the old one. + + Args: + user_id(:obj:`str`): the public key that identifies the user (33-bytes hex str). + new_appointment (:obj:`ExtendedAppointment `): the new + appointment the user is requesting to add. + old_appointment (:obj:`ExtendedAppointment `): the old + appointment the user wants to replace. Optional. + + Returns: + :obj:`int`: the number of remaining appointment slots. + + Raises: + :obj:`NotEnoughSlots`: If the user does not have enough slots to fill. + """ + self.lock.acquire() if old_appointment: # For updates the difference between the existing appointment and the update is computed. - used_slots = ceil(new_appointment.get("size") / ENCRYPTED_BLOB_MAX_SIZE_HEX) - required_slots = ceil(old_appointment.get("size") / ENCRYPTED_BLOB_MAX_SIZE_HEX) - used_slots + used_slots = ceil(old_appointment.get("size") / ENCRYPTED_BLOB_MAX_SIZE_HEX) + required_slots = ceil(new_appointment.get("size") / ENCRYPTED_BLOB_MAX_SIZE_HEX) - used_slots else: # For regular appointments 1 slot is reserved per ENCRYPTED_BLOB_MAX_SIZE_HEX block. required_slots = ceil(new_appointment.get("size") / ENCRYPTED_BLOB_MAX_SIZE_HEX) @@ -145,8 +178,19 @@ class Gatekeeper: self.lock.release() return self.registered_users.get(user_id).available_slots - def get_expiring_appointments(self, block_height): - expiring_appointments = [] + def get_expired_appointments(self, block_height): + """ + Gets a list of appointments that are expiring at a given block height. + + Args: + block_height: the block height that wants to be checked. + + Returns: + :obj:`list`: a list of appointment uuids that will expire at ``block_height``. + """ + expired_appointments = [] for user_id, user_info in self.registered_users.items(): - if block_height > user_info.subscription_expiry + self.expiry_delta: - expiring_appointments.extend(user_info.appointments) + if block_height == user_info.subscription_expiry + self.expiry_delta: + expired_appointments.extend(user_info.appointments) + + return expired_appointments diff --git a/teos/responder.py b/teos/responder.py index 16d19c8..4e29cf5 100644 --- a/teos/responder.py +++ b/teos/responder.py @@ -116,6 +116,8 @@ class Responder: is populated by the :obj:`ChainMonitor `. db_manager (:obj:`AppointmentsDBM `): a ``AppointmentsDBM`` instance to interact with the database. + gatekeeper (:obj:`Gatekeeper `): a `Gatekeeper` instance in charge to control the + user access and subscription expiry. carrier (:obj:`Carrier `): a ``Carrier`` instance to send transactions to bitcoind. block_processor (:obj:`BlockProcessor `): a ``BlockProcessor`` instance to get data from bitcoind. @@ -394,7 +396,7 @@ class Responder: """ expired_trackers = [ - uuid for uuid in self.gatekeeper.get_expired_appointment(height) if uuid in self.unconfirmed_txs + uuid for uuid in self.gatekeeper.get_expired_appointments(height) if uuid in self.unconfirmed_txs ] return expired_trackers @@ -402,7 +404,7 @@ class Responder: def rebroadcast(self, txs_to_rebroadcast): """ Rebroadcasts a ``penalty_tx`` that has missed too many confirmations. In the current approach this would loop - forever if the transaction keeps not getting it. + until the tracker expires if the penalty transactions keeps getting rejected due to fees. Potentially, the fees could be bumped here if the transaction has some tower dedicated outputs (or allows it trough ``ANYONECANPAY`` or something similar). diff --git a/teos/watcher.py b/teos/watcher.py index a060bd4..729c652 100644 --- a/teos/watcher.py +++ b/teos/watcher.py @@ -52,6 +52,8 @@ class Watcher: populated by the :obj:`ChainMonitor `. db_manager (:obj:`AppointmentsDBM `): a ``AppointmentsDBM`` instance to interact with the database. + gatekeeper (:obj:`Gatekeeper `): a `Gatekeeper` instance in charge to control the + user access and subscription expiry. block_processor (:obj:`BlockProcessor `): a ``BlockProcessor`` instance to get block from bitcoind. responder (:obj:`Responder `): a ``Responder`` instance. @@ -129,7 +131,7 @@ class Watcher: appointment_dict = {"locator": appointment.locator, "user_id": user_id, "size": len(appointment.encrypted_blob)} available_slots = self.gatekeeper.update_available_slots(user_id, appointment_dict, self.appointments.get(uuid)) - self.gatekeeper.registered_users.appointments.append(uuid) + self.gatekeeper.registered_users[user_id].appointments.append(uuid) self.appointments[uuid] = appointment_dict if appointment.locator in self.locator_uuid_map: @@ -180,7 +182,9 @@ class Watcher: if len(self.appointments) > 0 and block is not None: txids = block.get("tx") - expired_appointments = self.gatekeeper.get_expired_appointment(block["height"]) + expired_appointments = self.gatekeeper.get_expired_appointments(block["height"]) + # Make sure we only try to delete what is on the Watcher (some appointments may have been triggered) + expired_appointments = list(set(expired_appointments).intersection(self.appointments.keys())) Cleaner.delete_expired_appointments( expired_appointments, self.appointments, self.locator_uuid_map, self.db_manager