Exemplo n.º 1
0
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
Exemplo n.º 2
0
    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()))))
Exemplo n.º 3
0
    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
Exemplo n.º 4
0
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
Exemplo n.º 5
0
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)
Exemplo n.º 6
0
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"])
Exemplo n.º 7
0
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)
Exemplo n.º 8
0
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)
Exemplo n.º 9
0
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()
Exemplo n.º 10
0
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)
Exemplo n.º 11
0
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"
Exemplo n.º 12
0
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
Exemplo n.º 13
0
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
Exemplo n.º 14
0
    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
Exemplo n.º 15
0
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
Exemplo n.º 16
0
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()
Exemplo n.º 17
0
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
Exemplo n.º 18
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"])
Exemplo n.º 19
0
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()
Exemplo n.º 20
0
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
Exemplo n.º 21
0
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"
Exemplo n.º 22
0
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
Exemplo n.º 23
0
    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
Exemplo n.º 24
0
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()
    ]
Exemplo n.º 25
0
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)
Exemplo n.º 26
0
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
Exemplo n.º 27
0
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)
Exemplo n.º 28
0
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"
Exemplo n.º 29
0
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")
Exemplo n.º 30
0
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"