Refactors tests folder to separate different modules

Each module has a different folder and they do not share methods now. At some point they should be split in different repos
This commit is contained in:
Sergi Delgado Segura
2019-12-16 11:26:51 +01:00
parent bc250bd814
commit 61663e89d7
26 changed files with 29 additions and 15 deletions

View File

143
test/pisa/unit/conftest.py Normal file
View File

@@ -0,0 +1,143 @@
import pytest
import random
import requests
from time import sleep
from shutil import rmtree
from threading import Thread
from binascii import hexlify
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives.asymmetric import ec
from cryptography.hazmat.primitives import serialization
from apps.cli.blob import Blob
from pisa.responder import TransactionTracker
from pisa.watcher import Watcher
from pisa.tools import bitcoin_cli
from pisa.db_manager import DBManager
from pisa.appointment import Appointment
from test.simulator.utils import sha256d
from test.simulator.transaction import TX
from test.simulator.bitcoind_sim import run_simulator, HOST, PORT
from common.constants import LOCATOR_LEN_HEX
from common.cryptographer import Cryptographer
@pytest.fixture(scope="session")
def run_bitcoind():
bitcoind_thread = Thread(target=run_simulator, kwargs={"mode": "event"})
bitcoind_thread.daemon = True
bitcoind_thread.start()
# It takes a little bit of time to start the API (otherwise the requests are sent too early and they fail)
sleep(0.1)
@pytest.fixture(scope="session", autouse=True)
def prng_seed():
random.seed(0)
@pytest.fixture(scope="session")
def db_manager():
manager = DBManager("test_db")
yield manager
manager.db.close()
rmtree("test_db")
def generate_keypair():
client_sk = ec.generate_private_key(ec.SECP256K1, default_backend())
client_pk = client_sk.public_key()
return client_sk, client_pk
def get_random_value_hex(nbytes):
pseudo_random_value = random.getrandbits(8 * nbytes)
prv_hex = "{:x}".format(pseudo_random_value)
return prv_hex.zfill(2 * nbytes)
def generate_block():
requests.post(url="http://{}:{}/generate".format(HOST, PORT), timeout=5)
sleep(0.5)
def generate_blocks(n):
for _ in range(n):
generate_block()
def generate_dummy_appointment_data(real_height=True, start_time_offset=5, end_time_offset=30):
if real_height:
current_height = bitcoin_cli().getblockcount()
else:
current_height = 10
dispute_tx = TX.create_dummy_transaction()
dispute_txid = sha256d(dispute_tx)
penalty_tx = TX.create_dummy_transaction(dispute_txid)
dummy_appointment_data = {
"tx": penalty_tx,
"tx_id": dispute_txid,
"start_time": current_height + start_time_offset,
"end_time": current_height + end_time_offset,
"to_self_delay": 20,
}
# dummy keys for this test
client_sk, client_pk = generate_keypair()
client_pk_der = client_pk.public_bytes(
encoding=serialization.Encoding.DER, format=serialization.PublicFormat.SubjectPublicKeyInfo
)
locator = Watcher.compute_locator(dispute_txid)
blob = Blob(dummy_appointment_data.get("tx"))
encrypted_blob = Cryptographer.encrypt(blob, dummy_appointment_data.get("tx_id"))
appointment_data = {
"locator": locator,
"start_time": dummy_appointment_data.get("start_time"),
"end_time": dummy_appointment_data.get("end_time"),
"to_self_delay": dummy_appointment_data.get("to_self_delay"),
"encrypted_blob": encrypted_blob,
}
signature = Cryptographer.sign(Cryptographer.signature_format(appointment_data), client_sk)
pk_hex = hexlify(client_pk_der).decode("utf-8")
data = {"appointment": appointment_data, "signature": signature, "public_key": pk_hex}
return data, dispute_tx
def generate_dummy_appointment(real_height=True, start_time_offset=5, end_time_offset=30):
appointment_data, dispute_tx = generate_dummy_appointment_data(
real_height=real_height, start_time_offset=start_time_offset, end_time_offset=end_time_offset
)
return Appointment.from_dict(appointment_data["appointment"]), dispute_tx
def generate_dummy_tracker():
dispute_txid = get_random_value_hex(32)
penalty_txid = get_random_value_hex(32)
penalty_rawtx = get_random_value_hex(100)
locator = dispute_txid[:LOCATOR_LEN_HEX]
tracker_data = dict(
locator=locator,
dispute_txid=dispute_txid,
penalty_txid=penalty_txid,
penalty_rawtx=penalty_rawtx,
appointment_end=100,
)
return TransactionTracker.from_dict(tracker_data)

191
test/pisa/unit/test_api.py Normal file
View File

@@ -0,0 +1,191 @@
import json
import pytest
import requests
from time import sleep
from threading import Thread
from cryptography.hazmat.primitives import serialization
from pisa.api import start_api
from pisa.watcher import Watcher
from pisa.tools import bitcoin_cli
from pisa import HOST, PORT, c_logger
from pisa.conf import MAX_APPOINTMENTS
from test.pisa.unit.conftest import (
generate_block,
generate_blocks,
get_random_value_hex,
generate_dummy_appointment_data,
generate_keypair,
)
from common.constants import LOCATOR_LEN_BYTES
c_logger.disabled = True
PISA_API = "http://{}:{}".format(HOST, PORT)
MULTIPLE_APPOINTMENTS = 10
appointments = []
locator_dispute_tx_map = {}
@pytest.fixture(scope="module")
def run_api(db_manager):
sk, pk = generate_keypair()
sk_der = sk.private_bytes(
encoding=serialization.Encoding.DER,
format=serialization.PrivateFormat.TraditionalOpenSSL,
encryption_algorithm=serialization.NoEncryption(),
)
watcher = Watcher(db_manager, sk_der)
api_thread = Thread(target=start_api, args=[watcher])
api_thread.daemon = True
api_thread.start()
# It takes a little bit of time to start the API (otherwise the requests are sent too early and they fail)
sleep(0.1)
@pytest.fixture
def new_appt_data():
appt_data, dispute_tx = generate_dummy_appointment_data()
locator_dispute_tx_map[appt_data["appointment"]["locator"]] = dispute_tx
return appt_data
def add_appointment(new_appt_data):
r = requests.post(url=PISA_API, json=json.dumps(new_appt_data), timeout=5)
if r.status_code == 200:
appointments.append(new_appt_data["appointment"])
return r
def test_add_appointment(run_api, run_bitcoind, new_appt_data):
# Properly formatted appointment
r = add_appointment(new_appt_data)
assert r.status_code == 200
# Incorrect appointment
new_appt_data["appointment"]["to_self_delay"] = 0
r = add_appointment(new_appt_data)
assert r.status_code == 400
def test_request_random_appointment():
r = requests.get(url=PISA_API + "/get_appointment?locator=" + get_random_value_hex(LOCATOR_LEN_BYTES))
assert r.status_code == 200
received_appointments = json.loads(r.content)
appointment_status = [appointment.pop("status") for appointment in received_appointments]
assert all([status == "not_found" for status in appointment_status])
def test_add_appointment_multiple_times(new_appt_data, n=MULTIPLE_APPOINTMENTS):
# Multiple appointments with the same locator should be valid
# DISCUSS: #34-store-identical-appointments
for _ in range(n):
r = add_appointment(new_appt_data)
assert r.status_code == 200
def test_request_multiple_appointments_same_locator(new_appt_data, n=MULTIPLE_APPOINTMENTS):
for _ in range(n):
r = add_appointment(new_appt_data)
assert r.status_code == 200
test_request_appointment_watcher(new_appt_data)
def test_add_too_many_appointment(new_appt_data):
for _ in range(MAX_APPOINTMENTS - len(appointments)):
r = add_appointment(new_appt_data)
assert r.status_code == 200
r = add_appointment(new_appt_data)
assert r.status_code == 503
def test_get_all_appointments_watcher():
r = requests.get(url=PISA_API + "/get_all_appointments")
assert r.status_code == 200 and r.reason == "OK"
received_appointments = json.loads(r.content)
# Make sure there all the locators re in the watcher
watcher_locators = [v["locator"] for k, v in received_appointments["watcher_appointments"].items()]
local_locators = [appointment["locator"] for appointment in appointments]
assert set(watcher_locators) == set(local_locators)
assert len(received_appointments["responder_trackers"]) == 0
def test_get_all_appointments_responder():
# Trigger all disputes
locators = [appointment["locator"] for appointment in appointments]
for locator, dispute_tx in locator_dispute_tx_map.items():
if locator in locators:
bitcoin_cli().sendrawtransaction(dispute_tx)
# Confirm transactions
generate_blocks(6)
# Get all appointments
r = requests.get(url=PISA_API + "/get_all_appointments")
received_appointments = json.loads(r.content)
# Make sure there is not pending locator in the watcher
responder_trackers = [v["locator"] for k, v in received_appointments["responder_trackers"].items()]
local_locators = [appointment["locator"] for appointment in appointments]
assert set(responder_trackers) == set(local_locators)
assert len(received_appointments["watcher_appointments"]) == 0
def test_request_appointment_watcher(new_appt_data):
# First we need to add an appointment
r = add_appointment(new_appt_data)
assert r.status_code == 200
# Next we can request it
r = requests.get(url=PISA_API + "/get_appointment?locator=" + new_appt_data["appointment"]["locator"])
assert r.status_code == 200
# Each locator may point to multiple appointments, check them all
received_appointments = json.loads(r.content)
# Take the status out and leave the received appointments ready to compare
appointment_status = [appointment.pop("status") for appointment in received_appointments]
# Check that the appointment is within the received appoints
assert new_appt_data["appointment"] in received_appointments
# Check that all the appointments are being watched
assert all([status == "being_watched" for status in appointment_status])
def test_request_appointment_responder(new_appt_data):
# Let's do something similar to what we did with the watcher but now we'll send the dispute tx to the network
dispute_tx = locator_dispute_tx_map[new_appt_data["appointment"]["locator"]]
bitcoin_cli().sendrawtransaction(dispute_tx)
r = add_appointment(new_appt_data)
assert r.status_code == 200
# Generate a block to trigger the watcher
generate_block()
r = requests.get(url=PISA_API + "/get_appointment?locator=" + new_appt_data["appointment"]["locator"])
assert r.status_code == 200
received_appointments = json.loads(r.content)
appointment_status = [appointment.pop("status") for appointment in received_appointments]
appointment_locators = [appointment["locator"] for appointment in received_appointments]
assert new_appt_data["appointment"]["locator"] in appointment_locators and len(received_appointments) == 1
assert all([status == "dispute_responded" for status in appointment_status]) and len(appointment_status) == 1

