diff --git a/.coveragerc b/.coveragerc index 74c5a28..53a6100 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1,6 +1,7 @@ [run] omit = pisa/pisad.py + pisa/logger.py pisa/sample_conf.py pisa/time_traveler.py pisa/utils/auth_proxy.py \ No newline at end of file diff --git a/pisa/api.py b/pisa/api.py index 55b94b6..e8e4894 100644 --- a/pisa/api.py +++ b/pisa/api.py @@ -93,7 +93,7 @@ def get_appointment(): response.append(job_data) if not response: - response.append({"locator": locator, "status": "not found"}) + response.append({"locator": locator, "status": "not_found"}) response = jsonify(response) diff --git a/pisa/block_processor.py b/pisa/block_processor.py index 83b0441..26f16c1 100644 --- a/pisa/block_processor.py +++ b/pisa/block_processor.py @@ -65,6 +65,7 @@ class BlockProcessor: return potential_matches @staticmethod + # NOTCOVERED def get_matches(potential_matches, locator_uuid_map, appointments): matches = [] @@ -89,6 +90,7 @@ class BlockProcessor: # DISCUSS: This method comes from the Responder and seems like it could go back there. @staticmethod + # NOTCOVERED def check_confirmations(txs, unconfirmed_txs, tx_job_map, missed_confirmations): for tx in txs: diff --git a/pisa/carrier.py b/pisa/carrier.py index 130cfca..a4faab2 100644 --- a/pisa/carrier.py +++ b/pisa/carrier.py @@ -2,7 +2,7 @@ from pisa.rpc_errors import * from pisa.logger import Logger from pisa.tools import bitcoin_cli from pisa.utils.auth_proxy import JSONRPCException -from pisa.errors import UNKNOWN_JSON_RPC_EXCEPTION +from pisa.errors import UNKNOWN_JSON_RPC_EXCEPTION, RPC_TX_REORGED_AFTER_BROADCAST logger = Logger("Carrier") @@ -17,6 +17,7 @@ class Receipt: class Carrier: + # NOTCOVERED def send_transaction(self, rawtx, txid): try: logger.info("Pushing transaction to the network", txid=txid, rawtx=rawtx) @@ -50,8 +51,9 @@ class Carrier: else: # There's a really unlikely edge case where a transaction can be reorged between receiving the - # notification and querying the data. In such a case we just resend - self.send_transaction(rawtx, txid) + # notification and querying the data. Notice that this implies the tx being also kicked off the + # mempool, which again is really unlikely. + receipt = Receipt(delivered=False, reason=RPC_TX_REORGED_AFTER_BROADCAST) elif errno == RPC_DESERIALIZATION_ERROR: # Adding this here just for completeness. We should never end up here. The Carrier only sends txs diff --git a/pisa/errors.py b/pisa/errors.py index 8aec660..c2e5a4d 100644 --- a/pisa/errors.py +++ b/pisa/errors.py @@ -9,6 +9,9 @@ APPOINTMENT_WRONG_FIELD = -7 APPOINTMENT_CIPHER_NOT_SUPPORTED = -8 APPOINTMENT_HASH_FUNCTION_NOT_SUPPORTED = -9 +# Custom RPC errors +RPC_TX_REORGED_AFTER_BROADCAST = -98 + # UNHANDLED UNKNOWN_JSON_RPC_EXCEPTION = -99 diff --git a/pisa/responder.py b/pisa/responder.py index d5f0f63..2309b03 100644 --- a/pisa/responder.py +++ b/pisa/responder.py @@ -18,14 +18,12 @@ logger = Logger("Responder") class Job: - def __init__(self, dispute_txid, justice_txid, justice_rawtx, appointment_end, retry_counter=0): + def __init__(self, dispute_txid, justice_txid, justice_rawtx, appointment_end): self.dispute_txid = dispute_txid self.justice_txid = justice_txid self.justice_rawtx = justice_rawtx self.appointment_end = appointment_end - self.retry_counter = retry_counter - # FIXME: locator is here so we can give info about jobs for now. It can be either passed from watcher or info # can be directly got from DB self.locator = sha256(unhexlify(dispute_txid)).hexdigest() @@ -56,11 +54,11 @@ class Responder: carrier = Carrier() receipt = carrier.send_transaction(justice_rawtx, justice_txid) + # do_watch can call add_response recursively if a broadcast transaction does not get confirmations + # retry holds that information. If retry is true the job already exists if receipt.delivered: - # do_watch can call add_response recursively if a broadcast transaction does not get confirmations - # retry holds such information. - self.create_job(uuid, dispute_txid, justice_txid, justice_rawtx, appointment_end, retry=retry, - confirmations=receipt.confirmations) + if not retry: + self.create_job(uuid, dispute_txid, justice_txid, justice_rawtx, appointment_end, receipt.confirmations) else: # TODO: Add the missing reasons (e.g. RPC_VERIFY_REJECTED) @@ -68,25 +66,17 @@ class Responder: return receipt - def create_job(self, uuid, dispute_txid, justice_txid, justice_rawtx, appointment_end, confirmations=0, - retry=False): + def create_job(self, uuid, dispute_txid, justice_txid, justice_rawtx, appointment_end, confirmations=0): + self.jobs[uuid] = Job(dispute_txid, justice_txid, justice_rawtx, appointment_end) - # ToDo: #23-define-behaviour-approaching-end - if retry: - self.jobs[uuid].retry_counter += 1 - self.missed_confirmations[justice_txid] = 0 + if justice_txid in self.tx_job_map: + self.tx_job_map[justice_txid].append(uuid) else: - self.jobs[uuid] = Job(dispute_txid, justice_txid, justice_rawtx, appointment_end, confirmations) + self.tx_job_map[justice_txid] = [uuid] - if justice_txid in self.tx_job_map: - self.tx_job_map[justice_txid].append(uuid) - - else: - self.tx_job_map[justice_txid] = [uuid] - - if confirmations == 0: - self.unconfirmed_txs.append(justice_txid) + if confirmations == 0: + self.unconfirmed_txs.append(justice_txid) logger.info("New job added.", dispute_txid=dispute_txid, justice_txid=justice_txid, appointment_end=appointment_end) @@ -106,7 +96,7 @@ class Responder: def do_watch(self): # ToDo: #9-add-data-persistence # change prev_block_hash to the last known tip when bootstrapping - prev_block_hash = 0 + prev_block_hash = BlockProcessor.get_best_block_hash() while len(self.jobs) > 0: # We get notified for every new received block @@ -121,21 +111,22 @@ class Responder: block_hash=block_hash, prev_block_hash=block.get('previousblockhash'), txs=txs) # ToDo: #9-add-data-persistence - # change prev_block_hash condition - if prev_block_hash == block.get('previousblockhash') or prev_block_hash == 0: + if prev_block_hash == block.get('previousblockhash'): self.unconfirmed_txs, self.missed_confirmations = BlockProcessor.check_confirmations( txs, self.unconfirmed_txs, self.tx_job_map, self.missed_confirmations) txs_to_rebroadcast = self.get_txs_to_rebroadcast(txs) - Cleaner.delete_completed_jobs(self.jobs, self.tx_job_map, self.get_completed_jobs(height), height) + completed_jobs = self.get_completed_jobs(height) + Cleaner.delete_completed_jobs(self.jobs, self.tx_job_map, completed_jobs, height) self.rebroadcast(txs_to_rebroadcast) + # NOTCOVERED else: - logger.warning("Reorg found", - local_prev_block_hash=prev_block_hash, + logger.warning("Reorg found", local_prev_block_hash=prev_block_hash, remote_prev_block_hash=block.get('previousblockhash')) + # ToDo: #24-properly-handle-reorgs self.handle_reorgs() prev_block_hash = block.get('hash') @@ -160,31 +151,42 @@ class Responder: completed_jobs = [] for uuid, job in self.jobs.items(): - if job.appointment_end <= height: + if job.appointment_end <= height and job.justice_txid not in self.unconfirmed_txs: tx = Carrier.get_transaction(job.justice_txid) # FIXME: Should be improved with the librarian - confirmations = tx.get('confirmations') - if tx is not None and confirmations > MIN_CONFIRMATIONS: - # The end of the appointment has been reached - completed_jobs.append((uuid, confirmations)) + if tx is not None: + confirmations = tx.get('confirmations') + + if confirmations >= MIN_CONFIRMATIONS: + # The end of the appointment has been reached + completed_jobs.append((uuid, confirmations)) return completed_jobs - def rebroadcast(self, jobs_to_rebroadcast): + def rebroadcast(self, txs_to_rebroadcast): # DISCUSS: #22-discuss-confirmations-before-retry # ToDo: #23-define-behaviour-approaching-end - for tx in jobs_to_rebroadcast: - for uuid in self.tx_job_map[tx]: - self.add_response(uuid, self.jobs[uuid].dispute_txid, self.jobs[uuid].justice_txid, - self.jobs[uuid].justice_rawtx, self.jobs[uuid].appointment_end, retry=True) + receipts = [] + + for txid in txs_to_rebroadcast: + self.missed_confirmations[txid] = 0 + + for uuid in self.tx_job_map[txid]: + job = self.jobs[uuid] + receipt = self.add_response(uuid, job.dispute_txid, job.justice_txid, job.justice_rawtx, + job.appointment_end, retry=True) logger.warning("Transaction has missed many confirmations. Rebroadcasting.", - justice_txid=self.jobs[uuid].justice_txid, - confirmations_missed=CONFIRMATIONS_BEFORE_RETRY) + justice_txid=job.justice_txid, confirmations_missed=CONFIRMATIONS_BEFORE_RETRY) + + receipts.append((txid, receipt)) + + return receipts # FIXME: Legacy code, must be checked and updated/fixed + # NOTCOVERED def handle_reorgs(self): for uuid, job in self.jobs.items(): # First we check if the dispute transaction is still in the blockchain. If not, the justice can not be diff --git a/pisa/tools.py b/pisa/tools.py index adba5e5..b1e76d0 100644 --- a/pisa/tools.py +++ b/pisa/tools.py @@ -7,12 +7,14 @@ from pisa.rpc_errors import RPC_INVALID_ADDRESS_OR_KEY from pisa.utils.auth_proxy import AuthServiceProxy, JSONRPCException +# NOTCOVERED def bitcoin_cli(): return AuthServiceProxy("http://%s:%s@%s:%d" % (conf.BTC_RPC_USER, conf.BTC_RPC_PASSWD, conf.BTC_RPC_HOST, conf.BTC_RPC_PORT)) # TODO: currently only used in the Responder; might move there or in the BlockProcessor +# NOTCOVERED def check_tx_in_chain(tx_id, logger=Logger(), tx_label='Transaction'): tx_in_chain = False confirmations = 0 @@ -39,6 +41,7 @@ def check_tx_in_chain(tx_id, logger=Logger(), tx_label='Transaction'): return tx_in_chain, confirmations +# NOTCOVERED def can_connect_to_bitcoind(): can_connect = True diff --git a/test/simulator/utils.py b/test/simulator/utils.py index f2f2883..98ac072 100644 --- a/test/simulator/utils.py +++ b/test/simulator/utils.py @@ -1,6 +1,6 @@ # Porting some functionality from https://github.com/sr-gi/bitcoin_tools with some modifications <3 from hashlib import sha256 -from binascii import unhexlify +from binascii import unhexlify, hexlify def change_endianness(x): @@ -15,9 +15,9 @@ def change_endianness(x): if (len(x) % 2) == 1: x += "0" - y = bytes(x, 'utf-8') + y = unhexlify(x) z = y[::-1] - return z.decode('utf-8') + return hexlify(z).decode('utf-8') def parse_varint(tx): diff --git a/test/unit/conftest.py b/test/unit/conftest.py index 2c0bdea..e337de9 100644 --- a/test/unit/conftest.py +++ b/test/unit/conftest.py @@ -32,3 +32,8 @@ def generate_block(): sleep(0.5) +def generate_blocks(n): + for _ in range(n): + generate_block() + + diff --git a/test/unit/test_api.py b/test/unit/test_api.py index ba052e5..63b2967 100644 --- a/test/unit/test_api.py +++ b/test/unit/test_api.py @@ -1,6 +1,7 @@ import json import pytest import requests +from os import urandom from hashlib import sha256 from binascii import unhexlify @@ -99,6 +100,16 @@ def test_request_appointment(new_appointment): assert (all([status == "being_watched" for status in appointment_status])) +def test_request_random_appointment(): + r = requests.get(url=PISA_API + "/get_appointment?locator=" + urandom(32).hex()) + assert (r.status_code == 200) + + received_appointments = json.loads(r.content) + appointment_status = [appointment.pop("status") for appointment in received_appointments] + + assert (all([status == "not_found" for status in appointment_status])) + + def test_add_appointment_multiple_times(new_appointment, n=MULTIPLE_APPOINTMENTS): # Multiple appointments with the same locator should be valid # DISCUSS: #34-store-identical-appointments diff --git a/test/unit/test_block_processor.py b/test/unit/test_block_processor.py index a0ec37f..5183842 100644 --- a/test/unit/test_block_processor.py +++ b/test/unit/test_block_processor.py @@ -43,6 +43,12 @@ def test_get_block(best_block_hash): assert block.get('hash') == best_block_hash and 'height' in block and 'previousblockhash' in block and 'tx' in block +def test_get_random_block(): + block = BlockProcessor.get_block(urandom(32).hex()) + + assert block is None + + def test_get_block_count(): block_count = BlockProcessor.get_block_count() assert isinstance(block_count, int) and block_count >= 0 @@ -55,6 +61,15 @@ def test_potential_matches(txids, locator_uuid_map): assert locator_uuid_map.keys() == potential_matches.keys() +def test_potential_matches_random(locator_uuid_map): + txids = [urandom(32).hex() for _ in range(len(locator_uuid_map))] + + potential_matches = BlockProcessor.get_potential_matches(txids, locator_uuid_map) + + # None of the ids should match + assert len(potential_matches) == 0 + + def test_potential_matches_random_data(locator_uuid_map): # The likelihood of finding a potential match with random data should be negligible txids = [urandom(32).hex() for _ in range(TEST_SET_SIZE)] diff --git a/test/unit/test_carrier.py b/test/unit/test_carrier.py index d73c749..165d417 100644 --- a/test/unit/test_carrier.py +++ b/test/unit/test_carrier.py @@ -6,7 +6,7 @@ from os import urandom from pisa.carrier import Carrier from test.simulator.utils import sha256d from test.simulator.transaction import TX -from test.unit.conftest import generate_block +from test.unit.conftest import generate_blocks from pisa.rpc_errors import RPC_VERIFY_ALREADY_IN_CHAIN, RPC_DESERIALIZATION_ERROR logging.getLogger().disabled = True @@ -25,7 +25,6 @@ def carrier(): def test_send_transaction(run_bitcoind, carrier): - # We are mocking bitcoind and in our simulator txid == tx tx = TX.create_dummy_transaction() txid = sha256d(tx) @@ -43,8 +42,7 @@ def test_send_double_spending_transaction(carrier): sent_txs.append(txid) # Wait for a block to be mined - for _ in range(2): - generate_block() + generate_blocks(2) # Try to send it again receipt2 = carrier.send_transaction(tx, txid) diff --git a/test/unit/test_cleaner.py b/test/unit/test_cleaner.py index 237a2e3..0fdf5df 100644 --- a/test/unit/test_cleaner.py +++ b/test/unit/test_cleaner.py @@ -45,14 +45,14 @@ def set_up_jobs(total_jobs): txid = urandom(32).hex() # Assign both justice_txid and dispute_txid the same id (it shouldn't matter) - jobs[uuid] = Job(txid, txid, None, None, None) + jobs[uuid] = Job(txid, txid, None, None) tx_job_map[txid] = [uuid] # Each justice_txid can have more than one uuid assigned to it. Do a coin toss to add multiple ones while random.randint(0, 1): uuid = uuid4().hex - jobs[uuid] = Job(txid, txid, None, None, None) + jobs[uuid] = Job(txid, txid, None, None) tx_job_map[txid].append(uuid) return jobs, tx_job_map diff --git a/test/unit/test_responder.py b/test/unit/test_responder.py new file mode 100644 index 0000000..72e617e --- /dev/null +++ b/test/unit/test_responder.py @@ -0,0 +1,333 @@ +import json +import pytest +from os import urandom +from uuid import uuid4 +from threading import Thread +from queue import Queue, Empty + +from pisa.tools import check_txid_format +from test.simulator.utils import sha256d +from pisa.responder import Responder, Job +from test.simulator.bitcoind_sim import TX +from test.unit.conftest import generate_block, generate_blocks +from pisa.utils.auth_proxy import AuthServiceProxy +from pisa.conf import BTC_RPC_USER, BTC_RPC_PASSWD, BTC_RPC_HOST, BTC_RPC_PORT + + +@pytest.fixture(scope="module") +def responder(): + return Responder() + + +def create_dummy_job_data(random_txid=False, justice_rawtx=None): + bitcoin_cli = AuthServiceProxy("http://%s:%s@%s:%d" % (BTC_RPC_USER, BTC_RPC_PASSWD, BTC_RPC_HOST, BTC_RPC_PORT)) + + # The following transaction data corresponds to a valid transaction. For some test it may be interesting to have + # some valid data, but for others we may need multiple different justice_txids. + + dispute_txid = "0437cd7f8525ceed2324359c2d0ba26006d92d856a9c20fa0241106ee5a597c9" + justice_txid = "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16" + + if justice_rawtx is None: + justice_rawtx = "0100000001c997a5e56e104102fa209c6a852dd90660a20b2d9c352423edce25857fcd3704000000004847304402" \ + "204e45e16932b8af514961a1d3a1a25fdf3f4f7732e9d624c6c61548ab5fb8cd410220181522ec8eca07de4860a4" \ + "acdd12909d831cc56cbbac4622082221a8768d1d0901ffffffff0200ca9a3b00000000434104ae1a62fe09c5f51b" \ + "13905f07f06b99a2f7159b2225f374cd378d71302fa28414e7aab37397f554a7df5f142c21c1b7303b8a0626f1ba" \ + "ded5c72a704f7e6cd84cac00286bee0000000043410411db93e1dcdb8a016b49840f8c53bc1eb68a382e97b1482e" \ + "cad7b148a6909a5cb2e0eaddfb84ccf9744464f82e160bfa9b8b64f9d4c03f999b8643f656b412a3ac00000000" + + else: + justice_txid = sha256d(justice_rawtx) + + if random_txid is True: + justice_txid = urandom(32).hex() + + appointment_end = bitcoin_cli.getblockcount() + 2 + + return dispute_txid, justice_txid, justice_rawtx, appointment_end + + +def create_dummy_job(random_txid=False, justice_rawtx=None): + dispute_txid, justice_txid, justice_rawtx, appointment_end = create_dummy_job_data(random_txid, justice_rawtx) + return Job(dispute_txid, justice_txid, justice_rawtx, appointment_end) + + +def test_job_init(run_bitcoind): + dispute_txid, justice_txid, justice_rawtx, appointment_end = create_dummy_job_data() + job = Job(dispute_txid, justice_txid, justice_rawtx, appointment_end) + + assert job.dispute_txid == dispute_txid and job.justice_txid == justice_txid \ + and job.justice_rawtx == justice_rawtx and job.appointment_end == appointment_end + + +def test_job_to_dict(): + job = create_dummy_job() + job_dict = job.to_dict() + + assert job.locator == job_dict["locator"] and job.justice_rawtx == job_dict["justice_rawtx"] \ + and job.appointment_end == job_dict["appointment_end"] + + +def test_job_to_json(): + job = create_dummy_job() + job_dict = json.loads(job.to_json()) + + assert job.locator == job_dict["locator"] and job.justice_rawtx == job_dict["justice_rawtx"] \ + and job.appointment_end == job_dict["appointment_end"] + + +def test_init_responder(responder): + assert type(responder.jobs) is dict and len(responder.jobs) == 0 + assert type(responder.tx_job_map) is dict and len(responder.tx_job_map) == 0 + assert type(responder.unconfirmed_txs) is list and len(responder.unconfirmed_txs) == 0 + assert type(responder.missed_confirmations) is dict and len(responder.missed_confirmations) == 0 + assert responder.block_queue is None + assert responder.asleep is True + assert responder.zmq_subscriber is None + + +def test_add_response(responder): + uuid = uuid4().hex + job = create_dummy_job() + + # The responder automatically fires create_job on adding a job if it is asleep (initial state). Avoid this by + # setting the state to awake. + responder.asleep = False + + receipt = responder.add_response(uuid, job.dispute_txid, job.justice_txid, job.justice_rawtx, job.appointment_end) + + assert receipt.delivered is True + + +def test_create_job(responder): + responder.asleep = False + + for _ in range(20): + uuid = uuid4().hex + confirmations = 0 + dispute_txid, justice_txid, justice_rawtx, appointment_end = create_dummy_job_data(random_txid=True) + + # Check the job is not within the responder jobs before adding it + assert uuid not in responder.jobs + assert justice_txid not in responder.tx_job_map + assert justice_txid not in responder.unconfirmed_txs + + # And that it is afterwards + responder.create_job(uuid, dispute_txid, justice_txid, justice_rawtx, appointment_end, confirmations) + assert uuid in responder.jobs + assert justice_txid in responder.tx_job_map + assert justice_txid in responder.unconfirmed_txs + + # Check that the rest of job data also matches + job = responder.jobs[uuid] + assert job.dispute_txid == dispute_txid and job.justice_txid == justice_txid \ + and job.justice_rawtx == justice_rawtx and job.appointment_end == appointment_end \ + and job.appointment_end == appointment_end + + +def test_create_job_already_confirmed(responder): + responder.asleep = False + + for i in range(20): + uuid = uuid4().hex + confirmations = i+1 + dispute_txid, justice_txid, justice_rawtx, appointment_end = create_dummy_job_data( + justice_rawtx=TX.create_dummy_transaction()) + + responder.create_job(uuid, dispute_txid, justice_txid, justice_rawtx, appointment_end, confirmations) + + assert justice_txid not in responder.unconfirmed_txs + + +def test_do_subscribe(responder): + responder.block_queue = Queue() + + zmq_thread = Thread(target=responder.do_subscribe) + zmq_thread.daemon = True + zmq_thread.start() + + try: + generate_block() + block_hash = responder.block_queue.get() + assert check_txid_format(block_hash) + + except Empty: + assert False + + +def test_do_watch(responder): + # Reinitializing responder (but keeping the subscriber) + responder.jobs = dict() + responder.tx_job_map = dict() + responder.unconfirmed_txs = [] + responder.missed_confirmations = dict() + + bitcoin_cli = AuthServiceProxy("http://%s:%s@%s:%d" % (BTC_RPC_USER, BTC_RPC_PASSWD, BTC_RPC_HOST, BTC_RPC_PORT)) + + jobs = [create_dummy_job(justice_rawtx=TX.create_dummy_transaction()) for _ in range(20)] + + # Let's set up the jobs first + for job in jobs: + uuid = uuid4().hex + + responder.jobs[uuid] = job + responder.tx_job_map[job.justice_txid] = [uuid] + responder.missed_confirmations[job.justice_txid] = 0 + responder.unconfirmed_txs.append(job.justice_txid) + + # Let's start to watch + watch_thread = Thread(target=responder.do_watch) + watch_thread.daemon = True + watch_thread.start() + + # And broadcast some of the transactions + broadcast_txs = [] + for job in jobs[:5]: + bitcoin_cli.sendrawtransaction(job.justice_rawtx) + broadcast_txs.append(job.justice_txid) + + # Mine a block + generate_block() + + # The transactions we sent shouldn't be in the unconfirmed transaction list anymore + assert not set(broadcast_txs).issubset(responder.unconfirmed_txs) + + # TODO: test that reorgs can be detected once data persistence is merged (new version of the simulator) + + # Generating 5 additional blocks should complete the 5 jobs + generate_blocks(5) + + assert not set(broadcast_txs).issubset(responder.tx_job_map) + + # Do the rest + broadcast_txs = [] + for job in jobs[5:]: + bitcoin_cli.sendrawtransaction(job.justice_rawtx) + broadcast_txs.append(job.justice_txid) + + # Mine a block + generate_blocks(6) + + assert len(responder.tx_job_map) == 0 + assert responder.asleep is True + + +def test_get_txs_to_rebroadcast(responder): + # Let's create a few fake txids and assign at least 6 missing confirmations to each + txs_missing_too_many_conf = {urandom(32).hex(): 6+i for i in range(10)} + + # Let's create some other transaction that has missed some confirmations but not that many + txs_missing_some_conf = {urandom(32).hex(): 3 for _ in range(10)} + + # All the txs in the first dict should be flagged as to_rebroadcast + responder.missed_confirmations = txs_missing_too_many_conf + txs_to_rebroadcast = responder.get_txs_to_rebroadcast(txs_missing_too_many_conf) + assert txs_to_rebroadcast == list(txs_missing_too_many_conf.keys()) + + # Non of the txs in the second dict should be flagged + responder.missed_confirmations = txs_missing_some_conf + txs_to_rebroadcast = responder.get_txs_to_rebroadcast(txs_missing_some_conf) + assert txs_to_rebroadcast == [] + + # Let's check that it also works with a mixed dict + responder.missed_confirmations.update(txs_missing_too_many_conf) + txs_to_rebroadcast = responder.get_txs_to_rebroadcast(txs_missing_some_conf) + assert txs_to_rebroadcast == list(txs_missing_too_many_conf.keys()) + + +def test_get_completed_jobs(): + bitcoin_cli = AuthServiceProxy("http://%s:%s@%s:%d" % (BTC_RPC_USER, BTC_RPC_PASSWD, BTC_RPC_HOST, BTC_RPC_PORT)) + initial_height = bitcoin_cli.getblockcount() + + # Let's use a fresh responder for this to make it easier to compare the results + responder = Responder() + + # A complete job is a job that has reached the appointment end with enough confirmations (> MIN_CONFIRMATIONS) + # We'll create three type of transactions: end reached + enough conf, end reached + no enough conf, end not reached + jobs_end_conf = {uuid4().hex: create_dummy_job(justice_rawtx=TX.create_dummy_transaction()) for _ in range(10)} + + jobs_end_no_conf = {} + for _ in range(10): + job = create_dummy_job(justice_rawtx=TX.create_dummy_transaction()) + responder.unconfirmed_txs.append(job.justice_txid) + jobs_end_no_conf[uuid4().hex] = job + + jobs_no_end = {} + for _ in range(10): + job = create_dummy_job(justice_rawtx=TX.create_dummy_transaction()) + job.appointment_end += 10 + jobs_no_end[uuid4().hex] = job + + # Let's add all to the responder + responder.jobs.update(jobs_end_conf) + responder.jobs.update(jobs_end_no_conf) + responder.jobs.update(jobs_no_end) + + for uuid, job in responder.jobs.items(): + bitcoin_cli.sendrawtransaction(job.justice_rawtx) + + # The dummy appointments have a end_appointment time of current + 2, but jobs need at least 6 confs by default + generate_blocks(6) + + # And now let's check + completed_jobs = responder.get_completed_jobs(initial_height + 6) + completed_jobs_ids = [job_id for job_id, confirmations in completed_jobs] + ended_jobs_keys = list(jobs_end_conf.keys()) + assert set(completed_jobs_ids) == set(ended_jobs_keys) + + # Generating 6 additional blocks should also confirm jobs_no_end + generate_blocks(6) + + completed_jobs = responder.get_completed_jobs(initial_height + 12) + completed_jobs_ids = [job_id for job_id, confirmations in completed_jobs] + ended_jobs_keys.extend(list(jobs_no_end.keys())) + + assert set(completed_jobs_ids) == set(ended_jobs_keys) + + +def test_rebroadcast(): + responder = Responder() + responder.asleep = False + + txs_to_rebroadcast = [] + + # Rebroadcast calls add_response with retry=True. The job data is already in jobs. + for i in range(20): + uuid = uuid4().hex + dispute_txid, justice_txid, justice_rawtx, appointment_end = create_dummy_job_data( + justice_rawtx=TX.create_dummy_transaction()) + + responder.jobs[uuid] = Job(dispute_txid, justice_txid, justice_rawtx, appointment_end) + responder.tx_job_map[justice_txid] = [uuid] + responder.unconfirmed_txs.append(justice_txid) + + # Let's add some of the txs in the rebroadcast list + if (i % 2) == 0: + txs_to_rebroadcast.append(justice_txid) + + receipts = responder.rebroadcast(txs_to_rebroadcast) + + # All txs should have been delivered and the missed confirmation reset + for txid, receipt in receipts: + # Sanity check + assert txid in txs_to_rebroadcast + + assert receipt.delivered is True + assert responder.missed_confirmations[txid] == 0 + + + + + + + + + + + + + + + + + diff --git a/test/unit/test_tools.py b/test/unit/test_tools.py index 251663e..f9bdca2 100644 --- a/test/unit/test_tools.py +++ b/test/unit/test_tools.py @@ -1,6 +1,6 @@ from pisa import logging from pisa.tools import check_txid_format -from pisa.tools import can_connect_to_bitcoind, in_correct_network +from pisa.tools import can_connect_to_bitcoind, in_correct_network, bitcoin_cli logging.getLogger().disabled = True @@ -22,6 +22,15 @@ def test_can_connect_to_bitcoind(): # assert can_connect_to_bitcoind() is False +def test_bitcoin_cli(): + try: + bitcoin_cli().help() + assert True + + except Exception: + assert False + + def test_check_txid_format(): assert(check_txid_format(None) is False) assert(check_txid_format("") is False) diff --git a/test/unit/test_watcher.py b/test/unit/test_watcher.py index 0637157..e60254a 100644 --- a/test/unit/test_watcher.py +++ b/test/unit/test_watcher.py @@ -20,8 +20,8 @@ from pisa.appointment import Appointment from pisa.tools import check_txid_format from test.simulator.utils import sha256d from test.simulator.transaction import TX -from test.unit.conftest import generate_block from pisa.utils.auth_proxy import AuthServiceProxy +from test.unit.conftest import generate_block, generate_blocks from pisa.conf import EXPIRY_DELTA, BTC_RPC_USER, BTC_RPC_PASSWD, BTC_RPC_HOST, BTC_RPC_PORT, PISA_SECRET_KEY logging.getLogger().disabled = True @@ -156,20 +156,17 @@ def test_do_watch(watcher): # Broadcast the first two for dispute_tx in dispute_txs[:2]: - r = bitcoin_cli.sendrawtransaction(dispute_tx) + bitcoin_cli.sendrawtransaction(dispute_tx) # After leaving some time for the block to be mined and processed, the number of appointments should have reduced # by two - for _ in range(START_TIME_OFFSET + END_TIME_OFFSET): - generate_block() + generate_blocks(START_TIME_OFFSET + END_TIME_OFFSET) assert len(watcher.appointments) == APPOINTMENTS - 2 # The rest of appointments will timeout after the end (2) + EXPIRY_DELTA # Wait for an additional block to be safe - - for _ in range(EXPIRY_DELTA + START_TIME_OFFSET + END_TIME_OFFSET): - generate_block() + generate_blocks(EXPIRY_DELTA + START_TIME_OFFSET + END_TIME_OFFSET) assert len(watcher.appointments) == 0 assert watcher.asleep is True