Merge branch 'master' into chainmonitor

This commit is contained in:
Sergi Delgado Segura
2020-01-15 11:38:32 +01:00
17 changed files with 606 additions and 194 deletions

View File

@@ -17,21 +17,22 @@ logger = Logger("API")
class API:
def __init__(self, watcher):
def __init__(self, watcher, config):
self.watcher = watcher
self.config = config
def add_appointment(self):
"""
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.
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.
Returns:
:obj:`tuple`: A tuple containing the response (``json``) and response code (``int``). For accepted appointments,
the ``rcode`` is always 0 and the response contains the signed receipt. For rejected appointments, the ``rcode``
is a negative value and the response contains the error message. Error messages can be found at
:mod:`Errors <pisa.errors>`.
:obj:`tuple`: A tuple containing the response (``json``) and response code (``int``). For accepted
appointments, the ``rcode`` is always 0 and the response contains the signed receipt. For rejected
appointments, the ``rcode`` is a negative value and the response contains the error message. Error messages
can be found at :mod:`Errors <pisa.errors>`.
"""
remote_addr = request.environ.get("REMOTE_ADDR")
@@ -41,7 +42,7 @@ class API:
# Check content type once if properly defined
request_data = json.loads(request.get_json())
inspector = Inspector()
inspector = Inspector(self.config)
appointment = inspector.inspect(
request_data.get("appointment"), request_data.get("signature"), request_data.get("public_key")
)
@@ -166,8 +167,8 @@ class API:
"""
Provides the block height of the Watchtower.
This is a testing endpoint that (most likely) will be removed in production. Its purpose is to give information to
testers about the current block so they can define a dummy appointment without having to run a bitcoin node.
This is a testing endpoint that (most likely) will be removed in production. Its purpose is to give information
to testers about the current block so they can define a dummy appointment without having to run a bitcoin node.
Returns:
:obj:`dict`: A json encoded dictionary containing the block height.

View File

@@ -5,7 +5,6 @@ from common.constants import LOCATOR_LEN_HEX
from common.cryptographer import Cryptographer
from pisa import errors
import pisa.conf as conf
from common.logger import Logger
from common.appointment import Appointment
from pisa.block_processor import BlockProcessor
@@ -23,6 +22,9 @@ class Inspector:
The :class:`Inspector` class is in charge of verifying that the appointment data provided by the user is correct.
"""
def __init__(self, config):
self.config = config
def inspect(self, appointment_data, signature, public_key):
"""
Inspects whether the data provided by the user is correct.
@@ -221,8 +223,7 @@ class Inspector:
return rcode, message
@staticmethod
def check_to_self_delay(to_self_delay):
def check_to_self_delay(self, to_self_delay):
"""
Checks if the provided ``to_self_delay`` is correct.
@@ -255,10 +256,10 @@ class Inspector:
rcode = errors.APPOINTMENT_WRONG_FIELD_TYPE
message = "wrong to_self_delay data type ({})".format(t)
elif to_self_delay < conf.MIN_TO_SELF_DELAY:
elif to_self_delay < self.config.get("MIN_TO_SELF_DELAY"):
rcode = errors.APPOINTMENT_FIELD_TOO_SMALL
message = "to_self_delay too small. The to_self_delay should be at least {} (current: {})".format(
conf.MIN_TO_SELF_DELAY, to_self_delay
self.config.get("MIN_TO_SELF_DELAY"), to_self_delay
)
if message is not None:

View File