View File

@@ -0,0 +1,112 @@
import json
from pytest import fixture
from pisa import c_logger
from pisa.appointment import Appointment
from pisa.encrypted_blob import EncryptedBlob
from test.pisa.unit.conftest import get_random_value_hex
from common.constants import LOCATOR_LEN_BYTES
c_logger.disabled = True
# Not much to test here, adding it for completeness
@fixture
def appointment_data():
locator = get_random_value_hex(LOCATOR_LEN_BYTES)
start_time = 100
end_time = 120
to_self_delay = 20
encrypted_blob_data = get_random_value_hex(100)
return {
"locator": locator,
"start_time": start_time,
"end_time": end_time,
"to_self_delay": to_self_delay,
"encrypted_blob": encrypted_blob_data,
}
def test_init_appointment(appointment_data):
# The appointment has no checks whatsoever, since the inspector is the one taking care or that, and the only one
# creating appointments.
# DISCUSS: whether this makes sense by design or checks should be ported from the inspector to the appointment
# 35-appointment-checks
appointment = Appointment(
appointment_data["locator"],
appointment_data["start_time"],
appointment_data["end_time"],
appointment_data["to_self_delay"],
appointment_data["encrypted_blob"],
)
assert (
appointment_data["locator"] == appointment.locator
and appointment_data["start_time"] == appointment.start_time
and appointment_data["end_time"] == appointment.end_time
and appointment_data["to_self_delay"] == appointment.to_self_delay
and EncryptedBlob(appointment_data["encrypted_blob"]) == appointment.encrypted_blob
)
def test_to_dict(appointment_data):
appointment = Appointment(
appointment_data["locator"],
appointment_data["start_time"],
appointment_data["end_time"],
appointment_data["to_self_delay"],
appointment_data["encrypted_blob"],
)
dict_appointment = appointment.to_dict()
assert (
appointment_data["locator"] == dict_appointment["locator"]
and appointment_data["start_time"] == dict_appointment["start_time"]
and appointment_data["end_time"] == dict_appointment["end_time"]
and appointment_data["to_self_delay"] == dict_appointment["to_self_delay"]
and EncryptedBlob(appointment_data["encrypted_blob"]) == EncryptedBlob(dict_appointment["encrypted_blob"])
)
def test_to_json(appointment_data):
appointment = Appointment(
appointment_data["locator"],
appointment_data["start_time"],
appointment_data["end_time"],
appointment_data["to_self_delay"],
appointment_data["encrypted_blob"],
)
dict_appointment = json.loads(appointment.to_json())
assert (
appointment_data["locator"] == dict_appointment["locator"]
and appointment_data["start_time"] == dict_appointment["start_time"]
and appointment_data["end_time"] == dict_appointment["end_time"]
and appointment_data["to_self_delay"] == dict_appointment["to_self_delay"]
and EncryptedBlob(appointment_data["encrypted_blob"]) == EncryptedBlob(dict_appointment["encrypted_blob"])
)
def test_from_dict(appointment_data):
# The appointment should be build if we don't miss any field
appointment = Appointment.from_dict(appointment_data)
assert isinstance(appointment, Appointment)
# Otherwise it should fail
for key in appointment_data.keys():
prev_val = appointment_data[key]
appointment_data[key] = None
try:
Appointment.from_dict(appointment_data)
assert False
except ValueError:
appointment_data[key] = prev_val
assert True

View File

@@ -0,0 +1,21 @@
from binascii import unhexlify
from pisa import c_logger
from apps.cli.blob import Blob
from test.pisa.unit.conftest import get_random_value_hex
c_logger.disabled = True
def test_init_blob():
data = get_random_value_hex(64)
blob = Blob(data)
assert isinstance(blob, Blob)
# Wrong data
try:
Blob(unhexlify(get_random_value_hex(64)))
assert False, "Able to create blob with wrong data"
except ValueError:
assert True

View File

@@ -0,0 +1,91 @@
import pytest
from pisa import c_logger
from pisa.block_processor import BlockProcessor
from test.pisa.unit.conftest import get_random_value_hex, generate_block, generate_blocks
c_logger.disabled = True
hex_tx = (
"0100000001c997a5e56e104102fa209c6a852dd90660a20b2d9c352423edce25857fcd3704000000004847304402"
"204e45e16932b8af514961a1d3a1a25fdf3f4f7732e9d624c6c61548ab5fb8cd410220181522ec8eca07de4860a4"
"acdd12909d831cc56cbbac4622082221a8768d1d0901ffffffff0200ca9a3b00000000434104ae1a62fe09c5f51b"
"13905f07f06b99a2f7159b2225f374cd378d71302fa28414e7aab37397f554a7df5f142c21c1b7303b8a0626f1ba"
"ded5c72a704f7e6cd84cac00286bee0000000043410411db93e1dcdb8a016b49840f8c53bc1eb68a382e97b1482e"
"cad7b148a6909a5cb2e0eaddfb84ccf9744464f82e160bfa9b8b64f9d4c03f999b8643f656b412a3ac00000000"
)
@pytest.fixture
def best_block_hash():
return BlockProcessor.get_best_block_hash()
def test_get_best_block_hash(run_bitcoind, best_block_hash):
# As long as bitcoind is running (or mocked in this case) we should always a block hash
assert best_block_hash is not None and isinstance(best_block_hash, str)
def test_get_block(best_block_hash):
# Getting a block from a block hash we are aware of should return data
block = BlockProcessor.get_block(best_block_hash)
# Checking that the received block has at least the fields we need
# FIXME: We could be more strict here, but we'll need to add those restrictions to bitcoind_sim too
assert isinstance(block, dict)
assert block.get("hash") == best_block_hash and "height" in block and "previousblockhash" in block and "tx" in block
def test_get_random_block():
block = BlockProcessor.get_block(get_random_value_hex(32))
assert block is None
def test_get_block_count():
block_count = BlockProcessor.get_block_count()
assert isinstance(block_count, int) and block_count >= 0
def test_decode_raw_transaction():
# We cannot exhaustively test this (we rely on bitcoind for this) but we can try to decode a correct transaction
assert BlockProcessor.decode_raw_transaction(hex_tx) is not None
def test_decode_raw_transaction_invalid():
# Same but with an invalid one
assert BlockProcessor.decode_raw_transaction(hex_tx[::-1]) is None
def test_get_missed_blocks():
block_processor = BlockProcessor()
target_block = block_processor.get_best_block_hash()
# Generate some blocks and store the hash in a list
missed_blocks = []
for _ in range(5):
generate_block()
missed_blocks.append(BlockProcessor.get_best_block_hash())
# Check what we've missed
assert block_processor.get_missed_blocks(target_block) == missed_blocks
# We can see how it does not work if we replace the target by the first element in the list
block_tip = missed_blocks[0]
assert block_processor.get_missed_blocks(block_tip) != missed_blocks
# But it does again if we skip that block
assert block_processor.get_missed_blocks(block_tip) == missed_blocks[1:]
def test_get_distance_to_tip():
target_distance = 5
block_processor = BlockProcessor()
target_block = block_processor.get_best_block_hash()
# Mine some blocks up to the target distance
generate_blocks(target_distance)
# Check if the distance is properly computed
assert block_processor.get_distance_to_tip(target_block) == target_distance

View File

