def test_add_too_many_appointments(watcher): # Simulate the user is registered user_sk, user_pk = generate_keypair() available_slots = 100 user_id = Cryptographer.get_compressed_pk(user_pk) watcher.gatekeeper.registered_users[user_id] = UserInfo(available_slots=available_slots, subscription_expiry=10) # Appointments on top of the limit should be rejected watcher.appointments = dict() for i in range(MAX_APPOINTMENTS): appointment, dispute_tx = generate_dummy_appointment() appointment_signature = Cryptographer.sign(appointment.serialize(), user_sk) response = watcher.add_appointment(appointment, appointment_signature) assert response.get("locator") == appointment.locator assert Cryptographer.get_compressed_pk(watcher.signing_key.public_key) == Cryptographer.get_compressed_pk( Cryptographer.recover_pk(appointment.serialize(), response.get("signature")) ) assert response.get("available_slots") == available_slots - (i + 1) with pytest.raises(AppointmentLimitReached): appointment, dispute_tx = generate_dummy_appointment() appointment_signature = Cryptographer.sign(appointment.serialize(), user_sk) watcher.add_appointment(appointment, appointment_signature)
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].get("locator") == appointment.get( "locator") assert appointments_data[uuid].get("end_time") == appointment.get( "end_time") assert uuid in locator_uuid_map[appointment.get("locator")]
def test_add_update_appointment(gatekeeper): # add_update_appointment should decrease the slot count if a new appointment is added # let's add a new user sk, pk = generate_keypair() user_id = Cryptographer.get_compressed_pk(pk) gatekeeper.add_update_user(user_id) # And now update add a new appointment appointment, _ = generate_dummy_appointment() appointment_uuid = get_random_value_hex(16) remaining_slots = gatekeeper.add_update_appointment( user_id, appointment_uuid, appointment) # This is a standard size appointment, so it should have reduced the slots by one assert appointment_uuid in gatekeeper.registered_users[ user_id].appointments assert remaining_slots == config.get("SUBSCRIPTION_SLOTS") - 1 # Updates can leave the count as is, decrease it, or increase it, depending on the appointment size (modulo # ENCRYPTED_BLOB_MAX_SIZE_HEX) # Appointments of the same size leave it as is appointment_same_size, _ = generate_dummy_appointment() remaining_slots = gatekeeper.add_update_appointment( user_id, appointment_uuid, appointment) assert appointment_uuid in gatekeeper.registered_users[ user_id].appointments assert remaining_slots == config.get("SUBSCRIPTION_SLOTS") - 1 # Bigger appointments decrease it appointment_x2_size = appointment_same_size appointment_x2_size.encrypted_blob = "A" * (ENCRYPTED_BLOB_MAX_SIZE_HEX + 1) remaining_slots = gatekeeper.add_update_appointment( user_id, appointment_uuid, appointment_x2_size) assert appointment_uuid in gatekeeper.registered_users[ user_id].appointments assert remaining_slots == config.get("SUBSCRIPTION_SLOTS") - 2 # Smaller appointments increase it remaining_slots = gatekeeper.add_update_appointment( user_id, appointment_uuid, appointment) assert remaining_slots == config.get("SUBSCRIPTION_SLOTS") - 1 # If the appointment needs more slots than there's free, it should fail gatekeeper.registered_users[user_id].available_slots = 1 appointment_uuid = get_random_value_hex(16) with pytest.raises(NotEnoughSlots): gatekeeper.add_update_appointment(user_id, appointment_uuid, appointment_x2_size)
def test_add_appointment_in_cache(watcher): # 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=10) appointment, dispute_tx = generate_dummy_appointment() 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 response = watcher.add_appointment(appointment, Cryptographer.sign(appointment.serialize(), user_sk)) # 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.serialize(), 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))
def test_filter_valid_breaches(watcher): dispute_txid = "0437cd7f8525ceed2324359c2d0ba26006d92d856a9c20fa0241106ee5a597c9" encrypted_blob = ( "a62aa9bb3c8591e4d5de10f1bd49db92432ce2341af55762cdc9242c08662f97f5f47da0a1aa88373508cd6e67e87eefddeca0cee98c1" "967ec1c1ecbb4c5e8bf08aa26159214e6c0bc4b2c7c247f87e7601d15c746fc4e711be95ba0e363001280138ba9a65b06c4aa6f592b21" "3635ee763984d522a4c225814510c8f7ab0801f36d4a68f5ee7dd3930710005074121a172c29beba79ed647ebaf7e7fab1bbd9a208251" "ef5486feadf2c46e33a7d66adf9dbbc5f67b55a34b1b3c4909dd34a482d759b0bc25ecd2400f656db509466d7479b5b92a2fadabccc9e" "c8918da8979a9feadea27531643210368fee494d3aaa4983e05d6cf082a49105e2f8a7c7821899239ba7dee12940acd7d8a629894b5d31" "e94b439cfe8d2e9f21e974ae5342a70c91e8" ) dummy_appointment, _ = generate_dummy_appointment() dummy_appointment.encrypted_blob = encrypted_blob dummy_appointment.locator = compute_locator(dispute_txid) uuid = uuid4().hex appointments = {uuid: dummy_appointment} locator_uuid_map = {dummy_appointment.locator: [uuid]} breaches = {dummy_appointment.locator: dispute_txid} for uuid, appointment in appointments.items(): watcher.appointments[uuid] = {"locator": appointment.locator, "user_id": appointment.user_id} watcher.db_manager.store_watcher_appointment(uuid, dummy_appointment.to_dict()) watcher.db_manager.create_append_locator_map(dummy_appointment.locator, uuid) watcher.locator_uuid_map = locator_uuid_map valid_breaches, invalid_breaches = watcher.filter_breaches(breaches) # We have "triggered" a single breach and it was valid. assert len(invalid_breaches) == 0 and len(valid_breaches) == 1
def test_filter_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] = {"locator": dummy_appointment.locator, "user_id": dummy_appointment.user_id} watcher.db_manager.store_watcher_appointment(uuid, dummy_appointment.to_dict()) watcher.db_manager.create_append_locator_map(dummy_appointment.locator, uuid) 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 valid_breaches, invalid_breaches = watcher.filter_breaches(breaches) # We have "triggered" TEST_SET_SIZE/2 breaches, all of them invalid. assert len(valid_breaches) == 0 and len(invalid_breaches) == TEST_SET_SIZE / 2
def test_check_breach(watcher): # A breach will be flagged as valid only if the encrypted blob can be properly decrypted and the resulting data # matches a transaction format. uuid = uuid4().hex appointment, dispute_tx = generate_dummy_appointment() dispute_txid = watcher.block_processor.decode_raw_transaction(dispute_tx).get("txid") penalty_txid, penalty_rawtx = watcher.check_breach(uuid, appointment, dispute_txid) assert Cryptographer.encrypt(penalty_rawtx, dispute_txid) == appointment.encrypted_blob
def test_add_appointment_non_registered(watcher): # Appointments from non-registered users should fail user_sk, user_pk = generate_keypair() appointment, dispute_tx = generate_dummy_appointment() appointment_signature = Cryptographer.sign(appointment.serialize(), user_sk) with pytest.raises(AuthenticationFailure, match="User not found"): watcher.add_appointment(appointment, appointment_signature)
def test_add_appointment_no_slots(watcher): # Appointments from register users with no available slots should aso fail user_sk, user_pk = generate_keypair() user_id = Cryptographer.get_compressed_pk(user_pk) watcher.gatekeeper.registered_users[user_id] = UserInfo(available_slots=0, subscription_expiry=10) appointment, dispute_tx = generate_dummy_appointment() appointment_signature = Cryptographer.sign(appointment.serialize(), user_sk) with pytest.raises(NotEnoughSlots): watcher.add_appointment(appointment, appointment_signature)
def test_check_breach_random_data(watcher): # If a breach triggers an appointment with random data as encrypted blob, the check should fail. uuid = uuid4().hex appointment, dispute_tx = generate_dummy_appointment() dispute_txid = watcher.block_processor.decode_raw_transaction(dispute_tx).get("txid") # Set the blob to something "random" appointment.encrypted_blob = get_random_value_hex(200) with pytest.raises(EncryptionError): watcher.check_breach(uuid, appointment, dispute_txid)
def test_check_breach_invalid_transaction(watcher): # If the breach triggers an appointment with data that can be decrypted but does not match a transaction, it should # fail uuid = uuid4().hex appointment, dispute_tx = generate_dummy_appointment() dispute_txid = watcher.block_processor.decode_raw_transaction(dispute_tx).get("txid") # Set the blob to something "random" appointment.encrypted_blob = Cryptographer.encrypt(get_random_value_hex(200), dispute_txid) with pytest.raises(InvalidTransactionFormat): watcher.check_breach(uuid, appointment, dispute_txid)
def test_add_too_many_appointments(watcher): # Any appointment on top of those should fail watcher.appointments = dict() for _ in range(config.get("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_rpk( watcher.signing_key.public_key, Cryptographer.recover_pk(appointment.serialize(), sig)) 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 create_appointments(n): locator_uuid_map = dict() appointments = dict() dispute_txs = [] for i in range(n): appointment, dispute_tx = generate_dummy_appointment() 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_get_all_appointments_watcher(api, client, get_all_db_manager): # Let's reset the dbs so we can test this clean api.watcher.db_manager = get_all_db_manager api.watcher.responder.db_manager = get_all_db_manager # Check that they are wiped clean r = client.get(get_all_appointment_endpoint) assert r.status_code == HTTP_OK assert len(r.json.get("watcher_appointments")) == 0 and len(r.json.get("responder_trackers")) == 0 # Add some appointments to the Watcher db non_triggered_appointments = {} for _ in range(10): uuid = get_random_value_hex(16) appointment, _ = generate_dummy_appointment() appointment.locator = get_random_value_hex(16) non_triggered_appointments[uuid] = appointment.to_dict() api.watcher.db_manager.store_watcher_appointment(uuid, appointment.to_dict()) triggered_appointments = {} for _ in range(10): uuid = get_random_value_hex(16) appointment, _ = generate_dummy_appointment() appointment.locator = get_random_value_hex(16) triggered_appointments[uuid] = appointment.to_dict() api.watcher.db_manager.store_watcher_appointment(uuid, appointment.to_dict()) api.watcher.db_manager.create_triggered_appointment_flag(uuid) # We should only get the non-triggered appointments r = client.get(get_all_appointment_endpoint) assert r.status_code == HTTP_OK watcher_locators = [v["locator"] for k, v in r.json["watcher_appointments"].items()] local_locators = [appointment["locator"] for uuid, appointment in non_triggered_appointments.items()] assert set(watcher_locators) == set(local_locators) assert len(r.json["responder_trackers"]) == 0
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_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()) db_manager.create_triggered_appointment_flag(uuid) # 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_add_appointment_in_cache_cannot_decrypt(api, client): api.watcher.gatekeeper.registered_users[user_id] = UserInfo(available_slots=1, subscription_expiry=0) appointment, dispute_tx = generate_dummy_appointment() appointment.encrypted_blob = appointment.encrypted_blob[::-1] appointment_signature = Cryptographer.sign(appointment.serialize(), user_sk) # Add the data to the cache dispute_txid = api.watcher.block_processor.decode_raw_transaction(dispute_tx).get("txid") api.watcher.locator_cache.cache[dispute_txid] = appointment.locator # The appointment should be accepted r = add_appointment(client, {"appointment": appointment.to_dict(), "signature": appointment_signature}, user_id) assert ( r.status_code == HTTP_OK and r.json.get("available_slots") == 0 and r.json.get("start_block") == api.watcher.last_known_block )
def test_add_too_many_appointment(api, client): # Give slots to the user api.watcher.gatekeeper.registered_users[user_id] = UserInfo(available_slots=200, subscription_expiry=0) free_appointment_slots = MAX_APPOINTMENTS - len(api.watcher.appointments) for i in range(free_appointment_slots + 1): appointment, dispute_tx = generate_dummy_appointment() locator_dispute_tx_map[appointment.locator] = dispute_tx appointment_signature = Cryptographer.sign(appointment.serialize(), user_sk) r = add_appointment(client, {"appointment": appointment.to_dict(), "signature": appointment_signature}, user_id) if i < free_appointment_slots: assert r.status_code == HTTP_OK and r.json.get("start_block") == api.watcher.last_known_block else: assert r.status_code == HTTP_SERVICE_UNAVAILABLE
def test_add_appointment(watcher): # Simulate the user is registered user_sk, user_pk = generate_keypair() available_slots = 100 user_id = Cryptographer.get_compressed_pk(user_pk) watcher.gatekeeper.registered_users[user_id] = UserInfo(available_slots=available_slots, subscription_expiry=10) appointment, dispute_tx = generate_dummy_appointment() appointment_signature = Cryptographer.sign(appointment.serialize(), user_sk) response = watcher.add_appointment(appointment, appointment_signature) assert response.get("locator") == appointment.locator assert Cryptographer.get_compressed_pk(watcher.signing_key.public_key) == Cryptographer.get_compressed_pk( Cryptographer.recover_pk(appointment.serialize(), response.get("signature")) ) assert response.get("available_slots") == available_slots - 1 # Check that we can also add an already added appointment (same locator) response = watcher.add_appointment(appointment, appointment_signature) assert response.get("locator") == appointment.locator assert Cryptographer.get_compressed_pk(watcher.signing_key.public_key) == Cryptographer.get_compressed_pk( Cryptographer.recover_pk(appointment.serialize(), response.get("signature")) ) # The slot count should not have been reduced and only one copy is kept. assert response.get("available_slots") == available_slots - 1 assert len(watcher.locator_uuid_map[appointment.locator]) == 1 # If two appointments with the same locator come from different users, they are kept. another_user_sk, another_user_pk = generate_keypair() another_user_id = Cryptographer.get_compressed_pk(another_user_pk) watcher.gatekeeper.registered_users[another_user_id] = UserInfo( available_slots=available_slots, subscription_expiry=10 ) appointment_signature = Cryptographer.sign(appointment.serialize(), another_user_sk) response = watcher.add_appointment(appointment, appointment_signature) assert response.get("locator") == appointment.locator assert Cryptographer.get_compressed_pk(watcher.signing_key.public_key) == Cryptographer.get_compressed_pk( Cryptographer.recover_pk(appointment.serialize(), response.get("signature")) ) assert response.get("available_slots") == available_slots - 1 assert len(watcher.locator_uuid_map[appointment.locator]) == 2
def test_add_appointment(watcher): # 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_rpk( watcher.signing_key.public_key, Cryptographer.recover_pk(appointment.serialize(), sig)) # 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_rpk( watcher.signing_key.public_key, Cryptographer.recover_pk(appointment.serialize(), sig))
def test_add_appointment_in_cache(api, client): api.watcher.gatekeeper.registered_users[user_id] = UserInfo(available_slots=1, subscription_expiry=0) appointment, dispute_tx = generate_dummy_appointment() appointment_signature = Cryptographer.sign(appointment.serialize(), user_sk) # Add the data to the cache dispute_txid = api.watcher.block_processor.decode_raw_transaction(dispute_tx).get("txid") api.watcher.locator_cache.cache[appointment.locator] = dispute_txid r = add_appointment(client, {"appointment": appointment.to_dict(), "signature": appointment_signature}, user_id) assert ( r.status_code == HTTP_OK and r.json.get("available_slots") == 0 and r.json.get("start_block") == api.watcher.last_known_block ) # Trying to add it again should fail, since it is already in the Responder r = add_appointment(client, {"appointment": appointment.to_dict(), "signature": appointment_signature}, user_id) assert r.status_code == HTTP_BAD_REQUEST and r.json.get("error_code") == errors.APPOINTMENT_ALREADY_TRIGGERED # The appointment would be rejected even if the data is not in the cache provided it has been triggered del api.watcher.locator_cache.cache[appointment.locator] r = add_appointment(client, {"appointment": appointment.to_dict(), "signature": appointment_signature}, user_id) assert r.status_code == HTTP_BAD_REQUEST and r.json.get("error_code") == errors.APPOINTMENT_ALREADY_TRIGGERED
def test_add_appointment_in_cache_invalid_transaction(watcher): # Generate an appointment that cannot be decrypted 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=10) appointment, dispute_tx = generate_dummy_appointment() appointment.encrypted_blob = appointment.encrypted_blob[::-1] 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 response = watcher.add_appointment(appointment, Cryptographer.sign(appointment.serialize(), user_sk)) # The appointment is accepted but dropped (same as an invalid appointment that gets triggered) assert ( response and response.get("locator") == appointment.locator and Cryptographer.get_compressed_pk(watcher.signing_key.public_key) == Cryptographer.get_compressed_pk(Cryptographer.recover_pk(appointment.serialize(), response.get("signature"))) ) assert not watcher.locator_uuid_map.get(appointment.locator) assert appointment.locator not in [tracker.get("locator") for tracker in watcher.responder.trackers.values()]
def appointment(): appointment, dispute_tx = generate_dummy_appointment() locator_dispute_tx_map[appointment.locator] = dispute_tx return appointment
def watcher_appointments(): return { uuid4().hex: generate_dummy_appointment(real_height=False)[0] for _ in range(10) }
def watcher_appointments(): return {uuid4().hex: generate_dummy_appointment()[0] for _ in range(10)}