Beispiel #1
0
def test_add_appointment_trigger_on_cache_cannot_decrypt(teosd):
    _, teos_id = teosd

    commitment_tx, _, penalty_tx = create_txs()

    # Let's send the commitment to the network and mine a block
    generate_block_with_transactions(commitment_tx)
    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_client.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_client.post_request(data, teos_add_appointment_endpoint)
    response_json = teos_client.process_post_response(response)

    # Check that the server has accepted the appointment
    tower_signature = response_json.get("signature")
    appointment_receipt = receipts.create_appointment_receipt(signature, response_json.get("start_block"))
    rpk = Cryptographer.recover_pk(appointment_receipt, tower_signature)
    assert teos_id == Cryptographer.get_compressed_pk(rpk)
    assert response_json.get("locator") == appointment.locator

    # The appointment should should have been immediately dropped
    with pytest.raises(TowerResponseError):
        get_appointment_info(teos_id, appointment_data["locator"])
Beispiel #2
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
Beispiel #3
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)
Beispiel #4
0
def test_get_completed_trackers(db_manager, gatekeeper, carrier, responder,
                                block_processor, generate_dummy_tracker):
    commitment_txs = [create_commitment_tx() for _ in range(30)]
    generate_block_with_transactions(commitment_txs)
    # A complete tracker is a tracker whose penalty transaction has been irrevocably resolved (i.e. has reached 100
    # confirmations)
    # We'll create 3 type of txs: irrevocably resolved, confirmed but not irrevocably resolved, and unconfirmed
    trackers_ir_resolved = {
        uuid4().hex: generate_dummy_tracker(commitment_tx)
        for commitment_tx in commitment_txs[:10]
    }

    trackers_confirmed = {
        uuid4().hex: generate_dummy_tracker(commitment_tx)
        for commitment_tx in commitment_txs[10:20]
    }

    trackers_unconfirmed = {}
    for commitment_tx in commitment_txs[20:]:
        tracker = generate_dummy_tracker(commitment_tx)
        responder.unconfirmed_txs.append(tracker.penalty_txid)
        trackers_unconfirmed[uuid4().hex] = tracker

    all_trackers = {}
    all_trackers.update(trackers_ir_resolved)
    all_trackers.update(trackers_confirmed)
    all_trackers.update(trackers_unconfirmed)

    # Let's add all to the Responder
    for uuid, tracker in all_trackers.items():
        responder.trackers[uuid] = tracker.get_summary()

    for uuid, tracker in trackers_ir_resolved.items():
        bitcoin_cli.sendrawtransaction(tracker.penalty_rawtx)

    generate_blocks_with_delay(1)

    for uuid, tracker in trackers_confirmed.items():
        bitcoin_cli.sendrawtransaction(tracker.penalty_rawtx)

    # ir_resolved have 100 confirmations and confirmed have 99
    generate_blocks_with_delay(99)

    # Let's check
    completed_trackers = responder.get_completed_trackers()
    ended_trackers_keys = list(trackers_ir_resolved.keys())
    assert set(completed_trackers) == set(ended_trackers_keys)

    # Generating 1 additional blocks should also include confirmed
    generate_blocks_with_delay(1)

    completed_trackers = responder.get_completed_trackers()
    ended_trackers_keys.extend(list(trackers_confirmed.keys()))
    assert set(completed_trackers) == set(ended_trackers_keys)
Beispiel #5
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
Beispiel #6
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)
Beispiel #7
0
def test_handle_breach(db_manager, gatekeeper, carrier, responder,
                       block_processor, generate_dummy_tracker):
    uuid = uuid4().hex
    commitment_tx = create_commitment_tx()
    tracker = generate_dummy_tracker(commitment_tx)
    generate_block_with_transactions(commitment_tx)

    # The block_hash passed to add_response does not matter much now. It will in the future to deal with errors
    receipt = responder.handle_breach(
        tracker.locator,
        uuid,
        tracker.dispute_txid,
        tracker.penalty_txid,
        tracker.penalty_rawtx,
        tracker.user_id,
        block_hash=get_random_value_hex(32),
    )

    assert receipt.delivered is True
