From fe73ee7298bf20b34c21d26bc680550e905f9bf1 Mon Sep 17 00:00:00 2001 From: Sergi Delgado Segura Date: Thu, 2 Apr 2020 15:20:04 +0200 Subject: [PATCH] Fixes comments, docstrings and some renamings --- common/appointment.py | 18 +++++------ common/config_loader.py | 17 +++++----- common/cryptographer.py | 59 ++++++++++++++++++---------------- common/logger.py | 11 ++++--- common/tools.py | 25 +++++++------- teos/api.py | 42 ++++++++++++------------ teos/appointments_dbm.py | 36 +++++++++++++-------- teos/carrier.py | 17 +++++----- teos/chain_monitor.py | 8 ++--- teos/cleaner.py | 12 ++++--- teos/db_manager.py | 2 +- teos/gatekeeper.py | 27 +++++++--------- teos/inspector.py | 31 ++++++++---------- teos/responder.py | 26 ++++++++------- teos/teosd.py | 7 ++-- teos/tools.py | 6 ++-- teos/users_dbm.py | 22 +++++++------ teos/watcher.py | 22 ++++++------- test/common/unit/test_tools.py | 28 ++++++++-------- test/teos/unit/test_tools.py | 30 ----------------- 20 files changed, 213 insertions(+), 233 deletions(-) diff --git a/common/appointment.py b/common/appointment.py index 2d5f7ad..7f0f5d4 100644 --- a/common/appointment.py +++ b/common/appointment.py @@ -9,18 +9,17 @@ class Appointment: The :class:`Appointment` contains the information regarding an appointment between a client and the Watchtower. Args: - locator (:mod:`str`): A 16-byte hex-encoded value used by the tower to detect channel breaches. It serves as a + locator (:obj:`str`): A 16-byte hex-encoded value used by the tower to detect channel breaches. It serves as a trigger for the tower to decrypt and broadcast the penalty transaction. - start_time (:mod:`int`): The block height where the tower is hired to start watching for breaches. - end_time (:mod:`int`): The block height where the tower will stop watching for breaches. - to_self_delay (:mod:`int`): The ``to_self_delay`` encoded in the ``csv`` of the ``htlc`` that this appointment - is covering. + start_time (:obj:`int`): The block height where the tower is hired to start watching for breaches. + end_time (:obj:`int`): The block height where the tower will stop watching for breaches. + to_self_delay (:obj:`int`): The ``to_self_delay`` encoded in the ``csv`` of the ``to_remote`` output of the + commitment transaction that this appointment is covering. encrypted_blob (:obj:`EncryptedBlob `): An ``EncryptedBlob`` object containing an encrypted penalty transaction. The tower will decrypt it and broadcast the penalty transaction upon seeing a breach on the blockchain. """ - # DISCUSS: 35-appointment-checks def __init__(self, locator, start_time, end_time, to_self_delay, encrypted_blob): self.locator = locator self.start_time = start_time # ToDo: #4-standardize-appointment-fields @@ -36,7 +35,7 @@ class Appointment: This method is useful to load data from a database. Args: - appointment_data (:mod:`dict`): a dictionary containing the following keys: + appointment_data (:obj:`dict`): a dictionary containing the following keys: ``{locator, start_time, end_time, to_self_delay, encrypted_blob}`` Returns: @@ -62,11 +61,10 @@ class Appointment: def to_dict(self): """ - Exports an appointment as a dictionary. + Encodes an appointment as a dictionary. Returns: :obj:`dict`: A dictionary containing the appointment attributes. - """ # ToDO: #3-improve-appointment-structure @@ -90,7 +88,7 @@ class Appointment: All values are big endian. Returns: - :mod:`bytes`: The serialized data to be signed. + :obj:`bytes`: The serialized data to be signed. """ return ( unhexlify(self.locator) diff --git a/common/config_loader.py b/common/config_loader.py index a7c1dd8..d0bafb0 100644 --- a/common/config_loader.py +++ b/common/config_loader.py @@ -21,14 +21,14 @@ class ConfigLoader: data_dir (:obj:`str`): the path to the data directory where the configuration file may be found. conf_file_path (:obj:`str`): the path to the config file (the file may not exist). conf_fields (:obj:`dict`): a dictionary populated with the configuration params and the expected types. - follows the same format as default_conf. + It follows the same format as default_conf. command_line_conf (:obj:`dict`): a dictionary containing the command line parameters that may replace the ones in default / config file. """ def __init__(self, data_dir, conf_file_name, default_conf, command_line_conf): self.data_dir = data_dir - self.conf_file_path = self.data_dir + conf_file_name + self.conf_file_path = os.path.join(self.data_dir, conf_file_name) self.conf_fields = default_conf self.command_line_conf = command_line_conf @@ -36,13 +36,13 @@ class ConfigLoader: """ Builds a config dictionary from command line, config file and default configuration parameters. - The priority if as follows: + The priority is as follows: - command line - config file - defaults Returns: - obj:`dict`: a dictionary containing all the configuration parameters. + :obj:`dict`: a dictionary containing all the configuration parameters. """ @@ -50,6 +50,7 @@ class ConfigLoader: file_config = configparser.ConfigParser() file_config.read(self.conf_file_path) + # Load parameters and cast them to int if necessary if file_config: for sec in file_config.sections(): for k, v in file_config.items(sec): @@ -82,10 +83,10 @@ class ConfigLoader: Returns: :obj:`dict`: A dictionary with the same keys as the provided one, but containing only the "value" field as - value if the provided ``conf_fields`` where correct. + value if the provided ``conf_fields`` are correct. Raises: - ValueError: If any of the dictionary elements does not have the expected type + :obj:`ValueError`: If any of the dictionary elements does not have the expected type. """ conf_dict = {} @@ -104,11 +105,11 @@ class ConfigLoader: def extend_paths(self): """ - Extends the relative paths of the ``conf_fields`` dictionary with ``data_dir``. + Extends the relative paths of the ``conf_fields`` dictionary with ``data_dir``. If an absolute path is given, it'll remain the same. """ for key, field in self.conf_fields.items(): - if field.get("path") is True and isinstance(field.get("value"), str): + if field.get("path") and isinstance(field.get("value"), str): self.conf_fields[key]["value"] = os.path.join(self.data_dir, self.conf_fields[key]["value"]) diff --git a/common/cryptographer.py b/common/cryptographer.py index e7d4fac..e4fa7f8 100644 --- a/common/cryptographer.py +++ b/common/cryptographer.py @@ -6,14 +6,14 @@ from coincurve import PrivateKey, PublicKey from cryptography.exceptions import InvalidTag from cryptography.hazmat.primitives.ciphers.aead import ChaCha20Poly1305 -from common.tools import check_sha256_hex_format +from common.tools import is_256b_hex_str LN_MESSAGE_PREFIX = b"Lightning Signed Message:" def sha256d(message): """ - Compute the sha245d (double sha256) of a given by message. + Compute the sha256 (double sha256) of a given by message. Args: message(:obj:`bytes`): the message to be used as input to the hash function. @@ -46,10 +46,9 @@ def hash_160(message): # NOTCOVERED def sigrec_encode(rsig_rid): """ - Encodes a pk-recoverable signature to be used in LN. ```rsig_rid`` can be obtained trough + Encodes a pk-recoverable signature to be used in LN. ``rsig_rid`` can be obtained trough ``PrivateKey.sign_recoverable``. The required format has the recovery id as the last byte, and for signing LN - messages we need it as the first. - From: https://twitter.com/rusty_twit/status/1182102005914800128 + messages we need it as the first. From: https://twitter.com/rusty_twit/status/1182102005914800128 Args: rsig_rid(:obj:`bytes`): the signature to be encoded. @@ -94,7 +93,7 @@ logger = None class Cryptographer: """ - The :class:`Cryptographer` is the class in charge of all the cryptography in the tower. + The :class:`Cryptographer` is in charge of all the cryptography in the tower. """ @staticmethod @@ -104,21 +103,21 @@ class Cryptographer: formatted. Args: - data(:mod:`str`): the data to be encrypted. - secret(:mod:`str`): the secret used to derive the encryption key. + data(:obj:`str`): the data to be encrypted. + secret(:obj:`str`): the secret used to derive the encryption key. Returns: :obj:`bool`: Whether or not the ``key`` and ``data`` are properly formatted. Raises: - ValueError: if either the ``key`` or ``data`` is not properly formatted. + :obj:`ValueError`: if either the ``key`` or ``data`` is not properly formatted. """ if len(data) % 2: error = "Incorrect (Odd-length) value" raise ValueError(error) - if not check_sha256_hex_format(secret): + if not is_256b_hex_str(secret): error = "Secret must be a 32-byte hex value (64 hex chars)" raise ValueError(error) @@ -127,16 +126,19 @@ class Cryptographer: @staticmethod def encrypt(blob, secret): """ - Encrypts a given :mod:`Blob ` data using ``CHACHA20POLY1305``. + Encrypts a given :obj:`Blob ` data using ``CHACHA20POLY1305``. ``SHA256(secret)`` is used as ``key``, and ``0 (12-byte)`` as ``iv``. Args: - blob (:mod:`Blob `): a ``Blob`` object containing a raw penalty transaction. - secret (:mod:`str`): a value to used to derive the encryption key. Should be the dispute txid. + blob (:obj:`Blob `): a ``Blob`` object containing a raw penalty transaction. + secret (:obj:`str`): a value to used to derive the encryption key. Should be the dispute txid. Returns: :obj:`str`: The encrypted data (hex encoded). + + Raises: + :obj:`ValueError`: if either the ``secret`` or ``blob`` is not properly formatted. """ Cryptographer.check_data_key_format(blob.data, secret) @@ -162,17 +164,20 @@ class Cryptographer: # ToDo: #20-test-tx-decrypting-edge-cases def decrypt(encrypted_blob, secret): """ - Decrypts a given :mod:`EncryptedBlob ` using ``CHACHA20POLY1305``. + Decrypts a given :obj:`EncryptedBlob ` using ``CHACHA20POLY1305``. ``SHA256(secret)`` is used as ``key``, and ``0 (12-byte)`` as ``iv``. Args: - encrypted_blob(:mod:`EncryptedBlob `): an ``EncryptedBlob`` + encrypted_blob(:obj:`EncryptedBlob `): an ``EncryptedBlob`` potentially containing a penalty transaction. - secret (:mod:`str`): a value to used to derive the decryption key. Should be the dispute txid. + secret (:obj:`str`): a value to used to derive the decryption key. Should be the dispute txid. Returns: :obj:`str`: The decrypted data (hex encoded). + + Raises: + :obj:`ValueError`: if either the ``secret`` or ``encrypted_blob`` is not properly formatted. """ Cryptographer.check_data_key_format(encrypted_blob.data, secret) @@ -234,17 +239,14 @@ class Cryptographer: @staticmethod def load_private_key_der(sk_der): """ - Creates a :mod:`PrivateKey` object from a given ``DER`` encoded private key. + Creates a :obj:`PrivateKey` from a given ``DER`` encoded private key. Args: - sk_der(:mod:`str`): a private key encoded in ``DER`` format. + sk_der(:obj:`str`): a private key encoded in ``DER`` format. Returns: - :mod:`PrivateKey`: A ``PrivateKey`` object. - - Raises: - ValueError: if the provided ``pk_der`` data cannot be deserialized (wrong size or format). - TypeError: if the provided ``pk_der`` data is not a string. + :obj:`PrivateKey` or :obj:`None`: A ``PrivateKey`` object. if the private key can be loaded. `None` + otherwise. """ try: sk = PrivateKey.from_der(sk_der) @@ -261,14 +263,14 @@ class Cryptographer: @staticmethod def sign(message, sk): """ - Signs a given data using a given secret key using ECDSA. + Signs a given data using a given secret key using ECDSA over secp256k1. Args: message(:obj:`bytes`): the data to be signed. sk(:obj:`PrivateKey`): the ECDSA secret key used to signed the data. Returns: - :obj:`str`: The zbase32 signature of the given message. + :obj:`str` or :obj:`None`: The zbase32 signature of the given message is it can be signed. `None` otherwise. """ if not isinstance(message, bytes): @@ -295,7 +297,7 @@ class Cryptographer: zb32_sig(:obj:`str`): the zbase32 signature of the message. Returns: - :obj:`PublicKey`: The recovered public key. + :obj:`PublicKey` or :obj:`None`: The recovered public key if it can be recovered. `None` otherwise. """ if not isinstance(message, bytes): @@ -345,13 +347,14 @@ class Cryptographer: @staticmethod def get_compressed_pk(pk): """ - Computes a compressed, hex encoded, public key given a ``PublicKey`` object. + Computes a compressed, hex-encoded, public key given a ``PublicKey``. Args: pk(:obj:`PublicKey`): a given public key. Returns: - :obj:`str`: A compressed, hex encoded, public key (33-byte long) + :obj:`str` or :obj:`None`: A compressed, hex-encoded, public key (33-byte long) if it can be compressed. + `None` oterwise. """ if not isinstance(pk, PublicKey): diff --git a/common/logger.py b/common/logger.py index 136b330..791a0ed 100644 --- a/common/logger.py +++ b/common/logger.py @@ -15,9 +15,10 @@ class _StructuredMessage: class Logger: """ - The :class:`Logger` is the class in charge of logging events into the log file. + The :class:`Logger` is in charge of logging events into the log file. Args: + log_name_prefix (:obj:`str`): the prefix of the logger where the data will be stored in (server, client, ...). actor (:obj:`str`): the system actor that is logging the event (e.g. ``Watcher``, ``Cryptographer``, ...). """ @@ -52,7 +53,7 @@ class Logger: Args: msg (:obj:`str`): the message to be logged. - kwargs: a ``key:value`` collection parameters to be added to the output. + kwargs (:obj:`dict`): a ``key:value`` collection parameters to be added to the output. """ self.f_logger.info(self._create_file_message(msg, **kwargs)) @@ -64,7 +65,7 @@ class Logger: Args: msg (:obj:`str`): the message to be logged. - kwargs: a ``key:value`` collection parameters to be added to the output. + kwargs (:obj:`dict`): a ``key:value`` collection parameters to be added to the output. """ self.f_logger.debug(self._create_file_message(msg, **kwargs)) @@ -76,7 +77,7 @@ class Logger: Args: msg (:obj:`str`): the message to be logged. - kwargs: a ``key:value`` collection parameters to be added to the output. + kwargs (:obj:`dict`): a ``key:value`` collection parameters to be added to the output. """ self.f_logger.error(self._create_file_message(msg, **kwargs)) @@ -88,7 +89,7 @@ class Logger: Args: msg (:obj:`str`): the message to be logged. - kwargs: a ``key:value`` collection parameters to be added to the output. + kwargs (:obj:`dict`): a ``key:value`` collection parameters to be added to the output. """ self.f_logger.warning(self._create_file_message(msg, **kwargs)) diff --git a/common/tools.py b/common/tools.py index ccd5973..7609a2c 100644 --- a/common/tools.py +++ b/common/tools.py @@ -4,21 +4,21 @@ from pathlib import Path from common.constants import LOCATOR_LEN_HEX -def check_compressed_pk_format(compressed_pk): +def is_compressed_pk(value): """ - Checks if a given value is a 33-byte hex encoded string. + Checks if a given value is a 33-byte hex-encoded string starting by 02 or 03. Args: - compressed_pk(:obj:`str`): the value to be checked. + value(:obj:`str`): the value to be checked. Returns: :obj:`bool`: Whether or not the value matches the format. """ - return isinstance(compressed_pk, str) and re.match(r"^0[2-3][0-9A-Fa-f]{64}$", compressed_pk) is not None + return isinstance(value, str) and re.match(r"^0[2-3][0-9A-Fa-f]{64}$", value) is not None -def check_sha256_hex_format(value): +def is_256b_hex_str(value): """ Checks if a given value is a 32-byte hex encoded string. @@ -31,7 +31,7 @@ def check_sha256_hex_format(value): return isinstance(value, str) and re.match(r"^[0-9A-Fa-f]{64}$", value) is not None -def check_locator_format(value): +def is_locator(value): """ Checks if a given value is a 16-byte hex encoded string. @@ -61,7 +61,7 @@ def setup_data_folder(data_folder): Create a data folder for either the client or the server side if the folder does not exists. Args: - data_folder (:obj:`str`): the path of the folder + data_folder (:obj:`str`): the path of the folder. """ Path(data_folder).mkdir(parents=True, exist_ok=True) @@ -69,9 +69,12 @@ def setup_data_folder(data_folder): def setup_logging(log_file_path, log_name_prefix): """ - Setups a couple of loggers (console and file) given a prefix and a file path. The log names are: + Setups a couple of loggers (console and file) given a prefix and a file path. - prefix | _file_log and prefix | _console_log + The log names are: + + prefix | _file_log + prefix | _console_log Args: log_file_path (:obj:`str`): the path of the file to output the file log. @@ -80,10 +83,10 @@ def setup_logging(log_file_path, log_name_prefix): if not isinstance(log_file_path, str): print(log_file_path) - raise ValueError("Wrong log file path.") + raise ValueError("Wrong log file path") if not isinstance(log_name_prefix, str): - raise ValueError("Wrong log file name.") + raise ValueError("Wrong log file name") # Create the file logger f_logger = logging.getLogger("{}_file_log".format(log_name_prefix)) diff --git a/teos/api.py b/teos/api.py index ec71206..0859c86 100644 --- a/teos/api.py +++ b/teos/api.py @@ -45,7 +45,7 @@ def get_remote_addr(): # NOTCOVERED: not sure how to monkey path this one. May be related to #77 def get_request_data_json(request): """ - Gets the content of a json POST request and makes sure ir decodes to a Python dictionary. + Gets the content of a json POST request and makes sure it decodes to a dictionary. Args: request (:obj:`Request`): the request sent by the user. @@ -54,7 +54,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 Python dictionary. + :obj:`TypeError`: if the request is not json encoded or it does not decodes to a dictionary. """ if request.is_json: @@ -69,13 +69,14 @@ def get_request_data_json(request): class API: """ - The :class:`API` is in charge of the interface between the user and the tower. It handles and server user requests. + The :class:`API` is in charge of the interface between the user and the tower. It handles and serves user requests. Args: inspector (:obj:`Inspector `): an ``Inspector`` instance to check the correctness of - the received data. + the received appointment data. watcher (:obj:`Watcher `): a ``Watcher`` instance to pass the requests to. - gatekeeper (:obj:`Watcher `): a `Gatekeeper` instance in charge to gatekeep the API. + gatekeeper (:obj:`Watcher `): a `Gatekeeper` instance in charge to control the user + access. """ def __init__(self, inspector, watcher, gatekeeper): @@ -104,12 +105,11 @@ class API: Users register by sending a public key to the proper endpoint. This is exploitable atm, but will be solved when payments are introduced. - Returns: - :obj:`tuple`: A tuple containing the response (``json``) and response code (``int``). For accepted requests, - the ``rcode`` is always 200 and the response contains a json with the public key and number of slots in the - subscription. For rejected requests, the ``rcode`` is a 404 and the value contains an application specific - error, and an error message. Error messages can be found at :mod:`Errors `. + :obj:`tuple`: A tuple containing the response (:obj:`str`) and response code (:obj:`int`). For accepted + requests, the ``rcode`` is always 200 and the response contains a json with the public key and number of + slots in the subscription. For rejected requests, the ``rcode`` is a 404 and the value contains an + application error, and an error message. Error messages can be found at :mod:`Errors `. """ remote_addr = get_remote_addr() @@ -150,12 +150,12 @@ class API: Main endpoint of the Watchtower. The client sends requests (appointments) to this endpoint to request a job to the Watchtower. Requests must be - json encoded and contain an ``appointment`` field and optionally a ``signature`` and ``public_key`` fields. + json encoded and contain an ``appointment`` and ``signature`` fields. Returns: - :obj:`tuple`: A tuple containing the response (``json``) and response code (``int``). For accepted - appointments, the ``rcode`` is always 200 and the response contains the receipt signature. For rejected - appointments, the ``rcode`` is a 404 and the value contains an application specific error, and an error + :obj:`tuple`: A tuple containing the response (:obj:`str`) and response code (:obj:`int`). For accepted + appointments, the ``rcode`` is always 200 and the response contains the receipt signature (json). For + rejected appointments, the ``rcode`` is a 404 and the value contains an application error, and an error message. Error messages can be found at :mod:`Errors `. """ @@ -185,16 +185,16 @@ class API: appointment_uuid = hash_160("{}{}".format(appointment.locator, user_pk)) appointment_summary = self.watcher.get_appointment_summary(appointment_uuid) - # For updates we only reserve the slot difference provided the new one is bigger. if appointment_summary: used_slots = ceil(appointment_summary.get("size") / ENCRYPTED_BLOB_MAX_SIZE_HEX) required_slots = ceil(len(appointment.encrypted_blob.data) / ENCRYPTED_BLOB_MAX_SIZE_HEX) slot_diff = required_slots - used_slots + # For updates we only reserve the slot difference provided the new one is bigger. required_slots = slot_diff if slot_diff > 0 else 0 - # For regular appointments 1 slot is reserved per ENCRYPTED_BLOB_MAX_SIZE_HEX block. else: + # For regular appointments 1 slot is reserved per ENCRYPTED_BLOB_MAX_SIZE_HEX block. slot_diff = 0 required_slots = ceil(len(appointment.encrypted_blob.data) / ENCRYPTED_BLOB_MAX_SIZE_HEX) @@ -245,7 +245,9 @@ class API: The information is requested by ``locator``. Returns: - :obj:`dict`: A json formatted dictionary containing information about the requested appointment. + :obj:`str`: A json formatted dictionary containing information about the requested appointment. + + Returns not found if the user does not have the requested appointment or the locator is invalid. A ``status`` flag is added to the data provided by either the :obj:`Watcher ` or the :obj:`Responder ` that signals the status of the appointment. @@ -312,10 +314,8 @@ class API: This endpoint should only be accessible by the administrator. Requests are only allowed from localhost. Returns: - :obj:`dict`: A json formatted dictionary containing all the appointments hold by the - :obj:`Watcher ` (``watcher_appointments``) and by the - :obj:`Responder ` (``responder_trackers``). - + :obj:`str`: A json formatted dictionary containing all the appointments hold by the ``Watcher`` + (``watcher_appointments``) and by the ``Responder>`` (``responder_trackers``). """ # ToDo: #15-add-system-monitor diff --git a/teos/appointments_dbm.py b/teos/appointments_dbm.py index ba1a9f8..ebd027a 100644 --- a/teos/appointments_dbm.py +++ b/teos/appointments_dbm.py @@ -19,7 +19,7 @@ TRIGGERED_APPOINTMENTS_PREFIX = "ta" class AppointmentsDBM(DBManager): """ - The :class:`AppointmentsDBM` is the class in charge of interacting with the appointments database (``LevelDB``). + The :class:`AppointmentsDBM` is in charge of interacting with the appointments database (``LevelDB``). Keys and values are stored as bytes in the database but processed as strings by the manager. The database is split in six prefixes: @@ -36,8 +36,8 @@ class AppointmentsDBM(DBManager): database will be create if the specified path does not contain one. Raises: - ValueError: If the provided ``db_path`` is not a string. - plyvel.Error: If the db is currently unavailable (being used by another process). + :obj:`ValueError`: If the provided ``db_path`` is not a string. + :obj:`plyvel.Error`: If the db is currently unavailable (being used by another process). """ def __init__(self, db_path): @@ -78,7 +78,11 @@ class AppointmentsDBM(DBManager): def get_last_known_block(self, key): """ - Loads the last known block given a key (either ``WATCHER_LAST_BLOCK_KEY`` or ``RESPONDER_LAST_BLOCK_KEY``). + Loads the last known block given a key. + + Args: + key (:obj:`str`): the identifier of the db to look into (either ``WATCHER_LAST_BLOCK_KEY`` or + ``RESPONDER_LAST_BLOCK_KEY``). Returns: :obj:`str` or :obj:`None`: A 16-byte hex-encoded str representing the last known block hash. @@ -93,9 +97,12 @@ class AppointmentsDBM(DBManager): return last_block - def load_watcher_appointment(self, key): + def load_watcher_appointment(self, uuid): """ - Loads an appointment from the database using ``WATCHER_PREFIX`` as prefix to the given ``key``. + Loads an appointment from the database using ``WATCHER_PREFIX`` as prefix to the given ``uuid``. + + Args: + uuid (:obj:`str`): the appointment's unique identifier. Returns: :obj:`dict`: A dictionary containing the appointment data if they ``key`` is found. @@ -104,16 +111,19 @@ class AppointmentsDBM(DBManager): """ try: - data = self.load_entry(key, prefix=WATCHER_PREFIX) + data = self.load_entry(uuid, prefix=WATCHER_PREFIX) data = json.loads(data) except (TypeError, json.decoder.JSONDecodeError): data = None return data - def load_responder_tracker(self, key): + def load_responder_tracker(self, uuid): """ - Loads a tracker from the database using ``RESPONDER_PREFIX`` as a prefix to the given ``key``. + Loads a tracker from the database using ``RESPONDER_PREFIX`` as a prefix to the given ``uuid``. + + Args: + uuid (:obj:`str`): the tracker's unique identifier. Returns: :obj:`dict`: A dictionary containing the tracker data if they ``key`` is found. @@ -122,7 +132,7 @@ class AppointmentsDBM(DBManager): """ try: - data = self.load_entry(key, prefix=RESPONDER_PREFIX) + data = self.load_entry(uuid, prefix=RESPONDER_PREFIX) data = json.loads(data) except (TypeError, json.decoder.JSONDecodeError): data = None @@ -134,7 +144,7 @@ class AppointmentsDBM(DBManager): Loads all the appointments from the database (all entries with the ``WATCHER_PREFIX`` prefix). Args: - include_triggered (:obj:`bool`): Whether to include the appointments flagged as triggered or not. ``False`` + include_triggered (:obj:`bool`): whether to include the appointments flagged as triggered or not. ``False`` by default. Returns: @@ -168,7 +178,7 @@ class AppointmentsDBM(DBManager): Args: uuid (:obj:`str`): the identifier of the appointment to be stored. - appointment (:obj: `dict`): an appointment encoded as dictionary. + appointment (:obj:`dict`): an appointment encoded as dictionary. Returns: :obj:`bool`: True if the appointment was stored in the db. False otherwise. @@ -193,7 +203,7 @@ class AppointmentsDBM(DBManager): Args: uuid (:obj:`str`): the identifier of the appointment to be stored. - tracker (:obj: `dict`): a tracker encoded as dictionary. + tracker (:obj:`dict`): a tracker encoded as dictionary. Returns: :obj:`bool`: True if the tracker was stored in the db. False otherwise. diff --git a/teos/carrier.py b/teos/carrier.py index 587afa4..c8bba1c 100644 --- a/teos/carrier.py +++ b/teos/carrier.py @@ -36,12 +36,12 @@ class Receipt: class Carrier: """ - The :class:`Carrier` is the class in charge of interacting with ``bitcoind`` to send/get transactions. It uses - :obj:`Receipt` objects to report about the sending outcome. + The :class:`Carrier` is in charge of interacting with ``bitcoind`` to send/get transactions. It uses :obj:`Receipt` + objects to report about the sending outcome. Args: btc_connect_params (:obj:`dict`): a dictionary with the parameters to connect to bitcoind - (rpc user, rpc passwd, host and port) + (rpc user, rpc password, host and port) Attributes: issued_receipts (:obj:`dict`): a dictionary of issued receipts to prevent resending the same transaction over @@ -135,18 +135,17 @@ class Carrier: Returns: :obj:`dict` or :obj:`None`: A dictionary with the transaction data if the transaction can be found on the - chain. - Returns ``None`` otherwise. + chain. ``None`` otherwise. """ try: tx_info = bitcoin_cli(self.btc_connect_params).getrawtransaction(txid, 1) + return tx_info except JSONRPCException as e: - tx_info = None # While it's quite unlikely, the transaction that was already in the blockchain could have been - # reorged while we were querying bitcoind to get the confirmation count. In such a case we just - # restart the tracker + # reorged while we were querying bitcoind to get the confirmation count. In that case we just restart + # the tracker if e.error.get("code") == rpc_errors.RPC_INVALID_ADDRESS_OR_KEY: logger.info("Transaction not found in mempool nor blockchain", txid=txid) @@ -154,4 +153,4 @@ class Carrier: # If something else happens (unlikely but possible) log it so we can treat it in future releases logger.error("JSONRPCException", method="Carrier.get_transaction", error=e.error) - return tx_info + return None diff --git a/teos/chain_monitor.py b/teos/chain_monitor.py index 186a31d..654fffa 100644 --- a/teos/chain_monitor.py +++ b/teos/chain_monitor.py @@ -10,8 +10,8 @@ logger = Logger(actor="ChainMonitor", log_name_prefix=LOG_PREFIX) class ChainMonitor: """ - The :class:`ChainMonitor` is the class in charge of monitoring the blockchain (via ``bitcoind``) to detect new - blocks on top of the best chain. If a new best block is spotted, the chain monitor will notify the + The :class:`ChainMonitor` is in charge of monitoring the blockchain (via ``bitcoind``) to detect new blocks on top + of the best chain. If a new best block is spotted, the chain monitor will notify the :obj:`Watcher ` and the :obj:`Responder ` using ``Queues``. The :class:`ChainMonitor` monitors the chain using two methods: ``zmq`` and ``polling``. Blocks are only notified @@ -34,7 +34,6 @@ class ChainMonitor: watcher_queue (:obj:`Queue`): a queue to send new best tips to the :obj:`Watcher `. responder_queue (:obj:`Queue`): a queue to send new best tips to the :obj:`Responder `. - polling_delta (:obj:`int`): time between polls (in seconds). max_block_window_size (:obj:`int`): max size of last_tips. block_processor (:obj:`BlockProcessor `): a blockProcessor instance. @@ -75,7 +74,6 @@ class ChainMonitor: Args: block_hash (:obj:`str`): the new block hash to be sent to the subscribers. - block_hash (:obj:`str`): the new block hash to be sent to the subscribers. """ self.watcher_queue.put(block_hash) @@ -90,7 +88,7 @@ class ChainMonitor: block_hash (:obj:`block_hash`): the new best tip. Returns: - (:obj:`bool`): ``True`` is the state was successfully updated, ``False`` otherwise. + :obj:`bool`: True is the state was successfully updated, False otherwise. """ if block_hash != self.best_tip and block_hash not in self.last_tips: diff --git a/teos/cleaner.py b/teos/cleaner.py index e0aefdc..a9cba0a 100644 --- a/teos/cleaner.py +++ b/teos/cleaner.py @@ -7,7 +7,7 @@ logger = Logger(actor="Cleaner", log_name_prefix=LOG_PREFIX) class Cleaner: """ - The :class:`Cleaner` is the class in charge of removing expired/completed data from the tower. + The :class:`Cleaner` is in charge of removing expired/completed data from the tower. Mutable objects (like dicts) are passed-by-reference in Python, so no return is needed for the Cleaner. """ @@ -15,15 +15,16 @@ class Cleaner: @staticmethod def delete_appointment_from_memory(uuid, appointments, locator_uuid_map): """ - Deletes an appointment from memory (appointments and locator_uuid_map dictionaries). If the given appointment - does not share locator with any other, the map will completely removed, otherwise, the uuid will be removed from - the map. + Deletes an appointment from memory (``appointments`` and ``locator_uuid_map`` dictionaries). If the given + appointment does not share locator with any other, the map will completely removed, otherwise, the uuid will be + removed from the map. Args: uuid (:obj:`str`): the identifier of the appointment to be deleted. appointments (:obj:`dict`): the appointments dictionary from where the appointment should be removed. locator_uuid_map (:obj:`dict`): the locator:uuid map from where the appointment should also be removed. """ + locator = appointments[uuid].get("locator") # Delete the appointment @@ -136,6 +137,7 @@ class Cleaner: db_manager (:obj:`AppointmentsDBM `): a ``AppointmentsDBM`` instance to interact with the database. """ + locator_maps_to_update = {} for uuid in completed_appointments: @@ -161,7 +163,7 @@ class Cleaner: @staticmethod def flag_triggered_appointments(triggered_appointments, appointments, locator_uuid_map, db_manager): """ - Deletes a list of triggered appointment from memory (:obj:`Watcher `) and flags them as + Deletes a list of triggered appointment from memory (:obj:`Watcher `) and flags them as triggered on disk. Args: diff --git a/teos/db_manager.py b/teos/db_manager.py index d911a4b..678147e 100644 --- a/teos/db_manager.py +++ b/teos/db_manager.py @@ -3,7 +3,7 @@ import plyvel class DBManager: """ - The :class:`DBManager` is the class in charge of interacting with a database (``LevelDB``). + The :class:`DBManager` is in charge of interacting with a database (``LevelDB``). Keys and values are stored as bytes in the database but processed as strings by the manager. Args: diff --git a/teos/gatekeeper.py b/teos/gatekeeper.py index 935afac..a4a65fb 100644 --- a/teos/gatekeeper.py +++ b/teos/gatekeeper.py @@ -1,9 +1,9 @@ -from common.tools import check_compressed_pk_format +from common.tools import is_compressed_pk from common.cryptographer import Cryptographer class NotEnoughSlots(ValueError): - """Raise this when trying to subtract more slots than a user has available.""" + """Raise this when trying to subtract more slots than a user has available""" def __init__(self, user_pk, requested_slots): self.user_pk = user_pk @@ -21,8 +21,8 @@ class IdentificationFailure(Exception): class Gatekeeper: """ - The Gatekeeper is in charge of managing the access to the tower. Only registered users are allowed to perform - actions. + The :class:`Gatekeeper` is in charge of managing the access to the tower. Only registered users are allowed to + perform actions. Attributes: registered_users (:obj:`dict`): a map of user_pk:appointment_slots. @@ -44,7 +44,7 @@ class Gatekeeper: :obj:`int`: the number of available slots in the user subscription. """ - if not check_compressed_pk_format(user_pk): + if not is_compressed_pk(user_pk): raise ValueError("provided public key does not match expected format (33-byte hex string)") if user_pk not in self.registered_users: @@ -58,17 +58,17 @@ class Gatekeeper: def identify_user(self, message, signature): """ - Checks if the provided user signature comes from a registered user. + Checks if a request comes from a registered user by ec-recovering their public key from a signed message. Args: message (:obj:`bytes`): byte representation of the original message from where the signature was generated. - signature (:obj:`str`): the user's signature (hex encoded). + signature (:obj:`str`): the user's signature (hex-encoded). Returns: :obj:`str`: a compressed key recovered from the signature and matching a registered user. Raises: - :obj:``: if the user cannot be identified. + :obj:`IdentificationFailure`: if the user cannot be identified. """ if isinstance(message, bytes) and isinstance(signature, str): @@ -89,18 +89,16 @@ class Gatekeeper: Args: user_pk(:obj:`str`): the public key that identifies the user (33-bytes hex str). - n: the number of slots to fill. n (:obj:`int`): the number of slots to fill. Raises: - :obj:``: if the user subscription does not have enough slots. + :obj:`NotEnoughSlots`: if the user subscription does not have enough slots. """ - # We are not making sure the value passed is a integer, but the value is computed by the API and rounded before - # passing it to the gatekeeper. # DISCUSS: we may want to return a different exception if the user does not exist if user_pk in self.registered_users and n <= self.registered_users.get(user_pk).get("available_slots"): self.registered_users[user_pk]["available_slots"] -= n + self.user_db.store_user(user_pk, self.registered_users[user_pk]) else: raise NotEnoughSlots(user_pk, n) @@ -110,11 +108,10 @@ class Gatekeeper: Args: user_pk(:obj:`str`): the public key that identifies the user (33-bytes hex str). - n: the number of slots to free. + n (:obj:`int`): the number of slots to free. """ - # We are not making sure the value passed is a integer, but the value is computed by the API and rounded before - # passing it to the gatekeeper. # DISCUSS: if the user does not exist we may want to log or return an exception. if user_pk in self.registered_users: self.registered_users[user_pk]["available_slots"] += n + self.user_db.store_user(user_pk, self.registered_users[user_pk]) diff --git a/teos/inspector.py b/teos/inspector.py index c5fa9af..235de12 100644 --- a/teos/inspector.py +++ b/teos/inspector.py @@ -2,6 +2,7 @@ import re import common.cryptographer from common.logger import Logger +from common.tools import is_locator from common.constants import LOCATOR_LEN_HEX from common.appointment import Appointment @@ -50,11 +51,10 @@ class Inspector: Returns: - :obj:`Appointment `: An appointment initialized with the - provided data. + :obj:`Appointment `: An appointment initialized with the provided data. Raises: - :obj:`InspectionFailed `: if any of the fields is wrong. + :obj:`InspectionFailed`: if any of the fields is wrong. """ if appointment_data is None: @@ -64,7 +64,7 @@ class Inspector: block_height = self.block_processor.get_block_count() if block_height is None: - raise InspectionFailed(errors.UNKNOWN_JSON_RPC_EXCEPTION, "Unexpected error occurred") + raise InspectionFailed(errors.UNKNOWN_JSON_RPC_EXCEPTION, "unexpected error occurred") self.check_locator(appointment_data.get("locator")) self.check_start_time(appointment_data.get("start_time"), block_height) @@ -79,13 +79,13 @@ class Inspector: """ Checks if the provided ``locator`` is correct. - Locators must be 16-byte hex encoded strings. + Locators must be 16-byte hex-encoded strings. Args: locator (:obj:`str`): the locator to be checked. Raises: - :obj:`InspectionFailed `: if any of the fields is wrong. + :obj:`InspectionFailed`: if any of the fields is wrong. """ if locator is None: @@ -99,7 +99,7 @@ class Inspector: elif len(locator) != LOCATOR_LEN_HEX: raise InspectionFailed(errors.APPOINTMENT_WRONG_FIELD_SIZE, "wrong locator size ({})".format(len(locator))) - elif re.search(r"^[0-9A-Fa-f]+$", locator) is None: + elif not is_locator(locator): raise InspectionFailed(errors.APPOINTMENT_WRONG_FIELD_FORMAT, "wrong locator format ({})".format(locator)) @staticmethod @@ -114,12 +114,9 @@ class Inspector: block_height (:obj:`int`): the chain height. Raises: - :obj:`InspectionFailed `: if any of the fields is wrong. + :obj:`InspectionFailed`: if any of the fields is wrong. """ - # TODO: What's too close to the current height is not properly defined. Right now any appointment that is in the - # future will be accepted (even if it's only one block away). - if start_time is None: raise InspectionFailed(errors.APPOINTMENT_EMPTY_FIELD, "empty start_time received") @@ -156,7 +153,7 @@ class Inspector: block_height (:obj:`int`): the chain height. Raises: - :obj:`InspectionFailed `: if any of the fields is wrong. + :obj:`InspectionFailed`: if any of the fields is wrong. """ # TODO: What's too close to the current height is not properly defined. Right now any appointment that ends in @@ -193,11 +190,11 @@ class Inspector: To self delays must be greater or equal to ``MIN_TO_SELF_DELAY``. Args: - to_self_delay (:obj:`int`): The ``to_self_delay`` encoded in the ``csv`` of the ``htlc`` that this - appointment is covering. + to_self_delay (:obj:`int`): The ``to_self_delay`` encoded in the ``csv`` of ``to_remote`` output of the + commitment transaction this appointment is covering. Raises: - :obj:`InspectionFailed `: if any of the fields is wrong. + :obj:`InspectionFailed`: if any of the fields is wrong. """ if to_self_delay is None: @@ -229,10 +226,10 @@ class Inspector: Checks if the provided ``encrypted_blob`` may be correct. Args: - encrypted_blob (:obj:`str`): the encrypted blob to be checked (hex encoded). + encrypted_blob (:obj:`str`): the encrypted blob to be checked (hex-encoded). Raises: - :obj:`InspectionFailed `: if any of the fields is wrong. + :obj:`InspectionFailed`: if any of the fields is wrong. """ if encrypted_blob is None: diff --git a/teos/responder.py b/teos/responder.py index 7f6dce9..a5d6843 100644 --- a/teos/responder.py +++ b/teos/responder.py @@ -13,7 +13,7 @@ logger = Logger(actor="Responder", log_name_prefix=LOG_PREFIX) class TransactionTracker: """ - A :class:`TransactionTracker` is used to monitor a ``penalty_tx``. Once the dispute is seen by the + A :class:`TransactionTracker` is used to monitor a ``penalty_tx``. Once the dispute is seen by the :obj:`Watcher ` the penalty transaction is decrypted and the relevant appointment data is passed along to the :obj:`Responder`. @@ -53,7 +53,7 @@ class TransactionTracker: :obj:`TransactionTracker`: A ``TransactionTracker`` instantiated with the provided data. Raises: - ValueError: if any of the required fields is missing. + :obj:`ValueError`: if any of the required fields is missing. """ locator = tx_tracker_data.get("locator") @@ -72,7 +72,7 @@ class TransactionTracker: def to_dict(self): """ - Exports a :obj:`TransactionTracker` as a dictionary. + Encodes a :obj:`TransactionTracker` as a dictionary. Returns: :obj:`dict`: A dictionary containing the :obj:`TransactionTracker` data. @@ -91,13 +91,16 @@ class TransactionTracker: class Responder: """ - The :class:`Responder` is the class in charge of ensuring that channel breaches are dealt with. It does so handling + The :class:`Responder` is in charge of ensuring that channel breaches are dealt with. It does so handling the decrypted ``penalty_txs`` handed by the :obj:`Watcher ` and ensuring the they make it to the blockchain. Args: db_manager (:obj:`AppointmentsDBM `): a ``AppointmentsDBM`` instance to interact with the database. + carrier (:obj:`Carrier `): a ``Carrier`` instance to send transactions to bitcoind. + block_processor (:obj:`BlockProcessor `): a ``BlockProcessor`` instance to + get data from bitcoind. Attributes: trackers (:obj:`dict`): A dictionary containing the minimum information about the :obj:`TransactionTracker` @@ -116,7 +119,6 @@ class Responder: block_processor (:obj:`BlockProcessor `): a ``BlockProcessor`` instance to get data from bitcoind. last_known_block (:obj:`str`): the last block known by the ``Responder``. - """ def __init__(self, db_manager, carrier, block_processor): @@ -131,6 +133,7 @@ class Responder: self.last_known_block = db_manager.load_last_block_hash_responder() def awake(self): + """Starts a new thread to monitor the blockchain to make sure triggered appointments get enough depth""" responder_thread = Thread(target=self.do_watch, daemon=True) responder_thread.start() @@ -140,7 +143,7 @@ class Responder: """ Whether the :obj:`Responder` is on sync with ``bitcoind`` or not. Used when recovering from a crash. - The Watchtower can be instantiated with fresh or with backed up data. In the later, some triggers may have been + The Watchtower can be instantiated with fresh or with backed up data. In the later, some triggers may have been missed. In order to go back on sync both the :obj:`Watcher ` and the :obj:`Responder` need to perform the state transitions until they catch up. @@ -205,9 +208,8 @@ class Responder: """ Creates a :obj:`TransactionTracker` after successfully broadcasting a ``penalty_tx``. - A reduction of :obj:`TransactionTracker` is stored in ``trackers`` and ``tx_tracker_map`` and the - ``penalty_txid`` added to ``unconfirmed_txs`` if ``confirmations=0``. Finally, all the data is stored in the - database. + A summary of :obj:`TransactionTracker` is stored in ``trackers`` and ``tx_tracker_map`` and the ``penalty_txid`` + added to ``unconfirmed_txs`` if ``confirmations=0``. Finally, all the data is stored in the database. Args: uuid (:obj:`str`): a unique identifier for the appointment. @@ -248,7 +250,7 @@ class Responder: def do_watch(self): """ - Monitors the blockchain whilst there are pending trackers. + Monitors the blockchain for reorgs and appointment ends. This is the main method of the :obj:`Responder` and triggers tracker cleaning, rebroadcasting, reorg managing, etc. @@ -384,9 +386,9 @@ 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 si the transaction keeps not getting it. + forever if the transaction keeps not getting it. - Potentially the fees could be bumped here if the transaction has some tower dedicated outputs (or allows it + Potentially, the fees could be bumped here if the transaction has some tower dedicated outputs (or allows it trough ``ANYONECANPAY`` or something similar). Args: diff --git a/teos/teosd.py b/teos/teosd.py index 694b664..3287581 100644 --- a/teos/teosd.py +++ b/teos/teosd.py @@ -45,9 +45,10 @@ def main(command_line_conf): signal(SIGQUIT, handle_signals) # Loads config and sets up the data folder and log file - config_loader = ConfigLoader(DATA_DIR, CONF_FILE_NAME, DEFAULT_CONF, command_line_conf) + data_dir = command_line_conf.get("DATA_DIR") if "DATA_DIR" in command_line_conf else DATA_DIR + config_loader = ConfigLoader(data_dir, CONF_FILE_NAME, DEFAULT_CONF, command_line_conf) config = config_loader.build_config() - setup_data_folder(DATA_DIR) + setup_data_folder(data_dir) setup_logging(config.get("LOG_FILE"), LOG_PREFIX) logger.info("Starting TEOS") @@ -183,7 +184,7 @@ if __name__ == "__main__": except ValueError: exit("btcrpcport must be an integer") if opt in ["--datadir"]: - DATA_DIR = os.path.expanduser(arg) + command_line_conf["DATA_DIR"] = os.path.expanduser(arg) if opt in ["-h", "--help"]: exit(show_usage()) diff --git a/teos/tools.py b/teos/tools.py index dded387..269a41d 100644 --- a/teos/tools.py +++ b/teos/tools.py @@ -15,7 +15,7 @@ def bitcoin_cli(btc_connect_params): Args: btc_connect_params (:obj:`dict`): a dictionary with the parameters to connect to bitcoind - (rpc user, rpc passwd, host and port) + (rpc user, rpc password, host and port) Returns: :obj:`AuthServiceProxy `: An authenticated service proxy to ``bitcoind`` @@ -40,7 +40,7 @@ def can_connect_to_bitcoind(btc_connect_params): Args: btc_connect_params (:obj:`dict`): a dictionary with the parameters to connect to bitcoind - (rpc user, rpc passwd, host and port) + (rpc user, rpc password, host and port) Returns: :obj:`bool`: ``True`` if the connection can be established. ``False`` otherwise. """ @@ -62,7 +62,7 @@ def in_correct_network(btc_connect_params, network): Args: btc_connect_params (:obj:`dict`): a dictionary with the parameters to connect to bitcoind - (rpc user, rpc passwd, host and port) + (rpc user, rpc password, host and port) network (:obj:`str`): the network the tower is connected to. Returns: diff --git a/teos/users_dbm.py b/teos/users_dbm.py index d9809be..b9f3b21 100644 --- a/teos/users_dbm.py +++ b/teos/users_dbm.py @@ -5,14 +5,14 @@ from teos import LOG_PREFIX from teos.db_manager import DBManager from common.logger import Logger -from common.tools import check_compressed_pk_format +from common.tools import is_compressed_pk logger = Logger(actor="UsersDBM", log_name_prefix=LOG_PREFIX) class UsersDBM(DBManager): """ - The :class:`UsersDBM` is the class in charge of interacting with the users database (``LevelDB``). + The :class:`UsersDBM` is in charge of interacting with the users database (``LevelDB``). Keys and values are stored as bytes in the database but processed as strings by the manager. Args: @@ -20,8 +20,8 @@ class UsersDBM(DBManager): database will be create if the specified path does not contain one. Raises: - ValueError: If the provided ``db_path`` is not a string. - plyvel.Error: If the db is currently unavailable (being used by another process). + :obj:`ValueError`: If the provided ``db_path`` is not a string. + :obj:`plyvel.Error`: If the db is currently unavailable (being used by another process). """ def __init__(self, db_path): @@ -46,31 +46,33 @@ class UsersDBM(DBManager): user_data (:obj:`dict`): the user associated data, as a dictionary. Returns: - :obj:`bool`: True if the user was stored in the database, false otherwise. + :obj:`bool`: True if the user was stored in the database, False otherwise. """ - if check_compressed_pk_format(user_pk): + if is_compressed_pk(user_pk): try: self.create_entry(user_pk, json.dumps(user_data)) logger.info("Adding user to Gatekeeper's db", user_pk=user_pk) return True except json.JSONDecodeError: - logger.info("Could't add user to db. Wrong user data format.", user_pk=user_pk, user_data=user_data) + logger.info("Could't add user to db. Wrong user data format", user_pk=user_pk, user_data=user_data) return False except TypeError: - logger.info("Could't add user to db.", user_pk=user_pk, user_data=user_data) + logger.info("Could't add user to db", user_pk=user_pk, user_data=user_data) return False else: - logger.info("Could't add user to db. Wrong pk format.", user_pk=user_pk, user_data=user_data) + logger.info("Could't add user to db. Wrong pk format", user_pk=user_pk, user_data=user_data) return False def load_user(self, user_pk): """ Loads a user record from the database using the ``user_pk`` as identifier. - use_pk (:obj:`str`): a 33-byte hex-encoded string identifying the user. + Args: + + user_pk (:obj:`str`): a 33-byte hex-encoded string identifying the user. Returns: :obj:`dict`: A dictionary containing the appointment data if they ``key`` is found. diff --git a/teos/watcher.py b/teos/watcher.py index 79c5cfe..bb02ff7 100644 --- a/teos/watcher.py +++ b/teos/watcher.py @@ -16,8 +16,7 @@ common.cryptographer.logger = Logger(actor="Cryptographer", log_name_prefix=LOG_ class Watcher: """ - The :class:`Watcher` is the class in charge to watch for channel breaches for the appointments accepted by the - tower. + The :class:`Watcher` is in charge of watching for channel breaches for the appointments accepted by the tower. The :class:`Watcher` keeps track of the accepted appointments in ``appointments`` and, for new received block, checks if any breach has happened by comparing the txids with the appointment locators. If a breach is seen, the @@ -36,7 +35,7 @@ class Watcher: get block from bitcoind. responder (:obj:`Responder `): a ``Responder`` instance. sk_der (:obj:`bytes`): a DER encoded private key used to sign appointment receipts (signaling acceptance). - max_appointments (:obj:`int`): the maximum ammount of appointments accepted by the ``Watcher`` at the same time. + max_appointments (:obj:`int`): the maximum amount of appointments accepted by the ``Watcher`` at the same time. expiry_delta (:obj:`int`): the additional time the ``Watcher`` will keep an expired appointment around. Attributes: @@ -53,7 +52,7 @@ class Watcher: get block from bitcoind. responder (:obj:`Responder `): a ``Responder`` instance. signing_key (:mod:`PrivateKey`): a private key used to sign accepted appointments. - max_appointments (:obj:`int`): the maximum ammount of appointments accepted by the ``Watcher`` at the same time. + max_appointments (:obj:`int`): the maximum amount of appointments accepted by the ``Watcher`` at the same time. expiry_delta (:obj:`int`): the additional time the ``Watcher`` will keep an expired appointment around. Raises: @@ -73,9 +72,7 @@ class Watcher: self.signing_key = Cryptographer.load_private_key_der(sk_der) def awake(self): - """ - Starts a new thread to monitor the blockchain for channel breaches. - """ + """Starts a new thread to monitor the blockchain for channel breaches""" watcher_thread = Thread(target=self.do_watch, daemon=True) watcher_thread.start() @@ -85,13 +82,13 @@ class Watcher: def get_appointment_summary(self, uuid): """ Returns the summary of an appointment. The summary consists of the data kept in memory: - locator, end_time, and size. + {locator, end_time, and size} Args: uuid (:obj:`str`): a 16-byte hex string identifying the appointment. Returns: - :obj:`dict` or :obj:`None`: a dictionary with the appointment summary, or None if the appointment is not + :obj:`dict` or :obj:`None`: a dictionary with the appointment summary, or ``None`` if the appointment is not found. """ return self.appointments.get(uuid) @@ -100,8 +97,8 @@ class Watcher: """ Adds a new appointment to the ``appointments`` dictionary if ``max_appointments`` has not been reached. - ``add_appointment`` is the entry point of the Watcher. Upon receiving a new appointment it will start monitoring - the blockchain (``do_watch``) until ``appointments`` is empty. + ``add_appointment`` is the entry point of the ``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 :obj:`EncryptedBlob ` and pass the information to the @@ -123,7 +120,6 @@ class Watcher: - ``(True, signature)`` if the appointment has been accepted. - ``(False, None)`` otherwise. - """ if len(self.appointments) < self.max_appointments: @@ -164,7 +160,7 @@ class Watcher: def do_watch(self): """ - Monitors the blockchain whilst there are pending appointments. + Monitors the blockchain for channel breaches. This is the main method of the :obj:`Watcher` and the one in charge to pass appointments to the :obj:`Responder ` upon detecting a breach. diff --git a/test/common/unit/test_tools.py b/test/common/unit/test_tools.py index 3e862b1..4276444 100644 --- a/test/common/unit/test_tools.py +++ b/test/common/unit/test_tools.py @@ -3,9 +3,9 @@ import logging from common.constants import LOCATOR_LEN_BYTES from common.tools import ( - check_compressed_pk_format, - check_sha256_hex_format, - check_locator_format, + is_compressed_pk, + is_256b_hex_str, + is_locator, compute_locator, setup_data_folder, setup_logging, @@ -13,7 +13,7 @@ from common.tools import ( from test.common.unit.conftest import get_random_value_hex -def test_check_compressed_pk_format(): +def test_is_compressed_pk(): wrong_values = [ None, 3, @@ -34,21 +34,21 @@ def test_check_compressed_pk_format(): prefix = "02" else: prefix = "03" - assert check_compressed_pk_format(prefix + get_random_value_hex(32)) + assert is_compressed_pk(prefix + get_random_value_hex(32)) # check_user_pk must only accept values that is not a 33-byte hex string for value in wrong_values: - assert not check_compressed_pk_format(value) + assert not is_compressed_pk(value) -def test_check_sha256_hex_format(): +def test_is_256b_hex_str(): # Only 32-byte hex encoded strings should pass the test wrong_inputs = [None, str(), 213, 46.67, dict(), "A" * 63, "C" * 65, bytes(), get_random_value_hex(31)] for wtype in wrong_inputs: - assert check_sha256_hex_format(wtype) is False + assert is_256b_hex_str(wtype) is False for v in range(100): - assert check_sha256_hex_format(get_random_value_hex(32)) is True + assert is_256b_hex_str(get_random_value_hex(32)) is True def test_check_locator_format(): @@ -66,20 +66,20 @@ def test_check_locator_format(): get_random_value_hex(LOCATOR_LEN_BYTES - 1), ] for wtype in wrong_inputs: - assert check_locator_format(wtype) is False + assert is_locator(wtype) is False for _ in range(100): - assert check_locator_format(get_random_value_hex(LOCATOR_LEN_BYTES)) is True + assert is_locator(get_random_value_hex(LOCATOR_LEN_BYTES)) is True def test_compute_locator(): - # The best way of checking that compute locator is correct is by using check_locator_format + # The best way of checking that compute locator is correct is by using is_locator for _ in range(100): - assert check_locator_format(compute_locator(get_random_value_hex(LOCATOR_LEN_BYTES))) is True + assert is_locator(compute_locator(get_random_value_hex(LOCATOR_LEN_BYTES))) is True # String of length smaller than LOCATOR_LEN_BYTES bytes must fail for i in range(1, LOCATOR_LEN_BYTES): - assert check_locator_format(compute_locator(get_random_value_hex(i))) is False + assert is_locator(compute_locator(get_random_value_hex(i))) is False def test_setup_data_folder(): diff --git a/test/teos/unit/test_tools.py b/test/teos/unit/test_tools.py index 45bceab..9a68a19 100644 --- a/test/teos/unit/test_tools.py +++ b/test/teos/unit/test_tools.py @@ -1,5 +1,4 @@ from teos.tools import can_connect_to_bitcoind, in_correct_network, bitcoin_cli -from common.tools import check_sha256_hex_format from test.teos.unit.conftest import bitcoind_connect_params @@ -27,32 +26,3 @@ def test_bitcoin_cli(): except Exception: assert False - - -def test_check_sha256_hex_format(): - assert check_sha256_hex_format(None) is False - assert check_sha256_hex_format("") is False - assert ( - check_sha256_hex_format(0x0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF) is False - ) # wrong type - assert ( - check_sha256_hex_format("abcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcd") is True - ) # lowercase - assert ( - check_sha256_hex_format("ABCDEFABCDEFABCDEFABCDEFABCDEFABCDEFABCDEFABCDEFABCDEFABCDEFABCD") is True - ) # uppercase - assert ( - check_sha256_hex_format("0123456789abcdef0123456789ABCDEF0123456789abcdef0123456789ABCDEF") is True - ) # mixed case - assert ( - check_sha256_hex_format("0123456789012345678901234567890123456789012345678901234567890123") is True - ) # only nums - assert ( - check_sha256_hex_format("0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdf") is False - ) # too short - assert ( - check_sha256_hex_format("0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0") is False - ) # too long - assert ( - check_sha256_hex_format("g123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef") is False - ) # non-hex