Adds new gatekeeper tests

This commit is contained in:
Sergi Delgado Segura
2020-04-19 19:35:05 +02:00
parent 66dce42526
commit 797cb9786e
5 changed files with 137 additions and 20 deletions

View File

@@ -27,6 +27,7 @@ class UserInfo:
self.available_slots = available_slots
self.subscription_expiry = subscription_expiry
# FIXME: this list is currently never wiped
if not appointments:
self.appointments = []
else:
@@ -139,17 +140,17 @@ class Gatekeeper:
"""
Updates (add/removes) slots from a user subscription.
Slots are removed if a new subscription is given, or an update is given with a new subscription bigger than the
Slots are removed if a new appointment is given, or an update is given with an appointment bigger than the
old one.
Slots are added if an update is given but the new appointment is smaller than the old one.
Args:
user_id(:obj:`str`): the public key that identifies the user (33-bytes hex str).
new_appointment (:obj:`ExtendedAppointment <teos.extended_appointment.ExtendedAppointment>`): the new
appointment the user is requesting to add.
old_appointment (:obj:`ExtendedAppointment <teos.extended_appointment.ExtendedAppointment>`): the old
appointment the user wants to replace. Optional.
new_appointment (:obj:`dict`): the summary of new appointment the user is requesting
to add.
old_appointment (:obj:`dict`): the summary old appointment the user wants to replace.
Optional.
Returns:
:obj:`int`: the number of remaining appointment slots.
@@ -180,7 +181,7 @@ class Gatekeeper:
def get_expired_appointments(self, block_height):
"""
Gets a list of appointments that are expiring at a given block height.
Gets a list of appointments that expire at a given block height.
Args:
block_height: the block height that wants to be checked.

View File

