Full Nodes vs Light Nodes in the Swarm
Crate: kinetic-network Stage: 8 Reading time: 30 minutes Depends on: docs/learn/network/12_event_loop_behavior.md
What Is This?
This file documents the two primary network instantiation entry points in the kinetic-network crate: build_full_swarm and build_light_swarm. These two functions are responsible for initializing the libp2p::Swarm, which is the core state machine for all peer-to-peer networking in Kinetic. In a decentralized network, the network layer is fundamentally defined by the capabilities and behaviors configured at the exact moment the Swarm is built. Kinetic purposefully implements a bifurcated network topology. A Full Node is the heavy-duty infrastructure of the network. A Light Node is a lightweight, edge-device consumer (such as a mobile app or a web browser).
By separating the logic into two distinct builders, Kinetic ensures that resource-constrained devices do not buckle under the weight of routing traffic for the global network, while high-availability servers can bind to public ports and provide the necessary infrastructure for DHT routing and NAT traversal.
Why Kinetic Needs This
If you build a standard peer-to-peer network where every node behaves exactly the same (a “flat” topology), the network will quickly degrade when deployed to real-world consumer devices. Here is exactly why Kinetic requires this strict dichotomy between full and light nodes:
1. The Mobile and Browser Battery Problem
Most end-users interact with decentralized networks via mobile applications or web browsers. Mobile operating systems, such as iOS and Android, suspend background applications to preserve battery life. If a mobile device acted as a full node, it would establish dozens of connections to maintain the routing table. When the user locks their phone, the OS suspends the app, abruptly severing all those TCP connections. This creates massive “churn” in the network. The network’s DHT would be forced to constantly repair itself as mobile nodes randomly appear and disappear. Data queries would frequently time out because the node that claimed to hold the data just went to sleep. Furthermore, web browsers are sandboxed; they physically cannot listen on arbitrary TCP or UDP ports, making it impossible for them to act as servers.
2. The NAT and Firewall Traversal Problem
Most consumer devices sit behind Network Address Translators (NATs) and strict firewalls (like home Wi-Fi routers). They cannot easily accept incoming connections from the public internet. If two mobile devices want to communicate (for example, to exchange a piece of data directly), they usually cannot connect directly. They need a public, stable server to act as an intermediary, which is known as a Relay. If the Kinetic network did not have dedicated Full Nodes acting as Relays, peer-to-peer communication between consumer devices would fail completely. The full nodes guarantee that there are stable, dialable endpoints on the internet.
3. Resource Exhaustion and Storage
Full nodes actively store Kademlia provider records and values for the network. They allocate memory and disk space to keep the DHT functional and the routing tables updated. A light client on a low-end device should not be burdened with storing megabytes of random network data that it does not care about. Light nodes need to be selfish. They should only fetch what they need. They should immediately drop connections when idle to save memory, CPU cycles, and network bandwidth. By splitting the Swarm initialization, Kinetic ensures that high-uptime infrastructure servers carry the heavy lifting.
How It Works
The instantiation of a Libp2p Swarm is the most critical phase of the network lifecycle. Let’s break down how build_full_swarm and build_light_swarm differ across all the sub-components of the P2P stack.
1. Transports: How Bytes Move
The “Transport” is the underlying protocol used to move bytes across the internet.
Full Nodes:
-> See: kinetic-network/src/event_loop/fullnode.rs — Lines 26 to 48
Full nodes build a transport stack that includes TCP and QUIC. For TCP, they attempt to enable port_reuse(true). Port reuse (SO_REUSEPORT at the OS level) allows multiple sockets to bind to the same port. This is beneficial for NAT traversal and hole punching. However, because some operating systems do poorly with port reuse, fullnode.rs includes a fallback to port_reuse(false) if the initial build fails. After TCP, the builder chains .with_quic(). QUIC is a modern UDP-based protocol. It is faster at establishing secure handshakes than TCP because it bakes TLS directly into the connection handshake (0-RTT or 1-RTT). It also avoids the TCP head-of-line blocking problem by natively multiplexing multiple streams over a single connection. Finally, full nodes chain .with_dns() so they can resolve domain names into IP addresses when dialing peers.
Light Nodes:
-> See: kinetic-network/src/event_loop/lightnode.rs — Lines 19 to 32
When compiled for a web browser (target_arch = "wasm32"), light nodes cannot use raw TCP or QUIC. Instead, the builder uses WebSockets via libp2p::websocket_websys::Transport. This allows the WASM browser environment to dial out to Full Nodes that support WebSockets. When compiled for native platforms (like iOS/Android), light nodes use the same TCP and QUIC stack as full nodes.
2. Kademlia DHT Mode Selection
The Distributed Hash Table (DHT) is how peers discover data and routes without a central server.
Full Nodes:
-> See: kinetic-network/src/event_loop/fullnode.rs — Line 92
#![allow(unused)]
fn main() {
kademlia.set_mode(Some(kad::Mode::Server));
}
By setting the mode to Server, the full node advertises itself as a routing node in the DHT. When other peers are looking for data, they will ask this node to search its routing table. This node takes on the responsibility of keeping the network graph connected. It also allocates local storage via the KineticRecordStore to hold Kademlia provider records and actual values.
Light Nodes:
-> See: kinetic-network/src/event_loop/lightnode.rs — Line 102
#![allow(unused)]
fn main() {
kademlia.set_mode(Some(kad::Mode::Client));
}
Setting the mode to Client activates “parasite” mode. The light node can still issue GET requests to the DHT to find data. It can issue PUT requests to store its own data on full nodes. However, it tells the network: “Do not route requests through me, and do not ask me to store anything.” This protects the light node’s battery and prevents it from acting as a public database for random peers.
3. Gossipsub Mesh Tuning
Gossipsub is the protocol used to broadcast messages, such as new blocks or network alerts, to the entire network.
Full Nodes: Full nodes use the default Gossipsub configuration. They form large, stable meshes with other full nodes to ensure messages propagate globally with high reliability. They validate messages and handle large transmit sizes up to the network limit.
Light Nodes:
-> See: kinetic-network/src/event_loop/lightnode.rs — Lines 104 to 116
Light nodes tweak the Gossipsub parameters to reduce background bandwidth.
heartbeat_interval(web_time::Duration::from_secs(10)): Slows down the internal mesh maintenance loop to save CPU.prune_backoff(web_time::Duration::from_secs(60)): Wait longer before reconnecting to pruned peers.mesh_n(4): Target only 4 peers in the local mesh (the default is usually 6).mesh_n_low(3): Only drop down to a minimum of 3 peers before looking for new ones.mesh_n_high(8): Maximum of 8 peers.mesh_outbound_min(2): Ensure at least 2 connections are outbound to prevent being eclipsed.gossip_lazy(1): Reduce the number of peers that receive metadata-only gossip. By artificially limiting the number of Gossipsub connections, the light node avoids being overwhelmed with incoming broadcast traffic.
4. Relays and NAT Traversal (DCUtR)
DCUtR (Direct Connection Upgrade through Relay) is the mechanism libp2p uses to punch holes in firewalls.
Full Nodes:
-> See: kinetic-network/src/event_loop/fullnode.rs — Lines 183 to 202
Full nodes instantiate libp2p::relay::Behaviour::new(...). This turns the full node into a TURN-like relay server for the rest of the network. Light nodes can connect to this full node and ask it to reserve a “circuit”. The full node will then blindly forward packets between two NAT’ed light nodes while they attempt to hole-punch a direct connection. Because relaying costs bandwidth, Kinetic restricts it:
max_circuit_duration: Capped at 2 minutes. Hole punching should complete quickly.max_circuit_bytes: Hard limit on data transfer over the relay.max_reservations_per_peer: Stops a single malicious peer from hogging all relay slots.
Light Nodes:
-> See: kinetic-network/src/event_loop/lightnode.rs — Line 175
Light nodes disable the Relay Server behavior using Toggle::from(None). They can act as a relay client (using a full node’s relay circuit), but they will never relay traffic for anyone else.
5. Universal Plug and Play (UPnP)
UPnP allows an application to automatically configure a home router to open a port and forward it to the internal device.
Full Nodes:
-> See: kinetic-network/src/event_loop/fullnode.rs — Lines 175 to 181
If not in test mode, full nodes include the libp2p::upnp::tokio::Behaviour. If a user runs a full node on their home computer, this behavior attempts to talk to their home router over the local network. It automatically requests a port forward, making the node accessible from the public internet without the user having to manually log into their router admin panel.
Light Nodes:
-> See: kinetic-network/src/event_loop/lightnode.rs — Line 173
Light nodes disable UPnP completely. Since they don’t listen on any ports, there is no reason to ask the router to open a port.
6. AutoNAT and Network Address Discovery
AutoNAT is how a libp2p node figures out what its public IP address is by asking its peers.
Full Nodes and Native Light Nodes:
Both use AutoNAT. In production mode, the boot_delay is set to 10 seconds, and retry_interval is 90 seconds. This gives the node time to connect to peers before asking them “What is my IP address?”. In test mode, these values are lowered to 2 seconds to speed up local integration tests.
7. Power Management and Idle Timeouts
Maintaining active TCP or QUIC connections requires sending occasional keep-alive packets.
Full Nodes:
-> See: kinetic-network/src/event_loop/fullnode.rs — Line 222
Full nodes set with_idle_connection_timeout(web_time::Duration::from_secs(300)). They will keep connections open for 5 minutes of inactivity before dropping them. This promotes network stability, as establishing new connections is computationally expensive (due to Noise encryption handshakes).
Light Nodes:
-> See: kinetic-network/src/event_loop/lightnode.rs — Line 222
Light nodes set with_idle_connection_timeout(web_time::Duration::from_secs(60)). This is an aggressive power saving measure for mobile devices. If a light node hasn’t sent or received anything for 1 minute, it forcefully drops the connection. This allows the mobile device’s cellular or Wi-Fi radio to return to a low-power state, significantly saving battery life over the course of a day.
8. Listening Sockets vs Dial-Out Only
The final and most crucial difference is how the swarm interacts with the host OS networking stack after initialization.
Full Nodes:
-> See: kinetic-network/src/event_loop/fullnode.rs — Lines 226 to 245
The swarm iterates over config.listen_addrs and config.quic_listen_addrs and calls swarm.listen_on(addr). This makes the underlying OS open a socket and begin accepting incoming TCP SYNs and QUIC handshakes. If a configured external address is provided, it registers it via swarm.add_external_address.
Light Nodes:
-> See: kinetic-network/src/event_loop/lightnode.rs — Line 226
Light nodes never call swarm.listen_on. They operate in dial-out mode. They connect to the network to perform specific actions and do not accept unsolicited inbound connections.
Key Pieces
build_full_swarm
- What it does: Constructs the libp2p Swarm with server-level capabilities. It wires up the Kademlia store, the relay behaviors, UPnP, and actively binds to network interfaces.
- Location:
kinetic-network/src/event_loop/fullnode.rs - Why it matters: Without this function, the network has no backbone. These are the nodes that store the DHT, route the gossip, and provide relay circuits for NAT traversal.
build_light_swarm
- What it does: Constructs the libp2p Swarm with client-level capabilities, optimized for battery preservation and WASM browser compatibility.
- Location:
kinetic-network/src/event_loop/lightnode.rs - Why it matters: This function allows the Kinetic mobile app and web clients to participate in the P2P network. It ensures they do not destroy the user’s device performance or cause network fragmentation due to rapid offline/online churn.
libp2p::swarm::behaviour::toggle::Toggle
- What it does: A wrapper type that allows a specific Network Behavior to be optionally included at runtime.
- Location: Used throughout both files.
- Why it matters: If an option like mDNS or Relay is disabled in the configuration, we use
Toggle::from(None)to remove it from the swarm logic. This avoids unnecessary CPU cycles and memory allocations for behaviors that are turned off.
kad::Mode::Server vs kad::Mode::Client
- What it does: Dictates whether the Kademlia DHT will store records and answer routing queries, or just act as a consumer.
- Location: Inside the
with_behaviourclosure for Kademlia initialization. - Why it matters: This is the single most important parameter distinction. A network of all clients cannot discover data. A network of all servers on mobile devices will collapse under routing churn. This binary split is what allows Kinetic to scale.
KineticBehavior Construction
- What it does: Assembles all the sub-behaviors (Kademlia, Gossipsub, Ping, Identify, Relay, Proxy, CDN, etc.) into the monolithic struct.
- Location: At the end of the
with_behaviourclosure in both files. - Why it matters: This struct defines the complete protocol suite that the node supports. The light node’s struct conditionally omits fields like
streamorrelay_serverdepending on the compilation target.
How This Connects to the Rest of Kinetic
Both functions return a tuple: Result<(libp2p::Swarm<KineticBehavior>, NetworkClient), anyhow::Error>.
-
The Swarm (
libp2p::Swarm<KineticBehavior>): This is the actual libp2p state machine. It manages raw network sockets, encrypts traffic using the Noise protocol, multiplexes streams using Yamux, and handles all the custom sub-protocols. This swarm object is passed directly into the coreEventLoopstruct, where it is polled continuously in an asynchronous background thread. -
The Client (
NetworkClient): While theEventLooptakes ownership of the Swarm and runs in a background thread, the rest of the application (like the UI, or the RPC server) needs a way to communicate with the network layer. TheNetworkClientis the handle that gets returned to the application. It contains an MPSC channel sender (tx) that allows the app to send commands, such asPutRecord,DialPeer, orPublishMessage, into the Swarm thread for execution. -
Storage Dependencies: Both functions take an
Arc<dyn kinetic_core::traits::StorageEngine>. CROSS-CRATE: This storage engine is defined inkinetic-core(Stage 7). The DHT behavior needs it to actually persist Kademlia records to disk, which is managed by theKineticRecordStorewrapper passed into the Kademlia config. -
VDF Engine: Both functions also take an
Arc<dyn kinetic_core::traits::VdfEngine>. CROSS-CRATE: This is defined inkinetic-core(Stage 7) and implemented bykyn-vdf(Stage 6). It is passed into the record store to validate Verifiable Delay Functions when verifying data integrity during DHT put operations. -
Libp2p Stream Control Channel: In native builds, the setup creates a
(control_tx, control_rx)channel. Thelibp2p_stream::Behaviour::new()creates a behavior that allows opening raw byte streams to peers. Thestream.new_control()handle is passed through the channel into theNetworkClient, allowing the user-facing application to open custom data streams bypassing standard RPC.
Quick Reference
| Feature | Full Node | Light Node | | :— | :— | :— | | Kademlia Mode | Server (Stores data, routes) | Client (Queries only, parasite) | | Transports | TCP + QUIC | TCP + QUIC, or WebSockets (WASM) | | Listening Sockets| Binds to config ports | Never binds to ports | | Gossipsub Mesh | Default (Large mesh) | Reduced parameters (mesh_n = 4) | | Relay Server | Enabled (Allows hole punching) | Disabled (Toggle::from(None)) | | UPnP | Enabled (Auto-opens router ports) | Disabled | | Idle Timeout | 300 seconds | 60 seconds (Aggressive power save) |
Open Questions / Things to Revisit
-
WASM Support Complexity and Readability: The
lightnode.rsfile is littered with conditional compilation flags, specifically#[cfg(not(target_arch = "wasm32"))]. While necessary for browser support, this makes the code hard to read and modify. If new behaviors are added toKineticBehaviorin the future, developers must be careful to implement them correctly for both WASM and native targets. We should evaluate if creating a third file, such aswasmnode.rs, would be architecturally cleaner than pollutinglightnode.rswith macro noise. -
Relay Server Bandwidth Limits: The full node restricts relay usage to
max_circuit_bytes, defined inkinetic_core::constants::LIMITS_P2P_MAX_CIRCUIT_BYTES. If this value is too small, light nodes might fail to exchange large blocks during initial synchronization before their direct hole punching connection completes. We need to monitor this limit in production. If hole punching fails often, the fallback relay might choke on tight bandwidth limits, severing the connection prematurely. -
Bootstrapping and Initial Discovery: These functions initialize the swarm perfectly, but they do not actively dial anyone upon startup (except whatever AutoNAT attempts internally). The logic for finding the initial full nodes to connect to (bootstrapping) must happen outside this file, likely in the
EventLoopinitialization or polling phase. We must ensure light nodes are given a robust list of hardcoded bootstrap nodes. Otherwise, they will be stranded offline, unable to join the P2P network. -
Port Reuse Warning Trigger: In
fullnode.rs, there is a fallback mechanism ifport_reuse(true)fails. Some operating systems (like older versions of Windows or specific Linux kernels) do not supportSO_REUSEPORTeffectively. If this fallback triggers frequently on user machines, it will severely impact the node’s ability to seamlessly handle DCUtR hole punching, which relies on port reuse to work efficiently. We should ensure this warning is visible in the node operator logs.
9. Configuration Variable Injection
Before the Swarm is built, several configuration parameters are extracted from the NetworkConfig and passed into the closures that construct the behaviors.
-> See: kinetic-network/src/event_loop/fullnode.rs — Lines 50 to 55
initial_drand_kyn: Passed into theKineticRecordStore. This allows the DHT to understand the current DRAND round for validation purposes.enable_mdns: A boolean flag used to toggle local peer discovery on or off.lru_cache_size: Defines the maximum number of DHT records the node will store in memory.max_reveals_per_hour: A rate-limiting parameter for the VDF engine.test_mode: A critical boolean that significantly alters timeout and delay behaviors to speed up local integration testing.
10. Deep Dive: Kademlia Configuration
The Kademlia DHT requires specific tuning to function correctly within the Kinetic network.
-> See: kinetic-network/src/event_loop/fullnode.rs — Lines 70 to 88
set_protocol_names: The protocol name is formatted as"/{}/kad/2.0.0", where{}is injected withkinetic_core::constants::NETWORK_ID. This is an essential security measure. It ensures that a node running ontestnetcannot accidentally connect to the Kademlia DHT of themainnet.set_max_packet_size: Limits the maximum size of a DHT RPC message. This prevents a malicious node from sending a 5GB Kademlia request and crashing the node with an Out-Of-Memory (OOM) error.set_provider_record_ttl: Defines how long a provider record lives in the DHT. Since nodes frequently go offline, provider records must eventually expire to prevent the DHT from returning “dead” addresses.set_provider_publication_interval: The frequency at which the node must actively republish its provider records to the DHT to keep them alive before the TTL expires.set_query_timeout: Intest_mode, the query timeout is lowered to 5 seconds. In production, Kademlia requires a longer timeout because querying the global DHT can take multiple round trips across high-latency internet links.
11. Deep Dive: Gossipsub Configuration
Gossipsub is the nervous system of the Kinetic network, propagating real-time events.
-> See: kinetic-network/src/event_loop/fullnode.rs — Lines 94 to 104
validation_mode(libp2p::gossipsub::ValidationMode::Strict): This forces the Gossipsub router to validate all messages before forwarding them. If a message fails validation (e.g., signature is invalid, or the payload is malformed), it is dropped immediately and not forwarded to peers, preventing spam amplification.MessageAuthenticity::Signed(key.clone()): Every single Gossipsub message is cryptographically signed using the node’s private key (local_key). This guarantees that messages cannot be spoofed and that the sender can be held accountable for the data they broadcast.validate_messages(): Enables the application-level validation hook, meaning theEventLoopwill have a chance to inspect the message payload before deciding if it should be accepted into the local mesh.
12. Request-Response Protocols: Proxy and CDN
Kinetic includes two custom request-response protocols alongside standard libp2p behaviors.
-> See: kinetic-network/src/event_loop/fullnode.rs — Lines 113 to 137
- Proxy Behavior: Uses
libp2p::request_response::cbor::Behaviour::<ProxyRequest, ProxyResponse>. This protocol allows a node to send a CBOR-encodedProxyRequestto a specific peer and await aProxyResponse. Like Kademlia, it is scoped to the specificNETWORK_ID. - CDN Behavior: Uses a similar CBOR-encoded behavior for
CdnRequestandCdnResponse. This is intended for direct node-to-node content delivery, bypassing the gossip network for large static payloads. ProtocolSupport::Full: Indicates that this node both supports sending requests and answering them.
13. Peer Identification (Identify)
-> See: kinetic-network/src/event_loop/fullnode.rs — Lines 106 to 109
The libp2p::identify::Behaviour is a crucial background protocol. When two peers connect, they automatically exchange an “Identify” message. This message includes the node’s public key, its listening addresses, and the specific protocol string (e.g., /{}/1.0.0). This allows nodes to dynamically discover the public IP addresses of the peers they are connected to, which feeds directly into the Kademlia routing table.
14. Network Ping
-> See: kinetic-network/src/event_loop/fullnode.rs — Line 112
The libp2p::ping::Behaviour sends periodic small packets to connected peers. If a peer stops responding to pings, the connection is considered dead and is forcefully closed. This helps clean up “zombie” TCP connections that were severed abruptly (e.g., a laptop closing its lid) without sending a proper TCP FIN packet.
15. The Impact of Target Operating Systems
The network topology is modified not just by the choice of Full vs Light node, but also by the target compilation platform.
-> See: kinetic-network/src/event_loop/lightnode.rs — Lines 41 to 60
You will notice a stark difference in how the transport builder is initialized for Android versus standard Desktop builds.
#![allow(unused)]
fn main() {
// Android Target
#[cfg(all(target_os = "android", not(target_arch = "wasm32")))]
}
For native desktop Linux/macOS/Windows builds, the transport builder ends with .with_quic().with_dns()?. However, for Android builds, the .with_dns()? call is omitted. Why? Because Android’s internal DNS resolution mechanisms (via Bionic libc) historically conflict with the pure-Rust DNS resolvers used by libp2p::dns (such as trust-dns). If we attempt to compile .with_dns() on Android, it either fails to compile or fails to resolve hostnames at runtime, causing the node to crash on startup. Instead, Android relies on raw IP addresses or delegates DNS resolution to a higher layer outside the Libp2p swarm.
16. WASM Exclusions and libp2p_stream
WebAssembly (WASM) running inside a browser environment is restrictive.
-> See: kinetic-network/src/event_loop/lightnode.rs — Lines 156 to 175
You will see dozens of #[cfg(not(target_arch = "wasm32"))] macros disabling specific behaviors:
libp2p_stream: This behavior allows opening custom, raw Yamux streams between peers for arbitrary data transfer. WASM cannot support this because thewebsocket_websystransport handles stream multiplexing internally at the Javascript level, making it incompatible withlibp2p_stream’s raw byte-level expectations.libp2p::mdns: mDNS relies on broadcasting UDP packets to the local subnet (usually address224.0.0.251). Browsers do not allow JavaScript or WASM to construct and emit raw UDP broadcast packets for security reasons. Thus, mDNS is physically impossible in the browser and must be disabled.libp2p::upnp: UPnP relies on sending UDP packets to the home router (usually address239.255.255.250). Like mDNS, the browser sandbox blocks this.libp2p::relay_server: A browser cannot act as a relay for the rest of the internet because it cannot accept incoming sockets.
17. The Yamux Multiplexer
-> See: kinetic-network/src/event_loop/fullnode.rs — Lines 20 to 24
#![allow(unused)]
fn main() {
let yamux_config = || {
let mut config = libp2p::yamux::Config::default();
config.set_max_num_streams(1024);
config
};
}
Yamux (Yet Another Multiplexer) is the unsung hero of the network layer. When a Full Node connects to another peer over TCP, it only opens one single TCP connection. However, Libp2p needs to run Kademlia, Gossipsub, Ping, Identify, and custom Proxy requests all at the same time. Yamux acts as a virtual switchboard. It takes that single TCP connection and splits it into hundreds of virtual “streams”. The set_max_num_streams(1024) configuration ensures that a single peer cannot maliciously open 100,000 streams and exhaust the node’s memory. It caps the virtual streams at 1024 per physical connection.
18. Execution Flow of build_full_swarm
To fully grasp the instantiation process, here is the chronological flow of execution when a Full Node starts:
- Logging: Emits a
tracing::info!log announcing the initialization and the ports it intends to bind to. - Multiplexer Prep: Creates the
yamux_configclosure. - Transport Build: Attempts to build a Tokio-backed TCP transport with
port_reuseenabled. - Transport Fallback: If port reuse fails (due to OS limitations), it logs a warning and builds a standard TCP transport.
- QUIC & DNS: Appends the QUIC UDP transport and DNS resolution capabilities.
- Channel Creation: Instantiates a standard library MPSC channel
(control_tx, control_rx). This is specifically used to extract thelibp2p_streamcontrol handle out of the behavior closure. - Swarm Assembly: The
.with_behaviourclosure is executed. This closure consumes the local keypair and initializes theKineticRecordStore. - Behavior Injection: Kademlia, Gossipsub, Ping, Identify, DCUtR, Proxy, CDN, and mDNS are instantiated.
- Stream Extraction: The
stream.new_control()handle is passed intocontrol_tx.send(). This allows the control handle to escape the closure. - Swarm Config: The final
Swarmobject is built, andwith_idle_connection_timeoutis applied. - TCP Binding: The code iterates over
config.listen_addrsand executesswarm.listen_on(addr). If the port is already in use by another application, it emits atracing::warn!but continues execution. - QUIC Binding: Iterates over
config.quic_listen_addrsand executesswarm.listen_on(quic_addr). - External Address Advertising: If the user provided an
external_addressin the config, it is forcibly added to the swarm viaadd_external_address. This forces the node to advertise this IP to the DHT, regardless of what AutoNAT detects. - Client Instantiation: The
control_rx.recv()call blocks until the behavior closure sends the stream handle. Then, theNetworkClientis constructed using thetokio::mpscsender and the stream handle. - Return: Returns the
Swarmand theNetworkClientto the caller.
19. Handling Configuration Fallbacks gracefully
One of the architectural strengths of these builder functions is how they handle inevitable failures gracefully. If a user specifies a port that is already in use, swarm.listen_on(addr) will return an Err. Instead of panicking and crashing the entire daemon, the code handles the error:
-> See: kinetic-network/src/event_loop/fullnode.rs — Lines 229 to 231
#![allow(unused)]
fn main() {
if let Err(e) = swarm.listen_on(addr.clone()) {
tracing::warn!("Failed to bind TCP on {}: {}", addr, e);
}
}
This ensures that if the node was configured to bind to three different IP addresses, and one fails, the node will still successfully start and bind to the other two. This resilience is critical for cloud deployments where virtual network interfaces might be misconfigured.
20. Deep Dive: The Execution Model and Tokio Integration
When constructing the Swarm, both Full and native Light nodes invoke a critical method on the builder:
-> See: kinetic-network/src/event_loop/fullnode.rs — Line 28
#![allow(unused)]
fn main() {
.with_tokio()
}
Libp2p is asynchronous. Under the hood, establishing a TLS handshake, encrypting packets with the Noise protocol, and multiplexing streams requires managing hundreds of simultaneous I/O operations. Libp2p does not ship with its own async runtime. By calling .with_tokio(), we are binding the Libp2p internal state machine to the Tokio async runtime that drives the rest of the Kinetic daemon. This means that when Libp2p needs to spawn a background task (for example, to maintain a UPnP port mapping with a router), it will spawn a tokio::task. This tight integration ensures that the network layer shares the same thread pool as the storage and VDF engines, minimizing context-switching overhead. If we omitted this, the TCP transport would literally panic at runtime because it would have no executor to schedule its socket polling.
21. AutoNAT Configuration Metrics
AutoNAT is the mechanism by which a node discovers its own public IP address. It does this by sending an AutoNAT request to a peer, asking “Hey, what IP address do you see me connecting from?”.
-> See: kinetic-network/src/event_loop/fullnode.rs — Lines 153 to 173
Kinetic configures AutoNAT differently based on whether test_mode is enabled. In production (test_mode = false):
boot_delay: std::time::Duration::from_secs(10): The node waits 10 seconds after starting before it asks anyone for its IP. This is because right after startup, the node has zero peers. It needs time for Kademlia to bootstrap and Gossipsub to form a mesh. Asking for an IP instantly would fail.retry_interval: std::time::Duration::from_secs(90): If the request fails, wait a full 90 seconds before trying again. This prevents network spam.refresh_interval: std::time::Duration::from_secs(3600): Once the node knows its public IP, it only re-checks it once every hour (3600 seconds). Public IP addresses rarely change while a connection is active, so aggressive polling is unnecessary.
22. Network Client and WASM Discrepancies
The return type of the builders includes a NetworkClient. This client is the application’s sole bridge into the locked Swarm thread.
-> See: kinetic-network/src/event_loop/lightnode.rs — Lines 228 to 234
#![allow(unused)]
fn main() {
#[cfg(not(target_arch = "wasm32"))]
let client = NetworkClient::new(tx, stream_control);
#[cfg(target_arch = "wasm32")]
let client = NetworkClient::new(tx);
}
In native environments, the NetworkClient takes both the MPSC sender (tx) and the stream_control handle. This allows a native Light or Full node to open raw data streams to peers. However, in WASM, the NetworkClient constructor only takes tx. This is because, as detailed in Section 16, WASM lacks support for libp2p_stream. The structural implication for Kinetic is profound: any application-level feature that relies on opening custom raw streams will simply fail to compile or fail to function on the web client. The web client must rely on Gossipsub for broadcasting, and the Proxy/CDN request-response behaviors for direct data fetching, because it has no raw stream handle available.
23. Conclusion on Architectural Roles
The codebase separates fullnode.rs and lightnode.rs not just to avoid messy if-else blocks, but because they represent fundamentally different philosophical roles in the network. A Full Node is designed for Altruism. It accepts incoming connections, routes traffic for strangers, stores data it doesn’t need, and keeps connections alive for 5 minutes just in case a peer returns. A Light Node is designed for Selfish Efficiency. It refuses to route, refuses to store, drops connections after 60 seconds of silence, and connects only when it needs something. This balance is what allows the Kinetic network to span from high-powered data centers down to constrained browser tabs without collapsing.
24. Deriving the Peer ID
Before any Kademlia or Gossipsub behavior can be instantiated, the node must know its cryptographic identity.
-> See: kinetic-network/src/event_loop/fullnode.rs — Line 60
#![allow(unused)]
fn main() {
let peer_id = key.public().to_peer_id();
}
In libp2p, a PeerId is not a random UUID. It is a cryptographic hash (specifically multihash) of the node’s public key. By extracting key.public() from the local_key and converting it, we guarantee that the node’s network identity is inextricably linked to its private key. This is why Gossipsub’s MessageAuthenticity::Signed(key) works: other nodes receive a message, see the PeerId of the sender, and can verify the cryptographic signature against the public key embedded in that PeerId. If a node tries to spoof another node’s PeerId, the signature validation will instantly fail.
25. The Toggle Pattern for Optional Behaviors
The builder pattern in libp2p is typed. The with_behaviour closure expects you to return a struct (in our case, KineticBehavior) where every field has a concrete type. However, behaviors like mDNS and Relay Server are optional based on the NetworkConfig. You cannot have a struct field that is sometimes libp2p::mdns::tokio::Behaviour and sometimes ().
-> See: kinetic-network/src/event_loop/fullnode.rs — Lines 142 to 151
Kinetic solves this using the Toggle struct:
#![allow(unused)]
fn main() {
let mdns = if enable_mdns && !test_mode {
libp2p::swarm::behaviour::toggle::Toggle::from(Some( ... ))
} else {
libp2p::swarm::behaviour::toggle::Toggle::from(None)
};
}
Toggle<T> implements the NetworkBehaviour trait. If it is constructed with None, it simply does nothing when polled by the swarm. This allows the KineticBehavior struct to have a consistent type signature (mdns: Toggle<libp2p::mdns::tokio::Behaviour>) regardless of the user’s runtime configuration.
26. WebAssembly Bindgen vs Tokio
-> See: kinetic-network/src/event_loop/lightnode.rs — Lines 20 to 21
#![allow(unused)]
fn main() {
#[cfg(target_arch = "wasm32")]
let builder = libp2p::SwarmBuilder::with_existing_identity(local_key.clone())
.with_wasm_bindgen()
}
Notice how the WASM build calls .with_wasm_bindgen() instead of .with_tokio(). The Tokio runtime utilizes OS-level threads and epoll/kqueue event loops. A web browser sandbox does not expose these underlying OS primitives to WebAssembly. Instead, WASM must interact with the browser’s JavaScript event loop (the microtask queue). with_wasm_bindgen() wires the libp2p swarm into the browser’s native Javascript Promises and timeouts, rather than attempting to spawn Tokio threads. Without this switch, compiling kinetic-network to wasm32-unknown-unknown would immediately fail.