def test_get_all_appointments(teosd, rpc_client):
    _, teos_id = teosd

    # Check that there is no appointment, so far
    all_appointments = json.loads(rpc_client.get_all_appointments())
    watching = all_appointments.get("watcher_appointments")
    responding = all_appointments.get("responder_trackers")
    assert len(watching) == 0 and len(responding) == 0

    # Register a user
    teos_client.register(user_id, teos_id, teos_base_endpoint)

    # After that we can build an appointment and send it to the tower
    commitment_tx, commitment_txid, penalty_tx = create_txs()
    appointment_data = build_appointment_data(commitment_txid, penalty_tx)
    appointment = teos_client.create_appointment(appointment_data)
    add_appointment(teos_id, appointment)

    # Now there should now be one appointment in the watcher
    all_appointments = json.loads(rpc_client.get_all_appointments())
    watching = all_appointments.get("watcher_appointments")
    responding = all_appointments.get("responder_trackers")
    assert len(watching) == 1 and len(responding) == 0

    # Trigger a breach and check again; now the appointment should be in the responder
    generate_block_with_transactions(commitment_tx)
    sleep(1)

    all_appointments = json.loads(rpc_client.get_all_appointments())
    watching = all_appointments.get("watcher_appointments")
    responding = all_appointments.get("responder_trackers")
    assert len(watching) == 0 and len(responding) == 1

    # Now let's mine some blocks so the appointment reaches its end. We need 100 + EXPIRY_DELTA -1
    generate_blocks(100 + config.get("EXPIRY_DELTA"))
    sleep(1)

    # Now the appointment should not be in the tower, back to 0
    all_appointments = json.loads(rpc_client.get_all_appointments())
    watching = all_appointments.get("watcher_appointments")
    responding = all_appointments.get("responder_trackers")
    assert len(watching) == 0 and len(responding) == 0
Beispiel #9
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")
Beispiel #10
0
def test_add_appointment_in_cache(watcher, generate_dummy_appointment):
    # Generate an appointment 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() + 10)

    appointment, dispute_tx = generate_dummy_appointment()

    # Broadcast the transaction and add it manually to the Watcher cache (since the Watcher is not currently watching)
    generate_block_with_transactions(dispute_tx)
    dispute_txid = watcher.block_processor.decode_raw_transaction(
        dispute_tx).get("txid")
    watcher.locator_cache.cache[appointment.locator] = dispute_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 it's not in the Watcher
    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)

    # It went to the Responder straightaway
    assert appointment.locator in [
        tracker.get("locator")
        for tracker in watcher.responder.trackers.values()
    ]

    # Trying to send it again should fail since it is already in the Responder
    with pytest.raises(AppointmentAlreadyTriggered):
        watcher.add_appointment(
            appointment, Cryptographer.sign(appointment.serialize(), user_sk))
Beispiel #11
0
def test_rebroadcast(db_manager, gatekeeper, carrier, responder,
                     block_processor, generate_dummy_tracker):
    # Include the commitment txs in a block
    commitment_txs = [create_commitment_tx() for _ in range(20)]
    generate_block_with_transactions(commitment_txs)
    txs_to_rebroadcast = []

    # Rebroadcast calls add_response with retry=True. The tracker data is already in trackers.
    for i, commitment_tx in enumerate(commitment_txs):
        uuid = uuid4().hex
        tracker = generate_dummy_tracker(commitment_tx)

        responder.trackers[uuid] = {
            "locator": tracker.locator,
            "penalty_txid": tracker.penalty_txid,
            "user_id": tracker.user_id,
        }

        # We need to add it to the db too
        responder.db_manager.create_triggered_appointment_flag(uuid)
        responder.db_manager.store_responder_tracker(uuid, tracker.to_dict())

        responder.tx_tracker_map[tracker.penalty_txid] = [uuid]
        responder.unconfirmed_txs.append(tracker.penalty_txid)

        # Let's add some of the txs in the rebroadcast list
        if (i % 2) == 0:
            txs_to_rebroadcast.append(tracker.penalty_txid)

    # The block_hash passed to rebroadcast does not matter much now. It will in the future to deal with errors
    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