@@ -23,7 +23,7 @@ class Watcher:
"""
The :class:`Watcher` is in charge of watching for channel breaches for the appointments accepted by the tower.
The :class:`Watcher` keeps track of the accepted appointments in ``appointments`` and, for new received block,
The :class:`Watcher` keeps track of the accepted appointments in ``appointments`` and, for new received blocks,
checks if any breach has happened by comparing the txids with the appointment locators. If a breach is seen, the
``encrypted_blob`` of the corresponding appointment is decrypted and the data is passed to the
:obj:`Responder <teos.responder.Responder>`.
@@ -115,7 +115,7 @@ class Watcher:
:obj:`AppointmentLimitReached`: If the tower cannot hold more appointments (cap reached).
:obj:`AuthenticationFailure <teos.gatekeeper.AuthenticationFailure>`: If the user cannot be authenticated.
:obj:`NotEnoughSlots <teos.gatekeeper.NotEnoughSlots>`: If the user does not have enough available slots,
so the appointment is rejected
so the appointment is rejected.
"""
if len(self.appointments) >= self.max_appointments:
@@ -129,6 +129,7 @@ class Watcher:
# If an appointment is requested by the user the uuid can be recomputed and queried straightaway (no maps).
uuid = hash_160("{}{}".format(appointment.locator, user_id))
# The third argument is the previous version of the same appointment (optional, returns None if missing)
available_slots = self.gatekeeper.update_available_slots(
user_id, appointment.get_summary(), self.appointments.get(uuid)
)
@@ -140,6 +141,7 @@ class Watcher:
if uuid not in self.locator_uuid_map[appointment.locator]:
self.locator_uuid_map[appointment.locator].append(uuid)
else:
# Otherwise two users have sent an appointment with the same locator, so we need to store both.
self.locator_uuid_map[appointment.locator] = [uuid]
self.db_manager.store_watcher_appointment(uuid, appointment.to_dict())
@@ -191,7 +193,7 @@ class Watcher:
expired_appointments, self.appointments, self.locator_uuid_map, self.db_manager
)
valid_breaches, invalid_breaches = self.filter_valid_breaches(self.get_breaches(txids))
valid_breaches, invalid_breaches = self.filter_breaches(self.get_breaches(txids))
triggered_flags = []
appointments_to_delete = []
@@ -264,9 +266,9 @@ class Watcher:
return breaches
def filter_valid_breaches(self, breaches):
def filter_breaches(self, breaches):
"""
Filters what of the found breaches contain valid transaction data.
Filters the valid from the invalid channel breaches.
The :obj:`Watcher` cannot if a given ``encrypted_blob`` contains a valid transaction until a breach if seen.
Blobs that contain arbitrary data are dropped and not sent to the :obj:`Responder <teos.responder.Responder>`.

View File

@@ -41,6 +41,14 @@ def test_init_appointment(appointment_data):
)
def test_get_summary(appointment_data):
assert ExtendedAppointment.from_dict(appointment_data).get_summary() == {
"locator": appointment_data["locator"],
"user_id": appointment_data["user_id"],
"size": len(appointment_data["encrypted_blob"]),
}
def test_from_dict(appointment_data):
# The appointment should be build if we don't miss any field
appointment = ExtendedAppointment.from_dict(appointment_data)

View File

@@ -1,11 +1,14 @@
import pytest
from teos.gatekeeper import AuthenticationFailure, NotEnoughSlots
from teos.users_dbm import UsersDBM
from teos.block_processor import BlockProcessor
from teos.gatekeeper import AuthenticationFailure, NotEnoughSlots, UserInfo
from common.cryptographer import Cryptographer
from common.exceptions import InvalidParameter
from common.constants import ENCRYPTED_BLOB_MAX_SIZE_HEX
from test.teos.unit.conftest import get_random_value_hex, generate_keypair, get_config
from test.teos.unit.conftest import get_random_value_hex, generate_keypair, get_config, generate_dummy_appointment
config = get_config()
@@ -13,11 +16,18 @@ config = get_config()
def test_init(gatekeeper, run_bitcoind):
assert isinstance(gatekeeper.default_slots, int) and gatekeeper.default_slots == config.get("DEFAULT_SLOTS")
assert isinstance(
gatekeeper.default_subscription_duration, int
) and gatekeeper.default_subscription_duration == config.get("DEFAULT_SUBSCRIPTION_DURATION")
assert isinstance(gatekeeper.expiry_delta, int) and gatekeeper.expiry_delta == config.get("EXPIRY_DELTA")
assert isinstance(gatekeeper.block_processor, BlockProcessor)
assert isinstance(gatekeeper.user_db, UsersDBM)
assert isinstance(gatekeeper.registered_users, dict) and len(gatekeeper.registered_users) == 0
def test_add_update_user(gatekeeper):
# add_update_user adds DEFAULT_SLOTS to a given user as long as the identifier is {02, 03}| 32-byte hex str
# it also add DEFAULT_SUBSCRIPTION_DURATION + current_block_height to the user
user_pk = "02" + get_random_value_hex(32)
for _ in range(10):
@@ -27,6 +37,11 @@ def test_add_update_user(gatekeeper):
gatekeeper.add_update_user(user_pk)
assert gatekeeper.registered_users.get(user_pk).available_slots == current_slots + config.get("DEFAULT_SLOTS")
assert gatekeeper.registered_users[
user_pk
].subscription_expiry == gatekeeper.block_processor.get_block_count() + config.get(
"DEFAULT_SUBSCRIPTION_DURATION"
)
# The same can be checked for multiple users
for _ in range(10):
@@ -35,6 +50,11 @@ def test_add_update_user(gatekeeper):
gatekeeper.add_update_user(user_pk)
assert gatekeeper.registered_users.get(user_pk).available_slots == config.get("DEFAULT_SLOTS")
assert gatekeeper.registered_users[
user_pk
].subscription_expiry == gatekeeper.block_processor.get_block_count() + config.get(
"DEFAULT_SUBSCRIPTION_DURATION"
)
def test_add_update_user_wrong_pk(gatekeeper):
@@ -108,4 +128,63 @@ def test_identify_user_wrong(gatekeeper):
gatekeeper.authenticate_user(message, signature.encode())
# FIXME: MISSING TESTS
def test_update_available_slots(gatekeeper):
# update_available_slots should decrease the slot count if a new appointment is added
# let's add a new user
sk, pk = generate_keypair()
compressed_pk = Cryptographer.get_compressed_pk(pk)
gatekeeper.add_update_user(compressed_pk)
# And now update the slots given an appointment
appointment, _ = generate_dummy_appointment()
gatekeeper.update_available_slots(compressed_pk, appointment.get_summary())
# This is a standard size appointment, so it should have reduced the slots by one
assert gatekeeper.registered_users[compressed_pk].available_slots == config.get("DEFAULT_SLOTS") - 1
# Updates can leave the count as it, 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.update_available_slots(
compressed_pk, appointment.get_summary(), appointment_same_size.get_summary()
)
assert remaining_slots == config.get("DEFAULT_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.update_available_slots(
compressed_pk, appointment_x2_size.get_summary(), appointment.get_summary()
)
assert remaining_slots == config.get("DEFAULT_SLOTS") - 2
# Smaller appointments increase it (using the same data but flipped)
remaining_slots = gatekeeper.update_available_slots(
compressed_pk, appointment.get_summary(), appointment_x2_size.get_summary()
)
assert remaining_slots == config.get("DEFAULT_SLOTS") - 1
# If the appointment needs more slots than there's free, it should fail
gatekeeper.registered_users[compressed_pk].available_slots = 1
with pytest.raises(NotEnoughSlots):
gatekeeper.update_available_slots(compressed_pk, appointment_x2_size.get_summary())
def test_get_expired_appointments(gatekeeper):
# get_expired_appointments returns a list of appointment uuids expiring at a given block
appointment = {}
# Let's simulate adding some users with dummy expiry times
gatekeeper.registered_users = {}
for i in reversed(range(100)):
uuid = get_random_value_hex(16)
user_appointments = [get_random_value_hex(16)]
# Add a single appointment to the user
gatekeeper.registered_users[uuid] = UserInfo(100, i, user_appointments)
appointment[i] = user_appointments
# Now let's check that reversed
for i in range(100):
assert gatekeeper.get_expired_appointments(i + gatekeeper.expiry_delta) == appointment[i]

View File

@@ -8,11 +8,11 @@ from teos.carrier import Carrier
from teos.tools import bitcoin_cli
from teos.responder import Responder
from teos.gatekeeper import UserInfo
from teos.gatekeeper import Gatekeeper
from teos.chain_monitor import ChainMonitor
from teos.appointments_dbm import AppointmentsDBM
from teos.block_processor import BlockProcessor
from teos.watcher import Watcher, AppointmentLimitReached
from teos.gatekeeper import Gatekeeper, AuthenticationFailure, NotEnoughSlots
from common.tools import compute_locator
from common.cryptographer import Cryptographer
@@ -95,13 +95,38 @@ def test_init(run_bitcoind, watcher):
assert isinstance(watcher.appointments, dict) and len(watcher.appointments) == 0
assert isinstance(watcher.locator_uuid_map, dict) and len(watcher.locator_uuid_map) == 0
assert watcher.block_queue.empty()
assert isinstance(watcher.db_manager, AppointmentsDBM)
assert isinstance(watcher.gatekeeper, Gatekeeper)
assert isinstance(watcher.block_processor, BlockProcessor)
assert isinstance(watcher.responder, Responder)
assert isinstance(watcher.max_appointments, int)
assert isinstance(watcher.gatekeeper, Gatekeeper)
assert isinstance(watcher.signing_key, PrivateKey)
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_add_appointment(watcher):
# Simulate the user is registered
user_sk, user_pk = generate_keypair()
@@ -184,7 +209,7 @@ def test_do_watch(watcher, temp_db_manager):
watcher.appointments = {}
watcher.gatekeeper.registered_users = {}
# Simulate a register
# Simulate a register (times out in 10 bocks)
user_id = get_random_value_hex(16)
watcher.gatekeeper.registered_users[user_id] = UserInfo(
available_slots=100, subscription_expiry=watcher.block_processor.get_block_count() + 10
@@ -215,6 +240,8 @@ def test_do_watch(watcher, temp_db_manager):
assert len(watcher.appointments) == 0
# FIXME: We should also add cases where the transactions are invalid. bitcoind_mock needs to be extended for this.
def test_get_breaches(watcher, txids, locator_uuid_map):
watcher.locator_uuid_map = locator_uuid_map
@@ -235,7 +262,7 @@ def test_get_breaches_random_data(watcher, locator_uuid_map):
assert len(potential_breaches) == 0
def test_filter_valid_breaches_random_data(watcher):
def test_filter_breaches_random_data(watcher):
appointments = {}
locator_uuid_map = {}
breaches = {}
@@ -256,7 +283,7 @@ def test_filter_valid_breaches_random_data(watcher):
watcher.locator_uuid_map = locator_uuid_map
watcher.appointments = appointments
valid_breaches, invalid_breaches = watcher.filter_valid_breaches(breaches)
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
@@ -289,7 +316,7 @@ def test_filter_valid_breaches(watcher):
watcher.locator_uuid_map = locator_uuid_map
valid_breaches, invalid_breaches = watcher.filter_valid_breaches(breaches)
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