@@ -0,0 +1,76 @@
from uuid import uuid4
from pisa.builder import Builder
from test.pisa.unit.conftest import get_random_value_hex, generate_dummy_appointment, generate_dummy_tracker
def test_build_appointments():
appointments_data = {}
# Create some appointment data
for i in range(10):
appointment, _ = generate_dummy_appointment(real_height=False)
uuid = uuid4().hex
appointments_data[uuid] = appointment.to_dict()
# Add some additional appointments that share the same locator to test all the builder's cases
if i % 2 == 0:
locator = appointment.locator
appointment, _ = generate_dummy_appointment(real_height=False)
uuid = uuid4().hex
appointment.locator = locator
appointments_data[uuid] = appointment.to_dict()
# Use the builder to create the data structures
appointments, locator_uuid_map = Builder.build_appointments(appointments_data)
# Check that the created appointments match the data
for uuid, appointment in appointments.items():
assert uuid in appointments_data.keys()
assert appointments_data[uuid] == appointment.to_dict()
assert uuid in locator_uuid_map[appointment.locator]
def test_build_trackers():
trackers_data = {}
# Create some trackers data
for i in range(10):
tracker = generate_dummy_tracker()
trackers_data[uuid4().hex] = tracker.to_dict()
# Add some additional trackers that share the same locator to test all the builder's cases
if i % 2 == 0:
penalty_txid = tracker.penalty_txid
tracker = generate_dummy_tracker()
tracker.penalty_txid = penalty_txid
trackers_data[uuid4().hex] = tracker.to_dict()
trackers, tx_tracker_map = Builder.build_trackers(trackers_data)
# Check that the built trackers match the data
for uuid, tracker in trackers.items():
assert uuid in trackers_data.keys()
tracker_dict = tracker.to_dict()
# The locator is not part of the tracker_data found in the database (for now)
assert trackers_data[uuid] == tracker_dict
assert uuid in tx_tracker_map[tracker.penalty_txid]
def test_build_block_queue():
# Create some random block hashes and construct the queue with them
blocks = [get_random_value_hex(32) for _ in range(10)]
queue = Builder.build_block_queue(blocks)
# Make sure every block is in the queue and that there are not additional ones
while not queue.empty():
block = queue.get()
assert block in blocks
blocks.remove(block)
assert len(blocks) == 0

View File

@@ -0,0 +1,74 @@
import pytest
from pisa import c_logger
from pisa.carrier import Carrier
from test.simulator.utils import sha256d
from test.simulator.transaction import TX
from test.pisa.unit.conftest import generate_blocks, get_random_value_hex
from pisa.rpc_errors import RPC_VERIFY_ALREADY_IN_CHAIN, RPC_DESERIALIZATION_ERROR
c_logger.disabled = True
# FIXME: This test do not fully cover the carrier since the simulator does not support every single error bitcoind may
# return for RPC_VERIFY_REJECTED and RPC_VERIFY_ERROR. Further development of the simulator / mocks or simulation
# with bitcoind is required
sent_txs = []
@pytest.fixture(scope="module")
def carrier():
return Carrier()
def test_send_transaction(run_bitcoind, carrier):
tx = TX.create_dummy_transaction()
txid = sha256d(tx)
receipt = carrier.send_transaction(tx, txid)
assert receipt.delivered is True
def test_send_double_spending_transaction(carrier):
# We can test what happens if the same transaction is sent twice
tx = TX.create_dummy_transaction()
txid = sha256d(tx)
receipt = carrier.send_transaction(tx, txid)
sent_txs.append(txid)
# Wait for a block to be mined
generate_blocks(2)
# Try to send it again
receipt2 = carrier.send_transaction(tx, txid)
# The carrier should report delivered True for both, but in the second case the transaction was already delivered
# (either by himself or someone else)
assert receipt.delivered is True
assert receipt2.delivered is True and receipt2.confirmations >= 1 and receipt2.reason == RPC_VERIFY_ALREADY_IN_CHAIN
def test_send_transaction_invalid_format(carrier):
# Test sending a transaction that does not fits the format
tx = TX.create_dummy_transaction()
txid = sha256d(tx)
receipt = carrier.send_transaction(txid, txid)
assert receipt.delivered is False and receipt.reason == RPC_DESERIALIZATION_ERROR
def test_get_transaction():
# We should be able to get back every transaction we've sent
for tx in sent_txs:
tx_info = Carrier.get_transaction(tx)
assert tx_info is not None
def test_get_non_existing_transaction():
tx_info = Carrier.get_transaction(get_random_value_hex(32))
assert tx_info is None

View File