Beispiel #12
0
def test_two_identical_appointments(teosd):
    _, teos_id = teosd
    # Tests sending two identical appointments to the tower.
    # This tests sending an appointment with two valid transaction with the same locator.
    # If they come from the same user, the last one will be kept.
    commitment_tx, commitment_txid, penalty_tx = create_txs()

    appointment_data = build_appointment_data(commitment_txid, penalty_tx)
    locator = compute_locator(commitment_txid)

    # Send the appointment twice
    appointment = teos_client.create_appointment(appointment_data)
    add_appointment(teos_id, appointment)
    add_appointment(teos_id, appointment)

    # Broadcast the commitment transaction and mine a block
    generate_block_with_transactions(commitment_tx)

    # The last appointment should have made it to the Responder
    appointment_info = get_appointment_info(teos_id, locator)

    assert appointment_info.get("status") == AppointmentStatus.DISPUTE_RESPONDED
    assert appointment_info.get("appointment").get("penalty_rawtx") == penalty_tx
Beispiel #13
0
def test_appointment_wrong_decryption_key(teosd):
    _, teos_id = teosd

    # 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(get_random_value_hex(32), penalty_tx)

    # We cannot use teos_client.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_client.post_request(data, teos_add_appointment_endpoint)
    response_json = teos_client.process_post_response(response)

    # Check that the server has accepted the appointment
    tower_signature = response_json.get("signature")
    appointment_receipt = receipts.create_appointment_receipt(signature, response_json.get("start_block"))
    rpk = Cryptographer.recover_pk(appointment_receipt, tower_signature)
    assert teos_id == Cryptographer.get_compressed_pk(rpk)
    assert response_json.get("locator") == appointment.locator

    # Trigger the appointment
    generate_block_with_transactions(commitment_tx)

    # The appointment should have been removed since the decryption failed.
    with pytest.raises(TowerResponseError):
        get_appointment_info(teos_id, appointment.locator)