@@ -2,12 +2,11 @@ from getopt import getopt
from sys import argv, exit
from signal import signal, SIGINT, SIGQUIT, SIGTERM
from pisa.conf import DB_PATH
from common.logger import Logger
from pisa.api import API
from pisa.watcher import Watcher
from pisa.builder import Builder
from pisa.conf import BTC_NETWORK, PISA_SECRET_KEY
import pisa.conf as conf
from pisa.db_manager import DBManager
from pisa.chain_monitor import ChainMonitor
from pisa.block_processor import BlockProcessor
@@ -25,6 +24,53 @@ def handle_signals(signal_received, frame):
exit(0)
def load_config(config):
"""
Looks through all of the config options to make sure they contain the right type of data and builds a config
dictionary.
Args:
config (:obj:`module`): It takes in a config module object.
Returns:
:obj:`dict` A dictionary containing the config values.
"""
conf_dict = {}
conf_fields = {
"BTC_RPC_USER": {"value": config.BTC_RPC_USER, "type": str},
"BTC_RPC_PASSWD": {"value": config.BTC_RPC_PASSWD, "type": str},
"BTC_RPC_HOST": {"value": config.BTC_RPC_HOST, "type": str},
"BTC_RPC_PORT": {"value": config.BTC_RPC_PORT, "type": int},
"BTC_NETWORK": {"value": config.BTC_NETWORK, "type": str},
"FEED_PROTOCOL": {"value": config.FEED_PROTOCOL, "type": str},
"FEED_ADDR": {"value": config.FEED_ADDR, "type": str},
"FEED_PORT": {"value": config.FEED_PORT, "type": int},
"MAX_APPOINTMENTS": {"value": config.MAX_APPOINTMENTS, "type": int},
"EXPIRY_DELTA": {"value": config.EXPIRY_DELTA, "type": int},
"MIN_TO_SELF_DELAY": {"value": config.MIN_TO_SELF_DELAY, "type": int},
"SERVER_LOG_FILE": {"value": config.SERVER_LOG_FILE, "type": str},
"PISA_SECRET_KEY": {"value": config.PISA_SECRET_KEY, "type": str},
"CLIENT_LOG_FILE": {"value": config.CLIENT_LOG_FILE, "type": str},
"TEST_LOG_FILE": {"value": config.TEST_LOG_FILE, "type": str},
"DB_PATH": {"value": config.DB_PATH, "type": str},
}
for field in conf_fields:
value = conf_fields[field]["value"]
correct_type = conf_fields[field]["type"]
if (value is not None) and isinstance(value, correct_type):
conf_dict[field] = value
else:
err_msg = "{} variable in config is of the wrong type".format(field)
logger.error(err_msg)
raise ValueError(err_msg)
return conf_dict
if __name__ == "__main__":
logger.info("Starting PISA")
@@ -37,15 +83,17 @@ if __name__ == "__main__":
# FIXME: Leaving this here for future option/arguments
pass
pisa_config = load_config(conf)
if not can_connect_to_bitcoind():
logger.error("Can't connect to bitcoind. Shutting down")
elif not in_correct_network(BTC_NETWORK):
elif not in_correct_network(pisa_config.get("BTC_NETWORK")):
logger.error("bitcoind is running on a different network, check conf.py and bitcoin.conf. Shutting down")
else:
try:
db_manager = DBManager(DB_PATH)
db_manager = DBManager(pisa_config.get("DB_PATH"))
# Create the chain monitor and start monitoring the chain
chain_monitor = ChainMonitor()
@@ -54,10 +102,10 @@ if __name__ == "__main__":
watcher_appointments_data = db_manager.load_watcher_appointments()
responder_trackers_data = db_manager.load_responder_trackers()
with open(PISA_SECRET_KEY, "rb") as key_file:
with open(pisa_config.get("PISA_SECRET_KEY"), "rb") as key_file:
secret_key_der = key_file.read()
watcher = Watcher(db_manager, chain_monitor, secret_key_der)
watcher = Watcher(db_manager, chain_monitor, secret_key_der, pisa_config)
chain_monitor.attach_watcher(watcher.block_queue, watcher.asleep)
chain_monitor.attach_responder(watcher.responder.block_queue, watcher.responder.asleep)
@@ -103,7 +151,7 @@ if __name__ == "__main__":
watcher.block_queue = Builder.build_block_queue(missed_blocks_watcher)
# Fire the API
API(watcher).start()
API(watcher, config=pisa_config).start()
except Exception as e:
logger.error("An error occurred: {}. Shutting down".format(e))

View File

