mirror of
https://github.com/aljazceru/python-teos.git
synced 2025-12-17 14:14:22 +01:00
Fixes comments, docstrings and some renamings
This commit is contained in:
@@ -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 <common.encrypted_blob.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)
|
||||
|
||||
@@ -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"])
|
||||
|
||||
@@ -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 <common.cli.blob.Blob>` data using ``CHACHA20POLY1305``.
|
||||
Encrypts a given :obj:`Blob <common.cli.blob.Blob>` data using ``CHACHA20POLY1305``.
|
||||
|
||||
``SHA256(secret)`` is used as ``key``, and ``0 (12-byte)`` as ``iv``.
|
||||
|
||||
Args:
|
||||
blob (:mod:`Blob <common.cli.blob.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 <common.cli.blob.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 <common.encrypted_blob.EncryptedBlob>` using ``CHACHA20POLY1305``.
|
||||
Decrypts a given :obj:`EncryptedBlob <common.encrypted_blob.EncryptedBlob>` using ``CHACHA20POLY1305``.
|
||||
|
||||
``SHA256(secret)`` is used as ``key``, and ``0 (12-byte)`` as ``iv``.
|
||||
|
||||
Args:
|
||||
encrypted_blob(:mod:`EncryptedBlob <common.encrypted_blob.EncryptedBlob>`): an ``EncryptedBlob``
|
||||
encrypted_blob(:obj:`EncryptedBlob <common.encrypted_blob.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):
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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))
|
||||
|
||||
42
teos/api.py
42
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 <teos.inspector.Inspector>`): an ``Inspector`` instance to check the correctness of
|
||||
the received data.
|
||||
the received appointment data.
|
||||
watcher (:obj:`Watcher <teos.watcher.Watcher>`): a ``Watcher`` instance to pass the requests to.
|
||||
gatekeeper (:obj:`Watcher <teos.gatekeeper.Gatekeeper>`): a `Gatekeeper` instance in charge to gatekeep the API.
|
||||
gatekeeper (:obj:`Watcher <teos.gatekeeper.Gatekeeper>`): 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 <teos.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 <teos.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 <teos.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 <teos.watcher.Watcher>` or the
|
||||
:obj:`Responder <teos.responder.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 <teos.watcher.Watcher>` (``watcher_appointments``) and by the
|
||||
:obj:`Responder <teos.responder.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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 <teos.watcher.Watcher>` and the :obj:`Responder <teos.responder.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 <teos.watcher.Watcher>`.
|
||||
responder_queue (:obj:`Queue`): a queue to send new best tips to the
|
||||
:obj:`Responder <teos.responder.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 <teos.block_processor.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:
|
||||
|
||||
@@ -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 <teos.appointments_dbm.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 <teos.watcher.Watcher>`) and flags them as
|
||||
Deletes a list of triggered appointment from memory (:obj:`Watcher <teos.watcher.Watcher>`) and flags them as
|
||||
triggered on disk.
|
||||
|
||||
Args:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:`<teos.gatekeeper.IdentificationFailure>`: 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:`<teos.gatekeeper.NotEnoughSlots>`: 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])
|
||||
|
||||
@@ -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 <teos.appointment.Appointment>`: An appointment initialized with the
|
||||
provided data.
|
||||
:obj:`Appointment <teos.appointment.Appointment>`: An appointment initialized with the provided data.
|
||||
|
||||
Raises:
|
||||
:obj:`InspectionFailed <teos.inspector.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 <teos.inspector.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 <teos.inspector.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 <teos.inspector.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 <teos.inspector.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 <teos.inspector.InspectionFailed>`: if any of the fields is wrong.
|
||||
:obj:`InspectionFailed`: if any of the fields is wrong.
|
||||
"""
|
||||
|
||||
if encrypted_blob is None:
|
||||
|
||||
@@ -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 <teos.watcher.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 <teos.watcher.Watcher>` and ensuring the they make it to
|
||||
the blockchain.
|
||||
|
||||
Args:
|
||||
db_manager (:obj:`AppointmentsDBM <teos.appointments_dbm.AppointmentsDBM>`): a ``AppointmentsDBM`` instance
|
||||
to interact with the database.
|
||||
carrier (:obj:`Carrier <teos.carrier.Carrier>`): a ``Carrier`` instance to send transactions to bitcoind.
|
||||
block_processor (:obj:`BlockProcessor <teos.block_processor.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 <teos.block_processor.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 <teos.watcher.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:
|
||||
|
||||
@@ -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())
|
||||
|
||||
|
||||
@@ -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 <teos.utils.auth_proxy.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:
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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 <teos.responder.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 <teos.responder.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 <common.encrypted_blob.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 <teos.responder.Responder>` upon detecting a breach.
|
||||
|
||||
@@ -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():
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user