discv5

repository·master·Indexed 41 days ago

https://github.com/sigp/discv5

A Rust implementation of the Discovery v5 peer discovery protocol (version 0.11.0). It provides encrypted peer discovery using Ethereum Node Records (ENRs) and Kademlia-like routing, featuring NAT traversal, packet filtering, rate limiting, and an event stream for monitoring network activity.

Tokens
10.7K
Snippets
26
Records
46
Agent score
51%

What's inside discv5

  1. Overview of Discovery v5

    master

    Discovery v5 is a Rust implementation of the Discovery v5 protocol, designed for encrypted peer discovery.

    Key features include:

    • Peer Identification: Nodes are identified via an ENR (Ethereum Node Record), which is a signed key-value store containing the node's public key and optional IP/port information.
    • Routing: Uses a Kademlia-like routing table to manage discovered peers and topics.
    • NAT Traversal: Handles external IP discovery in NAT environments through PING/PONG exchanges. Nodes determine their external IP by choosing the majority address reported by other nodes. Updates to the external IP can trigger events to notify the swarm.
  2. How Discv5 handles ENR addresses and NAT

    master

    The protocol manages ENRs with optional ip and port fields to handle nodes behind NATs and prevent DoS attacks. The protocol filters and only advertises contactable ENRs based on the following logic:

    1. Empty Address ENRs: If a node provides an ENR without an address (common for nodes behind NAT that haven't determined their external IP), the node is considered valid. The server responds to its requests but does not add the ENR to the local routing table.
    2. Matching Address ENRs: If the IP address in the ENR matches the source address (src) from which the packet was received, the ENR is considered valid and is added to the local routing table (and may be advertised to others).
    3. Mismatched Address ENRs: If the ENR specifies an IP that does not match the source socket (e.g., 127.0.0.1 or an unreachable internal subnet), the ENR is considered invalid. The server will not add it to the routing table but will still respond to discovery requests.
  3. Create and start a Discv5 service

    master

    To use discv5, you need to generate a local ENR key, configure the listening socket, build a configuration using ConfigBuilder, and then initialize the Discv5 server. You can then start the server and perform queries like find_node using an async runtime like tokio.

    Note: To bootstrap the routing table, you should manually add an external ENR using discv5.add_enr(<ENR>).

       use discv5::{enr, enr::{CombinedKey, NodeId}, TokioExecutor, Discv5, ConfigBuilder};
       use discv5::socket::ListenConfig;
       use std::net::{SocketAddr, Ipv4Addr};
    
       // construct a local ENR
       let enr_key = CombinedKey::generate_secp256k1();
       let enr = enr::Enr::empty(&enr_key).unwrap();
    
       // build the tokio executor
       let mut runtime = tokio::runtime::Builder::new_multi_thread()
           .thread_name("Discv5-example")
           .enable_all()
           .build()
           .unwrap();
    
       // configuration for the sockets to listen on
       let listen_config = ListenConfig::Ipv4 {
           ip: Ipv4Addr::UNSPECIFIED,
           port: 9000,
       };
    
       // default configuration
       let config = ConfigBuilder::new(listen_config).build();
    
       // construct the discv5 server
       let mut discv5: Discv5 = Discv5::new(enr, enr_key, config).unwrap();
    
       // In order to bootstrap the routing table an external ENR should be added
       // This can be done via add_enr. I.e.:
       // discv5.add_enr(<ENR>)
    
       // start the discv5 server
       runtime.block_on(discv5.start());
    
       // run a find_node query
       runtime.block_on(async {
          let found_nodes = discv5.find_node(NodeId::random()).await.unwrap();
          println!("Found nodes: {:?}", found_nodes);
       });
  4. Manage Kademlia routing table buckets with KBucket

    master

    A KBucket is a collection of up to MAX_NODES_PER_BUCKET (16) nodes, ordered from least-recently connected to most-recently connected. It manages node connectivity states, handles pending insertions when the bucket is full, and enforces limits on incoming connections.

    Key Concepts

    • Node Ordering: Nodes are stored such that disconnected nodes appear first, followed by connected nodes. The first_connected_pos index marks the boundary between these two groups.
    • Pending Nodes: If a bucket is full and a new connected node is encountered, the node can be marked as Pending. It becomes eligible for insertion after a specified pending_timeout if the least-recently connected node is not re-established as connected.
    • Connection Limits: You can restrict the number of incoming connections per bucket using the max_incoming parameter during initialization.
    • Filtering: KBucket supports an optional Filter to validate new entries against existing ones.
    // Example initialization
    let mut bucket = KBucket::new(
        Duration::from_secs(30), // pending_timeout
        8,                       // max_incoming
        None                     // filter
    );
  5. Configure packet filtering and rate limiting

    master

    To protect the node from inbound request floods, enable the packet filter using .enable_packet_filter(). This activates several security features:

    • Rate Limiting: Set a custom RateLimiter via .filter_rate_limiter(Option<RateLimiter>).
    • Node Limits per IP: Control how many node-ids are allowed per IP address before the IP is banned using .filter_max_nodes_per_ip(Option<usize>). Default is 10.
    • Ban Thresholds: Control how many nodes can be banned from a single IP before the IP itself is banned using .filter_max_bans_per_ip(Option<usize>). Default is 5.
    • Ban Duration: Set how long an IP remains banned using .ban_duration(Option<Duration>). Note that bans are checked every 5 minutes, so precision is limited to the nearest 5-minute interval. Default is 1 hour.
  6. Handle KBucket insertion results

    master

    When calling insert on a KBucket, the operation returns an InsertResult which dictates how the routing table should react to the new node:

    • InsertResult::Inserted: The node was successfully added to the bucket.
    • InsertResult::Pending { disconnected }: The bucket is full, and instead of immediate insertion, the node is placed in a pending state. This result includes the Key of the disconnected node that is scheduled to be evicted to make room for the new node.
    • InsertResult::Full: The bucket is full and cannot accept the node (e.g., if the node is connected and there is no pending slot or eviction possible).
    • InsertResult::FailedFilter: The node did not pass the bucket's configured filter.
    • InsertResult::TooManyIncoming: The number of incoming connections exceeds the max_incoming limit configured for the bucket.
  7. Query and modify Kademlia bucket entries using the `Entry` API

    master

    The Entry API provides a way to interact with specific nodes within a KBucketsTable. Depending on the state of the key in the bucket, an Entry will resolve to one of several variants:

    • Present(PresentEntry, NodeStatus): The node is currently in a bucket. Use PresentEntry to access or mutate its value, update its connection status, or remove it.
    • Pending(PendingEntry, NodeStatus): The node is waiting for a slot to become available. Use PendingEntry to access its value, update its status, or remove it.
    • Absent(AbsentEntry): The node is not in the table. Use AbsentEntry to attempt an insertion.
    • SelfEntry: The entry represents the local node.

    To use this API, you typically obtain an Entry for a specific Key from a KBucketsTable (via the internal new constructor) and then match on the variant to perform the desired operation.

  8. How `PredicateQuery` handles progress and stalling

    master

    The PredicateQuery manages its search through two primary progress modes:

    • Iterating: The query searches for the target by waiting for a limited number of peers (parallelism) in parallel. It tracks no_progress (the number of consecutive successful results that did not yield a peer closer to the target than any seen so far).
    • Stalled: If no_progress reaches the parallelism limit, the query enters the Stalled state. In this state, the allowed parallelism is increased to num_results in an attempt to find more peers and break the stall.

    If the query finds a peer closer to the target or discovers new peers while not yet at num_results, it can transition back from Stalled to Iterating.

  9. Configure ENR updates and NAT discovery

    master

    Discv5 can automatically update its local ENR (Ethereum Node Record) IP and port based on PONG responses from peers.

    • Enable/Disable Updates: Use .disable_enr_update() to stop the node from updating its ENR based on peer feedback.
    • Update Consensus: Set .enr_peer_update_min(usize) to define the minimum number of peers that must agree on an external IP/port before the local ENR is updated. This value must be at least 2. Default is 10.
    • NAT Heuristic: The auto_nat_listen_duration determines how long the node waits for an incoming connection after updating its ENR to verify connectivity. If no connection is received within this window, the node revokes its ENR advertisement for 6 hours. Use .auto_nat_listen_duration(Option<Duration>) to customize this. Default is 5 minutes.
  10. Quickstart: Create a Discv5 service

    master

    To use discv5, you need to generate a local ENR, configure your listening socket, and provide a tokio runtime. The service manages a Kademlia-like routing table and allows you to perform queries like find_node.

    Note that discv5 requires a tokio runtime with timing and IO enabled. If you do not provide an explicit Executor via ConfigBuilder, the library will attempt to use the existing tokio runtime; if none is found, the Discv5::new call will panic.

        use discv5::{enr, enr::{CombinedKey, Enr, NodeId}, TokioExecutor, Discv5, ConfigBuilder};
        use discv5::socket::ListenConfig;
        use std::net::{Ipv4Addr, SocketAddr};
    
        // construct a local ENR
        let enr_key = CombinedKey::generate_secp256k1();
        let enr = enr::Enr::empty(&enr_key).unwrap();
    
        // build the tokio executor
        let mut runtime = tokio::runtime::Builder::new_multi_thread()
            .thread_name("Discv5-example")
            .enable_all()
            .build()
            .unwrap();
    
        // configuration for the sockets to listen on
        let listen_config = ListenConfig::Ipv4 {
            ip: Ipv4Addr::UNSPECIFIED,
            port: 9000,
        };
    
        // default configuration
        let config = ConfigBuilder::new(listen_config).build();
    
        // construct the discv5 server
        let mut discv5: Discv5 = Discv5::new(enr, enr_key, config).unwrap();
    
        // In order to bootstrap the routing table an external ENR should be added
        // This can be done via add_enr. I.e.:
        // discv5.add_enr(<ENR>)
    
        // start the discv5 server
        runtime.block_on(discv5.start());
    
        // run a find_node query
        runtime.block_on(async {
           let found_nodes = discv5.find_node(NodeId::random()).await.unwrap();
           println!("Found nodes: {:?}", found_nodes);
        });
  11. Configure the Discv5 discovery protocol

    master

    Use ConfigBuilder to tune the performance, security, and networking behavior of the discovery protocol. The builder requires a ListenConfig to initialize. You can customize timeouts, retries, parallelism, and security filters like IP limits or packet filtering.

    Note that if you disable enr_update, the auto_nat_listen_duration will be automatically set to None during the build process to ensure consistent behavior.

    // Example of initializing a custom configuration
    let config = ConfigBuilder::new(my_listen_config)
        .request_timeout(Duration::from_secs(2))
        .query_parallelism(5)
        .enable_packet_filter()
        .build();
  12. Initialize and manage the Discv5 service

    master

    The Discv5 struct is the primary interface for running a Discovery v5 server.

    Initialization

    To create a new instance, use Discv5::new. You must provide:

    • local_enr: The local node's ENR.
    • enr_key: The CombinedKey used to sign the ENR. The public key in this keypair must match the public key in the ENR.
    • config: A Config object (typically built via ConfigBuilder).

    If no executor is provided in the Config, the service defaults to using a TokioExecutor.

    Lifecycle

    1. Start: Call start() to spawn the background service tasks and begin listening on the configured UDP socket.
    2. Shutdown: Call shutdown() to terminate the service. The Discv5 struct also implements Drop, so it will attempt to shut down automatically when it goes out of scope.

    Note: start() will return an error if the service is already running.

    let sk = CombinedKey::generate_secp256k1();
    let enr = Enr::builder().build(&sk).unwrap();
    let config = ConfigBuilder::new(ListenConfig::default()).build();
    
    let mut discv5 = Discv5::new(enr, sk, config).unwrap();
    discv5.start().await.unwrap();
    
    // ... use service ...
    
    discv5.shutdown();