mirror of
https://github.com/aljazceru/python-teos.git
synced 2026-02-23 15:34:18 +01:00
Merge branch 'master' into chainmonitor
This commit is contained in:
21
pisa/api.py
21
pisa/api.py
@@ -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.
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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,
|
||||
|
||||
36
pisa/utils/zmq_subscriber.py
Normal file
36
pisa/utils/zmq_subscriber.py
Normal 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)
|
||||
@@ -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())
|
||||
|
||||
Reference in New Issue
Block a user