Skip to content

feat(l1): kademlia k-bucket routing table v2#6511

Draft
azteca1998 wants to merge 7 commits intomainfrom
feat/kademlia-v2
Draft

feat(l1): kademlia k-bucket routing table v2#6511
azteca1998 wants to merge 7 commits intomainfrom
feat/kademlia-v2

Conversation

@azteca1998
Copy link
Copy Markdown
Contributor

Summary

Re-introduces the Kademlia k-bucket routing table (reverted in #6505 for v10 release) with all fixes and performance improvements applied:

  1. Kademlia base — revert of the revert (feat(l1): reintroduce proper Kademlia k-bucket routing table #6458)
  2. Peer pruning fix — mark unresponsive peers as disposable (fix(l1): mark unresponsive peers as disposable to prevent snapsync stalls #6497)
  3. Replacement contacts — include replacements in peer discovery and iteration
  4. Flat connection pool — 10K candidate pool decoupled from k-buckets for RLPx initiation (matches Reth/Nethermind architecture)
  5. O(1) random index probe — replace O(n) collect-then-choose with rand() % len + forward scan, avoiding actor contention during snap sync
  6. Remove permanent blacklist — contacts pruned from k-buckets stay in the connection pool for retry

Performance issues addressed

  • Actor contention: The original connection pool implementation collected all 10K entries into a Vec on every get_contact_to_initiate() call (every 100ms), blocking get_best_peer() calls from snap sync workers. Now O(k) with random start index.
  • Permanent blacklisting: discarded_contacts permanently banned peers after a single timeout, shrinking the effective pool over time. Removed entirely — RLPx handshake handles rejection.
  • Peer pruning: Unresponsive peers were never removed from k-buckets, causing peer count to stagnate.

Pending

  • Multisync benchmarks comparing against pre-Kademlia baseline (waiting for server availability)

Test plan

  • Multisync (hoodi + sepolia + mainnet) sync times comparable to pre-Kademlia baseline
  • Daily snapsync CI passes
  • No peer count stagnation during long syncs

…talls

Fixes snapsync failures where peer count stays constant and sync
eventually fails with "Failed to receive block headers" after hours
of operation.

Root cause: After PR #6458 introduced Kademlia k-buckets, peers that
became unresponsive during sync weren't marked as disposable, so they
remained in the routing table indefinitely. New peers went into
replacement lists but were never promoted because dead peers weren't
pruned.

Changes:
- Enhanced prune() to remove disposable contacts from both main and
  replacement lists, with automatic promotion of replacements
- Mark peers as disposable when they timeout during RLPx operations
  (block headers, block bodies, sync head requests)
- Added periodic pruning in the snap_sync main loop to ensure dead
  peers are regularly removed and replaced

Evidence from CI artifacts showed peer count stuck at 6 throughout
3h35m sync before failure. This fix enables peer rotation so healthy
peers from replacement lists can take over when active peers become
unresponsive.
The Kademlia k-bucket implementation only iterated over main bucket
contacts, ignoring replacement entries. This caused peer starvation
because dead contacts in the main list were never replaced by fresher
peers from the replacement list.

Fix iter_contacts() and do_get_contact_to_initiate() to also check
replacement contacts, allowing the node to discover and connect to
peers that were previously invisible to the peer selection logic.
KBucket::get_mut and get_contact only searched the main contact list,
so any state mutation (set_disposable, ping tracking, find_node count,
mark_knows_us) silently failed for contacts in the replacement list.
Since iter_contacts and do_get_contact_to_initiate now return
replacement contacts, this caused phantom contacts that were visible
to selection but invisible to updates.

Update get_contact to use get_any (main + replacements) and get_mut
to search both lists, ensuring all contact state mutations work
regardless of which list holds the contact.
…able

Add a separate IndexMap<H256, Node> connection pool (capacity 50K) for
RLPx connection initiation, decoupled from the k-bucket routing table
(which is limited to 256 × 16 = 4,096 contacts by Kademlia design).

All discovered contacts are inserted into both the k-buckets (for
Kademlia protocol operations like FindNode/GetClosestNodes) and the
connection pool (for peer connection initiation). This restores the
large candidate pool that existed before the k-bucket migration while
preserving correct Kademlia routing semantics.

The connection pool is:
- Populated on every contact discovery (discv4, discv5, insert_if_new)
- Cleaned during prune() when contacts are marked disposable
- Capped at 50K entries with oldest-first eviction
- Used with random selection and k-bucket state filtering
Matches the candidate pool size used by Reth and Nethermind.
- Replace O(n) collect-then-choose in do_get_contact_to_initiate with
  O(k) random index probing on the IndexMap (rand % len, scan forward).
  The old approach scanned all 10K pool entries, cloned eligible ones
  into a Vec, then randomly picked — blocking the peer_table actor and
  starving snap sync's get_best_peer calls.

- Replace collect-then-choose in do_get_contact_for_lookup with
  IteratorRandom::choose (single-pass reservoir sampling, zero alloc).

- Remove discarded_contacts permanent blacklist entirely. Contacts
  pruned from k-buckets now remain in the connection pool so they can
  be retried — the RLPx handshake rejects truly incompatible peers.
  Previously, a single timeout permanently blacklisted a contact from
  both the pool and re-discovery.
@github-actions
Copy link
Copy Markdown

Lines of code report

Total lines added: 298
Total lines removed: 7
Total lines changed: 305

Detailed view
+------------------------------------------------+-------+------+
| File                                           | Lines | Diff |
+------------------------------------------------+-------+------+
| ethrex/cmd/ethrex/initializers.rs              | 637   | +1   |
+------------------------------------------------+-------+------+
| ethrex/cmd/ethrex/l2/initializers.rs           | 367   | +4   |
+------------------------------------------------+-------+------+
| ethrex/crates/networking/p2p/discv4/server.rs  | 693   | -7   |
+------------------------------------------------+-------+------+
| ethrex/crates/networking/p2p/discv5/server.rs  | 1518  | +10  |
+------------------------------------------------+-------+------+
| ethrex/crates/networking/p2p/peer_handler.rs   | 594   | +4   |
+------------------------------------------------+-------+------+
| ethrex/crates/networking/p2p/peer_table.rs     | 1334  | +278 |
+------------------------------------------------+-------+------+
| ethrex/crates/networking/p2p/sync/snap_sync.rs | 1149  | +1   |
+------------------------------------------------+-------+------+

@azteca1998 azteca1998 changed the title feat(p2p): Kademlia k-bucket routing table v2 feat(l1): Kademlia k-bucket routing table v2 Apr 21, 2026
@github-actions github-actions Bot added the L1 Ethereum client label Apr 21, 2026
@azteca1998 azteca1998 changed the title feat(l1): Kademlia k-bucket routing table v2 feat(l1): kademlia k-bucket routing table v2 Apr 21, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

L1 Ethereum client

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

1 participant