mirror of
https://github.com/aljazceru/python-teos.git
synced 2025-12-17 22:24:23 +01:00
Fixes severals bugs after testing
Basic case is working now
This commit is contained in:
committed by
Sergi Delgado Segura
parent
fb2bf05057
commit
f0f7f0b1ab
@@ -5,6 +5,8 @@ from flask import Flask, request, Response
|
||||
import json
|
||||
|
||||
app = Flask(__name__)
|
||||
HTTP_OK = 200
|
||||
HTTP_BAD_REQUEST = 400
|
||||
|
||||
|
||||
@app.route('/', methods=['POST'])
|
||||
@@ -21,7 +23,7 @@ def add_appointment():
|
||||
|
||||
if appointment:
|
||||
appointment_added = watcher.add_appointment(appointment, debug, logging)
|
||||
rcode = 200
|
||||
rcode = HTTP_OK
|
||||
|
||||
# FIXME: Response should be signed receipt (created and signed by the API)
|
||||
if appointment_added:
|
||||
@@ -31,7 +33,7 @@ def add_appointment():
|
||||
# FIXME: change the response code maybe?
|
||||
|
||||
else:
|
||||
rcode = 400
|
||||
rcode = HTTP_BAD_REQUEST
|
||||
response = "Appointment rejected. Request does not match the standard"
|
||||
|
||||
# Send response back. Change multiprocessing.connection for an http based connection
|
||||
|
||||
42
pisa-btc/pisa/errors.py
Normal file
42
pisa-btc/pisa/errors.py
Normal file
@@ -0,0 +1,42 @@
|
||||
# Ported from https://github.com/bitcoin/bitcoin/blob/0.18/src/rpc/protocol.h
|
||||
|
||||
# General application defined errors
|
||||
RPC_MISC_ERROR = -1 # std::exception thrown in command handling
|
||||
RPC_TYPE_ERROR = -3 # Unexpected type was passed as parameter
|
||||
RPC_INVALID_ADDRESS_OR_KEY = -5 # Invalid address or key
|
||||
RPC_OUT_OF_MEMORY = -7 # Ran out of memory during operation
|
||||
RPC_INVALID_PARAMETER = -8 # Invalid missing or duplicate parameter
|
||||
RPC_DATABASE_ERROR = -20 # Database error
|
||||
RPC_DESERIALIZATION_ERROR = -22 # Error parsing or validating structure in raw format
|
||||
RPC_VERIFY_ERROR = -25 # General error during transaction or block submission
|
||||
RPC_VERIFY_REJECTED = -26 # Transaction or block was rejected by network rules
|
||||
RPC_VERIFY_ALREADY_IN_CHAIN = -27 # Transaction already in chain
|
||||
RPC_IN_WARMUP = -28 # Client still warming up
|
||||
RPC_METHOD_DEPRECATED = -32 # RPC method is deprecated
|
||||
|
||||
# Aliases for backward compatibility
|
||||
RPC_TRANSACTION_ERROR = RPC_VERIFY_ERROR
|
||||
RPC_TRANSACTION_REJECTED = RPC_VERIFY_REJECTED
|
||||
RPC_TRANSACTION_ALREADY_IN_CHAIN= RPC_VERIFY_ALREADY_IN_CHAIN
|
||||
|
||||
# P2P client errors
|
||||
RPC_CLIENT_NOT_CONNECTED = -9 # Bitcoin is not connected
|
||||
RPC_CLIENT_IN_INITIAL_DOWNLOAD = -10 # Still downloading initial blocks
|
||||
RPC_CLIENT_NODE_ALREADY_ADDED = -23 # Node is already added
|
||||
RPC_CLIENT_NODE_NOT_ADDED = -24 # Node has not been added before
|
||||
RPC_CLIENT_NODE_NOT_CONNECTED = -29 # Node to disconnect not found in connected nodes
|
||||
RPC_CLIENT_INVALID_IP_OR_SUBNET = -30 # Invalid IP/Subnet
|
||||
RPC_CLIENT_P2P_DISABLED = -31 # No valid connection manager instance found
|
||||
|
||||
# Wallet errors
|
||||
RPC_WALLET_ERROR = -4 # Unspecified problem with wallet (key not found etc.)
|
||||
RPC_WALLET_INSUFFICIENT_FUNDS = -6 # Not enough funds in wallet or account
|
||||
RPC_WALLET_INVALID_LABEL_NAME = -11 # Invalid label name
|
||||
RPC_WALLET_KEYPOOL_RAN_OUT = -12 # Keypool ran out call keypoolrefill first
|
||||
RPC_WALLET_UNLOCK_NEEDED = -13 # Enter the wallet passphrase with walletpassphrase first
|
||||
RPC_WALLET_PASSPHRASE_INCORRECT = -14 # The wallet passphrase entered was incorrect
|
||||
RPC_WALLET_WRONG_ENC_STATE = -15 # Command given in wrong wallet encryption state (encrypting an encrypted wallet etc.)
|
||||
RPC_WALLET_ENCRYPTION_FAILED = -16 # Failed to encrypt the wallet
|
||||
RPC_WALLET_ALREADY_UNLOCKED = -17 # Wallet is already unlocked
|
||||
RPC_WALLET_NOT_FOUND = -18 # Invalid wallet specified
|
||||
RPC_WALLET_NOT_SPECIFIED = -19 # No wallet specified (error when there are multiple wallets loaded)
|
||||
@@ -6,7 +6,7 @@ class Inspector:
|
||||
pass
|
||||
|
||||
def inspect(self, data, debug):
|
||||
# TODO: We need to define standard names for the json fields, using Paddy's ones for now
|
||||
# TODO: We need to define standard names for the json fields, using Paddy's for now
|
||||
|
||||
appointment = None
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
from queue import Queue
|
||||
from threading import Thread
|
||||
from pisa.zmq_subscriber import ZMQHandler
|
||||
from pisa.errors import *
|
||||
from bitcoinrpc.authproxy import AuthServiceProxy, JSONRPCException
|
||||
from conf import BTC_RPC_USER, BTC_RPC_PASSWD, BTC_RPC_HOST, BTC_RPC_PORT
|
||||
|
||||
@@ -23,45 +24,79 @@ class Responder:
|
||||
def __init__(self):
|
||||
self.jobs = dict()
|
||||
self.confirmation_counter = dict()
|
||||
self.block_queue = Queue()
|
||||
self.block_queue = None
|
||||
self.asleep = True
|
||||
self.zmq_subscriber = None
|
||||
|
||||
def create_job(self, dispute_txid, txid, rawtx, appointment_end, debug, logging, conf_counter=0, retry=False):
|
||||
# DISCUSS: Check what to do if the retry counter gets too big
|
||||
if retry:
|
||||
self.jobs[txid].retry_counter += 1
|
||||
else:
|
||||
self.confirmation_counter[txid] = conf_counter
|
||||
self.jobs[txid] = Job(dispute_txid, rawtx, appointment_end)
|
||||
|
||||
if debug:
|
||||
logging.info('[Responder] new job added (dispute txid = {}, txid = {}, appointment end = {})'.format(
|
||||
dispute_txid, txid, appointment_end))
|
||||
|
||||
def add_response(self, dispute_txid, txid, rawtx, appointment_end, debug, logging, retry=False):
|
||||
if self.asleep:
|
||||
self.asleep = False
|
||||
zmq_subscriber = Thread(target=self.do_subscribe, args=[self.block_queue, debug, logging])
|
||||
self.block_queue = Queue()
|
||||
zmq_thread = Thread(target=self.do_subscribe, args=[self.block_queue, debug, logging])
|
||||
responder = Thread(target=self.handle_responses, args=[debug, logging])
|
||||
zmq_subscriber.start()
|
||||
zmq_thread.start()
|
||||
responder.start()
|
||||
|
||||
def add_response(self, dispute_txid, txid, rawtx, appointment_end, debug, logging, retry=False):
|
||||
bitcoin_cli = AuthServiceProxy("http://%s:%s@%s:%d" % (BTC_RPC_USER, BTC_RPC_PASSWD, BTC_RPC_HOST,
|
||||
BTC_RPC_PORT))
|
||||
try:
|
||||
# ToDo: All errors should be handled as JSONRPCException, check that holds (if so response if no needed)
|
||||
if debug:
|
||||
if self.asleep:
|
||||
logging.info("[Responder] waking up!")
|
||||
logging.info("[Responder] pushing transaction to the network (txid: {})".format(txid))
|
||||
|
||||
response = bitcoin_cli.sendrawtransaction(rawtx)
|
||||
print(response)
|
||||
|
||||
# handle_responses can call add_response recursively if a broadcast transaction does not get confirmations
|
||||
# retry holds such information.
|
||||
# DISCUSS: Check what to do if the retry counter gets too big
|
||||
if retry:
|
||||
self.jobs[txid].retry_counter += 1
|
||||
else:
|
||||
self.confirmation_counter[txid] = 0
|
||||
self.jobs[txid] = Job(dispute_txid, rawtx, appointment_end)
|
||||
|
||||
if debug:
|
||||
logging.info('[Responder] new job added (dispute txid = {}, txid = {}, appointment end = {})'.format(
|
||||
dispute_txid, txid, appointment_end))
|
||||
self.create_job(dispute_txid, txid, rawtx, appointment_end, debug, logging, conf_counter=0, retry=retry)
|
||||
|
||||
except JSONRPCException as e:
|
||||
if debug:
|
||||
# ToDo: Check type of error if transaction does not get through
|
||||
# Since we're pushing a raw transaction to the network we can get two kind of rejections:
|
||||
# RPC_VERIFY_REJECTED and RPC_VERIFY_ALREADY_IN_CHAIN. The former implies that the transaction is rejected
|
||||
# due to network rules, whereas the later implies that the transaction is already in the blockchain.
|
||||
if e.code == RPC_VERIFY_REJECTED:
|
||||
# DISCUSS: what to do in this case
|
||||
pass
|
||||
elif e.code == RPC_VERIFY_ALREADY_IN_CHAIN:
|
||||
try:
|
||||
# If the transaction is already in the chain, we get the number of confirmations and watch the job
|
||||
# until the end of the appointment
|
||||
tx_info = bitcoin_cli.gettransaction(txid)
|
||||
confirmations = int(tx_info.get("confirmations"))
|
||||
self.create_job(dispute_txid, txid, rawtx, appointment_end, debug, logging, retry=retry,
|
||||
conf_counter=confirmations)
|
||||
except JSONRPCException as e:
|
||||
# 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 job
|
||||
if e.code == RPC_INVALID_ADDRESS_OR_KEY:
|
||||
self.add_response(dispute_txid, txid, rawtx, appointment_end, debug, logging, retry=retry)
|
||||
elif debug:
|
||||
# If something else happens (unlikely but possible) log it so we can treat it in future releases
|
||||
logging.error("[Responder] JSONRPCException. Error code {}".format(e))
|
||||
elif debug:
|
||||
# If something else happens (unlikely but possible) log it so we can treat it in future releases
|
||||
logging.error("[Responder] JSONRPCException. Error code {}".format(e))
|
||||
|
||||
def handle_responses(self, debug, logging):
|
||||
bitcoin_cli = AuthServiceProxy("http://%s:%s@%s:%d" % (BTC_RPC_USER, BTC_RPC_PASSWD, BTC_RPC_HOST,
|
||||
BTC_RPC_PORT))
|
||||
prev_block_hash = None
|
||||
prev_block_hash = 0
|
||||
while len(self.jobs) > 0:
|
||||
# We get notified for every new received block
|
||||
block_hash = self.block_queue.get()
|
||||
@@ -82,22 +117,14 @@ class Responder:
|
||||
|
||||
continue
|
||||
|
||||
if prev_block_hash == block.get('previousblockhash'):
|
||||
for job_id, job in self.jobs.items():
|
||||
if job.appointment_end <= height:
|
||||
# The end of the appointment has been reached
|
||||
# ToDo: record job in DB
|
||||
del (self.jobs[job_id])
|
||||
if debug:
|
||||
logging.info("[Responder] job completed. Appointment ended at height {}".format(job_id,
|
||||
height))
|
||||
|
||||
jobs_to_delete = []
|
||||
if prev_block_hash == block.get('previousblockhash') or prev_block_hash == 0:
|
||||
# Handling new jobs (aka jobs with not enough confirmations), when a job receives MIN_CONFIRMATIONS
|
||||
# it will be passed to jobs and we will simply check for chain forks.
|
||||
for job_id, confirmations in self.confirmation_counter.items():
|
||||
# If we see the transaction for the first time, or MIN_CONFIRMATIONS hasn't been reached
|
||||
if job_id in txs or (0 < confirmations < MIN_CONFIRMATIONS):
|
||||
confirmations += 1
|
||||
self.confirmation_counter[job_id] += 1
|
||||
|
||||
if debug:
|
||||
logging.info("[Responder] new confirmation received for txid = {}".format(job_id))
|
||||
@@ -116,6 +143,23 @@ class Responder:
|
||||
# Otherwise we increase the number of missed confirmations
|
||||
self.jobs[job_id].missed_confirmations += 1
|
||||
|
||||
for job_id, job in self.jobs.items():
|
||||
if job.appointment_end <= height:
|
||||
# The end of the appointment has been reached
|
||||
jobs_to_delete.append(job_id)
|
||||
|
||||
for job_id in jobs_to_delete:
|
||||
# Trying to delete directly when iterating the last for causes dictionary changed size error during
|
||||
# iteration in Python3 (can not be solved iterating only trough keys in Python3 either)
|
||||
|
||||
if debug:
|
||||
logging.info("[Responder] {} completed. Appointment ended at block {} after {} confirmations"
|
||||
.format(job_id, height, self.confirmation_counter[job_id]))
|
||||
|
||||
# ToDo: record job in DB
|
||||
del self.jobs[job_id]
|
||||
del self.confirmation_counter[job_id]
|
||||
|
||||
else:
|
||||
# ToDo: REORG!!
|
||||
if debug:
|
||||
@@ -124,16 +168,17 @@ class Responder:
|
||||
|
||||
self.handle_reorgs(bitcoin_cli, debug, logging)
|
||||
|
||||
prev_block_hash = block.get('previousblockhash')
|
||||
prev_block_hash = block.get('hash')
|
||||
|
||||
# Go back to sleep if there are no more jobs
|
||||
self.asleep = True
|
||||
self.zmq_subscriber.terminate = True
|
||||
|
||||
if debug:
|
||||
logging.error("[Responder] no more pending jobs, going back to sleep.")
|
||||
|
||||
def handle_reorgs(self, bitcoin_cli, debug, logging):
|
||||
for job_id, job in self.jobs:
|
||||
for job_id, job in self.jobs.items():
|
||||
try:
|
||||
tx_info = bitcoin_cli.gettransaction(job_id)
|
||||
job.confirmations = int(tx_info.get("confirmations"))
|
||||
@@ -156,5 +201,5 @@ class Responder:
|
||||
pass
|
||||
|
||||
def do_subscribe(self, block_queue, debug, logging):
|
||||
daemon = ZMQHandler()
|
||||
daemon.handle(block_queue, debug, logging)
|
||||
self.zmq_subscriber = ZMQHandler(parent='Responder')
|
||||
self.zmq_subscriber.handle(block_queue, debug, logging)
|
||||
|
||||
@@ -10,9 +10,10 @@ from conf import BTC_RPC_USER, BTC_RPC_PASSWD, BTC_RPC_HOST, BTC_RPC_PORT, MAX_A
|
||||
class Watcher:
|
||||
def __init__(self, max_appointments=MAX_APPOINTMENTS):
|
||||
self.appointments = dict()
|
||||
self.block_queue = Queue()
|
||||
self.block_queue = None
|
||||
self.asleep = True
|
||||
self.max_appointments = max_appointments
|
||||
self.zmq_subscriber = None
|
||||
|
||||
def add_appointment(self, appointment, debug, logging):
|
||||
# DISCUSS: about validation of input data
|
||||
@@ -35,12 +36,16 @@ class Watcher:
|
||||
|
||||
if self.asleep:
|
||||
self.asleep = False
|
||||
zmq_subscriber = Thread(target=self.do_subscribe, args=[self.block_queue, debug, logging])
|
||||
self.block_queue = Queue()
|
||||
zmq_thread = Thread(target=self.do_subscribe, args=[self.block_queue, debug, logging])
|
||||
responder = Responder()
|
||||
watcher = Thread(target=self.do_watch, args=[responder, debug, logging])
|
||||
zmq_subscriber.start()
|
||||
zmq_thread.start()
|
||||
watcher.start()
|
||||
|
||||
if debug:
|
||||
logging.info("[Watcher] waking up!")
|
||||
|
||||
appointment_added = True
|
||||
|
||||
if debug:
|
||||
@@ -56,8 +61,8 @@ class Watcher:
|
||||
return appointment_added
|
||||
|
||||
def do_subscribe(self, block_queue, debug, logging):
|
||||
daemon = ZMQHandler()
|
||||
daemon.handle(block_queue, debug, logging)
|
||||
self.zmq_subscriber = ZMQHandler(parent='Watcher')
|
||||
self.zmq_subscriber.handle(block_queue, debug, logging)
|
||||
|
||||
def do_watch(self, responder, debug, logging):
|
||||
bitcoin_cli = AuthServiceProxy("http://%s:%s@%s:%d" % (BTC_RPC_USER, BTC_RPC_PASSWD, BTC_RPC_HOST,
|
||||
@@ -110,6 +115,7 @@ class Watcher:
|
||||
|
||||
# Go back to sleep if there are no more appointments
|
||||
self.asleep = True
|
||||
self.zmq_subscriber.terminate = True
|
||||
|
||||
if debug:
|
||||
logging.error("[Watcher] no more pending appointments, going back to sleep.")
|
||||
|
||||
@@ -5,22 +5,27 @@ from conf import FEED_PROTOCOL, FEED_ADDR, FEED_PORT
|
||||
|
||||
class ZMQHandler:
|
||||
""" Adapted from https://github.com/bitcoin/bitcoin/blob/master/contrib/zmq/zmq_sub.py"""
|
||||
def __init__(self):
|
||||
def __init__(self, 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" % (FEED_PROTOCOL, FEED_ADDR, FEED_PORT))
|
||||
self.parent = parent
|
||||
self.terminate = False
|
||||
|
||||
def handle(self, block_queue, debug, logging):
|
||||
while True:
|
||||
while not self.terminate:
|
||||
msg = self.zmqSubSocket.recv_multipart()
|
||||
topic = msg[0]
|
||||
body = msg[1]
|
||||
|
||||
if topic == b"hashblock":
|
||||
block_hash = binascii.hexlify(body).decode('UTF-8')
|
||||
block_queue.put(block_hash)
|
||||
# Terminate could have been set wile the thread was blocked in recv
|
||||
if not self.terminate:
|
||||
topic = msg[0]
|
||||
body = msg[1]
|
||||
|
||||
if debug:
|
||||
logging.info("[ZMQHandler] new block received via ZMQ".format(block_hash))
|
||||
if topic == b"hashblock":
|
||||
block_hash = binascii.hexlify(body).decode('UTF-8')
|
||||
block_queue.put(block_hash)
|
||||
|
||||
if debug:
|
||||
logging.info("[ZMQHandler-{}] new block received via ZMQ".format(self.parent, block_hash))
|
||||
|
||||
Reference in New Issue
Block a user