Fixes severals bugs after testing

Basic case is working now
This commit is contained in:
Sergi Delgado
2019-05-07 15:54:56 +01:00
committed by Sergi Delgado Segura
parent fb2bf05057
commit f0f7f0b1ab
6 changed files with 149 additions and 49 deletions

View File

@@ -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
View 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)

View File

@@ -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

View File

@@ -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)

View File

@@ -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.")

View File

@@ -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))