Event Loop Swarm Initialization & Handling
Crate: kinetic-network Stage: 8 Reading time: 30 minutes Depends on: 11_event_loop_core.md
What Is This?
This documentation covers the two vital components that bring the libp2p Swarm to life inside Kinetic’s NetworkEventLoop: the builder and the handler.
- The Builder (
swarm_builder.rs): This is where the Kinetic node physically constructs its network identity and internal state. It takes in configuration parameters, cryptographic keys, storage engines, and internal communication channels, and wires them all together. The output of the builder is a fully configured libp2pSwarminstance, embedded inside theNetworkEventLoopstruct, ready to connect to the internet. - The Handler (
swarm_handler.rs): This is the sensory nervous system and primary security checkpoint of the Kinetic node. Once the swarm is running, it constantly emits asynchronous events — peers connecting, peers disconnecting, DHT requests finishing, NAT statuses updating, and sub-protocols generating messages. The handler catches every single one of these raw events, interprets them according to Kinetic’s security and protocol rules, and decides what action to take next.
In short, this is where generic peer-to-peer networking becomes specifically a Kinetic network node. A vanilla libp2p swarm just passes messages. The Kinetic swarm handler applies Kinetic’s unique requirements — like Sybil-resistant Proof-of-Work checks, VDF resolution tie-breakers, and strict ban list enforcement — directly to the raw network events before they are allowed to influence the deeper application logic.
Why Kinetic Needs This
A libp2p Swarm out of the box is agnostic. It does not know about Kinetic’s drand rounds, it does not know about Verifiable Delay Functions (VDFs) for tie-breakers, and it definitely does not know that it needs to block Sybil attacks by requiring Proof-of-Work from connecting IP addresses. It assumes a cooperative, friendly environment.
Kinetic needs the builder because initializing a node is complex and depends on its environment. A node must decide whether it is a Light Node or a Full Node (which fundamentally changes its capabilities and the behaviors it runs). It must pre-load banned peers from the storage engine so it doesn’t accidentally reconnect to malicious nodes immediately upon restart. It must also set up concurrency limiters (Semaphores) so that processing heavy cryptographic tasks doesn’t freeze the entire networking stack.
Kinetic needs the handler because every connection on the internet is untrusted by default. When a new peer connects, the node cannot just blindly add them to its Kademlia routing table. If it did, the network would be vulnerable to specific peer-to-peer attack vectors:
- Sybil Attacks: A malicious actor could spin up 10,000 fake peer identities on a single server and connect to your node.
- Eclipse Attacks: By surrounding your node with malicious peers, they could control all data flowing into and out of your node, effectively blinding you to the real network state.
- Routing Table Poisoning: They could fill your Kademlia routing table with dead or malicious addresses, making it impossible for you to resolve legitimate domain names.
The handler enforces a hard, uncompromising line against these threats:
- “Are you on the ban list?” -> Drop connection immediately.
- “Are you a bootstrap node?” -> You get a temporary pass, but we will watch your behavior.
- “Have you done the required Proof-of-Work based on the current drand block?” -> If no, we drop your connection and ignore your IP entirely.
The Front Desk Analogy:
Think of the libp2p Swarm as a corporate office building. swarm_builder.rs is the construction crew that wires the electricity, installs the servers, and hires the security guards at the front desk. The network configuration file is the blueprint. swarm_handler.rs is the security guard at the front desk. The guard sees thousands of people (events) walking into the lobby every second. Some are known employees (bootstrap nodes), some are unknown visitors (peers), and some are known criminals (banned peers). The guard must instantly check IDs (Proof of Work), throw out the criminals, and direct the valid employees to their proper departments (dispatching protocol events). If the guard freezes up because they are thinking too hard about a single visitor’s math problem (blocking the async thread with heavy cryptography), the lobby fills up with a backlog and the building ceases to function.
Without this handler intercepting and scrutinizing every single SwarmEvent, the Kinetic network would instantly collapse under spam, attacks, and invalid routing data.
How It Works
The architecture is split logically between initializing the state once, and then continuously reacting to it in a loop.
1. Swarm Construction (swarm_builder.rs)
When NetworkEventLoop::new is called, it performs a massive assembly operation to construct the event loop and the swarm.
-> See: kinetic-network/src/event_loop/swarm_builder.rs — Lines 11 to 124
Node Type Branching:
The first major architectural decision is determining the node mode. If config.mode == NetworkMode::LightNode, it calls lightnode::build_light_swarm. If it’s a Full Node, it calls fullnode::build_full_swarm. (Note: WebAssembly targets panic if trying to run as a FullNode, as browsers cannot handle the full Kademlia and server-side storage requirements. They lack raw TCP/UDP socket access). This abstraction keeps the event loop agnostic to the exact behaviors being run under the hood.
Bootstrapping the Network:
A node cannot join a decentralized P2P network if it doesn’t know anyone else to talk to. It needs an entry point. The builder loops through the provided config.bootstrap_nodes array. For each multiaddress, it extracts the PeerId and immediately instructs the swarm to dial() it. It also adds the address to Kademlia’s routing table. This gives the node its initial anchor point into the broader Kinetic DHT, allowing it to begin discovering other peers through the bootstrap node.
-> See: kinetic-network/src/event_loop/swarm_builder.rs — Lines 44 to 59
State Hydration and the Ban List:
Before the event loop starts running, the builder pre-loads the ban list. It scans the StorageEngine for keys prefixed with DB_PREFIX_BANNED_PEER. It reads the expiration timestamp for each banned peer. If the ban has expired (compared to the current time), it deletes it from storage to clean up space. If the ban is still active, it loads it into an in-memory LruCache. This is crucial: by keeping the active ban list in memory, the node can reject connections instantly without having to perform a slow database read for every single incoming connection attempt.
-> See: kinetic-network/src/event_loop/swarm_builder.rs — Lines 81 to 109
Semaphores for Concurrency Control:
The builder initializes pow_semaphore (with 2 permits) and gossip_semaphore (with 8 permits). Because Proof-of-Work checking is CPU-intensive, if 50 peers connect at once, trying to verify all their hashes simultaneously would stall the node’s tokio runtime. The semaphore acts as a throttle, ensuring that only 2 PoW checks happen concurrently. The remaining connection attempts wait in line safely.
2. Event Handling (swarm_handler.rs)
Once built, the main event loop continuously calls swarm.select_next_some().await. The resulting raw SwarmEvent is passed to the handle_swarm_event function.
-> See: kinetic-network/src/event_loop/swarm_handler.rs — Lines 109 to 311
This function is a giant, comprehensive match statement on the type of event that occurred. It is the central router for network activity.
Deep Dive: Connection Establishment Lifecycle
When a peer physically connects over TCP or QUIC, libp2p fires a SwarmEvent::ConnectionEstablished event. The handler immediately executes a strict sequence of checks:
- The Ban Check: The handler peeks into
self.banned_peers. It compares the ban’s expiration timestamp to the current UNIX timestamp. If the ban is still active, it instantly callsswarm.disconnect_peer_id(peer_id). If expired, it silently pops it from the cache. This ensures the node doesn’t waste CPU cycles or memory on known malicious actors. - The Drand Synchronization Check: To verify a Proof-of-Work, the node needs to know the current network time (the
drand_kynepoch). Ifself.current_drand_kyn == 0, it means the node has just started and hasn’t received the first drand broadcast yet. If the connecting peer is NOT a bootstrap node, the node drops the connection. Why? Because it cannot verify their PoW, and accepting them blindly would open a Sybil vulnerability window where attackers could flood the node before it finishes syncing. - The Proof-of-Work Verification: Because computing the hash to verify PoW is CPU-bound, running it directly in the
handle_swarm_eventasync function would freeze the entire event loop. No other network packets could be processed. To solve this, Kinetic clones thepow_semaphoreand spawns a tokio background task.- The task waits to acquire one of the 2 available semaphore permits.
- Once acquired, it uses
tokio::task::spawn_blockingto move the hashing work to a dedicated OS thread pool specifically meant for blocking operations. - When the hashing completes and returns a boolean, it sends a
LoopbackCommand::ConnectionPoWVerifiedback to the main loop via theloopback_txchannel. This careful dance ensures the node remains responsive to network traffic even while under a heavy connection barrage from potential attackers. -> See:kinetic-network/src/event_loop/swarm_handler.rs— Lines 111 to 179
Deep Dive: The Identify Protocol and Kademlia Integration
Just connecting a TCP socket does not mean a peer is fully part of the Kinetic network. They must introduce themselves using libp2p’s Identify protocol. When the handler receives libp2p::identify::Event::Received, it receives the peer’s public keys, agent version string, and most importantly, their listening IP addresses. The handler first asks: is_valid_pow(&peer_id). If the answer is false, the handler takes no action. The peer remains connected at the transport layer, but they are invisible to the Kademlia DHT. They cannot route requests, and no requests will be routed to them. They are sandboxed. If the answer is true, the handler loops over the provided listen_addrs. It passes each address to a helper function called is_routable_multiaddr(). This strips out useless IPs (like 127.0.0.1 or 192.168.x.x when running in production) to prevent local network poisoning. For every routable, valid IP, the handler finally calls self.swarm.behaviour_mut().kademlia.add_address(&peer_id, addr). This single line of code is the ultimate destination. This is what physically adds the peer to the node’s routing table, integrating them fully into the decentralized network structure.
-> See: kinetic-network/src/event_loop/swarm_handler.rs — Lines 229 to 273
AutoNAT Status Changes:
Nodes on the internet are often stuck behind routers or firewalls (NAT). AutoNAT is a protocol that determines if a node is directly reachable from the outside world. If AutoNAT transitions to Public(address), it means the world can reach us directly. The handler takes this address, tells the Swarm to announce it as an external address, and pushes an Identify update to all currently connected peers so they update their routing tables to point to us. If it transitions to Private, the node knows it must rely on Relays or UPnP to be reachable by other peers.
-> See: kinetic-network/src/event_loop/swarm_handler.rs — Lines 192 to 213
Delegated Protocol Events: Not every event is handled inline. Many are delegated to specific sub-handlers to keep the code organized:
- Kademlia: When the swarm receives a
KineticBehaviorEvent::Kademlia, it means the DHT has done something — maybe a record was successfully stored, or a query timed out. The handler delegates this tocrate::event_loop::handlers::kademlia::handle(self, e). - Proxy & CDN: These are custom Kinetic protocols. When a peer requests a proxy tunnel or a CDN resource, the raw event bubbles up here. It is intercepted and passed to
handlers::proxyorhandlers::cdn. - Gossipsub: The pubsub system where messages are broadcast to many peers at once (like new drand blocks). Delegated to
handlers::gossipsub. - UPnP & DCUtR: These are NAT traversal protocols. When UPnP successfully opens a port on the user’s home router, it fires an event here. DCUtR (Direct Connection Upgrade through Relay) fires when two peers communicating via a Relay finally manage to punch a hole through their firewalls and establish a direct connection. The handler logs these transitions, which are critical for debugging connectivity.
- mDNS: On local networks, mDNS allows nodes to find each other without bootstrap nodes. When a local peer is discovered, the handler checks their PoW or if they are a bootstrap node. If valid, they are immediately added to Kademlia. This allows local Kinetic networks to self-assemble instantly for testing.
- OutgoingConnectionError & ConnectionClosed: The network is volatile. When a connection fails or drops, the handler intercepts this and actively scrubs the peer from its state. It removes them from the
bootstrap_connection_timetracker, removes them from thelight_nodesset, and most importantly, evicts them from the Kademlia routing table. If we do not evict disconnected peers, our DHT routing table fills up with “dead” nodes, making future lookups slow as we try to route through ghosts.
3. Resolving Network Requests
The handler also manages the lifecycle of outstanding network requests, specifically Kademlia DHT lookups.
Handling Get Completions (handle_get_completion):
When the network finishes trying to find a domain record via Kademlia, this function is triggered.
-> See: kinetic-network/src/event_loop/swarm_handler.rs — Lines 35 to 107
It checks if there are conflicting payloads returned by different peers. If so, it must run a VDF tie-breaker (which is CPU-intensive) to determine the absolute truth. Because this computation blocks the thread, it uses spawn_blocking inside a background task, just like the PoW verification. If the DHT failed to find the record across the entire network, the handler checks its local fallback — querying its own local Kademlia store directly. This is crucial for network resilience; if the network is temporarily fragmented, the node might still have the record safely cached locally from a previous lookup. Finally, it sends the resolved payload (or a ResolutionError) back through the responder channels to the original callers (like the REST API).
Key Pieces
Here is a breakdown of the most critical structural elements across these two files.
NetworkEventLoop::new
- Location:
kinetic-network/src/event_loop/swarm_builder.rs— Lines 11 to 124 - What it does: The primary constructor. Builds either a light or full swarm based on config, hydrates the ban list from the storage engine, dials bootstrap nodes, and sets up concurrency semaphores.
- Why it matters: It is the genesis of the node. Without this, the application has no physical connection to the outside world, and no way to manage the state of the connections it makes. It ensures the node wakes up with memory of who to avoid.
handle_swarm_event
- Location:
kinetic-network/src/event_loop/swarm_handler.rs— Lines 109 to 311 - What it does: The massive router for inbound P2P events. Matches on
ConnectionEstablished,Identify,AutoNAT, and delegates specific protocol events (like Kademlia or Gossipsub) to their respective sub-handler modules. - Why it matters: This enforces the network’s security perimeter. It prevents banned peers from connecting and ensures that routing tables are only populated by peers who have paid the computational PoW cost. It acts as the firewall for the application layer.
handle_get_completion
- Location:
kinetic-network/src/event_loop/swarm_handler.rs— Lines 35 to 107 - What it does: Finalizes a DHT lookup. Coordinates VDF tie-breaking by offloading to a blocking thread, and manages local fallback logic if the remote DHT lookup fails entirely.
- Why it matters: This is how domain names actually get resolved and returned to the user. It handles the edge cases where the network disagrees on a record’s state, preventing malicious actors from serving spoofed records by ensuring the VDF proves the correct version.
is_valid_pow
- Location:
kinetic-network/src/event_loop/swarm_handler.rs— Lines 11 to 21 - What it does: A small helper function that checks if a specific
PeerIdhas a valid PoW based on the currentdrand_kynepoch block. - Why it matters: It serves as the primary gatekeeper for entry into the Kademlia routing table. If
is_valid_powreturns false, you do not exist to the network, effectively neutralizing mass Sybil generation attacks because generating identities becomes computationally expensive.
How This Connects to the Rest of Kinetic
This module serves as the central hub connecting raw network I/O with Kinetic’s core business logic.
- Storage Integration: The builder loads banned peers directly from the database upon startup.
CROSS-CRATE:
kinetic_core::traits::StorageEngine— explained in Stage 7. - Core Loopback: When the handler verifies a peer’s PoW asynchronously, it doesn’t modify the state directly from the background task (which would violate Rust’s ownership rules). Instead, it sends a message over
loopback_txback toevent_loop_core.rs(Document 11) to finalize the peer’s inclusion safely on the main thread. - Sub-protocol Dispatch: While this handler catches all events, it delegates specific protocol events. For example,
KineticBehaviorEvent::Kademlia(e)is dispatched tohandlers::kademlia::handle(). This modularity keepsswarm_handler.rsfrom growing to unmanageable sizes and separates Kademlia logic from Gossipsub logic. - VDF Engine: Tie-breaking DHT results requires VDF evaluation.
CROSS-CRATE:
kinetic_core::traits::VdfEngine— explained in Stage 7.
Quick Reference
When scanning these files, keep these strict behavioral rules of thumb in mind:
-
Builder (
swarm_builder.rs):- Handles Light vs Full node branching securely.
- Handles Bootstrap node loading and dialing upon startup.
- Handles Ban list hydration from local storage into an LRU cache.
- Handles Semaphore creation for CPU-heavy concurrency limits (PoW and Gossip).
-
Handler (
swarm_handler.rs):- ConnectionEstablished: Checked against the ban list immediately, requires the Drand block to proceed. Spawns an async PoW check to avoid stalling the main executor.
- Identify Protocol: The gateway to the routing table. Only routable IPs with valid PoW get fully added to Kademlia.
- AutoNAT: Determines if we need Relays to function or if we are publicly reachable by anyone.
- Connection Closed: Actively removes the peer from Kademlia to prevent routing table pollution.
- Blocking Tasks: Any CPU-heavy work (Hashing for PoW, VDF checking for tie-breakers) is wrapped in
tokio::task::spawn_blockingto avoid stalling the tokio async runtime executor.
Open Questions / Things to Revisit
- Bootstrap Node PoW Grace Period: Currently, if a bootstrap node fails PoW, it is given a 24-hour grace period before being disconnected. This logic is hardcoded deeply inside the
Identifyevent handler. We may want to make this configurable or extract it into a dedicated peer-scoring system so it is not buried in event routing logic. - Local Fallback Risk:
handle_get_completionuses a local Kademlia store fallback if the DHT lookup fails. If the local store has stale data, it might return an outdated record to the client without knowing it. We should ensure the local store properly garbage collects old records based on their expiration times so the fallback is always fresh. - WebAssembly Panic: The builder panics if
FullNodeis selected on WebAssembly architecture. While technically correct (WASM can’t run full Kademlia servers), it might be better to return a gracefulanyhow::Errorrather than crashing the entire process abruptly if the configuration is accidentally flawed. - Semaphore Limits Configuration: The
pow_semaphoreis currently hardcoded to 2 permits. High-capacity nodes on strong hardware might be able to handle 8 or 16 concurrent PoW checks. This should probably be moved to theNetworkConfig.
Detailed Breakdown of Sub-Protocols in the Handler
To fully appreciate the responsibility of the swarm_handler.rs, we need to look at what happens when it delegates events. The handler is a router, but what is it routing to?
-
The Kademlia DHT Sub-Protocol When
KineticBehaviorEvent::Kademliais matched, it is passed to the Kademlia handler. Kademlia is the beating heart of Kinetic’s domain resolution system. It is responsible for routingGETrequests (for looking up domains) andPUTrequests (for registering domains or proxies). The swarm handler doesn’t process the internal logic of a DHT timeout or a DHT response; it merely catches the libp2p network trigger and routes it to the Kademlia subsystem. -
The Proxy Sub-Protocol Kinetic allows nodes to act as decentralized proxies. When a
KineticBehaviorEvent::Proxyevent occurs, it means another node on the network is either asking this node to open a proxy tunnel, or is sending traffic through an established tunnel. The swarm handler intercepts this and passes it to the proxy handler, which manages the encryption and traffic forwarding logic. -
The CDN Sub-Protocol Similar to the proxy, Kinetic supports decentralized content delivery (
KineticBehaviorEvent::Cdn). When a peer requests a static asset or a chunk of data, the libp2p swarm emits a CDN event. The handler routes this to the CDN logic, which verifies if the node has the requested file cached and streams it back. -
The Gossipsub Sub-Protocol Gossipsub is a publish-subscribe system used for network-wide broadcasts. In Kinetic, this is primarily used for distributing the latest
drandepoch blocks and network-wide alerts. WhenKineticBehaviorEvent::Gossipsubis emitted, it means a message was broadcasted to a topic we are subscribed to. The swarm handler catches this and passes it to the Gossipsub handler, which validates the message signature and decides whether to forward it to other peers. -
NAT Traversal: UPnP and DCUtR
- UPnP (Universal Plug and Play): This protocol talks to the user’s home router and asks it to open a port. When
KineticBehaviorEvent::Upnpfires, it indicates whether the port mapping succeeded or failed. - DCUtR (Direct Connection Upgrade through Relay): When two nodes are behind strict firewalls, they initially communicate through a third-party Relay node. DCUtR is a protocol where they coordinate through the relay to simultaneously punch holes in their firewalls, establishing a direct connection. The swarm handler logs these events so administrators can monitor NAT traversal success rates.
- UPnP (Universal Plug and Play): This protocol talks to the user’s home router and asks it to open a port. When
-
Relay Client and Server If a node cannot establish a direct connection, it falls back to Relays. The handler catches
RelayClientandRelayServerevents. A Full Node might act as a Relay Server for others, while a Light Node will exclusively act as a Relay Client. -
mDNS (Multicast DNS) On local area networks (LANs), mDNS allows nodes to discover each other without needing a bootstrap node or the global Kademlia DHT. When
KineticBehaviorEvent::Mdnsdetects a local peer, the handler verifies their Proof-of-Work. If valid, they are added to the Kademlia routing table. This is useful for local testing or for establishing mesh networks in environments without internet access.
Detailed Breakdown of Swarm Builder Constraints
Let’s look closely at the engineering decisions made inside swarm_builder.rs.
The Node Mode Split (Light vs Full)
In NetworkEventLoop::new, the very first branch checks config.mode == NetworkMode::LightNode. Why is this distinction hardcoded at the swarm builder level? Because a libp2p Swarm is fundamentally defined by its NetworkBehaviour. A Light Node requires a different set of behaviors than a Full Node.
- A Full Node participates fully in the Kademlia DHT as a server, storing records for other people. It also acts as a Relay server, helping NAT-restricted peers communicate.
- A Light Node acts only as a client in Kademlia (it queries data but refuses to store data for others). It does not act as a Relay server.
By splitting the initialization into
lightnode::build_light_swarmandfullnode::build_full_swarm, Kinetic ensures that Light Nodes do not waste CPU or bandwidth serving the network, while Full Nodes are configured with the robust behaviors required for infrastructure providers.
The WebAssembly (WASM) Constraint
Notice the #[cfg(target_arch = "wasm32")] compiler directive. If the code is compiled for WebAssembly (e.g., to run inside a web browser), attempting to initialize a Full Node will trigger a panic!("FullNode mode is not supported on WebAssembly"). This is a hard architectural limit. Web browsers do not have access to raw TCP or UDP sockets; they can only communicate over WebSockets or WebRTC. Furthermore, browsers cannot persist massive Kademlia DHT databases or act as reliable Relay servers because their lifecycle is controlled by the user closing a tab. Therefore, WASM builds are confined to Light Node behavior.
The Banned Peer LRU Cache Sizing
When loading the ban list from the storage engine, the builder initializes an LRU (Least Recently Used) cache: lru::LruCache::new(std::num::NonZeroUsize::new(100_000).unwrap()). Why exactly 100_000? In a global P2P network, an attacker might orchestrate a botnet with tens of thousands of IP addresses. If the ban list cache was too small (e.g., 1,000), a botnet of 5,000 nodes could easily push legitimate banned IPs out of the cache. If a banned IP falls out of the cache, the node has to hit the slow storage engine to check if they are banned every time they connect. By setting the limit to 100,000, Kinetic ensures that it can remember a massive swarm of attackers in RAM, allowing it to instantly drop their connections with zero disk I/O overhead.
The Connection Limits and Semaphores
The pow_semaphore is set to 2, and the gossip_semaphore is set to 8. These numbers are tuned. Proof-of-Work hashing is a serialized, CPU-intensive mathematical operation. If you allow 10 threads to do PoW hashing at once, you will max out the CPU of a standard quad-core server, leaving no resources for actual network traffic routing. Limiting it to 2 ensures that connection verification happens smoothly in the background without degrading the node’s primary responsibilities. Conversely, Gossipsub message verification (checking cryptographic signatures on drand blocks) is less intensive than PoW, so the limit is generously set to 8 concurrent checks.
Handling Outdated Ban Records During the ban list hydration phase, the builder reads the expiration timestamp of every banned peer stored in the database.
#![allow(unused)]
fn main() {
let expire = u64::from_be_bytes(val_bytes[..8].try_into().unwrap_or([0; 8])); let now = config.initial_drand_kyn; if expire > now {
peers.put(peer_id, expire);
} else {
let _ = storage.delete(&key_bytes);
}
}
If expire > now, the ban is still active, and the peer is loaded into the LRU cache. If the ban has expired, the builder calls storage.delete(&key_bytes). This acts as an automated garbage collection system. Without this step, the storage engine’s database would grow infinitely over time as temporary 24-hour bans accumulate, eventually consuming unnecessary disk space.
Understanding handle_get_completion
The handle_get_completion function in swarm_handler.rs is one of the most structurally complex pieces of the event loop. It bridges the asynchronous network layer with the synchronous, heavy cryptography layer.
When a DHT query finishes, we might have received conflicting records from different peers. Malicious nodes might have returned fake data. To resolve this, Kinetic uses a Verifiable Delay Function (VDF) as a tie-breaker. However, running a VDF tie-breaker takes significant time (often hundreds of milliseconds or even seconds).
If we ran this directly in the handle_swarm_event loop:
#![allow(unused)]
fn main() {
// BAD: This blocks the entire async runtime let result = Self::xor_tie_breaker(&name, p.received_payloads, current_drand_kyn);
}
The entire node would freeze. No other peers could connect, no other DHT queries could progress, and ping times would spike, causing peers to drop us.
Instead, Kinetic wraps the tie-breaker in a nested spawn architecture:
#![allow(unused)]
fn main() {
crate::event_loop::utils::spawn(async move {
let tie_breaker_result = crate::event_loop::utils::spawn_blocking(move || {
Self::xor_tie_breaker(&name, p.received_payloads, current_drand_kyn)
}).await;
// ... handle result ...
});
}
This pushes the heavy computation off the async executor and onto a dedicated OS thread designed for blocking operations. Once the OS thread finishes the VDF check, it returns the result, and the async task wakes back up to send the payload to the original requester.
The Local Fallback Mechanism
If the DHT fails (perhaps the node is currently partitioned from the main network), the function attempts a local fallback. It derives the Kademlia keys for the requested domain and queries its own local RecordStore. If it finds the record locally, it logs: "Resolved [name] locally from own store after DHT network failure". This is a critical resilience feature. It means that even if the global network is temporarily unstable, domains that the node has recently interacted with (and thus cached) will continue to resolve successfully, hiding network instability from the end user.
Deep Dive into Proof-of-Work Dynamics
The integration of Proof-of-Work (PoW) directly into the SwarmEvent::ConnectionEstablished and SwarmEvent::Behaviour(Identify) lifecycle is perhaps the most unique architectural feature of the Kinetic network. It is worth analyzing exactly how these two events interact to secure the network.
When a peer connects, ConnectionEstablished is fired. The node checks if the peer is banned, verifies that the drand_kyn block is synchronized, and spawns the PoW verification task. However, at this exact moment, the node does not know anything else about the peer. It does not know what protocols the peer supports, what its node version is, or what other IP addresses it might be listening on.
This is where the libp2p Identify protocol comes in. Shortly after the TCP connection is established, the peers automatically exchange Identify messages. This fires the SwarmEvent::Behaviour(Identify) event.
The critical security gate happens here:
#![allow(unused)]
fn main() {
let is_bootstrap = self.bootstrap_peers.contains(&peer_id); let pow_valid = self.is_valid_pow(&peer_id);
}
If a peer sends their Identify payload before their PoW is verified by the background task, self.is_valid_pow(&peer_id) will return false. The handler will ignore their addresses and refuse to add them to the Kademlia routing table. However, once the background PoW task finishes and sends the LoopbackCommand::ConnectionPoWVerified, the event_loop_core.rs state is updated so that is_valid_pow will return true.
But what if the Identify payload was already dropped? In libp2p, Identify periodically pushes updates. Furthermore, the node can manually trigger behavior to re-evaluate peers. If a peer fails PoW initially but later provides a valid one, they will be caught in subsequent network passes or when they attempt to participate in DHT queries.
The Bootstrap Node Exemption:
Bootstrap nodes are given a special exemption: if self.disable_pow || pow_valid || is_bootstrap { ... add to kademlia ... }. This is a calculated risk. Bootstrap nodes are trusted infrastructure. If we required them to constantly solve PoW to stay in the routing tables of thousands of connecting peers, their CPUs would melt. Instead, we trust their identity out of the gate, allowing them to rapidly seed the network routing tables without computational bottlenecking.
Understanding the Nat Status Lifecycle
In the modern internet, IPv4 exhaustion means that almost no consumer device has a direct, publicly routable IP address. They sit behind NAT (Network Address Translation) routers. This poses a massive problem for a peer-to-peer network like Kinetic, where nodes need to connect directly to each other.
The swarm_handler.rs mitigates this by actively listening to the libp2p::autonat::Event::StatusChanged event.
- The Unknown State: When the node starts, its NAT status is “Unknown”. It does not know if it can be reached.
- The AutoNAT Protocol: The node automatically asks trusted peers (usually bootstrap nodes or known Full Nodes) to try and dial it back on its advertised port.
- The Private State: If the external peer reports “I cannot reach you,” AutoNAT transitions to
NatStatus::Private. The handler logs this:"Node is PRIVATE (Behind NAT). Relay & UPnP fallback active."The node now knows it must use UPnP to try and map ports on the router, or rely on Relay servers to bounce traffic. - The Public State: If the external peer successfully dials back, AutoNAT transitions to
NatStatus::Public(address). The node now knows its true public IP address. The handler immediately callsself.swarm.add_external_address(address)to tell the networking stack to advertise this IP. Crucially, it then loops over every connected peer and pushes anIdentifyupdate. This shouts to the network: “I am publicly reachable! Here is my real address! Route traffic directly to me!”
This dynamic self-awareness allows the Kinetic network to automatically heal and optimize its topology, gracefully handling nodes that transition between WiFi networks, mobile data, or strict corporate firewalls.
Step-by-Step Data Flow: A Kademlia DHT Query Lifecycle
To truly cement how the event loop operates, let’s trace the complete lifecycle of a single operation: A user wants to resolve a domain name (e.g., saif.kyn).
Step 1: The Request Originates
The request starts outside this file, usually in the REST API or the daemon. It gets sent via a channel to event_loop_core.rs. The core event loop receives this command and initiates a Kademlia get_record operation on the swarm. The swarm generates a QueryId and begins searching the network.
Step 2: The Swarm Does the Heavy Lifting
Under the hood, the libp2p Swarm starts contacting peers. It asks: “Do you know who owns saif.kyn?” Peers reply either with the record, or with a list of other peers who might know. This happens within the libp2p Kademlia state machine.
Step 3: The Event Bubbles Up
Once the swarm finishes the query (either by finding the record, or exhausting all possibilities), it emits a SwarmEvent::Behaviour(KineticBehaviorEvent::Kademlia(...)).
Step 4: The Handler Catches the Event
Inside swarm_handler.rs, the handle_swarm_event function is running in an infinite loop. It matches this specific event on line 181:
#![allow(unused)]
fn main() {
SwarmEvent::Behaviour(KineticBehaviorEvent::Kademlia(e)) => {
crate::event_loop::handlers::kademlia::handle(self, e).await;
}
}
The raw event is delegated to the Kademlia sub-handler.
Step 5: The Sub-Handler Updates State
The handlers::kademlia::handle function (not shown in these files, but part of the module) processes the result. It aggregates the received payloads. If it determines that the query is fully complete, it calls back into our handler using self.handle_get_completion(name).
Step 6: Executing handle_get_completion
This brings us to lines 35-107 of swarm_handler.rs. The function removes the pending query state from self.pending_gets. It notes how many peers were successfully queried.
Step 7: The Tie-Breaker Execution
Because Kinetic operates in an adversarial environment, we cannot blindly trust the first result we get. We might have received conflicting IP addresses for saif.kyn from different peers. To solve this, the handler spawns a blocking task.
#![allow(unused)]
fn main() {
let tie_breaker_result = crate::event_loop::utils::spawn_blocking(move || {
Self::xor_tie_breaker(&name, p.received_payloads, current_drand_kyn)
}).await;
}
This blocking task calculates Verifiable Delay Functions (VDFs) over the received payloads to definitively prove which record is the correct, latest version.
Step 8: Resolution and Fallback
- If the tie-breaker succeeds (
Some(payload)), the handler iterates overp.responders(the original channels waiting for the answer) and sends the payload back. The REST API receives it and returns it to the user. - If the network failed to return anything (
None), the handler executes its local fallback mechanism. It derives the Kademlia keys forsaif.kynlocally. It loops over these keys and directly queries its ownRecordStore(lines 51-57).- If a cached record is found locally, it logs a message and returns the cached payload.
- If nothing is found locally either, it finally gives up and sends a
ResolutionError::NotFoundto the waiting channels.
This eight-step journey illustrates the immense complexity managed by swarm_handler.rs. It acts as the bridge between the raw, chaotic network topology and the deterministic, verifiable application logic required by the Kinetic protocol.
Understanding Concurrency and Blocking in Rust Async
A major theme in both the builder and the handler is managing concurrency, specifically distinguishing between async tasks and blocking tasks. This is a common pitfall in Rust async programming, and Kinetic’s handler demonstrates the correct patterns.
When you use tokio, the async runtime typically has one OS thread per CPU core (e.g., 8 threads for an 8-core machine). These threads constantly juggle thousands of lightweight “tasks.” They do this by swapping tasks whenever one is waiting on I/O (like waiting for a network packet).
However, what happens if a task starts doing heavy math (like computing a Proof-of-Work hash, or a VDF)? The math doesn’t yield back to the executor. The OS thread gets monopolized by that single task until the math finishes. If you have 8 cores, and 8 peers connect simultaneously requiring PoW checks, all 8 OS threads get blocked doing math. The entire node freezes. No ping responses are sent, no routing updates are processed. Other peers think your node has died and disconnect.
The Kinetic Solution:
To prevent this, the handler isolates heavy math from the main executor. When a peer connects (line 159), the handler uses crate::event_loop::utils::spawn to create an async task. Inside that task, it immediately awaits a Semaphore. This ensures that no matter how many peers connect, only 2 tasks can proceed to the math stage at any given time. Once a permit is acquired, it calls tokio::task::spawn_blocking. This is a special function that moves the execution off the main 8-core async executor, and onto a separate, dedicated thread pool managed by tokio specifically for blocking operations. The main async threads are immediately freed to continue processing network events. When the blocking math thread finishes, it seamlessly passes the result back to the async world.
This architecture—Semaphores combined with spawn_blocking—is what allows a Kinetic node to survive massive connection floods and Sybil attacks without its core routing logic collapsing. It is defensive engineering at its most fundamental level.
The Role of Light Nodes in the Swarm
The initialization logic in swarm_builder.rs fundamentally alters the behavior of the node depending on whether it is a Light Node or a Full Node. This distinction is critical for the network’s scalability.
What is a Light Node? A Light Node is typically a client application—a mobile phone, a web browser, or a desktop daemon run by a casual user. They want to resolve Kinetic domain names and perhaps register their own, but they do not have the bandwidth, uptime, or CPU power to serve as infrastructure for others.
When NetworkMode::LightNode is passed to the builder, it calls lightnode::build_light_swarm. While the internal details of that builder are in a separate file, the conceptual impact on the event loop is profound:
- DHT Mode: The Kademlia behavior is configured in “Client Mode.” This means the swarm handler will route queries to the network, but it will silently reject any attempts by other peers to store data on this node.
- Relay Servers: The node will not configure a Relay Server behavior. It will never route traffic for other NAT-restricted peers.
- Resource Usage: Because it isn’t serving data or routing traffic, the memory footprint and background CPU usage of the event loop remain low.
Why is this important for swarm_handler.rs?
Because the Light Node doesn’t run these behaviors, the handler will never receive events for them. It will never see a RelayServer event. It will never see inbound Kademlia PUT requests asking it to store data. The event loop naturally scales down its complexity based on the initialization branch taken in swarm_builder.rs. This allows Kinetic to use the exact same event loop codebase for both lightweight web clients and massive datacenter infrastructure nodes, ensuring protocol consistency across all environments.
Understanding the Multiaddress Concept
Throughout the handler, you will see references to listen_addrs and multiaddr. In standard networking, an IP address and a port (e.g., 192.168.1.5:8080) are enough to define a connection endpoint.
In libp2p and Kinetic, this is not enough. A node could be listening on TCP, or UDP (via QUIC), or WebSockets. Furthermore, because connections are encrypted and authenticated, you need to know the cryptographic identity of the node you are connecting to before you even dial them.
This is solved by the Multiaddr standard. A multiaddr looks like this: /ip4/198.51.100.1/tcp/4001/p2p/QmYyQSo1c1Ym7orWxLYvCrM2Wu3BkYrUrZW8K6HNRALMvv
It self-describes the entire stack needed to reach the peer:
/ip4/198.51.100.1-> The IPv4 address./tcp/4001-> The transport protocol and port./p2p/QmYy...-> The libp2pPeerIdexpected at that address.
When the swarm handler processes an Identify event, it iterates over an array of these multiaddrs. However, peers will often report every interface on their machine, including /ip4/127.0.0.1 (localhost) and /ip4/192.168.1.x (local LAN). If the Kinetic node is running on the public internet, adding a peer’s localhost address to the Kademlia routing table is useless and actively harmful (it wastes routing space on unreachable IPs). This is why the handler calls is_routable_multiaddr(&addr, self.disable_pow). This helper function parses the multiaddr, checks if the IP is globally routable, and only approves it if it is. The only exception is when self.disable_pow is true (usually in local testing environments), where local IPs are allowed so developers can test a cluster of nodes on a single laptop.
Understanding the Gossipsub Semaphore
We have extensively discussed the pow_semaphore which limits concurrent Proof-of-Work verifications. But what about the gossipsub_semaphore initialized in swarm_builder.rs?
Gossipsub is the protocol used to blast messages across the entire network. If a new drand block is mined, or if there is a critical network-wide configuration update, it is sent over Gossipsub. When a peer receives a Gossipsub message, they must verify its cryptographic signature to ensure it wasn’t forged by a malicious actor before they forward it to their neighbors.
Verifying an Ed25519 signature is fast, but it is not free. If an attacker floods the network with thousands of fake Gossipsub messages per second, the node’s async runtime could theoretically become overwhelmed just verifying signatures, leading to a Denial of Service (DoS).
To prevent this, the builder initializes the gossipsub_semaphore with 8 permits.
#![allow(unused)]
fn main() {
gossip_semaphore: std::sync::Arc::new(tokio::sync::Semaphore::new(8)),
}
When the swarm handler receives a KineticBehaviorEvent::Gossipsub(e) event, it passes it to the handlers::gossipsub::handle function. Inside that function, the handler must acquire a permit from this semaphore before it spawns a blocking task to verify the signature.
Why 8 permits instead of 2 (like PoW)? Because signature verification is significantly faster than computing a Sybil-resistant Proof-of-Work hash. The node can safely process more signatures concurrently without starving the OS threads. By setting it to 8, the node ensures that legitimate network broadcasts propagate instantly without delay, while still capping the absolute maximum CPU load an attacker can trigger via a flood attack.
This dual-semaphore architecture (2 for PoW, 8 for Gossip) demonstrates a tuned approach to peer-to-peer security. It prioritizes different types of cryptographic workloads based on their cost and their necessity to the network’s function, ensuring that the node remains resilient under all forms of stress.
The Lifecycle of Banned Peers in the Storage Engine
The ban list logic implemented in swarm_builder.rs is a fantastic example of balancing persistent storage with in-memory performance.
When a node misbehaves—perhaps by sending invalid data, failing PoW repeatedly, or launching a targeted attack—the Kinetic network application layer will ban them. This involves writing their PeerId and a ban expiration timestamp to the persistent StorageEngine.
However, looking up a peer in a database on disk for every single incoming connection is far too slow. A basic botnet could overwhelm the node simply by forcing it to perform thousands of disk reads per second (a classic resource exhaustion attack).
To solve this, swarm_builder.rs performs a “hydration” step during node startup (lines 81 to 109).
- It uses
storage.scan_prefix(DB_PREFIX_BANNED_PEER)to iterate over every banned peer in the database. - It parses the byte array back into a
libp2p::PeerId. - It parses the 8-byte value into a
u64representing the expiration timestamp. - It compares the expiration timestamp against the current network time (
config.initial_drand_kyn).
This creates a self-cleaning lifecycle:
- If the ban is still active: The peer is loaded into the
LruCache. From that point on, when theSwarmEvent::ConnectionEstablishedevent fires in the handler, checking the ban list is an instant, zero-cost memory lookup. - If the ban has expired: The builder issues a
storage.delete(&key_bytes)command. This ensures the database doesn’t grow infinitely large over months of operation, automatically garbage-collecting expired bans without requiring a separate background cron job.
Tracing and Observability Strategy
Throughout swarm_handler.rs, you will see macro calls like tracing::info!, tracing::warn!, and tracing::debug!. These are not just generic log statements; they are crucial observability tools for the network administrator.
tracing::debug!: Used for high-frequency, expected events. For example, “Discarding unroutable address” or “Dialing peer”. These logs are turned off by default in production because they would flood the console. However, if a developer is trying to figure out why a specific peer isn’t joining the routing table, enabling debug logs will reveal exactly which validation step they failed.tracing::info!: Used for significant state transitions. “Connection established”, “AutoNAT status changed from Private to Public”, or “Resolved domain locally”. These give the node operator a clear, real-time heartbeat of the node’s health and network position.tracing::warn!: Used for actionable anomalies or security events. “Banned peer attempted to connect” or “Bootstrap peer failed to provide valid PoW after 24 hours”. When a warning fires, it means the node’s security perimeter actively repelled an issue, or a trusted piece of infrastructure is failing.
This structured approach to logging ensures that swarm_handler.rs operates transparently. Because it is the central nervous system of the node, its logs are often the very first place a developer will look when diagnosing a network partition or a domain resolution failure.
Local Fallback Nuances and Considerations
We mentioned earlier that handle_get_completion includes a local fallback mechanism (lines 51-57). If the DHT lookup fails to find a domain on the wider network, the handler queries its own local RecordStore before returning an error.
While this is excellent for resilience, it introduces a subtle architectural nuance regarding data freshness. Kademlia records have expiration times. In a synchronized network, when a domain owner updates their DNS records, the new records propagate through the DHT and overwrite the old ones.
However, consider this edge case:
- Your node caches
saif.kyn = IP_Alocally because you looked it up yesterday. - Today, the owner updates it to
saif.kyn = IP_B. - Your node experiences a temporary network partition (your router goes offline, or your ISP blocks Kademlia traffic).
- You try to resolve
saif.kyn. The remote DHT lookup fails because you are offline. - The local fallback triggers, finds
IP_Ain the local store, and returns it.
In this scenario, the fallback mechanism returned stale data. The user thinks they successfully resolved the domain, but they are pointing to an outdated server. This is why the StorageEngine must enforce the exact TTL (Time To Live) on Kademlia records. If a record has expired, the local RecordStore must delete it so that the fallback mechanism does not accidentally serve stale infrastructure data during network partitions.
The Rationale Behind the WASM Panic
In swarm_builder.rs, lines 37-38 read:
#![allow(unused)]
fn main() {
#[cfg(target_arch = "wasm32")]
panic!("FullNode mode is not supported on WebAssembly");
}
A panic is a very aggressive way to handle a configuration error. Why not return an anyhow::Error and let the application shut down gracefully?
The answer lies in the target environment. When running inside a WebAssembly environment (like a web browser or a JS runtime), the application is usually a single-page application (SPA) or a lightweight background worker. If a developer accidentally configures a WASM client to run as a FullNode, it is a fundamental, unrecoverable architectural flaw. A browser tab simply cannot open a listening TCP socket to act as a Kademlia server. It cannot bind to port 4001.
By panicking immediately during the NetworkEventLoop::new construction phase, Kinetic ensures that the developer catches this mistake the very first time they hit “Refresh” in their browser. If it returned a silent error or tried to gracefully downgrade, the developer might spend hours wondering why their web client isn’t serving DHT records to the network, unaware that the underlying platform makes it physically impossible. The panic acts as a hard, fast guardrail for the network’s topology.