@@ -467,9 +467,6 @@ class Responder:
else:
# If the penalty transaction is missing, we need to reset the tracker.
# DISCUSS: Adding tracker back, should we flag it as retried?
# FIXME: Whether we decide to increase the retried counter or not, the current counter should be
# maintained. There is no way of doing so with the current approach. Update if required
self.handle_breach(
tracker.locator,
uuid,

View File

@@ -0,0 +1,36 @@
import zmq
import binascii
from common.logger import Logger
from pisa.conf import FEED_PROTOCOL, FEED_ADDR, FEED_PORT
# ToDo: #7-add-async-back-to-zmq
class ZMQSubscriber:
""" Adapted from https://github.com/bitcoin/bitcoin/blob/master/contrib/zmq/zmq_sub.py"""
def __init__(self, config, parent):
self.zmqContext = zmq.Context()
self.zmqSubSocket = self.zmqContext.socket(zmq.SUB)
self.zmqSubSocket.setsockopt(zmq.RCVHWM, 0)
self.zmqSubSocket.setsockopt_string(zmq.SUBSCRIBE, "hashblock")
self.zmqSubSocket.connect(
"%s://%s:%s" % (config.get("FEED_PROTOCOL"), config.get("FEED_ADDR"), config.get("FEED_PORT"))
)
self.logger = Logger("ZMQSubscriber-{}".format(parent))
self.terminate = False
def handle(self, block_queue):
while not self.terminate:
msg = self.zmqSubSocket.recv_multipart()
# Terminate could have been set wile the thread was blocked in recv
if not self.terminate:
topic = msg[0]
body = msg[1]
if topic == b"hashblock":
block_hash = binascii.hexlify(body).decode("utf-8")
block_queue.put(block_hash)
self.logger.info("New block received via ZMQ", block_hash=block_hash)

View File

@@ -3,13 +3,12 @@ from queue import Queue
from threading import Thread
from common.cryptographer import Cryptographer
from common.constants import LOCATOR_LEN_HEX
from common.tools import compute_locator
from common.logger import Logger
from pisa.cleaner import Cleaner
from pisa.responder import Responder
from pisa.block_processor import BlockProcessor
from pisa.conf import EXPIRY_DELTA, MAX_APPOINTMENTS
logger = Logger("Watcher")
@@ -31,11 +30,13 @@ class Watcher:
Args:
db_manager (:obj:`DBManager <pisa.db_manager>`): a ``DBManager`` instance to interact with the database.
chain_monitor (:obj:`ChainMonitor <pisa.chain_monitor.ChainMonitor>`): a ``ChainMonitor`` instance used to track
new blocks received by ``bitcoind``.
sk_der (:obj:`bytes`): a DER encoded private key used to sign appointment receipts (signaling acceptance).
config (:obj:`dict`): a dictionary containing all the configuration parameters. Used locally to retrieve
``MAX_APPOINTMENTS`` and ``EXPIRY_DELTA``.
responder (:obj:`Responder <pisa.responder.Responder>`): a ``Responder`` instance. If ``None`` is passed, a new
instance is created. Populated instances are useful when bootstrapping the system from backed-up data.
max_appointments(:obj:`int`): the maximum amount of appointments that the :obj:`Watcher` will keep at any given
time. Defaults to ``MAX_APPOINTMENTS``.
Attributes:
@@ -46,44 +47,31 @@ class Watcher:
asleep (:obj:`bool`): A flag that signals whether the :obj:`Watcher` is asleep or awake.
block_queue (:obj:`Queue`): A queue used by the :obj:`Watcher` to receive block hashes from ``bitcoind``. It is
populated by the :obj:`ChainMonitor <pisa.chain_monitor.ChainMonitor>`.
max_appointments(:obj:`int`): the maximum amount of appointments that the :obj:`Watcher` will keep at any given
time.
chain_monitor (:obj:`ChainMonitor <pisa.chain_monitor.ChainMonitor>`): a ``ChainMonitor`` instance used to track
new blocks received by ``bitcoind``.
config (:obj:`dict`): a dictionary containing all the configuration parameters. Used locally to retrieve
``MAX_APPOINTMENTS`` and ``EXPIRY_DELTA``.
db_manager (:obj:`DBManager <pisa.db_manager>`): A db manager instance to interact with the database.
signing_key (:mod:`EllipticCurvePrivateKey`): a private key used to sign accepted appointments.
Raises:
ValueError: if `pisa_sk_file` is not found.
"""
def __init__(self, db_manager, chain_monitor, sk_der, responder=None, max_appointments=MAX_APPOINTMENTS):
def __init__(self, db_manager, chain_monitor, sk_der, config, responder=None):
self.appointments = dict()
self.locator_uuid_map = dict()
self.asleep = True
self.block_queue = Queue()
self.max_appointments = max_appointments
self.chain_monitor = chain_monitor
self.config = config
self.db_manager = db_manager
self.signing_key = Cryptographer.load_private_key_der(sk_der)
if not isinstance(responder, Responder):
self.responder = Responder(db_manager, chain_monitor)
@staticmethod
def compute_locator(tx_id):
"""
Computes an appointment locator given a transaction id.
Args:
tx_id (:obj:`str`): the transaction id used to compute the locator.
Returns:
(:obj:`str`): The computed locator.
"""
return tx_id[:LOCATOR_LEN_HEX]
def add_appointment(self, appointment):
"""
Adds a new appointment to the ``appointments`` dictionary if ``max_appointments`` has not been reached.
@@ -114,7 +102,7 @@ class Watcher:
"""
if len(self.appointments) < self.max_appointments:
if len(self.appointments) < self.config.get("MAX_APPOINTMENTS"):
uuid = uuid4().hex
self.appointments[uuid] = appointment
@@ -170,7 +158,7 @@ class Watcher:
expired_appointments = [
uuid
for uuid, appointment in self.appointments.items()
if block["height"] > appointment.end_time + EXPIRY_DELTA
if block["height"] > appointment.end_time + self.config.get("EXPIRY_DELTA")
]
Cleaner.delete_expired_appointment(
@@ -225,7 +213,7 @@ class Watcher:
found.
"""
potential_locators = {Watcher.compute_locator(txid): txid for txid in txids}
potential_locators = {compute_locator(txid): txid for txid in txids}
# Check is any of the tx_ids in the received block is an actual match
intersection = set(self.locator_uuid_map.keys()).intersection(potential_locators.keys())