def test_compute_locator(): # The best way of checking that compute locator is correct is by using check_locator_format for _ in range(100): assert check_locator_format(compute_locator(get_random_value_hex(LOCATOR_LEN_BYTES))) is True # String of length smaller than LOCATOR_LEN_BYTES bytes must fail for i in range(1, LOCATOR_LEN_BYTES): assert check_locator_format(compute_locator(get_random_value_hex(i))) is False
def init(self, last_known_block, block_processor): """ Sets the initial state of the locator cache. Args: last_known_block (:obj:`str`): the last known block by the :obj:`Watcher`. block_processor (:obj:`BlockProcessor <teos.block_processor.BlockProcessor>`): a block processor instance. """ # This is needed as a separate method from __init__ since it has to be initialized right before start watching. # Not doing so implies store temporary variables in the Watcher and initialising the cache as None. target_block_hash = last_known_block for _ in range(self.cache_size): # In some setups, like regtest, it could be the case that there are no enough previous blocks. # In those cases we pull as many as we can (up to cache_size). if not target_block_hash: break target_block = block_processor.get_block(target_block_hash) if not target_block: break locator_txid_map = { compute_locator(txid): txid for txid in target_block.get("tx") } self.cache.update(locator_txid_map) self.blocks[target_block_hash] = list(locator_txid_map.keys()) target_block_hash = target_block.get("previousblockhash") self.blocks = OrderedDict(reversed((list(self.blocks.items()))))
def get_breaches(self, txids): """ Gets a list of channel breaches given the list of transaction ids. Args: txids (:obj:`list`): the list of transaction ids included in the last received block. Returns: :obj:`dict`: A dictionary (``locator:txid``) with all the breaches found. An empty dictionary if none are found. """ 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()) breaches = {locator: potential_locators[locator] for locator in intersection} if len(breaches) > 0: logger.info("List of breaches", breaches=breaches) else: logger.info("No breaches found") return breaches
def test_appointment_shutdown_teos_trigger_while_offline(teosd): teosd_process, teos_id = teosd # This tests data persistence. An appointment is sent to the tower and the tower is stopped. The appointment is then # triggered with the tower offline, and then the tower is brought back online. teos_pid = teosd_process.pid commitment_tx, commitment_txid, penalty_tx = create_txs() appointment_data = build_appointment_data(commitment_txid, penalty_tx) locator = compute_locator(commitment_txid) appointment = teos_client.create_appointment(appointment_data) add_appointment(teos_id, appointment) # Check that the appointment is still in the Watcher appointment_info = get_appointment_info(teos_id, locator) assert appointment_info.get("status") == AppointmentStatus.BEING_WATCHED assert appointment_info.get("appointment") == appointment.to_dict() # Shutdown and trigger rpc_client = RPCClient(config.get("RPC_BIND"), config.get("RPC_PORT")) rpc_client.stop() teosd_process.join() generate_block_with_transactions(commitment_tx) # Restart teosd_process, _ = run_teosd() assert teos_pid != teosd_process.pid # The appointment should have been moved to the Responder appointment_info = get_appointment_info(teos_id, locator) assert appointment_info.get("status") == AppointmentStatus.DISPUTE_RESPONDED
def test_appointment_malformed_penalty(bitcoin_cli): # Lets start by creating two valid transaction commitment_tx, penalty_tx = create_txs(bitcoin_cli) # Now we can modify the penalty so it is invalid when broadcast mod_penalty_tx = Tx.from_hex(penalty_tx) tx_in = mod_penalty_tx.tx_ins[0].copy(redeem_script=b"") mod_penalty_tx = mod_penalty_tx.copy(tx_ins=[tx_in]) commitment_tx_id = bitcoin_cli.decoderawtransaction(commitment_tx).get("txid") appointment_data = build_appointment_data(commitment_tx_id, mod_penalty_tx.hex()) locator = compute_locator(commitment_tx_id) appointment, _ = add_appointment(appointment_data) # Get the information from the tower to check that it matches appointment_info = get_appointment_info(locator) assert appointment_info.get("status") == "being_watched" assert appointment_info.get("locator") == locator assert appointment_info.get("appointment") == appointment.to_dict() # Broadcast the commitment transaction and mine a block new_addr = bitcoin_cli.getnewaddress() broadcast_transaction_and_mine_block(bitcoin_cli, commitment_tx, new_addr) # The appointment should have been removed since the penalty_tx was malformed. with pytest.raises(TowerResponseError): get_appointment_info(locator)
def test_add_appointment_trigger_on_cache_cannot_decrypt(bitcoin_cli): commitment_tx, penalty_tx = create_txs(bitcoin_cli) # Let's send the commitment to the network and mine a block broadcast_transaction_and_mine_block(bitcoin_cli, commitment_tx, bitcoin_cli.getnewaddress()) sleep(1) # The appointment data is built using a random 32-byte value. appointment_data = build_appointment_data(get_random_value_hex(32), penalty_tx) # We cannot use teos_cli.add_appointment here since it computes the locator internally, so let's do it manually. appointment_data["locator"] = compute_locator(bitcoin_cli.decoderawtransaction(commitment_tx).get("txid")) appointment_data["encrypted_blob"] = Cryptographer.encrypt(penalty_tx, get_random_value_hex(32)) appointment = Appointment.from_dict(appointment_data) signature = Cryptographer.sign(appointment.serialize(), user_sk) data = {"appointment": appointment.to_dict(), "signature": signature} # Send appointment to the server. response = teos_cli.post_request(data, teos_add_appointment_endpoint) response_json = teos_cli.process_post_response(response) # Check that the server has accepted the appointment signature = response_json.get("signature") rpk = Cryptographer.recover_pk(appointment.serialize(), signature) assert teos_id == Cryptographer.get_compressed_pk(rpk) assert response_json.get("locator") == appointment.locator # The appointment should should have been inmediately dropped with pytest.raises(TowerResponseError): get_appointment_info(appointment_data["locator"])
def test_appointment_wrong_decryption_key(bitcoin_cli): # This tests an appointment encrypted with a key that has not been derived from the same source as the locator. # Therefore the tower won't be able to decrypt the blob once the appointment is triggered. commitment_tx, penalty_tx = create_txs(bitcoin_cli) # The appointment data is built using a random 32-byte value. appointment_data = build_appointment_data(get_random_value_hex(32), penalty_tx) # We cannot use teos_cli.add_appointment here since it computes the locator internally, so let's do it manually. # We will encrypt the blob using the random value and derive the locator from the commitment tx. appointment_data["locator"] = compute_locator(bitcoin_cli.decoderawtransaction(commitment_tx).get("txid")) appointment_data["encrypted_blob"] = Cryptographer.encrypt(penalty_tx, get_random_value_hex(32)) appointment = Appointment.from_dict(appointment_data) signature = Cryptographer.sign(appointment.serialize(), user_sk) data = {"appointment": appointment.to_dict(), "signature": signature} # Send appointment to the server. response = teos_cli.post_request(data, teos_add_appointment_endpoint) response_json = teos_cli.process_post_response(response) # Check that the server has accepted the appointment signature = response_json.get("signature") rpk = Cryptographer.recover_pk(appointment.serialize(), signature) assert teos_id == Cryptographer.get_compressed_pk(rpk) assert response_json.get("locator") == appointment.locator # Trigger the appointment new_addr = bitcoin_cli.getnewaddress() broadcast_transaction_and_mine_block(bitcoin_cli, commitment_tx, new_addr) # The appointment should have been removed since the decryption failed. with pytest.raises(TowerResponseError): get_appointment_info(appointment.locator)
def test_appointment_malformed_penalty(teosd): _, teos_id = teosd # Lets start by creating two valid transaction commitment_tx, commitment_txid, penalty_tx = create_txs() # Now we can modify the penalty so it is invalid when broadcast (removing the witness should do) mod_penalty_tx = Tx.from_hex(penalty_tx).no_witness() appointment_data = build_appointment_data(commitment_txid, mod_penalty_tx.hex()) locator = compute_locator(commitment_txid) appointment = teos_client.create_appointment(appointment_data) add_appointment(teos_id, appointment) # Get the information from the tower to check that it matches appointment_info = get_appointment_info(teos_id, locator) assert appointment_info.get("status") == AppointmentStatus.BEING_WATCHED assert appointment_info.get("locator") == locator assert appointment_info.get("appointment") == appointment.to_dict() # Broadcast the commitment transaction and mine a block generate_block_with_transactions(commitment_tx) # The appointment should have been removed since the penalty_tx was malformed. with pytest.raises(TowerResponseError): get_appointment_info(teos_id, locator)
def test_appointment_shutdown_teos_trigger_while_offline(bitcoin_cli): global teosd_process teos_pid = teosd_process.pid commitment_tx, penalty_tx = create_txs(bitcoin_cli) commitment_tx_id = bitcoin_cli.decoderawtransaction(commitment_tx).get("txid") appointment_data = build_appointment_data(commitment_tx_id, penalty_tx) locator = compute_locator(commitment_tx_id) appointment, _ = add_appointment(appointment_data) # Check that the appointment is still in the Watcher appointment_info = get_appointment_info(locator) assert appointment_info.get("status") == "being_watched" assert appointment_info.get("appointment") == appointment.to_dict() # Shutdown and trigger teosd_process.terminate() new_addr = bitcoin_cli.getnewaddress() broadcast_transaction_and_mine_block(bitcoin_cli, commitment_tx, new_addr) # Restart teosd_process = run_teosd() assert teos_pid != teosd_process.pid # The appointment should have been moved to the Responder appointment_info = get_appointment_info(locator) assert appointment_info.get("status") == "dispute_responded" teosd_process.terminate()
def create_appointment(appointment_data): """ Creates an appointment object from an appointment data dictionary provided by the user. Performs all the required sanity checks on the input data: - Check that the given commitment_txid is correct (proper format and not missing) - Check that the transaction is correct (not missing) Args: appointment_data (:obj:`dict`): a dictionary containing the appointment data. Returns: :obj:`common.appointment.Appointment`: An appointment built from the appointment data provided by the user. """ tx_id = appointment_data.get("tx_id") tx = appointment_data.get("tx") if not tx_id: raise InvalidParameter("Missing tx_id, locator cannot be computed") elif not is_256b_hex_str(tx_id): raise InvalidParameter("Wrong tx_id, locator cannot be computed") elif not tx: raise InvalidParameter("The tx field is missing in the provided data") elif not isinstance(tx, str): raise InvalidParameter("The provided tx field is not a string") appointment_data["locator"] = compute_locator(tx_id) appointment_data["encrypted_blob"] = Cryptographer.encrypt(tx, tx_id) return Appointment.from_dict(appointment_data)
def test_appointment_malformed_penalty(bitcoin_cli, create_txs): # Lets start by creating two valid transaction commitment_tx, penalty_tx = create_txs # Now we can modify the penalty so it is invalid when broadcast mod_penalty_tx = Tx.from_hex(penalty_tx) tx_in = mod_penalty_tx.tx_ins[0].copy(redeem_script=b"") mod_penalty_tx = mod_penalty_tx.copy(tx_ins=[tx_in]) commitment_tx_id = bitcoin_cli.decoderawtransaction(commitment_tx).get("txid") appointment_data = build_appointment_data(bitcoin_cli, commitment_tx_id, mod_penalty_tx.hex()) locator = compute_locator(commitment_tx_id) assert teos_cli.add_appointment([json.dumps(appointment_data)], teos_add_appointment_endpoint, cli_config) is True # Broadcast the commitment transaction and mine a block new_addr = bitcoin_cli.getnewaddress() broadcast_transaction_and_mine_block(bitcoin_cli, commitment_tx, new_addr) # The appointment should have been removed since the penalty_tx was malformed. sleep(1) appointment_info = get_appointment_info(locator) assert appointment_info is not None assert len(appointment_info) == 1 assert appointment_info[0].get("status") == "not_found"
def test_two_appointment_same_locator_different_penalty(bitcoin_cli, create_txs): # This tests sending an appointment with two valid transaction with the same locator. commitment_tx, penalty_tx1 = create_txs commitment_tx_id = bitcoin_cli.decoderawtransaction(commitment_tx).get("txid") # We need to create a second penalty spending from the same commitment decoded_commitment_tx = bitcoin_cli.decoderawtransaction(commitment_tx) new_addr = bitcoin_cli.getnewaddress() penalty_tx2 = create_penalty_tx(bitcoin_cli, decoded_commitment_tx, new_addr) appointment1_data = build_appointment_data(bitcoin_cli, commitment_tx_id, penalty_tx1) appointment2_data = build_appointment_data(bitcoin_cli, commitment_tx_id, penalty_tx2) locator = compute_locator(commitment_tx_id) assert teos_cli.add_appointment([json.dumps(appointment1_data)], teos_add_appointment_endpoint, cli_config) is True assert teos_cli.add_appointment([json.dumps(appointment2_data)], teos_add_appointment_endpoint, cli_config) is True # Broadcast the commitment transaction and mine a block new_addr = bitcoin_cli.getnewaddress() broadcast_transaction_and_mine_block(bitcoin_cli, commitment_tx, new_addr) # The first appointment should have made it to the Responder, and the second one should have been dropped for # double-spending sleep(1) appointment_info = get_appointment_info(locator) assert appointment_info is not None assert len(appointment_info) == 1 assert appointment_info[0].get("status") == "dispute_responded" assert appointment_info[0].get("penalty_rawtx") == penalty_tx1
def test_two_identical_appointments(bitcoin_cli, create_txs): # Tests sending two identical appointments to the tower. # At the moment there are no checks for identical appointments, so both will be accepted, decrypted and kept until # the end. # TODO: 34-exact-duplicate-appointment # This tests sending an appointment with two valid transaction with the same locator. commitment_tx, penalty_tx = create_txs commitment_tx_id = bitcoin_cli.decoderawtransaction(commitment_tx).get("txid") appointment_data = build_appointment_data(bitcoin_cli, commitment_tx_id, penalty_tx) locator = compute_locator(commitment_tx_id) # Send the appointment twice assert teos_cli.add_appointment([json.dumps(appointment_data)], teos_add_appointment_endpoint, cli_config) is True assert teos_cli.add_appointment([json.dumps(appointment_data)], teos_add_appointment_endpoint, cli_config) is True # Broadcast the commitment transaction and mine a block new_addr = bitcoin_cli.getnewaddress() broadcast_transaction_and_mine_block(bitcoin_cli, commitment_tx, new_addr) # The first appointment should have made it to the Responder, and the second one should have been dropped for # double-spending sleep(1) appointment_info = get_appointment_info(locator) assert appointment_info is not None assert len(appointment_info) == 2 for info in appointment_info: assert info.get("status") == "dispute_responded" assert info.get("penalty_rawtx") == penalty_tx
def fix(self, last_known_block, block_processor): """ Fixes the cache after a reorg has been detected by feeding the most recent ``cache_size`` blocks to it. Args: last_known_block (:obj:`str`): the last known block hash after the reorg. block_processor (:obj:`BlockProcessor <teos.block_processor.BlockProcessor>`): a block processor instance. """ tmp_cache = LocatorCache(self.cache_size) # We assume there are no reorgs back to genesis. If so, this would raise some log warnings. And the cache will # be filled with less than cache_size blocks. target_block_hash = last_known_block for _ in range(tmp_cache.cache_size): target_block = block_processor.get_block(target_block_hash) if target_block: # Compute the locator:txid pair for every transaction in the block and update both the cache and # the block mapping. locator_txid_map = { compute_locator(txid): txid for txid in target_block.get("tx") } tmp_cache.cache.update(locator_txid_map) tmp_cache.blocks[target_block_hash] = list( locator_txid_map.keys()) target_block_hash = target_block.get("previousblockhash") with self.rw_lock.gen_wlock(): self.blocks = OrderedDict( reversed((list(tmp_cache.blocks.items())))) self.cache = tmp_cache.cache
def test_filter_valid_breaches(watcher): dispute_txid = "0437cd7f8525ceed2324359c2d0ba26006d92d856a9c20fa0241106ee5a597c9" encrypted_blob = ( "a62aa9bb3c8591e4d5de10f1bd49db92432ce2341af55762cdc9242c08662f97f5f47da0a1aa88373508cd6e67e87eefddeca0cee98c1" "967ec1c1ecbb4c5e8bf08aa26159214e6c0bc4b2c7c247f87e7601d15c746fc4e711be95ba0e363001280138ba9a65b06c4aa6f592b21" "3635ee763984d522a4c225814510c8f7ab0801f36d4a68f5ee7dd3930710005074121a172c29beba79ed647ebaf7e7fab1bbd9a208251" "ef5486feadf2c46e33a7d66adf9dbbc5f67b55a34b1b3c4909dd34a482d759b0bc25ecd2400f656db509466d7479b5b92a2fadabccc9e" "c8918da8979a9feadea27531643210368fee494d3aaa4983e05d6cf082a49105e2f8a7c7821899239ba7dee12940acd7d8a629894b5d31" "e94b439cfe8d2e9f21e974ae5342a70c91e8" ) dummy_appointment, _ = generate_dummy_appointment() dummy_appointment.encrypted_blob = encrypted_blob dummy_appointment.locator = compute_locator(dispute_txid) uuid = uuid4().hex appointments = {uuid: dummy_appointment} locator_uuid_map = {dummy_appointment.locator: [uuid]} breaches = {dummy_appointment.locator: dispute_txid} for uuid, appointment in appointments.items(): watcher.appointments[uuid] = {"locator": appointment.locator, "user_id": appointment.user_id} watcher.db_manager.store_watcher_appointment(uuid, dummy_appointment.to_dict()) watcher.db_manager.create_append_locator_map(dummy_appointment.locator, uuid) watcher.locator_uuid_map = locator_uuid_map valid_breaches, invalid_breaches = watcher.filter_breaches(breaches) # We have "triggered" a single breach and it was valid. assert len(invalid_breaches) == 0 and len(valid_breaches) == 1
def test_get_breaches(watcher, txids, locator_uuid_map): watcher.locator_uuid_map = locator_uuid_map locators_txid_map = {compute_locator(txid): txid for txid in txids} potential_breaches = watcher.get_breaches(locators_txid_map) # All the txids must breach assert locator_uuid_map.keys() == potential_breaches.keys()
def test_get_breaches_random_data(watcher, locator_uuid_map): # The likelihood of finding a potential breach with random data should be negligible watcher.locator_uuid_map = locator_uuid_map txids = [get_random_value_hex(32) for _ in range(TEST_SET_SIZE)] locators_txid_map = {compute_locator(txid): txid for txid in txids} potential_breaches = watcher.get_breaches(locators_txid_map) # None of the txids should breach assert len(potential_breaches) == 0
def test_multiple_appointments_life_cycle(bitcoin_cli): global appointments_in_watcher, appointments_in_responder # Tests that get_all_appointments returns all the appointments the tower is storing at various stages in the # appointment lifecycle. appointments = [] commitment_txs, penalty_txs = create_txs(bitcoin_cli, n=5) # Create five appointments. for commitment_tx, penalty_tx in zip(commitment_txs, penalty_txs): commitment_tx_id = bitcoin_cli.decoderawtransaction(commitment_tx).get("txid") appointment_data = build_appointment_data(commitment_tx_id, penalty_tx) locator = compute_locator(commitment_tx_id) appointment = { "locator": locator, "commitment_tx": commitment_tx, "penalty_tx": penalty_tx, "appointment_data": appointment_data, } appointments.append(appointment) # Send all of them to watchtower. for appt in appointments: add_appointment(appt.get("appointment_data")) appointments_in_watcher += 1 # Two of these appointments are breached, and the watchtower responds to them. breached_appointments = [] for i in range(2): new_addr = bitcoin_cli.getnewaddress() broadcast_transaction_and_mine_block(bitcoin_cli, appointments[i]["commitment_tx"], new_addr) bitcoin_cli.generatetoaddress(1, new_addr) breached_appointments.append(appointments[i]["locator"]) appointments_in_watcher -= 1 appointments_in_responder += 1 sleep(1) # Test that they all show up in get_all_appointments at the correct stages. all_appointments = get_all_appointments() watching = all_appointments.get("watcher_appointments") responding = all_appointments.get("responder_trackers") assert len(watching) == appointments_in_watcher and len(responding) == appointments_in_responder responder_locators = [appointment["locator"] for uuid, appointment in responding.items()] assert set(responder_locators) == set(breached_appointments) new_addr = bitcoin_cli.getnewaddress() # Now let's mine some blocks so the appointment reaches its end. We need 100 + EXPIRY_DELTA -1 bitcoin_cli.generatetoaddress(100 + teos_config.get("EXPIRY_DELTA") - 1, new_addr) # The appointment is no longer in the tower with pytest.raises(TowerResponseError): for appointment in appointments: get_appointment_info(appointment["locator"])
def test_get_breaches(watcher, txids, locator_uuid_map): # Get breaches returns a dictionary (locator:txid) of breaches given a map of locator:txid. # Test that it works with valid data # Create a locator_uuid_map and a locators_txid_map that fully match watcher.locator_uuid_map = locator_uuid_map locators_txid_map = {compute_locator(txid): txid for txid in txids} # All the txids must breach potential_breaches = watcher.get_breaches(locators_txid_map) assert locator_uuid_map.keys() == potential_breaches.keys()
def test_update_cache_full(): # Updating a full cache should be dropping the oldest block one by one locator_cache = LocatorCache(config.get("LOCATOR_CACHE_SIZE")) block_hashes = [] big_map = {} # Fill the cache first for i in range(locator_cache.cache_size): block_hash = get_random_value_hex(32) txs = [get_random_value_hex(32) for _ in range(10)] locator_txid_map = {compute_locator(txid): txid for txid in txs} locator_cache.update(block_hash, locator_txid_map) if i == 0: first_block_hash = block_hash first_locator_txid_map = locator_txid_map else: block_hashes.append(block_hash) big_map.update(locator_txid_map) # The cache is now full. assert first_block_hash in locator_cache.blocks for locator in first_locator_txid_map.keys(): assert locator in locator_cache.cache # Add one more block_hash = get_random_value_hex(32) txs = [get_random_value_hex(32) for _ in range(10)] locator_txid_map = {compute_locator(txid): txid for txid in txs} locator_cache.update(block_hash, locator_txid_map) # The first block is not there anymore, but the rest are there assert first_block_hash not in locator_cache.blocks for locator in first_locator_txid_map.keys(): assert locator not in locator_cache.cache for block_hash in block_hashes: assert block_hash in locator_cache.blocks for locator in big_map.keys(): assert locator in locator_cache.cache
def test_add_appointment_trigger_on_cache(bitcoin_cli): # This tests sending an appointment whose trigger is in the cache commitment_tx, penalty_tx = create_txs(bitcoin_cli) commitment_tx_id = bitcoin_cli.decoderawtransaction(commitment_tx).get("txid") appointment_data = build_appointment_data(commitment_tx_id, penalty_tx) locator = compute_locator(commitment_tx_id) # Let's send the commitment to the network and mine a block broadcast_transaction_and_mine_block(bitcoin_cli, commitment_tx, bitcoin_cli.getnewaddress()) # Send the data to the tower and request it back. It should have gone straightaway to the Responder add_appointment(appointment_data) assert get_appointment_info(locator).get("status") == "dispute_responded"
def test_add_appointment_trigger_on_cache(teosd): _, teos_id = teosd # This tests sending an appointment whose trigger is in the cache commitment_tx, commitment_txid, penalty_tx = create_txs() appointment_data = build_appointment_data(commitment_txid, penalty_tx) locator = compute_locator(commitment_txid) # Let's send the commitment to the network and mine a block generate_block_with_transactions(commitment_tx) # Send the data to the tower and request it back. It should have gone straightaway to the Responder appointment = teos_client.create_appointment(appointment_data) add_appointment(teos_id, appointment) assert get_appointment_info(teos_id, locator).get("status") == AppointmentStatus.DISPUTE_RESPONDED
def _generate_dummy_appointment(): commitment_txid = get_random_value_hex(32) penalty_tx = get_random_value_hex(150) appointment_data = { "locator": compute_locator(commitment_txid), "to_self_delay": 20, "encrypted_blob": Cryptographer.encrypt(penalty_tx, commitment_txid), "user_id": get_random_value_hex(16), "user_signature": get_random_value_hex(50), "start_block": 200, } return ExtendedAppointment.from_dict(appointment_data), commitment_txid
def test_add_appointment_in_cache_invalid_blob(watcher): # Generate an appointment with an invalid transaction and add the dispute txid to the cache user_sk, user_pk = generate_keypair() user_id = Cryptographer.get_compressed_pk(user_pk) watcher.gatekeeper.registered_users[user_id] = UserInfo( available_slots=1, subscription_expiry=watcher.block_processor.get_block_count() + 1) # We need to create the appointment manually commitment_tx, commitment_txid, penalty_tx = create_txs() locator = compute_locator(commitment_tx) dummy_appointment_data = { "tx": penalty_tx, "tx_id": commitment_txid, "to_self_delay": 20 } encrypted_blob = Cryptographer.encrypt(penalty_tx[::-1], commitment_txid) appointment_data = { "locator": locator, "to_self_delay": dummy_appointment_data.get("to_self_delay"), "encrypted_blob": encrypted_blob, "user_id": get_random_value_hex(16), } appointment = Appointment.from_dict(appointment_data) watcher.locator_cache.cache[appointment.locator] = commitment_txid # Try to add the appointment user_signature = Cryptographer.sign(appointment.serialize(), user_sk) response = watcher.add_appointment(appointment, user_signature) appointment_receipt = receipts.create_appointment_receipt( user_signature, response.get("start_block")) # The appointment is accepted but dropped (same as an invalid appointment that gets triggered) assert (response and response.get("locator") == appointment.locator and Cryptographer.get_compressed_pk(watcher.signing_key.public_key) == Cryptographer.get_compressed_pk( Cryptographer.recover_pk(appointment_receipt, response.get("signature")))) assert not watcher.locator_uuid_map.get(appointment.locator) assert appointment.locator not in [ tracker.get("locator") for tracker in watcher.responder.trackers.values() ]
def test_add_appointment_invalid_trigger_on_cache(bitcoin_cli): # This tests sending an invalid appointment which trigger is in the cache commitment_tx, penalty_tx = create_txs(bitcoin_cli) commitment_tx_id = bitcoin_cli.decoderawtransaction(commitment_tx).get("txid") # We can just flip the justice tx so it is invalid appointment_data = build_appointment_data(commitment_tx_id, penalty_tx[::-1]) locator = compute_locator(commitment_tx_id) # Let's send the commitment to the network and mine a block broadcast_transaction_and_mine_block(bitcoin_cli, commitment_tx, bitcoin_cli.getnewaddress()) sleep(1) # Send the data to the tower and request it back. It should get accepted but the data will be dropped. add_appointment(appointment_data) with pytest.raises(TowerResponseError): get_appointment_info(locator)
def test_update_cache(): # Update should add data about a new block in the cache. If the cache is full, the oldest block is dropped. locator_cache = LocatorCache(config.get("LOCATOR_CACHE_SIZE")) block_hash = get_random_value_hex(32) txs = [get_random_value_hex(32) for _ in range(10)] locator_txid_map = {compute_locator(txid): txid for txid in txs} # Cache is empty assert block_hash not in locator_cache.blocks for locator in locator_txid_map.keys(): assert locator not in locator_cache.cache # The data has been added to the cache locator_cache.update(block_hash, locator_txid_map) assert block_hash in locator_cache.blocks for locator in locator_txid_map.keys(): assert locator in locator_cache.cache
def test_add_appointment_invalid_trigger_on_cache(teosd): _, teos_id = teosd # This tests sending an invalid appointment which trigger is in the cache commitment_tx, commitment_txid, penalty_tx = create_txs() # We can just flip the justice tx so it is invalid appointment_data = build_appointment_data(commitment_txid, penalty_tx[::-1]) locator = compute_locator(commitment_txid) # Let's send the commitment to the network and mine a block generate_block_with_transactions(commitment_tx) sleep(1) # Send the data to the tower and request it back. It should get accepted but the data will be dropped. appointment = teos_client.create_appointment(appointment_data) add_appointment(teos_id, appointment) with pytest.raises(TowerResponseError): get_appointment_info(teos_id, locator)
def test_appointment_wrong_key(bitcoin_cli, create_txs): # This tests an appointment encrypted with a key that has not been derived from the same source as the locator. # Therefore the tower won't be able to decrypt the blob once the appointment is triggered. commitment_tx, penalty_tx = create_txs # The appointment data is built using a random 32-byte value. appointment_data = build_appointment_data(bitcoin_cli, get_random_value_hex(32), penalty_tx) # We can't use teos_cli.add_appointment here since it computes the locator internally, so let's do it manually. # We will encrypt the blob using the random value and derive the locator from the commitment tx. appointment_data["locator"] = compute_locator(bitcoin_cli.decoderawtransaction(commitment_tx).get("txid")) appointment_data["encrypted_blob"] = Cryptographer.encrypt(Blob(penalty_tx), get_random_value_hex(32)) appointment = Appointment.from_dict(appointment_data) teos_pk, cli_sk, cli_pk_der = teos_cli.load_keys( cli_config.get("TEOS_PUBLIC_KEY"), cli_config.get("CLI_PRIVATE_KEY"), cli_config.get("CLI_PUBLIC_KEY") ) hex_pk_der = binascii.hexlify(cli_pk_der) signature = Cryptographer.sign(appointment.serialize(), cli_sk) data = {"appointment": appointment.to_dict(), "signature": signature, "public_key": hex_pk_der.decode("utf-8")} # Send appointment to the server. response = teos_cli.post_appointment(data, teos_add_appointment_endpoint) response_json = teos_cli.process_post_appointment_response(response) # Check that the server has accepted the appointment signature = response_json.get("signature") assert signature is not None rpk = Cryptographer.recover_pk(appointment.serialize(), signature) assert Cryptographer.verify_rpk(teos_pk, rpk) is True assert response_json.get("locator") == appointment.locator # Trigger the appointment new_addr = bitcoin_cli.getnewaddress() broadcast_transaction_and_mine_block(bitcoin_cli, commitment_tx, new_addr) # The appointment should have been removed since the decryption failed. sleep(1) appointment_info = get_appointment_info(appointment.locator) assert appointment_info is not None assert len(appointment_info) == 1 assert appointment_info[0].get("status") == "not_found"
def test_two_appointment_same_locator_different_penalty_different_users(teosd): _, teos_id = teosd # This tests sending an appointment with two valid transaction with the same locator from different users commitment_tx, commitment_txid, penalty_tx1 = create_txs() # We need to create a second penalty spending from the same commitment decoded_commitment_tx = bitcoin_cli.decoderawtransaction(commitment_tx) new_addr = bitcoin_cli.getnewaddress() penalty_tx2 = create_penalty_tx(decoded_commitment_tx, new_addr) appointment1_data = build_appointment_data(commitment_txid, penalty_tx1) appointment2_data = build_appointment_data(commitment_txid, penalty_tx2) locator = compute_locator(commitment_txid) # tmp keys for a different user tmp_user_sk = PrivateKey() tmp_user_id = Cryptographer.get_compressed_pk(tmp_user_sk.public_key) teos_client.register(tmp_user_id, teos_id, teos_base_endpoint) appointment_1 = teos_client.create_appointment(appointment1_data) add_appointment(teos_id, appointment_1) appointment_2 = teos_client.create_appointment(appointment2_data) add_appointment(teos_id, appointment_2, sk=tmp_user_sk) # Broadcast the commitment transaction and mine a block generate_block_with_transactions(commitment_tx) # One of the transactions must have made it to the Responder while the other must have been dropped for # double-spending. That means that one of the responses from the tower should fail appointment_info = None with pytest.raises(TowerResponseError): appointment_info = get_appointment_info(teos_id, locator) appointment2_info = get_appointment_info(teos_id, locator, sk=tmp_user_sk) if appointment_info is None: appointment_info = appointment2_info appointment1_data = appointment2_data assert appointment_info.get("status") == AppointmentStatus.DISPUTE_RESPONDED assert appointment_info.get("locator") == appointment1_data.get("locator") assert appointment_info.get("appointment").get("penalty_tx") == appointment1_data.get("penalty_tx")
def test_appointment_life_cycle(bitcoin_cli, create_txs): commitment_tx, penalty_tx = create_txs commitment_tx_id = bitcoin_cli.decoderawtransaction(commitment_tx).get("txid") appointment_data = build_appointment_data(bitcoin_cli, commitment_tx_id, penalty_tx) locator = compute_locator(commitment_tx_id) assert teos_cli.add_appointment([json.dumps(appointment_data)], teos_add_appointment_endpoint, cli_config) is True appointment_info = get_appointment_info(locator) assert appointment_info is not None assert len(appointment_info) == 1 assert appointment_info[0].get("status") == "being_watched" new_addr = bitcoin_cli.getnewaddress() broadcast_transaction_and_mine_block(bitcoin_cli, commitment_tx, new_addr) appointment_info = get_appointment_info(locator) assert appointment_info is not None assert len(appointment_info) == 1 assert appointment_info[0].get("status") == "dispute_responded" # It can be also checked by ensuring that the penalty transaction made it to the network penalty_tx_id = bitcoin_cli.decoderawtransaction(penalty_tx).get("txid") try: bitcoin_cli.getrawtransaction(penalty_tx_id) assert True except JSONRPCException: # If the transaction if not found. assert False # Now let's mine some blocks so the appointment reaches its end. # Since we are running all the nodes remotely data may take more time than normal, and some confirmations may be # missed, so we generate more than enough confirmations and add some delays. for _ in range(int(1.5 * END_TIME_DELTA)): sleep(1) bitcoin_cli.generatetoaddress(1, new_addr) appointment_info = get_appointment_info(locator) assert appointment_info[0].get("status") == "not_found"