def test_enr_db_raises_DuplicateRecord(enr_db): private_key = PrivateKeyFactory().to_bytes() enr_a = ENRFactory( private_key=private_key, sequence_number=1, custom_kv_pairs={b"custom": b"enr-a"}, ) enr_b = ENRFactory( private_key=private_key, sequence_number=1, custom_kv_pairs={b"custom": b"enr-b"}, ) assert enr_a.node_id == enr_b.node_id assert enr_a.sequence_number == enr_b.sequence_number assert enr_a != enr_b # set it the first time. enr_db.set_enr(enr_a) with pytest.raises(DuplicateRecord): enr_db.set_enr(enr_b, raise_on_error=True) # without the flag it should silently ignore the error enr_db.set_enr(enr_b)
def test_query_for_ipv4_endpoint(enr_db, constraint): # doesn't have either key enr_a = ENRFactory.minimal() # two have the correct keys enr_b = ENRFactory() enr_c = ENRFactory() # missing port enr_d_manager = ENRManagerFactory() enr_d_manager.update((IP_V4_ADDRESS_ENR_KEY, IPv4Factory().packed)) enr_d = enr_d_manager.enr # missing ip address enr_e_manager = ENRManagerFactory() enr_e_manager.update((UDP_PORT_ENR_KEY, 30303)) enr_e_manager.update((TCP_PORT_ENR_KEY, 30303)) enr_e = enr_e_manager.enr enr_db.set_enr(enr_a) enr_db.set_enr(enr_b) enr_db.set_enr(enr_c) enr_db.set_enr(enr_d) enr_db.set_enr(enr_e) enr_results = tuple(enr_db.query(constraint)) assert len(enr_results) == 2 assert set(enr_results) == {enr_b, enr_c}
def test_query_excludes_outdated_matching_records(enr_db): private_key_a = PrivateKeyFactory().to_bytes() enr_a_0 = ENRFactory( sequence_number=0, private_key=private_key_a, custom_kv_pairs={b"test": b"value-A"}, ) enr_a_7 = ENRFactory(sequence_number=7, private_key=private_key_a) private_key_b = PrivateKeyFactory().to_bytes() enr_b_1 = ENRFactory( sequence_number=1, private_key=private_key_b, custom_kv_pairs={b"test": b"value-A"}, ) enr_db.set_enr(enr_a_0) enr_db.set_enr(enr_a_7) enr_db.set_enr(enr_b_1) enr_results = tuple(enr_db.query(KeyExists(b"test"))) assert len(enr_results) == 1 enr = enr_results[0] assert enr == enr_b_1
def test_wait_time_full_queue_and_table( topic_table, max_queue_size, target_ad_lifetime ): # fill one queue topic = TopicFactory() reg_time = 0 oldest_queue_eol = reg_time + target_ad_lifetime while not topic_table.is_queue_full(topic): topic_table.register(topic, ENRFactory(), reg_time) reg_time += 1 # fill the rest of the table oldest_table_eol = reg_time + target_ad_lifetime while not topic_table.is_full: topic_table.register(TopicFactory(), ENRFactory(), reg_time) reg_time += 1 assert topic_table.get_wait_time(topic, 0) == oldest_queue_eol assert topic_table.get_wait_time(TopicFactory(), 0) == oldest_queue_eol # refill queue oldest_queue_eol = reg_time + target_ad_lifetime for _ in range(max_queue_size): topic_table.register(topic, ENRFactory(), reg_time) reg_time += 1 assert topic_table.get_wait_time(topic, 0) == oldest_queue_eol assert topic_table.get_wait_time(TopicFactory(), 0) == oldest_table_eol
def test_registration_single_queue(topic_table, max_queue_size): topic = TopicFactory() enr = ENRFactory() other_enr = ENRFactory() topic_table.get_enrs_for_topic(topic) == () topic_table.register(topic, enr, 0) assert topic_table.get_enrs_for_topic(topic) == (enr,) topic_table.register(topic, other_enr, 0) assert topic_table.get_enrs_for_topic(topic) == (other_enr, enr) with pytest.raises(ValueError): topic_table.register(topic, enr, 0) while not topic_table.is_queue_full(topic): topic_table.register(topic, ENRFactory(), 0) with pytest.raises(ValueError): topic_table.register(topic, ENRFactory(), 0) enrs_before = topic_table.get_enrs_for_topic(topic) new_enr = ENRFactory() topic_table.register(topic, new_enr, topic_table.get_wait_time(topic, 0)) enrs_after = topic_table.get_enrs_for_topic(topic) assert enrs_after == (new_enr,) + enrs_before[:-1]
def test_record_query_with_multi_key_constraint(conn): record_a = Record.from_enr( ENRFactory(custom_kv_pairs={b"test-a": b"value-A"}, )) record_b = Record.from_enr( ENRFactory(custom_kv_pairs={ b"test-a": b"value-A", b"test-b": b"value-B" }, )) record_c = Record.from_enr( ENRFactory(custom_kv_pairs={b"test-b": b"value-B"}, )) record_d = Record.from_enr(ENRFactory()) record_e = Record.from_enr( ENRFactory(custom_kv_pairs={ b"test-a": b"value-A", b"test-b": b"value-B" }, )) insert_record(conn, record_a) insert_record(conn, record_b) insert_record(conn, record_c) insert_record(conn, record_d) insert_record(conn, record_e) matched_records = tuple( query_records(conn, required_keys=(b"test-a", b"test-b"))) assert len(matched_records) == 2 assert set(matched_records) == {record_b, record_e}
def test_record_query_with_no_constraints(conn): record_a = Record.from_enr(ENRFactory()) record_b = Record.from_enr(ENRFactory()) insert_record(conn, record_a) insert_record(conn, record_b) all_records = tuple(query_records(conn)) assert set(all_records) == {record_a, record_b}
def test_wait_time_full_table(topic_table, target_ad_lifetime): # fill one queue reg_time = 0 oldest_table_eol = reg_time + target_ad_lifetime while not topic_table.is_full: assert topic_table.get_wait_time(TopicFactory(), 0) == 0 topic_table.register(TopicFactory(), ENRFactory(), reg_time) reg_time += 1 assert topic_table.get_wait_time(TopicFactory(), 0) == oldest_table_eol topic_table.register(TopicFactory(), ENRFactory(), reg_time) assert topic_table.get_wait_time(TopicFactory(), 0) == oldest_table_eol + 1
def test_registration_two_queues(topic_table, max_queue_size): topic1 = TopicFactory() topic2 = TopicFactory() enr = ENRFactory() topic_table.register(topic1, enr, 0) while not topic_table.is_queue_full(topic1): topic_table.register(topic1, ENRFactory(), 0) topic_table.register(topic2, enr, 1) while not topic_table.is_queue_full(topic2): topic_table.register(topic2, ENRFactory(), 1) with pytest.raises(ValueError): topic_table.register(topic1, ENRFactory(), 1) with pytest.raises(ValueError): topic_table.register(topic2, ENRFactory(), 1) with pytest.raises(ValueError): topic_table.register(topic2, ENRFactory(), topic_table.get_wait_time(topic1, 0)) enrs_topic1_before = topic_table.get_enrs_for_topic(topic1) enrs_topic2_before = topic_table.get_enrs_for_topic(topic2) new_enr_topic1 = ENRFactory() new_enr_topic2 = ENRFactory() topic_table.register(topic1, new_enr_topic1, topic_table.get_wait_time(topic1, 0)) topic_table.register(topic2, new_enr_topic2, topic_table.get_wait_time(topic2, 0)) enrs_topic1_after = topic_table.get_enrs_for_topic(topic1) enrs_topic2_after = topic_table.get_enrs_for_topic(topic2) assert enrs_topic1_after == (new_enr_topic1,) + enrs_topic1_before[:-1] assert enrs_topic2_after == (new_enr_topic2,) + enrs_topic2_before[:-1]
def test_query_by_key_existence(enr_db): enr_a = ENRFactory(custom_kv_pairs={b"test": b"value-A"}) enr_b = ENRFactory(custom_kv_pairs={b"test": b"value-B"}) enr_c = ENRFactory() enr_db.set_enr(enr_a) enr_db.set_enr(enr_b) enr_db.set_enr(enr_c) enr_results = set(enr_db.query(KeyExists(b"test"))) assert len(enr_results) == 2 assert enr_a in enr_results assert enr_b in enr_results assert enr_c not in enr_results
async def test_client_request_response_stream_find_nodes_catches_invalid_response_total( alice, bob, alice_client, bob_client ): enrs = tuple(ENRFactory() for _ in range(10)) async with trio.open_nursery() as nursery: async with bob_client.dispatcher.subscribe(FindNodeMessage) as subscription: async def _respond(): request = await subscription.receive() message = AnyOutboundMessage( FoundNodesMessage(request.message.request_id, 0, enrs), alice.endpoint, alice.node_id, ) await bob_client.dispatcher.send_message(message) nursery.start_soon(_respond) with pytest.raises( ValidationError, match="Invalid `total` counter in response: total=0" ): async with alice_client.stream_find_nodes( bob.node_id, bob.endpoint, distances=[256] ) as resp_aiter: tuple([resp async for resp in resp_aiter]) nursery.cancel_scope.cancel()
def test_query_with_order_by_closest(enr_db): all_enrs = tuple(ENRFactory() for _ in range(4)) target, *enrs = all_enrs target_node_id_as_int = int.from_bytes(target.node_id, "big") def distance_fn(enr): node_id_as_int = int.from_bytes(enr.node_id, "big") return target_node_id_as_int ^ node_id_as_int enrs_by_proximity = tuple(sorted(enrs, key=distance_fn)) for enr in enrs: enr_db.set_enr(enr) enrs_closest_to_target = tuple(enr_db.query(ClosestTo(target.node_id))) assert enrs_closest_to_target == enrs_by_proximity enrs_closest_to_target_with_ip = tuple( enr_db.query(ClosestTo(target.node_id), KeyExists(b"ip"))) assert enrs_closest_to_target_with_ip == enrs_by_proximity enrs_closest_to_target_with_ipv4_endpoint = tuple( enr_db.query(ClosestTo(target.node_id), has_tcp_ipv4_endpoint)) assert enrs_closest_to_target_with_ipv4_endpoint == enrs_by_proximity
async def test_network_stream_find_nodes_api_validates_response_distances( alice, bob, bob_client, alice_network, response_enr): if response_enr == "own": enr_for_response = bob.enr elif response_enr == "wrong": for _ in range(200): enr_for_response = ENRFactory() if compute_log_distance(enr_for_response.node_id, bob.node_id) == 256: break else: raise Exception("failed") else: raise Exception(f"unsupported param: {response_enr}") async with bob.events.find_nodes_received.subscribe() as subscription: async with trio.open_nursery() as nursery: async def _respond(): request = await subscription.receive() await bob_client.send_found_nodes( alice.node_id, alice.endpoint, enrs=(enr_for_response, ), request_id=request.request_id, ) nursery.start_soon(_respond) with trio.fail_after(REQUEST_RESPONSE_TIMEOUT): with pytest.raises(ValidationError, match="Invalid response: distance="): async with alice_network.stream_find_nodes( bob.node_id, bob.endpoint, distances=[255]) as resp_aiter: tuple([resp async for resp in resp_aiter])
async def test_network_stream_find_nodes(alice, bob, alice_network, bob_client): enrs = tuple(ENRFactory() for _ in range(FOUND_NODES_MAX_PAYLOAD_SIZE + 1)) distances = set( [compute_log_distance(enr.node_id, bob.node_id) for enr in enrs]) async with trio.open_nursery() as nursery: async with bob.events.find_nodes_received.subscribe() as subscription: async def _send_response(): find_nodes = await subscription.receive() await bob_client.send_found_nodes( alice.node_id, alice.endpoint, enrs=enrs, request_id=find_nodes.message.request_id, ) nursery.start_soon(_send_response) with trio.fail_after(2): async with alice_network.stream_find_nodes( bob.node_id, bob.endpoint, distances=distances) as resp_aiter: actual_enrs = tuple([resp async for resp in resp_aiter]) assert actual_enrs == enrs nursery.cancel_scope.cancel()
async def test_bootstrap_nodes(): private_key = PrivateKeyFactory().to_bytes() bootnode1 = ENRFactory(private_key=private_key) bootnode2 = ENRFactory() discovery = MockDiscoveryService([Node(bootnode1), Node(bootnode2)]) assert discovery.enr_db.get_enr(bootnode1.node_id) == bootnode1 assert discovery.enr_db.get_enr(bootnode2.node_id) == bootnode2 assert [node.enr for node in discovery.bootstrap_nodes] == [bootnode1, bootnode2] # If our DB gets updated with a newer ENR of one of our bootnodes, the @bootstrap_nodes # property will reflect that. new_bootnode1 = ENRFactory( private_key=private_key, sequence_number=bootnode1.sequence_number + 1) discovery.enr_db.set_enr(new_bootnode1) assert [node.enr for node in discovery.bootstrap_nodes] == [new_bootnode1, bootnode2]
async def test_network_find_nodes_api(alice, bob): distances = {0} async with alice.network() as alice_network: async with bob.network() as bob_network: for _ in range(200): enr = ENRFactory() bob.enr_db.set_enr(enr) bob_network.routing_table.update(enr.node_id) distances.add(compute_log_distance(enr.node_id, bob.node_id)) if distances.issuperset({0, 256, 255}): break else: raise Exception("failed") with trio.fail_after(2): enrs = await alice_network.find_nodes(bob.node_id, 0, 255, 256) assert any(enr.node_id == bob.node_id for enr in enrs) response_distances = { compute_log_distance(enr.node_id, bob.node_id) for enr in enrs if enr.node_id != bob.node_id } assert response_distances == {256, 255}
def test_enr_manager_handles_existing_enr_in_database(enr_db): private_key = PrivateKeyFactory() enr = ENRFactory(private_key=private_key.to_bytes(), sequence_number=10) enr_db.set_enr(enr) enr_manager = ENRManager(private_key, enr_db) assert enr_manager.enr == enr
async def test_network_responds_to_find_node_requests(alice, bob): distances = {0} async with alice.network() as alice_network: async with bob.network() as bob_network: for _ in range(200): enr = ENRFactory() bob.enr_db.set_enr(enr) bob_network.routing_table.update(enr.node_id) distances.add(compute_log_distance(enr.node_id, bob.node_id)) if distances.issuperset({0, 256, 255}): break else: raise Exception("failed") with trio.fail_after(2): responses = await alice_network.client.find_nodes( bob.endpoint, bob.node_id, distances=(0, 255, 256), ) assert all( isinstance(response.message, FoundNodesMessage) for response in responses ) response_enrs = tuple( enr for response in responses for enr in response.message.enrs ) response_distances = { compute_log_distance(enr.node_id, bob.node_id) if enr.node_id != bob.node_id else 0 for enr in response_enrs } assert response_distances.issuperset({0, 255, 256})
async def test_v51_rpc_sendFindNodes_web3(bob_node_id_param_w3, bob, bob_network, w3): distances = set() for _ in range(10): enr = ENRFactory() distances.add(compute_log_distance(bob.node_id, enr.node_id)) bob.enr_db.set_enr(enr) async with bob_network.client.dispatcher.subscribe( FindNodeMessage) as subscription: first_response = await trio.to_thread.run_sync( w3.discv5.send_find_nodes, bob_node_id_param_w3, 0) with trio.fail_after(2): first_receipt = await subscription.receive() assert encode_hex( first_receipt.message.request_id) == first_response.value assert first_receipt.message.distances == (0, ) # request with multiple distances second_response = await trio.to_thread.run_sync( w3.discv5.send_find_nodes, bob_node_id_param_w3, tuple(distances)) with trio.fail_after(2): second_receipt = await subscription.receive() assert encode_hex( second_receipt.message.request_id) == second_response.value assert second_receipt.message.distances == tuple(distances)
async def test_v51_rpc_sendFindNodes(make_request, bob_node_id_param, bob, bob_network): distances = set() for _ in range(10): enr = ENRFactory() distances.add(compute_log_distance(bob.node_id, enr.node_id)) bob.enr_db.set_enr(enr) async with bob_network.client.dispatcher.subscribe( FindNodeMessage) as subscription: single_response = await make_request("discv5_sendFindNodes", [bob_node_id_param, 0]) with trio.fail_after(2): first_receipt = await subscription.receive() assert encode_hex(first_receipt.message.request_id) == single_response assert first_receipt.message.distances == (0, ) # request with multiple distances multiple_response = await make_request( "discv5_sendFindNodes", [bob_node_id_param, tuple(distances)], ) with trio.fail_after(2): second_receipt = await subscription.receive() assert encode_hex( second_receipt.message.request_id) == multiple_response assert second_receipt.message.distances == tuple(distances)
def test_found_nodes_message_encoding_round_trip(num_enr_records): enrs = tuple(ENRFactory() for _ in range(num_enr_records)) encoded_enrs = tuple(rlp.encode(enr) for enr in enrs) payload = FoundNodesPayload(num_enr_records, encoded_enrs) message = FoundNodesMessage(payload) encoded = message.to_wire_bytes() result = decode_message(encoded) assert result.payload == message.payload
async def test_generate_eth_cap_enr_field(): base_db = AtomicDB() ChainDB(base_db).persist_header(ROPSTEN_GENESIS_HEADER) enr_field = await generate_eth_cap_enr_field(ROPSTEN_VM_CONFIGURATION, AsyncHeaderDB(base_db)) enr = ENRFactory(custom_kv_pairs={enr_field[0]: enr_field[1]}) assert extract_forkid(enr) == ForkID(hash=to_bytes(hexstr='0x30c7ddbc'), next=10)
def enr(private_key, endpoint): return ENRFactory( private_key=private_key, custom_kv_pairs={ b"ip": endpoint.ip_address, b"udp": endpoint.port }, )
def test_wait_time_full_queue(topic_table, max_total_size, target_ad_lifetime): topic = TopicFactory() different_topic = TopicFactory() reg_time = 0 oldest_queue_eol = reg_time + target_ad_lifetime while not topic_table.is_queue_full(topic): assert topic_table.get_wait_time(topic, 0) == 0 assert topic_table.get_wait_time(different_topic, 0) == 0 topic_table.register(topic, ENRFactory(), reg_time) reg_time += 1 assert topic_table.get_wait_time(topic, 0) == oldest_queue_eol assert topic_table.get_wait_time(different_topic, 0) == 0 topic_table.register(topic, ENRFactory(), reg_time) assert topic_table.get_wait_time(topic, 0) == oldest_queue_eol + 1 assert topic_table.get_wait_time(different_topic, 0) == 0
def remote_enr(remote_private_key, remote_endpoint): return ENRFactory( private_key=remote_private_key, custom_kv_pairs={ b"ip": remote_endpoint.ip_address, b"udp": remote_endpoint.port, }, )
async def filled_routing_table(routing_table, enr_db): # add entries until the first bucket is full while len(routing_table.get_nodes_at_log_distance( 255)) < routing_table.bucket_size: enr = ENRFactory() routing_table.update(enr.node_id) enr_db.set_enr(enr) return routing_table
async def test_v51_rpc_sendFoundNodes_invalid_params(make_request, invalid_node_id, bob): enrs = set() for _ in range(10): enr = ENRFactory() bob.enr_db.set_enr(enr) enrs.add(repr(enr)) single_enr = next(iter(enrs)) # bad node_id with pytest.raises(Exception, match="'error':"): await make_request("discv5_sendFoundNodes", [invalid_node_id, tuple(enrs), 0]) with pytest.raises(Exception, match="'error':"): await make_request("discv5_sendFoundNodes", [invalid_node_id, tuple(enrs), [0]]) # invalid enr with pytest.raises(Exception, match="'error':"): await make_request("discv5_sendFoundNodes", [invalid_node_id, (single_enr[4:], ), 0]) # invalid distances with pytest.raises(Exception, match="'error':"): await make_request( "discv5_sendFoundNodes", [bob.node_id.hex(), tuple(enrs), -1]) with pytest.raises(Exception, match="'error':"): await make_request( "discv5_sendFoundNodes", [bob.node_id.hex(), tuple(enrs), 257]) with pytest.raises(Exception, match="'error':"): await make_request( "discv5_sendFoundNodes", [bob.node_id.hex(), tuple(enrs), 1.2]) with pytest.raises(Exception, match="'error':"): await make_request( "discv5_sendFoundNodes", [bob.node_id.hex(), tuple(enrs), []]) with pytest.raises(Exception, match="'error':"): await make_request( "discv5_sendFoundNodes", [bob.node_id.hex(), tuple(enrs), "xyz"]) with pytest.raises(Exception, match="'error':"): await make_request( "discv5_sendFoundNodes", [bob.node_id.hex(), tuple(enrs), [1, "2"]]) # wrong params count with pytest.raises(Exception, match="'error':"): await make_request("discv5_sendFoundNodes", []) with pytest.raises(Exception, match="'error':"): await make_request("discv5_sendFoundNodes", [bob.node_id.hex(), tuple(enrs)]) with pytest.raises(Exception, match="'error':"): await make_request( "discv5_sendFoundNodes", [bob.node_id.hex(), tuple(enrs), 0, "extra"])
def new_enr_manager(): enr_db = QueryableENRDB(sqlite3.connect(":memory:")) private_key = PrivateKeyFactory() base_enr = ENRFactory( private_key=private_key.to_bytes(), sequence_number=secrets.randbelow(100) + 1, ) enr_db.set_enr(base_enr) return ENRManager(private_key, enr_db)
def test_get_and_set_enr(enr_db): private_key = PrivateKeyFactory().to_bytes() db = enr_db enr = ENRFactory(private_key=private_key) with pytest.raises(KeyError): db.get_enr(enr.node_id) db.set_enr(enr) assert db.get_enr(enr.node_id) == enr
def test_record_deletion(conn): record = Record.from_enr(ENRFactory()) insert_record(conn, record) assert get_record(conn, record.node_id) == record row_count = delete_record(conn, record.node_id) assert row_count == 1 with pytest.raises(RecordNotFound): get_record(conn, record.node_id)