@@ -0,0 +1,158 @@
import random
from uuid import uuid4
from pisa import c_logger
from pisa.responder import TransactionTracker
from pisa.cleaner import Cleaner
from pisa.appointment import Appointment
from pisa.db_manager import WATCHER_PREFIX
from test.pisa.unit.conftest import get_random_value_hex
from common.constants import LOCATOR_LEN_BYTES, LOCATOR_LEN_HEX
CONFIRMATIONS = 6
ITEMS = 10
MAX_ITEMS = 100
ITERATIONS = 10
c_logger.disabled = True
# WIP: FIX CLEANER TESTS AFTER ADDING delete_complete_appointment
def set_up_appointments(db_manager, total_appointments):
appointments = dict()
locator_uuid_map = dict()
for i in range(total_appointments):
uuid = uuid4().hex
locator = get_random_value_hex(LOCATOR_LEN_BYTES)
appointment = Appointment(locator, None, None, None, None)
appointments[uuid] = appointment
locator_uuid_map[locator] = [uuid]
db_manager.store_watcher_appointment(uuid, appointment.to_json())
db_manager.store_update_locator_map(locator, uuid)
# Each locator can have more than one uuid assigned to it.
if i % 2:
uuid = uuid4().hex
appointments[uuid] = appointment
locator_uuid_map[locator].append(uuid)
db_manager.store_watcher_appointment(uuid, appointment.to_json())
db_manager.store_update_locator_map(locator, uuid)
return appointments, locator_uuid_map
def set_up_trackers(db_manager, total_trackers):
trackers = dict()
tx_tracker_map = dict()
for i in range(total_trackers):
uuid = uuid4().hex
# We use the same txid for penalty and dispute here, it shouldn't matter
penalty_txid = get_random_value_hex(32)
dispute_txid = get_random_value_hex(32)
locator = dispute_txid[:LOCATOR_LEN_HEX]
# Assign both penalty_txid and dispute_txid the same id (it shouldn't matter)
tracker = TransactionTracker(locator, dispute_txid, penalty_txid, None, None)
trackers[uuid] = tracker
tx_tracker_map[penalty_txid] = [uuid]
db_manager.store_responder_tracker(uuid, tracker.to_json())
db_manager.store_update_locator_map(tracker.locator, uuid)
# Each penalty_txid can have more than one uuid assigned to it.
if i % 2:
uuid = uuid4().hex
trackers[uuid] = tracker
tx_tracker_map[penalty_txid].append(uuid)
db_manager.store_responder_tracker(uuid, tracker.to_json())
db_manager.store_update_locator_map(tracker.locator, uuid)
return trackers, tx_tracker_map
def test_delete_expired_appointment(db_manager):
for _ in range(ITERATIONS):
appointments, locator_uuid_map = set_up_appointments(db_manager, MAX_ITEMS)
expired_appointments = random.sample(list(appointments.keys()), k=ITEMS)
Cleaner.delete_expired_appointment(expired_appointments, appointments, locator_uuid_map, db_manager)
assert not set(expired_appointments).issubset(appointments.keys())
def test_delete_completed_appointments(db_manager):
appointments, locator_uuid_map = set_up_appointments(db_manager, MAX_ITEMS)
uuids = list(appointments.keys())
for uuid in uuids:
Cleaner.delete_completed_appointment(uuid, appointments, locator_uuid_map, db_manager)
# All appointments should have been deleted
assert len(appointments) == 0
# Make sure that all appointments are flagged as triggered in the db
db_appointments = db_manager.load_appointments_db(prefix=WATCHER_PREFIX)
for uuid in uuids:
assert db_appointments[uuid]["triggered"] is True
def test_delete_completed_trackers_db_match(db_manager):
height = 0
for _ in range(ITERATIONS):
trackers, tx_tracker_map = set_up_trackers(db_manager, MAX_ITEMS)
selected_trackers = random.sample(list(trackers.keys()), k=ITEMS)
completed_trackers = [(tracker, 6) for tracker in selected_trackers]
Cleaner.delete_completed_trackers(completed_trackers, height, trackers, tx_tracker_map, db_manager)
assert not set(completed_trackers).issubset(trackers.keys())
def test_delete_completed_trackers_no_db_match(db_manager):
height = 0
for _ in range(ITERATIONS):
trackers, tx_tracker_map = set_up_trackers(db_manager, MAX_ITEMS)
selected_trackers = random.sample(list(trackers.keys()), k=ITEMS)
# Let's change some uuid's by creating new trackers that are not included in the db and share a penalty_txid
# with another tracker that is stored in the db.
for uuid in selected_trackers[: ITEMS // 2]:
penalty_txid = trackers[uuid].penalty_txid
dispute_txid = get_random_value_hex(32)
locator = dispute_txid[:LOCATOR_LEN_HEX]
new_uuid = uuid4().hex
trackers[new_uuid] = TransactionTracker(locator, dispute_txid, penalty_txid, None, None)
tx_tracker_map[penalty_txid].append(new_uuid)
selected_trackers.append(new_uuid)
# Let's add some random data
for i in range(ITEMS // 2):
uuid = uuid4().hex
penalty_txid = get_random_value_hex(32)
dispute_txid = get_random_value_hex(32)
locator = dispute_txid[:LOCATOR_LEN_HEX]
trackers[uuid] = TransactionTracker(locator, dispute_txid, penalty_txid, None, None)
tx_tracker_map[penalty_txid] = [uuid]
selected_trackers.append(uuid)
completed_trackers = [(tracker, 6) for tracker in selected_trackers]
# We should be able to delete the correct ones and not fail in the others
Cleaner.delete_completed_trackers(completed_trackers, height, trackers, tx_tracker_map, db_manager)
assert not set(completed_trackers).issubset(trackers.keys())

View File

@@ -0,0 +1,284 @@
import os
import json
import pytest
import shutil
from uuid import uuid4
from pisa.db_manager import DBManager
from pisa.db_manager import WATCHER_LAST_BLOCK_KEY, RESPONDER_LAST_BLOCK_KEY, LOCATOR_MAP_PREFIX
from common.constants import LOCATOR_LEN_BYTES
from test.pisa.unit.conftest import get_random_value_hex, generate_dummy_appointment
@pytest.fixture(scope="module")
def watcher_appointments():
return {uuid4().hex: generate_dummy_appointment(real_height=False)[0] for _ in range(10)}
@pytest.fixture(scope="module")
def responder_trackers():
return {get_random_value_hex(16): get_random_value_hex(32) for _ in range(10)}
def open_create_db(db_path):
try:
db_manager = DBManager(db_path)
return db_manager
except ValueError:
return False
def test_init():
db_path = "init_test_db"
# First we check if the db exists, and if so we delete it
if os.path.isdir(db_path):
shutil.rmtree(db_path)
# Check that the db can be created if it does not exist
db_manager = open_create_db(db_path)
assert isinstance(db_manager, DBManager)
db_manager.db.close()
# Check that we can open an already create db
db_manager = open_create_db(db_path)
assert isinstance(db_manager, DBManager)
db_manager.db.close()
# Check we cannot create/open a db with an invalid parameter
assert open_create_db(0) is False
# Removing test db
shutil.rmtree(db_path)
def test_load_appointments_db(db_manager):
# Let's made up a prefix and try to load data from the database using it
prefix = "XX"
db_appointments = db_manager.load_appointments_db(prefix)
assert len(db_appointments) == 0
# We can add a bunch of data to the db and try again (data is stored in json by the manager)
local_appointments = {}
for _ in range(10):
key = get_random_value_hex(16)
value = get_random_value_hex(32)
local_appointments[key] = value
db_manager.db.put((prefix + key).encode("utf-8"), json.dumps({"value": value}).encode("utf-8"))
db_appointments = db_manager.load_appointments_db(prefix)
# Check that both keys and values are the same
assert db_appointments.keys() == local_appointments.keys()
values = [appointment["value"] for appointment in db_appointments.values()]
assert set(values) == set(local_appointments.values()) and (len(values) == len(local_appointments))
def test_get_last_known_block():
db_path = "empty_db"
# First we check if the db exists, and if so we delete it
if os.path.isdir(db_path):
shutil.rmtree(db_path)
# Check that the db can be created if it does not exist
db_manager = open_create_db(db_path)
# Trying to get any last block for either the watcher or the responder should return None for an empty db
for key in [WATCHER_LAST_BLOCK_KEY, RESPONDER_LAST_BLOCK_KEY]:
assert db_manager.get_last_known_block(key) is None
# After saving some block in the db we should get that exact value
for key in [WATCHER_LAST_BLOCK_KEY, RESPONDER_LAST_BLOCK_KEY]:
block_hash = get_random_value_hex(32)
db_manager.db.put(key.encode("utf-8"), block_hash.encode("utf-8"))
assert db_manager.get_last_known_block(key) == block_hash
# Removing test db
shutil.rmtree(db_path)
def test_create_entry(db_manager):
key = get_random_value_hex(16)
value = get_random_value_hex(32)
# Adding a value with no prefix (create entry encodes values in utf-8 internally)
db_manager.create_entry(key, value)
# We should be able to get it straightaway from the key
assert db_manager.db.get(key.encode("utf-8")).decode("utf-8") == value
# If we prefix the key we should be able to get it if we add the prefix, but not otherwise
key = get_random_value_hex(16)
prefix = "w"
db_manager.create_entry(key, value, prefix=prefix)
assert db_manager.db.get((prefix + key).encode("utf-8")).decode("utf-8") == value
assert db_manager.db.get(key.encode("utf-8")) is None
# Same if we try to use any other prefix
another_prefix = "r"
assert db_manager.db.get((another_prefix + key).encode("utf-8")) is None
def test_delete_entry(db_manager):
# Let's first get the key all the things we've wrote so far in the db
data = [k.decode("utf-8") for k, v in db_manager.db.iterator()]
# Let's empty the db now
for key in data:
db_manager.delete_entry(key)
assert len([k for k, v in db_manager.db.iterator()]) == 0
# Let's check that the same works if a prefix is provided.
prefix = "r"
key = get_random_value_hex(16)
value = get_random_value_hex(32)
db_manager.create_entry(key, value, prefix)
# Checks it's there
assert db_manager.db.get((prefix + key).encode("utf-8")).decode("utf-8") == value
# And now it's gone
db_manager.delete_entry(key, prefix)
assert db_manager.db.get((prefix + key).encode("utf-8")) is None
def test_load_watcher_appointments_empty(db_manager):
assert len(db_manager.load_watcher_appointments()) == 0
def test_load_responder_trackers_empty(db_manager):
assert len(db_manager.load_responder_trackers()) == 0
def test_load_locator_map_empty(db_manager):
assert db_manager.load_locator_map(get_random_value_hex(LOCATOR_LEN_BYTES)) is None
def test_store_update_locator_map_empty(db_manager):
uuid = uuid4().hex
locator = get_random_value_hex(LOCATOR_LEN_BYTES)
db_manager.store_update_locator_map(locator, uuid)
# Check that the locator map has been properly stored
assert db_manager.load_locator_map(locator) == [uuid]
# If we try to add the same uuid again the list shouldn't change
db_manager.store_update_locator_map(locator, uuid)
assert db_manager.load_locator_map(locator) == [uuid]
# Add another uuid to the same locator and check that it also works
uuid2 = uuid4().hex
db_manager.store_update_locator_map(locator, uuid2)
assert set(db_manager.load_locator_map(locator)) == set([uuid, uuid2])
def test_delete_locator_map(db_manager):
locator_maps = db_manager.load_appointments_db(prefix=LOCATOR_MAP_PREFIX)
assert len(locator_maps) != 0
for locator, uuids in locator_maps.items():
db_manager.delete_locator_map(locator)
locator_maps = db_manager.load_appointments_db(prefix=LOCATOR_MAP_PREFIX)
assert len(locator_maps) == 0
def test_store_load_watcher_appointment(db_manager, watcher_appointments):
for uuid, appointment in watcher_appointments.items():
db_manager.store_watcher_appointment(uuid, appointment.to_json())
db_watcher_appointments = db_manager.load_watcher_appointments()
# Check that the two appointment collections are equal by checking:
# - Their size is equal
# - Each element in one collection exists in the other
assert watcher_appointments.keys() == db_watcher_appointments.keys()
for uuid, appointment in watcher_appointments.items():
assert json.dumps(db_watcher_appointments[uuid], sort_keys=True, separators=(",", ":")) == appointment.to_json()
def test_store_load_triggered_appointment(db_manager):
db_watcher_appointments = db_manager.load_watcher_appointments()
db_watcher_appointments_with_triggered = db_manager.load_watcher_appointments(include_triggered=True)
assert db_watcher_appointments == db_watcher_appointments_with_triggered
# Create an appointment flagged as triggered
triggered_appointment, _ = generate_dummy_appointment(real_height=False)
uuid = uuid4().hex
db_manager.store_watcher_appointment(uuid, triggered_appointment.to_json(triggered=True))
# The new appointment is grabbed only if we set include_triggered
assert db_watcher_appointments == db_manager.load_watcher_appointments()
assert uuid in db_manager.load_watcher_appointments(include_triggered=True)
def test_store_load_responder_trackers(db_manager, responder_trackers):
for key, value in responder_trackers.items():
db_manager.store_responder_tracker(key, json.dumps({"value": value}))
db_responder_trackers = db_manager.load_responder_trackers()
values = [tracker["value"] for tracker in db_responder_trackers.values()]
assert responder_trackers.keys() == db_responder_trackers.keys()
assert set(responder_trackers.values()) == set(values) and len(responder_trackers) == len(values)
def test_delete_watcher_appointment(db_manager, watcher_appointments):
# Let's delete all we added
db_watcher_appointments = db_manager.load_watcher_appointments(include_triggered=True)
assert len(db_watcher_appointments) != 0
for key in watcher_appointments.keys():
db_manager.delete_watcher_appointment(key)
db_watcher_appointments = db_manager.load_watcher_appointments()
assert len(db_watcher_appointments) == 0
def test_delete_responder_tracker(db_manager, responder_trackers):
# Same for the responder
db_responder_trackers = db_manager.load_responder_trackers()
assert len(db_responder_trackers) != 0
for key in responder_trackers.keys():
db_manager.delete_responder_tracker(key)
db_responder_trackers = db_manager.load_responder_trackers()
assert len(db_responder_trackers) == 0
def test_store_load_last_block_hash_watcher(db_manager):
# Let's first create a made up block hash
local_last_block_hash = get_random_value_hex(32)
db_manager.store_last_block_hash_watcher(local_last_block_hash)
db_last_block_hash = db_manager.load_last_block_hash_watcher()
assert local_last_block_hash == db_last_block_hash
def test_store_load_last_block_hash_responder(db_manager):
# Same for the responder
local_last_block_hash = get_random_value_hex(32)
db_manager.store_last_block_hash_responder(local_last_block_hash)
db_last_block_hash = db_manager.load_last_block_hash_responder()
assert local_last_block_hash == db_last_block_hash

View File

@@ -0,0 +1,19 @@
from pisa import c_logger
from pisa.encrypted_blob import EncryptedBlob
from test.pisa.unit.conftest import get_random_value_hex
c_logger.disabled = True
def test_init_encrypted_blob():
# No much to test here, basically that the object is properly created
data = get_random_value_hex(64)
assert EncryptedBlob(data).data == data
def test_equal():
data = get_random_value_hex(64)
e_blob1 = EncryptedBlob(data)
e_blob2 = EncryptedBlob(data)
assert e_blob1 == e_blob2 and id(e_blob1) != id(e_blob2)

View File

@@ -0,0 +1,237 @@
from binascii import hexlify, unhexlify
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives.asymmetric import ec
from cryptography.hazmat.primitives import serialization
from pisa import c_logger
from pisa.errors import *
from pisa.inspector import Inspector
from pisa.appointment import Appointment
from pisa.block_processor import BlockProcessor
from pisa.conf import MIN_TO_SELF_DELAY
from test.pisa.unit.conftest import get_random_value_hex, generate_dummy_appointment_data, generate_keypair
from common.constants import LOCATOR_LEN_BYTES, LOCATOR_LEN_HEX
from common.cryptographer import Cryptographer
c_logger.disabled = True
inspector = Inspector()
APPOINTMENT_OK = (0, None)
NO_HEX_STRINGS = [
"R" * LOCATOR_LEN_HEX,
get_random_value_hex(LOCATOR_LEN_BYTES - 1) + "PP",
"$" * LOCATOR_LEN_HEX,
" " * LOCATOR_LEN_HEX,
]
WRONG_TYPES = [
[],
"",
get_random_value_hex(LOCATOR_LEN_BYTES),
3.2,
2.0,
(),
object,
{},
" " * LOCATOR_LEN_HEX,
object(),
]
WRONG_TYPES_NO_STR = [[], unhexlify(get_random_value_hex(LOCATOR_LEN_BYTES)), 3.2, 2.0, (), object, {}, object()]
def test_check_locator():
# Right appointment type, size and format
locator = get_random_value_hex(LOCATOR_LEN_BYTES)
assert Inspector.check_locator(locator) == APPOINTMENT_OK
# Wrong size (too big)
locator = get_random_value_hex(LOCATOR_LEN_BYTES + 1)
assert Inspector.check_locator(locator)[0] == APPOINTMENT_WRONG_FIELD_SIZE
# Wrong size (too small)
locator = get_random_value_hex(LOCATOR_LEN_BYTES - 1)
assert Inspector.check_locator(locator)[0] == APPOINTMENT_WRONG_FIELD_SIZE
# Empty
locator = None
assert Inspector.check_locator(locator)[0] == APPOINTMENT_EMPTY_FIELD
# Wrong type (several types tested, it should do for anything that is not a string)
locators = [[], -1, 3.2, 0, 4, (), object, {}, object()]
for locator in locators:
assert Inspector.check_locator(locator)[0] == APPOINTMENT_WRONG_FIELD_TYPE
# Wrong format (no hex)
locators = NO_HEX_STRINGS
for locator in locators:
assert Inspector.check_locator(locator)[0] == APPOINTMENT_WRONG_FIELD_FORMAT
def test_check_start_time():
# Time is defined in block height
current_time = 100
# Right format and right value (start time in the future)
start_time = 101
assert Inspector.check_start_time(start_time, current_time) == APPOINTMENT_OK
# Start time too small (either same block or block in the past)
start_times = [100, 99, 98, -1]
for start_time in start_times:
assert Inspector.check_start_time(start_time, current_time)[0] == APPOINTMENT_FIELD_TOO_SMALL
# Empty field
start_time = None
assert Inspector.check_start_time(start_time, current_time)[0] == APPOINTMENT_EMPTY_FIELD
# Wrong data type
start_times = WRONG_TYPES
for start_time in start_times:
assert Inspector.check_start_time(start_time, current_time)[0] == APPOINTMENT_WRONG_FIELD_TYPE
def test_check_end_time():
# Time is defined in block height
current_time = 100
start_time = 120
# Right format and right value (start time before end and end in the future)
end_time = 121
assert Inspector.check_end_time(end_time, start_time, current_time) == APPOINTMENT_OK
# End time too small (start time after end time)
end_times = [120, 119, 118, -1]
for end_time in end_times:
assert Inspector.check_end_time(end_time, start_time, current_time)[0] == APPOINTMENT_FIELD_TOO_SMALL
# End time too small (either same height as current block or in the past)
current_time = 130
end_times = [130, 129, 128, -1]
for end_time in end_times:
assert Inspector.check_end_time(end_time, start_time, current_time)[0] == APPOINTMENT_FIELD_TOO_SMALL
# Empty field
end_time = None
assert Inspector.check_end_time(end_time, start_time, current_time)[0] == APPOINTMENT_EMPTY_FIELD
# Wrong data type
end_times = WRONG_TYPES
for end_time in end_times:
assert Inspector.check_end_time(end_time, start_time, current_time)[0] == APPOINTMENT_WRONG_FIELD_TYPE
def test_check_to_self_delay():
# Right value, right format
to_self_delays = [MIN_TO_SELF_DELAY, MIN_TO_SELF_DELAY + 1, MIN_TO_SELF_DELAY + 1000]
for to_self_delay in to_self_delays:
assert Inspector.check_to_self_delay(to_self_delay) == APPOINTMENT_OK
# to_self_delay too small
to_self_delays = [MIN_TO_SELF_DELAY - 1, MIN_TO_SELF_DELAY - 2, 0, -1, -1000]
for to_self_delay in to_self_delays:
assert Inspector.check_to_self_delay(to_self_delay)[0] == APPOINTMENT_FIELD_TOO_SMALL
# Empty field
to_self_delay = None
assert Inspector.check_to_self_delay(to_self_delay)[0] == APPOINTMENT_EMPTY_FIELD
# Wrong data type
to_self_delays = WRONG_TYPES
for to_self_delay in to_self_delays:
assert Inspector.check_to_self_delay(to_self_delay)[0] == APPOINTMENT_WRONG_FIELD_TYPE
def test_check_blob():
# Right format and length
encrypted_blob = get_random_value_hex(120)
assert Inspector.check_blob(encrypted_blob) == APPOINTMENT_OK
# # Wrong content
# # FIXME: There is not proper defined format for this yet. It should be restricted by size at least, and check it
# # is multiple of the block size defined by the encryption function.
# Wrong type
encrypted_blobs = WRONG_TYPES_NO_STR
for encrypted_blob in encrypted_blobs:
assert Inspector.check_blob(encrypted_blob)[0] == APPOINTMENT_WRONG_FIELD_TYPE
# Empty field
encrypted_blob = None
assert Inspector.check_blob(encrypted_blob)[0] == APPOINTMENT_EMPTY_FIELD
# Wrong format (no hex)
encrypted_blobs = NO_HEX_STRINGS
for encrypted_blob in encrypted_blobs:
assert Inspector.check_blob(encrypted_blob)[0] == APPOINTMENT_WRONG_FIELD_FORMAT
def test_check_appointment_signature():
# The inspector receives the public key as hex
client_sk, client_pk = generate_keypair()
client_pk_der = client_pk.public_bytes(
encoding=serialization.Encoding.DER, format=serialization.PublicFormat.SubjectPublicKeyInfo
)
client_pk_hex = hexlify(client_pk_der).decode("utf-8")
dummy_appointment_data, _ = generate_dummy_appointment_data(real_height=False)
assert Inspector.check_appointment_signature(
dummy_appointment_data["appointment"], dummy_appointment_data["signature"], dummy_appointment_data["public_key"]
)
fake_sk = ec.generate_private_key(ec.SECP256K1, default_backend())
# Create a bad signature to make sure inspector rejects it
bad_signature = Cryptographer.sign(Cryptographer.signature_format(dummy_appointment_data["appointment"]), fake_sk)
assert (
Inspector.check_appointment_signature(dummy_appointment_data["appointment"], bad_signature, client_pk_hex)[0]
== APPOINTMENT_INVALID_SIGNATURE
)
def test_inspect(run_bitcoind):
# At this point every single check function has been already tested, let's test inspect with an invalid and a valid
# appointments.
client_sk, client_pk = generate_keypair()
client_pk_der = client_pk.public_bytes(
encoding=serialization.Encoding.DER, format=serialization.PublicFormat.SubjectPublicKeyInfo
)
client_pk_hex = hexlify(client_pk_der).decode("utf-8")
# Invalid appointment, every field is empty
appointment_data = dict()
signature = Cryptographer.sign(Cryptographer.signature_format(appointment_data), client_sk)
appointment = inspector.inspect(appointment_data, signature, client_pk)
assert type(appointment) == tuple and appointment[0] != 0
# Valid appointment
locator = get_random_value_hex(LOCATOR_LEN_BYTES)
start_time = BlockProcessor.get_block_count() + 5
end_time = start_time + 20
to_self_delay = MIN_TO_SELF_DELAY
encrypted_blob = get_random_value_hex(64)
appointment_data = {
"locator": locator,
"start_time": start_time,
"end_time": end_time,
"to_self_delay": to_self_delay,
"encrypted_blob": encrypted_blob,
}
signature = Cryptographer.sign(Cryptographer.signature_format(appointment_data), client_sk)
appointment = inspector.inspect(appointment_data, signature, client_pk_hex)
assert (
type(appointment) == Appointment
and appointment.locator == locator
and appointment.start_time == start_time
and appointment.end_time == end_time
and appointment.to_self_delay == to_self_delay
and appointment.encrypted_blob.data == encrypted_blob
)

View File

@@ -0,0 +1,499 @@
import json
import pytest
import random
from uuid import uuid4
from shutil import rmtree
from copy import deepcopy
from threading import Thread
from queue import Queue, Empty
from pisa import c_logger
from pisa.db_manager import DBManager
from pisa.responder import Responder, TransactionTracker
from pisa.block_processor import BlockProcessor
from pisa.tools import bitcoin_cli
from common.constants import LOCATOR_LEN_HEX
from common.tools import check_sha256_hex_format
from test.simulator.utils import sha256d
from test.simulator.bitcoind_sim import TX
from test.pisa.unit.conftest import generate_block, generate_blocks, get_random_value_hex
c_logger.disabled = True
@pytest.fixture(scope="module")
def responder(db_manager):
return Responder(db_manager)
@pytest.fixture()
def temp_db_manager():
db_name = get_random_value_hex(8)
db_manager = DBManager(db_name)
yield db_manager
db_manager.db.close()
rmtree(db_name)
def create_dummy_tracker_data(random_txid=False, penalty_rawtx=None):
# The following transaction data corresponds to a valid transaction. For some test it may be interesting to have
# some valid data, but for others we may need multiple different penalty_txids.
dispute_txid = "0437cd7f8525ceed2324359c2d0ba26006d92d856a9c20fa0241106ee5a597c9"
penalty_txid = "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16"
if penalty_rawtx is None:
penalty_rawtx = (
"0100000001c997a5e56e104102fa209c6a852dd90660a20b2d9c352423edce25857fcd3704000000004847304402"
"204e45e16932b8af514961a1d3a1a25fdf3f4f7732e9d624c6c61548ab5fb8cd410220181522ec8eca07de4860a4"
"acdd12909d831cc56cbbac4622082221a8768d1d0901ffffffff0200ca9a3b00000000434104ae1a62fe09c5f51b"
"13905f07f06b99a2f7159b2225f374cd378d71302fa28414e7aab37397f554a7df5f142c21c1b7303b8a0626f1ba"
"ded5c72a704f7e6cd84cac00286bee0000000043410411db93e1dcdb8a016b49840f8c53bc1eb68a382e97b1482e"
"cad7b148a6909a5cb2e0eaddfb84ccf9744464f82e160bfa9b8b64f9d4c03f999b8643f656b412a3ac00000000"
)
else:
penalty_txid = sha256d(penalty_rawtx)
if random_txid is True:
penalty_txid = get_random_value_hex(32)
appointment_end = bitcoin_cli().getblockcount() + 2
locator = dispute_txid[:LOCATOR_LEN_HEX]
return locator, dispute_txid, penalty_txid, penalty_rawtx, appointment_end
def create_dummy_tracker(random_txid=False, penalty_rawtx=None):
locator, dispute_txid, penalty_txid, penalty_rawtx, appointment_end = create_dummy_tracker_data(
random_txid, penalty_rawtx
)
return TransactionTracker(locator, dispute_txid, penalty_txid, penalty_rawtx, appointment_end)
def test_tracker_init(run_bitcoind):
locator, dispute_txid, penalty_txid, penalty_rawtx, appointment_end = create_dummy_tracker_data()
tracker = TransactionTracker(locator, dispute_txid, penalty_txid, penalty_rawtx, appointment_end)
assert (
tracker.dispute_txid == dispute_txid
and tracker.penalty_txid == penalty_txid
and tracker.penalty_rawtx == penalty_rawtx
and tracker.appointment_end == appointment_end
)
def test_on_sync(run_bitcoind, responder):
# We're on sync if we're 1 or less blocks behind the tip
chain_tip = BlockProcessor.get_best_block_hash()
assert Responder.on_sync(chain_tip) is True
generate_block()
assert Responder.on_sync(chain_tip) is True
def test_on_sync_fail(responder):
# This should fail if we're more than 1 block behind the tip
chain_tip = BlockProcessor.get_best_block_hash()
generate_blocks(2)
assert Responder.on_sync(chain_tip) is False
def test_tracker_to_dict():
tracker = create_dummy_tracker()
tracker_dict = tracker.to_dict()
assert (
tracker.locator == tracker_dict["locator"]
and tracker.penalty_rawtx == tracker_dict["penalty_rawtx"]
and tracker.appointment_end == tracker_dict["appointment_end"]
)
def test_tracker_to_json():
tracker = create_dummy_tracker()
tracker_dict = json.loads(tracker.to_json())
assert (
tracker.locator == tracker_dict["locator"]
and tracker.penalty_rawtx == tracker_dict["penalty_rawtx"]
and tracker.appointment_end == tracker_dict["appointment_end"]
)
def test_tracker_from_dict():
tracker_dict = create_dummy_tracker().to_dict()
new_tracker = TransactionTracker.from_dict(tracker_dict)
assert tracker_dict == new_tracker.to_dict()
def test_tracker_from_dict_invalid_data():
tracker_dict = create_dummy_tracker().to_dict()
for value in ["dispute_txid", "penalty_txid", "penalty_rawtx", "appointment_end"]:
tracker_dict_copy = deepcopy(tracker_dict)
tracker_dict_copy[value] = None
try:
TransactionTracker.from_dict(tracker_dict_copy)
assert False
except ValueError:
assert True
def test_init_responder(responder):
assert type(responder.trackers) is dict and len(responder.trackers) == 0
assert type(responder.tx_tracker_map) is dict and len(responder.tx_tracker_map) == 0
assert type(responder.unconfirmed_txs) is list and len(responder.unconfirmed_txs) == 0
assert type(responder.missed_confirmations) is dict and len(responder.missed_confirmations) == 0
assert responder.block_queue.empty()
assert responder.asleep is True
assert responder.zmq_subscriber is None
def test_handle_breach(db_manager):
responder = Responder(db_manager)
uuid = uuid4().hex
tracker = create_dummy_tracker()
# 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.appointment_end,
block_hash=get_random_value_hex(32),
)
assert receipt.delivered is True
# The responder automatically fires add_tracker on adding a tracker if it is asleep. We need to stop the processes now.
# To do so we delete all the trackers, stop the zmq and create a new fake block to unblock the queue.get method
responder.trackers = dict()
responder.zmq_subscriber.terminate = True
responder.block_queue.put(get_random_value_hex(32))
def test_add_bad_response(responder):
uuid = uuid4().hex
tracker = create_dummy_tracker()
# Now that the asleep / awake functionality has been tested we can avoid manually killing the responder by setting
# to awake. That will prevent the zmq thread to be launched again.
responder.asleep = False
# A txid instead of a rawtx should be enough for unit tests using the bitcoind mock, better tests are needed though.
tracker.penalty_rawtx = tracker.penalty_txid
# 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.appointment_end,
block_hash=get_random_value_hex(32),
)
assert receipt.delivered is False
def test_add_tracker(responder):
responder.asleep = False
for _ in range(20):
uuid = uuid4().hex
confirmations = 0
locator, dispute_txid, penalty_txid, penalty_rawtx, appointment_end = create_dummy_tracker_data(
random_txid=True
)
# Check the tracker is not within the responder trackers before adding it
assert uuid not in responder.trackers
assert penalty_txid not in responder.tx_tracker_map
assert penalty_txid not in responder.unconfirmed_txs
# And that it is afterwards
responder.add_tracker(uuid, locator, dispute_txid, penalty_txid, penalty_rawtx, appointment_end, confirmations)
assert uuid in responder.trackers
assert penalty_txid in responder.tx_tracker_map
assert penalty_txid in responder.unconfirmed_txs
# Check that the rest of tracker data also matches
tracker = responder.trackers[uuid]
assert (
tracker.dispute_txid == dispute_txid
and tracker.penalty_txid == penalty_txid
and tracker.penalty_rawtx == penalty_rawtx
and tracker.appointment_end == appointment_end
and tracker.appointment_end == appointment_end
)
def test_add_tracker_same_penalty_txid(responder):
# Create the same tracker using two different uuids
confirmations = 0
locator, dispute_txid, penalty_txid, penalty_rawtx, appointment_end = create_dummy_tracker_data(random_txid=True)
uuid_1 = uuid4().hex
uuid_2 = uuid4().hex
responder.add_tracker(uuid_1, locator, dispute_txid, penalty_txid, penalty_rawtx, appointment_end, confirmations)
responder.add_tracker(uuid_2, locator, dispute_txid, penalty_txid, penalty_rawtx, appointment_end, confirmations)
# Check that both trackers have been added
assert uuid_1 in responder.trackers and uuid_2 in responder.trackers
assert penalty_txid in responder.tx_tracker_map
assert penalty_txid in responder.unconfirmed_txs
# Check that the rest of tracker data also matches
for uuid in [uuid_1, uuid_2]:
tracker = responder.trackers[uuid]
assert (
tracker.dispute_txid == dispute_txid
and tracker.penalty_txid == penalty_txid
and tracker.penalty_rawtx == penalty_rawtx
and tracker.appointment_end == appointment_end
and tracker.appointment_end == appointment_end
)
def test_add_tracker_already_confirmed(responder):
responder.asleep = False
for i in range(20):
uuid = uuid4().hex
confirmations = i + 1
locator, dispute_txid, penalty_txid, penalty_rawtx, appointment_end = create_dummy_tracker_data(
penalty_rawtx=TX.create_dummy_transaction()
)
responder.add_tracker(uuid, locator, dispute_txid, penalty_txid, penalty_rawtx, appointment_end, confirmations)
assert penalty_txid not in responder.unconfirmed_txs
def test_do_subscribe(responder):
responder.block_queue = Queue()
zmq_thread = Thread(target=responder.do_subscribe)
zmq_thread.daemon = True
zmq_thread.start()
try:
generate_block()
block_hash = responder.block_queue.get()
assert check_sha256_hex_format(block_hash)
except Empty:
assert False
def test_do_watch(temp_db_manager):
responder = Responder(temp_db_manager)
responder.block_queue = Queue()
zmq_thread = Thread(target=responder.do_subscribe)
zmq_thread.daemon = True
zmq_thread.start()
trackers = [create_dummy_tracker(penalty_rawtx=TX.create_dummy_transaction()) for _ in range(20)]
# Let's set up the trackers first
for tracker in trackers:
uuid = uuid4().hex
responder.trackers[uuid] = tracker
responder.tx_tracker_map[tracker.penalty_txid] = [uuid]
responder.missed_confirmations[tracker.penalty_txid] = 0
responder.unconfirmed_txs.append(tracker.penalty_txid)
# Let's start to watch
watch_thread = Thread(target=responder.do_watch)
watch_thread.daemon = True
watch_thread.start()
# And broadcast some of the transactions
broadcast_txs = []
for tracker in trackers[:5]:
bitcoin_cli().sendrawtransaction(tracker.penalty_rawtx)
broadcast_txs.append(tracker.penalty_txid)
# Mine a block
generate_block()
# The transactions we sent shouldn't be in the unconfirmed transaction list anymore
assert not set(broadcast_txs).issubset(responder.unconfirmed_txs)
# TODO: test that reorgs can be detected once data persistence is merged (new version of the simulator)
# Generating 5 additional blocks should complete the 5 trackers
generate_blocks(5)
assert not set(broadcast_txs).issubset(responder.tx_tracker_map)
# Do the rest
broadcast_txs = []
for tracker in trackers[5:]:
bitcoin_cli().sendrawtransaction(tracker.penalty_rawtx)
broadcast_txs.append(tracker.penalty_txid)
# Mine a block
generate_blocks(6)
assert len(responder.tx_tracker_map) == 0
assert responder.asleep is True
def test_check_confirmations(temp_db_manager):
responder = Responder(temp_db_manager)
responder.block_queue = Queue()
zmq_thread = Thread(target=responder.do_subscribe)
zmq_thread.daemon = True
zmq_thread.start()
# check_confirmations checks, given a list of transaction for a block, what of the known penalty transaction have
# been confirmed. To test this we need to create a list of transactions and the state of the responder
txs = [get_random_value_hex(32) for _ in range(20)]
# The responder has a list of unconfirmed transaction, let make that some of them are the ones we've received
responder.unconfirmed_txs = [get_random_value_hex(32) for _ in range(10)]
txs_subset = random.sample(txs, k=10)
responder.unconfirmed_txs.extend(txs_subset)
# We also need to add them to the tx_tracker_map since they would be there in normal conditions
responder.tx_tracker_map = {
txid: TransactionTracker(txid[:LOCATOR_LEN_HEX], txid, None, None, None) for txid in responder.unconfirmed_txs
}
# Let's make sure that there are no txs with missed confirmations yet
assert len(responder.missed_confirmations) == 0
responder.check_confirmations(txs)
# After checking confirmations the txs in txs_subset should be confirmed (not part of unconfirmed_txs anymore)
# and the rest should have a missing confirmation
for tx in txs_subset:
assert tx not in responder.unconfirmed_txs
for tx in responder.unconfirmed_txs:
assert responder.missed_confirmations[tx] == 1
# WIP: Check this properly, a bug pass unnoticed!
def test_get_txs_to_rebroadcast(responder):
# Let's create a few fake txids and assign at least 6 missing confirmations to each
txs_missing_too_many_conf = {get_random_value_hex(32): 6 + i for i in range(10)}
# Let's create some other transaction that has missed some confirmations but not that many
txs_missing_some_conf = {get_random_value_hex(32): 3 for _ in range(10)}
# All the txs in the first dict should be flagged as to_rebroadcast
responder.missed_confirmations = txs_missing_too_many_conf
txs_to_rebroadcast = responder.get_txs_to_rebroadcast()
assert txs_to_rebroadcast == list(txs_missing_too_many_conf.keys())
# Non of the txs in the second dict should be flagged
responder.missed_confirmations = txs_missing_some_conf
txs_to_rebroadcast = responder.get_txs_to_rebroadcast()
assert txs_to_rebroadcast == []
# Let's check that it also works with a mixed dict
responder.missed_confirmations.update(txs_missing_too_many_conf)
txs_to_rebroadcast = responder.get_txs_to_rebroadcast()
assert txs_to_rebroadcast == list(txs_missing_too_many_conf.keys())
def test_get_completed_trackers(db_manager):
initial_height = bitcoin_cli().getblockcount()
# Let's use a fresh responder for this to make it easier to compare the results
responder = Responder(db_manager)
# A complete tracker is a tracker that has reached the appointment end with enough confirmations (> MIN_CONFIRMATIONS)
# We'll create three type of transactions: end reached + enough conf, end reached + no enough conf, end not reached
trackers_end_conf = {
uuid4().hex: create_dummy_tracker(penalty_rawtx=TX.create_dummy_transaction()) for _ in range(10)
}
trackers_end_no_conf = {}
for _ in range(10):
tracker = create_dummy_tracker(penalty_rawtx=TX.create_dummy_transaction())
responder.unconfirmed_txs.append(tracker.penalty_txid)
trackers_end_no_conf[uuid4().hex] = tracker
trackers_no_end = {}
for _ in range(10):
tracker = create_dummy_tracker(penalty_rawtx=TX.create_dummy_transaction())
tracker.appointment_end += 10
trackers_no_end[uuid4().hex] = tracker
# Let's add all to the responder
responder.trackers.update(trackers_end_conf)
responder.trackers.update(trackers_end_no_conf)
responder.trackers.update(trackers_no_end)
for uuid, tracker in responder.trackers.items():
bitcoin_cli().sendrawtransaction(tracker.penalty_rawtx)
# The dummy appointments have a end_appointment time of current + 2, but trackers need at least 6 confs by default
generate_blocks(6)
# And now let's check
completed_trackers = responder.get_completed_trackers(initial_height + 6)
completed_trackers_ids = [tracker_id for tracker_id, confirmations in completed_trackers]
ended_trackers_keys = list(trackers_end_conf.keys())
assert set(completed_trackers_ids) == set(ended_trackers_keys)
# Generating 6 additional blocks should also confirm trackers_no_end
generate_blocks(6)
completed_trackers = responder.get_completed_trackers(initial_height + 12)
completed_trackers_ids = [tracker_id for tracker_id, confirmations in completed_trackers]
ended_trackers_keys.extend(list(trackers_no_end.keys()))
assert set(completed_trackers_ids) == set(ended_trackers_keys)
def test_rebroadcast(db_manager):
responder = Responder(db_manager)
responder.asleep = False
txs_to_rebroadcast = []
# Rebroadcast calls add_response with retry=True. The tracker data is already in trackers.
for i in range(20):
uuid = uuid4().hex
locator, dispute_txid, penalty_txid, penalty_rawtx, appointment_end = create_dummy_tracker_data(
penalty_rawtx=TX.create_dummy_transaction()
)
responder.trackers[uuid] = TransactionTracker(
locator, dispute_txid, penalty_txid, penalty_rawtx, appointment_end
)
responder.tx_tracker_map[penalty_txid] = [uuid]
responder.unconfirmed_txs.append(penalty_txid)
# Let's add some of the txs in the rebroadcast list
if (i % 2) == 0:
txs_to_rebroadcast.append(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

View File

@@ -0,0 +1,61 @@
from pisa import c_logger
from pisa.tools import can_connect_to_bitcoind, in_correct_network, bitcoin_cli
from common.tools import check_sha256_hex_format
c_logger.disabled = True
def test_in_correct_network(run_bitcoind):
# The simulator runs as if it was regtest, so every other network should fail
assert in_correct_network("mainnet") is False
assert in_correct_network("testnet") is False
assert in_correct_network("regtest") is True
def test_can_connect_to_bitcoind():
assert can_connect_to_bitcoind() is True
# def test_can_connect_to_bitcoind_bitcoin_not_running():
# # Kill the simulator thread and test the check fails
# bitcoind_process.kill()
# assert can_connect_to_bitcoind() is False
def test_bitcoin_cli():
try:
bitcoin_cli().help()
assert True
except Exception:
assert False
def test_check_sha256_hex_format():
assert check_sha256_hex_format(None) is False
assert check_sha256_hex_format("") is False
assert (
check_sha256_hex_format(0x0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF) is False
) # wrong type
assert (
check_sha256_hex_format("abcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcd") is True
) # lowercase
assert (
check_sha256_hex_format("ABCDEFABCDEFABCDEFABCDEFABCDEFABCDEFABCDEFABCDEFABCDEFABCDEFABCD") is True
) # uppercase
assert (
check_sha256_hex_format("0123456789abcdef0123456789ABCDEF0123456789abcdef0123456789ABCDEF") is True
) # mixed case
assert (
check_sha256_hex_format("0123456789012345678901234567890123456789012345678901234567890123") is True
) # only nums
assert (
check_sha256_hex_format("0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdf") is False
) # too short
assert (
check_sha256_hex_format("0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0") is False
) # too long
assert (
check_sha256_hex_format("g123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef") is False
) # non-hex

View File

@@ -0,0 +1,236 @@
import pytest
from uuid import uuid4
from threading import Thread
from queue import Queue, Empty
from cryptography.hazmat.primitives import serialization
from pisa import c_logger
from pisa.watcher import Watcher
from pisa.responder import Responder
from pisa.tools import bitcoin_cli
from test.pisa.unit.conftest import (
generate_block,
generate_blocks,
generate_dummy_appointment,
get_random_value_hex,
generate_keypair,
)
from pisa.conf import EXPIRY_DELTA, MAX_APPOINTMENTS
from common.tools import check_sha256_hex_format
from common.cryptographer import Cryptographer
c_logger.disabled = True
APPOINTMENTS = 5
START_TIME_OFFSET = 1
END_TIME_OFFSET = 1
TEST_SET_SIZE = 200
signing_key, public_key = generate_keypair()
sk_der = signing_key.private_bytes(
encoding=serialization.Encoding.DER,
format=serialization.PrivateFormat.TraditionalOpenSSL,
encryption_algorithm=serialization.NoEncryption(),
)
@pytest.fixture(scope="module")
def watcher(db_manager):
return Watcher(db_manager, sk_der)
@pytest.fixture(scope="module")
def txids():
return [get_random_value_hex(32) for _ in range(100)]
@pytest.fixture(scope="module")
def locator_uuid_map(txids):
return {Watcher.compute_locator(txid): uuid4().hex for txid in txids}
def create_appointments(n):
locator_uuid_map = dict()
appointments = dict()
dispute_txs = []
for i in range(n):
appointment, dispute_tx = generate_dummy_appointment(
start_time_offset=START_TIME_OFFSET, end_time_offset=END_TIME_OFFSET
)
uuid = uuid4().hex
appointments[uuid] = appointment
locator_uuid_map[appointment.locator] = [uuid]
dispute_txs.append(dispute_tx)
return appointments, locator_uuid_map, dispute_txs
def test_init(watcher):
assert type(watcher.appointments) is dict and len(watcher.appointments) == 0
assert type(watcher.locator_uuid_map) is dict and len(watcher.locator_uuid_map) == 0
assert watcher.block_queue.empty()
assert watcher.asleep is True
assert watcher.max_appointments == MAX_APPOINTMENTS
assert watcher.zmq_subscriber is None
assert type(watcher.responder) is Responder
def test_add_appointment(run_bitcoind, watcher):
# The watcher automatically fires do_watch and do_subscribe on adding an appointment if it is asleep (initial state)
# Avoid this by setting the state to awake.
watcher.asleep = False
# We should be able to add appointments up to the limit
for _ in range(10):
appointment, dispute_tx = generate_dummy_appointment(
start_time_offset=START_TIME_OFFSET, end_time_offset=END_TIME_OFFSET
)
added_appointment, sig = watcher.add_appointment(appointment)
assert added_appointment is True
assert Cryptographer.verify(Cryptographer.signature_format(appointment.to_dict()), sig, public_key)
# Check that we can also add an already added appointment (same locator)
added_appointment, sig = watcher.add_appointment(appointment)
assert added_appointment is True
assert Cryptographer.verify(Cryptographer.signature_format(appointment.to_dict()), sig, public_key)
def test_add_too_many_appointments(watcher):
# Any appointment on top of those should fail
watcher.appointments = dict()
for _ in range(MAX_APPOINTMENTS):
appointment, dispute_tx = generate_dummy_appointment(
start_time_offset=START_TIME_OFFSET, end_time_offset=END_TIME_OFFSET
)
added_appointment, sig = watcher.add_appointment(appointment)
assert added_appointment is True
assert Cryptographer.verify(Cryptographer.signature_format(appointment.to_dict()), sig, public_key)
appointment, dispute_tx = generate_dummy_appointment(
start_time_offset=START_TIME_OFFSET, end_time_offset=END_TIME_OFFSET
)
added_appointment, sig = watcher.add_appointment(appointment)
assert added_appointment is False
assert sig is None
def test_do_subscribe(watcher):
watcher.block_queue = Queue()
zmq_thread = Thread(target=watcher.do_subscribe)
zmq_thread.daemon = True
zmq_thread.start()
try:
generate_block()
block_hash = watcher.block_queue.get()
assert check_sha256_hex_format(block_hash)
except Empty:
assert False
def test_do_watch(watcher):
# We will wipe all the previous data and add 5 appointments
watcher.appointments, watcher.locator_uuid_map, dispute_txs = create_appointments(APPOINTMENTS)
watch_thread = Thread(target=watcher.do_watch)
watch_thread.daemon = True
watch_thread.start()
# Broadcast the first two
for dispute_tx in dispute_txs[:2]:
bitcoin_cli().sendrawtransaction(dispute_tx)
# After leaving some time for the block to be mined and processed, the number of appointments should have reduced
# by two
generate_blocks(START_TIME_OFFSET + END_TIME_OFFSET)
assert len(watcher.appointments) == APPOINTMENTS - 2
# The rest of appointments will timeout after the end (2) + EXPIRY_DELTA
# Wait for an additional block to be safe
generate_blocks(EXPIRY_DELTA + START_TIME_OFFSET + END_TIME_OFFSET)
assert len(watcher.appointments) == 0
assert watcher.asleep is True
def test_get_breaches(watcher, txids, locator_uuid_map):
watcher.locator_uuid_map = locator_uuid_map
potential_breaches = watcher.get_breaches(txids)
# 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)]
potential_breaches = watcher.get_breaches(txids)
# None of the txids should breach
assert len(potential_breaches) == 0
def test_filter_valid_breaches_random_data(watcher):
appointments = {}
locator_uuid_map = {}
breaches = {}
for i in range(TEST_SET_SIZE):
dummy_appointment, _ = generate_dummy_appointment()
uuid = uuid4().hex
appointments[uuid] = dummy_appointment
locator_uuid_map[dummy_appointment.locator] = [uuid]
if i % 2:
dispute_txid = get_random_value_hex(32)
breaches[dummy_appointment.locator] = dispute_txid
watcher.locator_uuid_map = locator_uuid_map
watcher.appointments = appointments
filtered_valid_breaches = watcher.filter_valid_breaches(breaches)
assert not any([fil_breach["valid_breach"] for uuid, fil_breach in filtered_valid_breaches.items()])
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.data = encrypted_blob
dummy_appointment.locator = Watcher.compute_locator(dispute_txid)
uuid = uuid4().hex
appointments = {uuid: dummy_appointment}
locator_uuid_map = {dummy_appointment.locator: [uuid]}
breaches = {dummy_appointment.locator: dispute_txid}
watcher.appointments = appointments
watcher.locator_uuid_map = locator_uuid_map
filtered_valid_breaches = watcher.filter_valid_breaches(breaches)
assert all([fil_breach["valid_breach"] for uuid, fil_breach in filtered_valid_breaches.items()])