Kademlia Event Handler
Crate: kinetic-network Stage: 8 Reading time: 25 minutes Depends on: kinetic-core, kinetic-types
What Is This?
This file contains the core handler for all Kademlia DHT (Distributed Hash Table) events that occur within the Kinetic network.
When the underlying libp2p swarm emits a Kademlia event, it flows directly into this handler. These events include:
- A remote peer responding to our request for a record.
- A search query finishing its network traversal across multiple nodes.
- Another peer attempting to push a new record into our local storage.
- The success or failure of our own attempts to publish data.
In a standard libp2p implementation, the Kademlia behaviour handles the raw routing and storage automatically without needing much application-level interference. However, Kinetic does not just blindly accept or route data.
This file serves as the critical intersection where generic Kademlia operations meet Kinetic’s specific, rigorous domain rules.
You can think of this handler as both the “border patrol” and the “mailroom” for the node:
- As the mailroom: It translates raw network events into actionable state changes for our Quorums, standard Gets, and Puts, making sure network replies are routed back to the right waiting async tasks.
- As the border patrol: It actively inspects inbound requests to ensure peers are not violating protocol rules. It applies bans to bad actors and intercepts heavy cryptographic work to keep the network stable.
Without this file, Kinetic would just be a generic, insecure data store. With it, it becomes a secure, consensus-driven network.
Why Kinetic Needs This
Kademlia by itself just stores and retrieves byte arrays. It has no concept of what those bytes mean, who is allowed to store them, or how much CPU power it takes to verify them.
If we just plugged standard Kademlia into Kinetic without this custom handler, the network would rapidly collapse under spam or freeze during heavy computations.
Here is exactly why Kinetic specifically built this custom interception layer:
-
Quorum Consensus Tracking: When Kinetic queries the DHT, it often doesn’t just want the first answer it finds. If we are checking the state of the network (like looking for a VDF reveal), a single malicious peer could lie to us and serve fake data. We want to ask multiple peers and verify they all agree. Standard Kademlia doesn’t have a concept of “quorums.” It just fires a query and streams back whatever it finds. This handler catches the individual asynchronous results as they stream in over time and aggregates them into our
pending_quorumsstate so we can prove network consensus. This prevents a single compromised node from hijacking the network state. -
Light Node Protection and Restriction: Light nodes in Kinetic are designed to be mobile or low-power clients. They are read-only participants on the network. They consume data but do not contribute to storage. If a light node attempts to write a record to the DHT, it is a severe protocol violation—they either have a broken client implementation or are attempting a malicious write. This handler acts as an immediate firewall. It catches those unauthorized write attempts and permanently bans the offending node before the data ever touches our local storage. Without this, light nodes could easily bloat the network.
-
VDF Verification Isolation (Preventing Event Loop Deadlocks): VDFs (Verifiable Delay Functions) are central to Kinetic’s architecture, but they take significant, real CPU time to verify. If a peer sends us a VDF reveal to store, we cannot verify it on the main network event loop. In Rust’s async model (using Tokio), if you block the thread for 500ms to do math, the entire system stops. While blocked, our node would stop responding to routing pings, heartbeat messages, and other DHT queries, causing other peers to think we went offline and drop our connections. This handler intercepts VDF records and safely offloads them to a background CPU thread, preserving the node’s responsiveness. It is a critical stability pattern.
-
The Network Immune System (Strikes): The network needs a way to defend itself against broken or malicious peers. If a peer repeatedly spams us with invalid data or incorrect VDFs, this handler tracks those failures over time. It implements a strike system that bans resource-wasting peers before they can exhaust our node’s capabilities, fill our disk with garbage, or perform a Denial of Service attack. A single failure might just be a network blip, but 3 failures in a minute shows intent or fatal bugs.
-
Asynchronous Flow Management: Kademlia queries in libp2p do not block and return a value like a normal function. You say “find this record”, and libp2p says “okay, I will emit events later as I find things.” This handler is the system that listens for those “later” events. Because multiple queries might be running at once, the handler uses an ID mapping system to identify what query an event belongs to, and reconnects it to the original caller waiting for the data.
How It Works
The core of this file is the handle function. It takes a mutable reference to the NetworkEventLoop (allowing it to mutate network state) and the incoming kad::Event.
The logic is split into two major categories based on the event enum:
- Handling Outbound Queries (data we asked for)
- Handling Inbound Requests (data peers are pushing to us)
Handling Outbound Queries (Our Requests)
When we send a query out to the DHT (e.g., searching for a record), Kademlia doesn’t block and give us the answer immediately. Instead, it periodically updates us on its asynchronous progress via the kad::Event::OutboundQueryProgressed event.
-> See kinetic-network/src/event_loop/handlers/kademlia.rs — Lines 6 to 76
Because Kademlia only identifies these progress updates with a random, opaque QueryId, the handler first has to figure out why we are getting this update. It looks up the ID in the event_loop.query_id_to_name map. This tells us what type of query this was: a Quorum, a Get, or a Put. This mapping is essential for tying async network events back to our specific domain needs.
When a Record is Found (GetRecordOk::FoundRecord):
- For Quorums:
If the lookup reveals this is a
Quorumquery, we check if the returned byte array exactly matches our expectedtarget_payload. If it does, we increment thematch_countfor that specific quorum. We ignore non-matching payloads entirely, as they don’t contribute to the consensus we are seeking. This is how Kinetic ensures multiple peers agree on the same value. - For Gets:
If the lookup reveals this is a
Getquery, we simply add the returned byte array to the list ofreceived_payloadsfor that query. This collects all variants the network offers so the caller can decide what to do with them. We don’t filter them here; we just aggregate them. - For Puts:
If the lookup reveals a
Putquery, nothing is done in this arm, as put progress is handled by a different event variant below.
When a Query Finishes or Errors:
- Kademlia eventually tells us it has exhausted the DHT search (
FinishedWithNoAdditionalRecord) or encountered a hard error (Err). - The handler immediately removes the query ID from the tracking map (
query_id_to_name.remove) to prevent memory leaks in the event loop over time. - It then calls the respective completion handler on the
event_loop(e.g.,handle_quorum_completion(name)orhandle_get_completion(name)). - This triggers the core logic to evaluate if the quorum reached its required threshold, or if the standard get returned enough usable data to proceed.
- Put completion events are similarly ignored in this specific block as they use a distinct match arm.
When a Put Completes (QueryResult::PutRecord):
- When we publish data to the network, we track how many peers successfully stored it via the
pending_putsstate map. - For every response we get back from a peer, we decrement
expected_responses. - If the peer’s response was an
Ok, we increment thesuccess_count. - Once
expected_responseshits exactly zero, we know the query is fully complete across all targeted peers. - If
success_countis greater than 0, we notify the original caller (via a one-shotresponderchannel) that the network publish succeeded. - If all peers failed to store it (success_count is 0), we send back a
PublishError::AllFailed. We then clean up the pending state map to free memory.
Handling Inbound Put Requests (Peer Requests)
When another peer wants our node to store a record, they send an InboundRequest::PutRecord. This is the most dangerous part of the network, and this is where Kinetic enforces its strictest domain rules.
-> See kinetic-network/src/event_loop/handlers/kademlia.rs — Lines 77 to 190
Step 1: The Light Node Firewall
- Before even looking at the payload data, we look at the sender’s identity.
- If the
sourcepeer ID is currently tracked in ourlight_nodesset, we immediately reject the request. - We call
swarm.disconnect_peer_idto sever the TCP connection. - We place them in the
banned_peerscache with an expiration time 24 hours (86,400 seconds) in the future. - Light nodes cannot write, period. This is an active defense mechanism against misconfigured or malicious light clients. It saves us from processing junk data.
Step 2: Intercepting VDF Reveals
- If the peer is a full node allowed to write, the handler attempts to parse the incoming raw bytes as a
serde_json::Value. - It specifically looks for a
vdf_prooffield. - If it finds one, it attempts to strongly type the JSON into a
kinetic_core::types::Reveal.
Because verifying this cryptographic reveal is expensive, we implement a strict offloading pattern:
- We use
crate::event_loop::utils::spawn_blockingto push the cryptographic verification to a background CPU thread. This frees the main async loop to continue processing network traffic. - We clone the necessary state (storage handles, VDF engine, current drand state) and move it into the background task so it can work independently.
- Once the background thread finishes verifying the VDF, it sends a
LoopbackCommand::CommitVerifiedRecordback to the main event loop via theloopback_txchannel. - The main event loop receives this command later and safely commits the record to the local store without ever having blocked its own execution. This is essential for preventing network stalling. Without this, a single bad VDF could bring down the node.
Step 3: Standard Storage and The Strike System
- If the record isn’t a VDF reveal (or if it fails the JSON peek), we assume it’s standard DHT data.
- We attempt to store it synchronously using our custom
store_mut().put_record(...).
If our custom store rejects the record with a fatal error (Severity::Error), we apply a strike against the peer to prevent abuse:
- We fetch their current strike count and the timestamp of their last error from the
bad_vdf_countsmap. - If their last error was more than 60 seconds ago, their slate is wiped clean, and we reset their count to 1.
- If it was within the last 60 seconds, we increment their count.
- If they hit 3 strikes within this rolling 60-second window, we drop the hammer.
- We disconnect them immediately, and ban them in memory for 24 hours.
- Critically, we persist that ban to the underlying storage database using
DB_PREFIX_BANNED_PEER. This ensures that even if our node restarts, the malicious peer remains banned. - If the error from the store is non-fatal (like a malformed record that isn’t malicious), we simply log a debug message and ignore the record, assigning no strikes to the peer. We don’t punish peers for simple bugs, only for persistent failures.
Key Pieces
-
kad::Event::OutboundQueryProgressed-> Seesrc/event_loop/handlers/kademlia.rs— Line 6 The primary envelope for DHT query updates from the libp2p behaviour. This tells us when Kademlia finds data, finishes searching, or successfully pushes data to a remote peer. -
kad::QueryResult::GetRecord-> Seesrc/event_loop/handlers/kademlia.rs— Lines 7 and 27 The specific result variant we match against to increment Quorum match counts or collect payloads for standard Get requests when data is successfully located on the network. -
kad::Event::InboundRequest::PutRecord-> Seesrc/event_loop/handlers/kademlia.rs— Line 77 Triggered when a remote peer asks our node to store a DHT record. This block is gated by Kinetic’s security, light-node checks, and VDF validation logic. -
event_loop.query_id_to_name-> Seesrc/event_loop/handlers/kademlia.rs— Line 9 The essential mapping dictionary that translates libp2p’s opaqueQueryIdback into our domain-specificQueryType(Quorum, Get, or Put), allowing us to route the data correctly. -
VDF Loopback Offloading Pattern -> See
src/event_loop/handlers/kademlia.rs— Lines 110 to 127 The architectural pattern of spawning a blocking task for VDF verification and using aloopback_txchannel to send theCommitVerifiedRecordverdict back to the main thread. This is vital for maintaining high network throughput. -
bad_vdf_countsStrike Tracking -> Seesrc/event_loop/handlers/kademlia.rs— Lines 144 to 156 The time-windowed tracking mechanism that ensures malicious or broken peers cannot spam the network with garbage data without facing a strict 24-hour ban. -
serde_json::from_slice-> Seesrc/event_loop/handlers/kademlia.rs— Line 97 The function used to peek into the raw bytes of an incoming record. It attempts to parse the bytes as generic JSON so we can check for specific Kinetic keys (vdf_proof) without knowing the exact type ahead of time. -
tokio::task::spawn_blocking(viautils::spawn_blocking) -> Seesrc/event_loop/handlers/kademlia.rs— Line 112 The Rust runtime tool used to push heavy CPU work off the async event loop. It creates a dedicated thread for the VDF math so the main loop can keep answering network pings.
How This Connects to the Rest of Kinetic
- CROSS-CRATE: Parses raw bytes into
kinetic_core::types::Revealto intercept VDF proofs before they hit the standard storage pipeline. - CROSS-CRATE: Uses
kinetic_core::error::Severityto determine if a rejected record is a fatal offense (warranting a strike) or a benign mistake. - CROSS-CRATE: Uses
kinetic_core::constants::DB_PREFIX_BANNED_PEERto persist 24-hour bans to the physical storage database. - Operates directly on the state held in the core
NetworkEventLoop(mutatingpending_quorums, checkinglight_nodes, and sending messages toloopback_tx), acting as the primary state-mutation engine for all DHT events.
Quick Reference
- Outbound Get Found: Update
match_count(if Quorum) or push payload toreceived_payloads(if Get). - Outbound Get Finished: Trigger the completion handler in the main event loop to evaluate the success or failure of the query.
- Outbound Put Result: Track successful peer stores; notify the calling function when expected responses reach zero.
- Inbound Light Node Write: Immediate disconnection and 24-hour memory ban applied to the sender.
- Inbound VDF Record: Parse JSON, offload to
spawn_blocking, verify mathematically, and loopback the result to the main thread for commitment. - Inbound Invalid Record: 1 strike. 3 strikes in a rolling 60s window = 24-hour ban, disconnected, and persisted to disk.
Open Questions / Things to Revisit
- Hardcoded Ban Durations: The 24-hour ban (86,400 seconds) and the 60-second strike window are currently hardcoded magic numbers directly in the handler. These should eventually be moved to a configuration file or a centralized network constants module to allow operators to tune node strictness dynamically.
- JSON Peeking Performance: The handler parses the incoming raw bytes into an untyped
serde_json::Valuesimply to check if thevdf_proofkey exists. If it does, it deserializes it again into the strongly typedReveal. This double-parsing is somewhat inefficient and could become a CPU bottleneck under extreme network load from many peers. - Strike System Memory Exhaustion: Currently, the strike system tracks
bad_vdf_countsin memory for every offending peer. If a resourced attacker uses thousands of distinct PeerIDs to send one bad record each, they could fill thebad_vdf_countsmap in memory, potentially causing a memory exhaustion attack. We may need an LRU cache or a periodic cleanup sweep for this tracking map to ensure safety. - VDF vs Generic Data: The way VDF reveals are special-cased right in the middle of the generic Kademlia handler feels slightly tightly coupled. In the future, this might be better handled by a middleware or interceptor pattern before it hits the raw DHT logic, keeping the DHT handler agnostic.
- Time Abstraction: The use of
web_timeis correct for WASM compatibility, but the mix ofSystemTimefor bans andInstantfor the strike window could be confusing.SystemTimeis vulnerable to system clock shifts, meaning a user changing their OS clock could potentially bypass a ban.