Beispiel #14
0
def test_multiple_appointments_life_cycle(teosd):
    global appointments_in_watcher, appointments_in_responder, available_slots
    _, teos_id = teosd
    # Tests that get_all_appointments returns all the appointments the tower is storing at various stages in the
    # appointment lifecycle.
    appointments = []

    txs = [create_txs() for _ in range(5)]

    # Create five appointments.
    for commitment_tx, commitment_txid, penalty_tx in txs:
        appointment_data = build_appointment_data(commitment_txid, penalty_tx)

        locator = compute_locator(commitment_txid)
        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:
        appointment = teos_client.create_appointment(appt.get("appointment_data"))
        add_appointment(teos_id, appointment)
        appointments_in_watcher += 1
        available_slots -= 1

    # Check that get_subscription_info also detects these new appointments and returns the correct info.
    r = get_subscription_info(teos_id)
    # We've added 6 appointments so far
    assert len(r.get("appointments")) == appointments_in_watcher + appointments_in_responder
    assert r.get("available_slots") == available_slots
    assert r.get("subscription_expiry") == subscription_expiry

    # Two of these appointments are breached, and the watchtower responds to them.
    breached_appointments = []
    for i in range(2):
        generate_block_with_transactions(appointments[i]["commitment_tx"])
        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.
    rpc_client = RPCClient(config.get("RPC_BIND"), config.get("RPC_PORT"))
    all_appointments = json.loads(rpc_client.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 + 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(teos_id, appointment["locator"])
Beispiel #15
0
def test_appointment_life_cycle(teosd):
    global appointments_in_watcher, appointments_in_responder, available_slots, subscription_expiry

    _, teos_id = teosd

    # First of all we need to register
    available_slots, subscription_expiry = teos_client.register(user_id, teos_id, teos_base_endpoint)

    # After that we can build an appointment and send it to the tower
    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)
    appointments_in_watcher += 1

    # 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()

    rpc_client = RPCClient(config.get("RPC_BIND"), config.get("RPC_PORT"))

    # Check also the get_all_appointments endpoint
    all_appointments = json.loads(rpc_client.get_all_appointments())
    watching = all_appointments.get("watcher_appointments")
    responding = all_appointments.get("responder_trackers")
    assert len(watching) == appointments_in_watcher and len(responding) == 0

    # Trigger a breach and check again
    generate_block_with_transactions(commitment_tx)
    appointment_info = get_appointment_info(teos_id, locator)
    assert appointment_info.get("status") == AppointmentStatus.DISPUTE_RESPONDED
    assert appointment_info.get("locator") == locator
    appointments_in_watcher -= 1
    appointments_in_responder += 1

    all_appointments = json.loads(rpc_client.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

    # 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 is not found.
        assert False

    # Now let's mine some blocks so the appointment reaches its end. We need 100 + EXPIRY_DELTA -1
    generate_blocks(100 + config.get("EXPIRY_DELTA") - 1)
    appointments_in_responder -= 1

    # The appointment is no longer in the tower
    with pytest.raises(TowerResponseError):
        get_appointment_info(teos_id, locator)

    assert get_subscription_info(teos_id).get("available_slots") == available_slots
Beispiel #16
0
def test_do_watch(temp_db_manager, gatekeeper, carrier, block_processor,
                  generate_dummy_tracker):
    commitment_txs = [create_commitment_tx() for _ in range(20)]
    trackers = [
        generate_dummy_tracker(commitment_tx)
        for commitment_tx in commitment_txs
    ]
    subscription_expiry = block_processor.get_block_count() + 110

    # Broadcast all commitment transactions
    generate_block_with_transactions(commitment_txs)

    # Create a fresh responder to simplify the test
    responder = Responder(temp_db_manager, gatekeeper, carrier,
                          block_processor)
    chain_monitor = ChainMonitor([Queue(), responder.block_queue],
                                 block_processor, bitcoind_feed_params)
    chain_monitor.monitor_chain()
    chain_monitor.activate()

    # Let's set up the trackers first
    for tracker in trackers:
        uuid = uuid4().hex

        # Simulate user registration so trackers can properly expire
        responder.gatekeeper.registered_users[tracker.user_id] = UserInfo(
            available_slots=10, subscription_expiry=subscription_expiry)

        # Add data to the Responder
        responder.trackers[uuid] = tracker.get_summary()
        responder.tx_tracker_map[tracker.penalty_txid] = [uuid]
        responder.missed_confirmations[tracker.penalty_txid] = 0
        responder.unconfirmed_txs.append(tracker.penalty_txid)
        # Assuming the appointment only took a single slot
        responder.gatekeeper.registered_users[
            tracker.user_id].appointments[uuid] = 1

        # We also need to store the info in the db
        responder.db_manager.create_triggered_appointment_flag(uuid)
        responder.db_manager.store_responder_tracker(uuid, tracker.to_dict())

    # Let's start to watch
    do_watch_thread = Thread(target=responder.do_watch, daemon=True)
    do_watch_thread.start()

    # And broadcast some of the penalties
    broadcast_txs = []
    for tracker in trackers[:5]:
        bitcoin_cli.sendrawtransaction(tracker.penalty_rawtx)
        broadcast_txs.append(tracker.penalty_txid)

    # Mine a block
    generate_blocks_with_delay(1)

    # The transactions we sent shouldn't be in the unconfirmed transaction list anymore
    assert not set(broadcast_txs).issubset(responder.unconfirmed_txs)

    # CONFIRMATIONS_BEFORE_RETRY+1 blocks after, the responder should rebroadcast the unconfirmed txs (15 remaining)
    generate_blocks_with_delay(CONFIRMATIONS_BEFORE_RETRY + 1)
    assert len(responder.unconfirmed_txs) == 0
    assert len(responder.trackers) == 20

    # Generating 100 - CONFIRMATIONS_BEFORE_RETRY -2 additional blocks should complete the first 5 trackers
    generate_blocks_with_delay(100 - CONFIRMATIONS_BEFORE_RETRY - 2)
    assert len(responder.unconfirmed_txs) == 0
    assert len(responder.trackers) == 15
    # Check they are not in the Gatekeeper either
    for tracker in trackers[:5]:
        assert len(responder.gatekeeper.registered_users[
            tracker.user_id].appointments) == 0

    # CONFIRMATIONS_BEFORE_RETRY additional blocks should complete the rest
    generate_blocks_with_delay(CONFIRMATIONS_BEFORE_RETRY)
    assert len(responder.unconfirmed_txs) == 0
    assert len(responder.trackers) == 0
    # Check they are not in the Gatekeeper either
    for tracker in trackers[5:]:
        assert len(responder.gatekeeper.registered_users[
            tracker.user_id].appointments) == 0

    chain_monitor.terminate()
    do_watch_thread.join()