def test_kbucket_replacement_cache(): # Check that the replacement cache has a limited size and doesn't contain duplicates. # The min/max IDs are irrelevant here as we'll forcibly add nodes to the bucket. bucket = KBucket(0, 10, size=10) for node in NodeFactory.create_batch(bucket.size): bucket.add(node) assert bucket.replacement_cache == [] # Our bucket is now full, so new entries will go to the replacement cache in the order they # were added. assert bucket.is_full overflow_nodes = NodeFactory.create_batch(bucket.size) for node in overflow_nodes: bucket.add(node) assert bucket.replacement_cache == overflow_nodes # If we try to add a node that is already in the replacement cache, it is simply moved to the # tail of the list. bucket.add(overflow_nodes[3]) assert bucket.replacement_cache.index(overflow_nodes[3]) == bucket.size - 1 # Adding a fresh batch of nodes will cause the ones currently in the replacement cache to be # discarded. cache_overflow_nodes = NodeFactory.create_batch(bucket.size) for node in cache_overflow_nodes: bucket.add(node) assert bucket.replacement_cache[-1] == node assert bucket.replacement_cache == cache_overflow_nodes
def test_kbucket_remove(): bucket = KBucket(0, 100, size=25) nodes = NodeFactory.create_batch(bucket.size) for node in nodes: bucket.add(node) assert bucket.nodes == nodes assert bucket.replacement_cache == [] replacement_count = 10 replacement_nodes = NodeFactory.create_batch(replacement_count) for replacement_node in replacement_nodes: bucket.add(replacement_node) assert bucket.nodes == nodes assert bucket.replacement_cache == replacement_nodes for node in nodes: bucket.remove_node(node) assert bucket.nodes == list(reversed(replacement_nodes)) assert bucket.replacement_cache == [] for replacement_node in replacement_nodes: bucket.remove_node(replacement_node) assert bucket.nodes == [] assert bucket.replacement_cache == []
async def test_aurora_tally(): proto = AuroraDiscoveryProtocolFactory.from_seed(b'foo') m = Mock() m.side_effect = [ (0.8, "block_a", set(NodeFactory.create_batch(16))), (0.9, "block_b", set(NodeFactory.create_batch(16))), (0.7, "block_c", set(NodeFactory.create_batch(16))), ] proto.aurora_walk = m result_key, _ = proto.aurora_tally(NodeFactory(), 10, 50, 16, 3) assert result_key == "block_b" assert m.call_count == 3
async def test_update_routing_table_triggers_bond_if_eviction_candidate( manually_driven_discovery, monkeypatch): discovery = manually_driven_discovery old_node, new_node = NodeFactory.create_batch(2) bond_called = False async def bond(node_id): nonlocal bond_called bond_called = True assert node_id == old_node.id monkeypatch.setattr(discovery, 'bond', bond) # Pretend our routing table failed to add the new node by returning the least recently seen # node for an eviction check. monkeypatch.setattr(discovery.routing, 'update', lambda n: old_node.id) discovery.update_routing_table(new_node) assert not discovery.routing._contains(new_node.id, include_replacement_cache=False) # The update_routing_table() call above will have scheduled a future call to discovery.bond() so # we need to wait a bit here to give it a chance to run. with trio.fail_after(0.5): while not bond_called: await trio.sleep(0.001)
async def test_protocol_bootstrap(monkeypatch): node1, node2 = NodeFactory.create_batch(2) discovery = MockDiscoveryService([node1, node2]) invalidated_bonds = [] def invalidate_bond(node_id): invalidated_bonds.append(node_id) async def bond(node_id): assert discovery.routing.update(node_id) is None return True monkeypatch.setattr(discovery, 'invalidate_bond', invalidate_bond) # Pretend we bonded successfully with our bootstrap nodes. monkeypatch.setattr(discovery, 'bond', bond) await discovery.bootstrap() assert sorted(invalidated_bonds) == sorted([node.id for node in [node1, node2]]) assert len(discovery.messages) == 2 # We don't care in which order the bootstrap nodes are contacted, nor which node_id was used # in the find_node request, so we just assert that we sent find_node msgs to both nodes. assert sorted([(node, cmd) for (node, cmd, _) in discovery.messages]) == sorted([ (node1, 'find_node'), (node2, 'find_node')])
async def test_wait_neighbours(nursery): service = MockDiscoveryService([]) node = NodeFactory() # Schedule a call to service.recv_neighbours_v4() simulating a neighbours response from the # node we expect. neighbours = tuple(NodeFactory.create_batch(3)) neighbours_msg_payload = [ [n.address.to_endpoint() + [n.pubkey.to_bytes()] for n in neighbours], discovery._get_msg_expiration()] async def recv_neighbours() -> None: service.recv_neighbours_v4(node, neighbours_msg_payload, b'') nursery.start_soon(recv_neighbours) received_neighbours = await service.wait_neighbours(node) assert neighbours == received_neighbours # Ensure wait_neighbours() cleaned up after itself. assert node not in service.neighbours_callbacks # If wait_neighbours() times out, we get an empty list of neighbours. received_neighbours = await service.wait_neighbours(node) assert received_neighbours == tuple() assert node not in service.neighbours_callbacks
async def test_wait_neighbours(): proto = MockDiscoveryProtocol([]) node = NodeFactory() # Schedule a call to proto.recv_neighbours_v4() simulating a neighbours response from the node # we expect. neighbours = tuple(NodeFactory.create_batch(3)) neighbours_msg_payload = [[ n.address.to_endpoint() + [n.pubkey.to_bytes()] for n in neighbours ], discovery._get_msg_expiration()] recv_neighbours_coroutine = asyncio.coroutine( lambda: proto.recv_neighbours_v4(node, neighbours_msg_payload, b'')) asyncio.ensure_future(recv_neighbours_coroutine()) received_neighbours = await proto.wait_neighbours(node) assert neighbours == received_neighbours # Ensure wait_neighbours() cleaned up after itself. assert node not in proto.neighbours_callbacks # If wait_neighbours() times out, we get an empty list of neighbours. received_neighbours = await proto.wait_neighbours(node) assert received_neighbours == tuple() assert node not in proto.neighbours_callbacks
async def test_aurora_walk(network_size, malpn, malpg, mistake_threshold, test_runs): """ TODO this is a non-deterministic test, should be changed""" response_size = constants.KADEMLIA_BUCKET_SIZE batch = NodeFactory.create_batch(network_size) pubkey_honesty: Dict[any, Tuple[NodeAPI, bool]] = {} honest_nodes: Set[NodeAPI] = set() malicious_nodes: Set[NodeAPI] = set() for index, node in enumerate(batch): if index < network_size * malpn: pubkey_honesty.update({node.pubkey: False}) malicious_nodes.add(node) else: pubkey_honesty.update({node.pubkey: True}) honest_nodes.add(node) proto = MockDiscoveryProtocolAurora(batch, honest_nodes, malicious_nodes, malpg) hit_number = 0 miss_number = 0 for _ in range(test_runs): entry_node = random.choice(tuple(malicious_nodes)) _, result_pubkey, _ = await proto.aurora_walk(entry_node, network_size, response_size, mistake_threshold) if pubkey_honesty[result_pubkey]: hit_number += 1 else: miss_number += 1 assert hit_number > miss_number
async def test_aurora_pick_existing_candidates(): candidates = NodeFactory.create_batch(4) node1, node2, *other_nodes = candidates exclusion_candidates = {node1, node2} result = aurora_pick(set(candidates), exclusion_candidates) assert result in candidates assert result not in exclusion_candidates
def test_multiplexer_pair_factory(): alice_remote, bob_remote = NodeFactory.create_batch(2) alice_multiplexer, bob_multiplexer = MultiplexerPairFactory( alice_remote=alice_remote, bob_remote=bob_remote, ) assert alice_multiplexer.remote == bob_remote assert bob_multiplexer.remote == alice_remote assert alice_multiplexer.get_base_protocol().version == DEVP2P_V5 assert bob_multiplexer.get_base_protocol().version == DEVP2P_V5
async def test_topic_query(event_loop): bob = await get_listening_discovery_protocol(event_loop) les_nodes = NodeFactory.create_batch(10) topic = b'les' for n in les_nodes: bob.topic_table.add_node(n, topic) alice = await get_listening_discovery_protocol(event_loop) echo = alice.send_topic_query(bob.this_node, topic) received_nodes = await alice.wait_topic_nodes(bob.this_node, echo) assert len(received_nodes) == 10 assert sorted(received_nodes) == sorted(les_nodes)
async def test_protocol_bootstrap(): node1, node2 = NodeFactory.create_batch(2) discovery = MockDiscoveryService([node1, node2]) async def bond(node): assert discovery.routing.add_node(node) is None return True # Pretend we bonded successfully with our bootstrap nodes. discovery.bond = bond await discovery.bootstrap() assert len(discovery.messages) == 2 # We don't care in which order the bootstrap nodes are contacted, nor which node_id was used # in the find_node request, so we just assert that we sent find_node msgs to both nodes. assert sorted([(node, cmd) for (node, cmd, _) in discovery.messages ]) == sorted([(node1, 'find_node'), (node2, 'find_node')])
async def test_get_peer_candidates(manually_driven_discovery, monkeypatch): total_nodes = 10 nodes = NodeFactory.create_batch(total_nodes) discovery = manually_driven_discovery for node in nodes: discovery.node_db.set_enr(node.enr) assert discovery.routing.update(node.id) is None discovery._random_lookup_calls = 0 async def mock_lookup_random(): discovery._random_lookup_calls += 1 monkeypatch.setattr(discovery, 'lookup_random', mock_lookup_random) def should_skip(skip_list, candidate): return candidate in skip_list candidates = discovery.get_peer_candidates( functools.partial(should_skip, tuple()), total_nodes) assert sorted(candidates) == sorted(nodes) candidates = discovery.get_peer_candidates( functools.partial(should_skip, tuple()), total_nodes + 10) assert sorted(candidates) == sorted(nodes) # When we don't have enough candidates, a random lookup should be triggered. with trio.fail_after(0.5): while discovery._random_lookup_calls != 1: await trio.sleep(0.01) candidates = discovery.get_peer_candidates( functools.partial(should_skip, tuple()), total_nodes - 1) assert len(candidates) == total_nodes - 1 skip_list = (nodes[0], nodes[5], nodes[8]) candidates = discovery.get_peer_candidates( functools.partial(should_skip, skip_list), total_nodes) assert sorted(candidates) == sorted(set(nodes).difference(skip_list)) with trio.fail_after(0.5): while discovery._random_lookup_calls != 2: await trio.sleep(0.01)
async def test_update_routing_table_triggers_bond_if_eviction_candidate(): proto = MockDiscoveryProtocol([]) old_node, new_node = NodeFactory.create_batch(2) bond_called = False def bond(node): nonlocal bond_called bond_called = True assert node == old_node proto.bond = asyncio.coroutine(bond) # Pretend our routing table failed to add the new node by returning the least recently seen # node for an eviction check. proto.routing.add_node = lambda n: old_node proto.update_routing_table(new_node) assert new_node not in proto.routing # The update_routing_table() call above will have scheduled a future call to proto.bond() so # we need to yield here to give it a chance to run. await asyncio.sleep(0.001) assert bond_called
async def test_update_routing_table_triggers_bond_if_eviction_candidate( nursery, manually_driven_discovery, monkeypatch): discovery = manually_driven_discovery old_node, new_node = NodeFactory.create_batch(2) bond_called = False async def bond(node): nonlocal bond_called bond_called = True assert node == old_node monkeypatch.setattr(discovery, 'bond', bond) # Pretend our routing table failed to add the new node by returning the least recently seen # node for an eviction check. monkeypatch.setattr(discovery.routing, 'add_node', lambda n: old_node) discovery.update_routing_table(new_node) assert new_node not in discovery.routing # The update_routing_table() call above will have scheduled a future call to discovery.bond() so # we need to yield here to give it a chance to run. await trio.sleep(0.001) assert bond_called
async def test_aurora_pick_non_existing_candidates(): candidates = set(NodeFactory.create_batch(2)) exclusion_candidates = candidates result = aurora_pick(candidates, exclusion_candidates) assert result in exclusion_candidates