05. Kademlia Local Store Implementation (Part 2)
Crate: kinetic-network Stage: 8 Reading time: 25 minutes Depends on: 04_store_core_1.md
What Is This?
This document covers the second half of KineticRecordStore. It specifically covers the code found in kinetic-network/src/store/core.rs from lines 291 to 581.
While the first half of this file dealt with initialization, persistence loading, and Verifiable Delay Function (VDF) background threads, this half is focused on the ingress firewall of the storage layer.
It details exactly how Kademlia network records are handled when they arrive. It shows how opaque byte arrays from the network are parsed dynamically to determine their Kinetic data structure. It explains how these structures are cryptographically verified before acceptance. It details how they are ultimately committed to disk.
Crucially, this section contains the actual Rust trait implementation of kad::store::RecordStore. This trait is the exact interface that libp2p uses to bridge the standard Kademlia routing protocol with our custom, application-specific storage logic.
Why Kinetic Needs This
To understand why this code is so complex, you must understand the critical flaw in how baseline Kademlia operates, and why Kinetic cannot use it out of the box.
The Baseline Kademlia Problem (Trust by Default)
In a standard Distributed Hash Table (DHT) like IPFS, Kademlia is agnostic to the data it holds. The network protocol is designed purely for routing, not for validation. If Peer A is closest to a given network key, and Peer B sends Peer A a PUT_VALUE network request containing random garbage data, Peer A will accept it. Peer A will store it on its hard drive and serve it to anyone who asks. It does not verify the content. It does not check if the content matches the key. It just blindly accepts data. This is fine for BitTorrent where clients verify chunks via hash trees after downloading, but it is fatal for a synchronous identity protocol. The standard Kademlia protocol operates on total trust.
The Kinetic Threat Model
Kinetic is a verifiable namespace. Trust is eliminated by design. If Kinetic used a standard Kademlia store, the network would collapse in seconds due to several specific attack vectors:
- Storage Exhaustion (Spam): Malicious peers could flood the DHT with megabytes of random junk data. This would rapidly fill up the hard drives of honest nodes, causing a denial of service.
- Namespace Squatting: Attackers could submit fake
NameRecordclaims without actually performing the required Verifiable Delay Function (VDF) computation. They would essentially steal namespaces for free. - Identity Forgery: Attackers could submit fake
AuthorizedKiddelegations, claiming that they have been authorized to speak for a namespace they do not actually own.
The Solution: An Intercepting Firewall
Because of these threats, KineticRecordStore must act as a hostile bouncer. Instead of giving libp2p a standard MemoryStore to use freely, we wrap the memory store inside our own struct. We intercept every single Kademlia put request before it touches memory or disk.
This file proves that in the Kinetic network, storage is not free. To store data on a peer’s disk, the incoming record must prove that it has the right to exist. If it fails the cryptographic checks, the peer simply drops the record. It refuses to propagate it further, starving malicious data out of the network at the edge.
Deep Dive: Sabotaging Provider Records (Case 183)
There is a specific feature in Kademlia called “Provider Records”. In a network like IPFS, if you have a massive 5GB video file, you don’t send the video data to the DHT. Instead, you send a tiny “Provider Record” to the DHT. This record says, “I don’t have the data here, but if you connect to my IP address, I can stream it to you.”
In Kinetic, Provider Records represent a critical vulnerability known as “Provider Spam”. Because provider records are just pointers, they contain no cryptographic proofs. An attacker could flood the DHT with provider records claiming they are the provider for every single namespace in existence. They could do this without performing a single VDF computation.
To prevent this, this file sabotages Provider Records globally. -> See: kinetic-network/src/store/core.rs — Lines 531 to 534
When implementing the trait, the add_provider method immediately returns an error: Err(kad::store::Error::MaxProvidedKeys) By returning this error, the libp2p swarm drops the provider record. In Kinetic, you must provide the actual, verifiable data via a standard Record, or you provide nothing at all.
How It Works
The lifecycle of an incoming network record is handled through a strict gauntlet of sequential checks. When the libp2p swarm receives data, it eventually calls the put method on the kad::store::RecordStore trait.
Step 1: The Trait Interception
-> See: kinetic-network/src/store/core.rs — Lines 510 to 546
When libp2p calls .put(record), it hits our implementation of the trait on line 518. Instead of saving the record directly, our trait implementation redirects the call to self.put_record(r). This pushes the generic Kademlia record into our custom Kinetic validation logic. Other standard operations, like .get() and .records(), are passed straight through to self.inner (the internal MemoryStore). Notice how the Rust associated types RecordsIter and ProvidedIter are simply passed through to the MemoryStore’s implementation. We don’t reinvent the wheel for iteration; we only hijack the mutation methods (put and add_provider). This means reads are fast and memory-bound, while writes are scrutinized.
Step 2: The Size Firewall
-> See: kinetic-network/src/store/core.rs — Lines 371 to 380
Inside put_record_internal, the very first check is a defensive size limit. Kademlia provides an opaque byte array (r.value). Before wasting CPU cycles on JSON parsing or cryptography, we check its total length. Kinetic enforces an 80 KB limit for all incoming network records.
Architectural Note:
The core schema defined in kinetic-core limits the pure user data payload to 64 KB. So why is the network storage limit set to 80 KB? Think of it like shipping a 64kg item in the mail. You need a sturdy box. The 16 KB buffer acts as the box. It safely accommodates the original 64 KB payload plus the necessary byte overhead of:
- JSON structure and keys
- Embedded Verifiable Delay Function (VDF) proofs
- ED25519 cryptographic signatures
If a record exceeds 80 KB, we throw KineticStoreError::PayloadTooLarge. We log this rejection under error code KIN-STORE-016.
Step 3: Dynamic Payload Identification (The Detective Work)
-> See: kinetic-network/src/store/core.rs — Lines 382 to 498
Kademlia strips all type information when transmitting data over the wire. When a record arrives, we just have a stream of raw bytes. We need to figure out what Kinetic object those bytes represent so we can apply the correct rules.
We do this by attempting to parse the bytes into a generic serde_json::Value. This creates an untyped DOM-like tree of the JSON. Then, we act as a detective, looking for identifying keys in the JSON structure:
- Commitments:
If the JSON has a
"hash"key but NO"vdf_proof"key, we assume it is aCommitment. We then attempt to rigidly parse it as akinetic_core::types::Commitmentstruct. - Reveals / NameRecords:
If the JSON contains
"vdf_proof"or"granted_at", it is parsed as aNameRecord. - Heartbeats:
If the JSON contains
"latest_drand_kyn", it is parsed as aHeartbeat. - Delegated Kids:
If the JSON contains
"delegation_signature", it is parsed as anAuthorizedKid. - Manifests:
If the JSON contains
"manifest", it is parsed as anAuthorizedManifest. - Routing Records:
If the JSON contains
"host_id", it is parsed as aHostRoutingRecord.
If the JSON matches none of these signatures, it is rejected entirely. It returns UnknownRecordType and logs Error Code KIN-STORE-019.
Step 4: Routing to Cryptographic Validations
Once we know exactly what the payload is, we must prove it cryptographically.
- For NameRecords:
The system delegates to
self.handle_record(). This method handles the brutal mathematical task of checking the VDF proof. - For AuthorizedKid / AuthorizedManifest:
-> See: kinetic-network/src/store/core.rs — Lines 431 to 462
These records represent delegated authority.
To verify them, we need the public key of the namespace owner.
The system calls
self.get_record_with_fallback(&auth_kid.name)to pull the activeNameRecordout of the Sled database. It then passes both the new delegated record and the owner’sNameRecordto the verification engine. The engine verifies the ED25519 signature on the delegation against the owner’s public key. If the signature fails, the delegation is forged, and it is dropped. - For HostRoutingRecords: -> See: kinetic-network/src/store/core.rs — Lines 463 to 480 The system verifies the routing record against the current Drand epoch. This ensures that the node claiming the IP address actually holds the private key for that Host ID at this very moment. This is critical because it prevents malicious nodes from hijacking IP addresses. If a node tries to route a namespace to an IP they don’t own, the Drand verification will fail and the record will be dropped.
Step 5: The Two-Tier Storage Commit
-> See: kinetic-network/src/store/core.rs — Lines 500 to 507
If the payload survives this intense gauntlet of checks, it has earned the right to exist on disk. First, we construct a key for the Sled database. We do this by prefixing the raw Kademlia key with the byte string kad_record:. We write the raw bytes to the persistent disk using self.storage.put.
Second, we push the record into self.inner. This inner variable holds the kad::store::MemoryStore. This is crucial because libp2p reads from self.inner when serving network requests to other peers. By keeping an in-memory copy, we avoid doing slow disk I/O on every single DHT read request.
The Async Backdoor (put_verified_record)
-> See: kinetic-network/src/store/core.rs — Lines 344 to 358
Verifying a VDF proof is intentionally, slow. If we did it synchronously inside the put method, the entire Kademlia event loop would stall. No other messages could be processed while the node crunched the numbers.
To prevent this network stall, the network layer can spawn a background thread to calculate the VDF. When the background thread succeeds, it needs to insert the record into the store. However, it doesn’t want to trigger the 5-second VDF check all over again.
It uses the put_verified_record backdoor. This function calls put_record_internal with the flag skip_reveal_verify: true. The store trusts that the VDF was already checked by the background thread. It performs the standard JSON parsing and size checks, and then commits it directly to disk.
Key Pieces
-
put_record(&mut self, r: kad::Record) -> Result<(), KineticStoreError>- Location:
kinetic-network/src/store/core.rs— Lines 340 to 342 - Purpose: The primary, untrusted entry point for storing a network record.
- Detail: It enforces all validation rules dynamically based on the payload type. It defaults
skip_reveal_verifyto false, forcing a full cryptographic check.
- Location:
-
put_verified_record(&mut self, r: kad::Record) -> Result<(), KineticStoreError>- Location:
kinetic-network/src/store/core.rs— Lines 356 to 358 - Purpose: The trusted, optimized entry point.
- Detail: It bypasses VDF verification for records that were already validated on a background async thread. This is the exact mechanism that prevents the main network loop from stalling.
- Location:
-
put_record_internal(&mut self, r: kad::Record, skip_reveal_verify: bool) -> Result<(), KineticStoreError>- Location:
kinetic-network/src/store/core.rs— Lines 360 to 508 - Purpose: The core gauntlet engine.
- Detail: This massive function enforces the 80 KB size limit. It parses the JSON dynamically into generic values, identifies the Kinetic record type via key inference, and routes the payload to specific cryptographic validation modules based on its type.
- Location:
-
get_record_with_fallback(&mut self, name: &str) -> Option<NameRecord>- Location:
kinetic-network/src/store/core.rs— Lines 307 to 324 - Purpose: A critical helper function used during delegation signature verification.
- Detail: It attempts to find a
NameRecordin the fast, in-memoryreveals_by_namecache. If it misses, it queries the slow, persistent Sled storage. We must have the parentNameRecordavailable to extract the public key needed to verify the signature on a childAuthorizedKid.
- Location:
-
impl kad::store::RecordStore for KineticRecordStore- Location:
kinetic-network/src/store/core.rs— Lines 510 to 546 - Purpose: The official Rust trait implementation that wires our custom store into the libp2p Kademlia swarm.
- Detail: It delegates standard lookups (
get,records,remove) to the innerMemoryStore. It interceptsputfor cryptographic validation. It returnsError::MaxProvidedKeysforadd_providerto prevent Provider Spam attacks on the network.
- Location:
How This Connects to the Rest of Kinetic
- CROSS-CRATE: The various data types parsed dynamically here (
Commitment,NameRecord,Heartbeat,AuthorizedKid,AuthorizedManifest,HostRoutingRecord) are defined and fully explained indocs/learn/types/02_records.mdand related files from Stage 1. - CROSS-CRATE: The size limit variables (such as
LIMITS_STORAGE_MAX_VALUE_BYTES) are sourced fromkinetic_core::constants, which is documented in Stage 7. - Internal Connection: The cryptographic verification functions called by this file (e.g.,
verify_authorized_kid,verify_host_routing_record,verify_authorized_manifest) live inkinetic-network/src/store/verification.rs. These are the actual mathematical implementations that this firewall relies upon. - Network Loop Connection: The Kademlia event loop in
kinetic-network/src/event_loop/is the primary consumer of this entire file. It continuously feeds incomingPUT_VALUEnetwork requests into theputmethod of the implemented trait.
Quick Reference
- Max Store Limit: 80 KB
- This accommodates the 64 KB schema limit.
- It allows room for JSON structure overhead.
- It allows room for cryptographic signature overhead.
- Error Codes:
KIN-STORE-016: Payload exceeds size limit.KIN-STORE-019: Payload rejected due to unknown type structure.
- JSON Inference Keys:
- If
"hash"(without"vdf_proof") $\rightarrow$ Parses asCommitment - If
"vdf_proof"or"granted_at"$\rightarrow$ Parses asNameRecord - If
"latest_drand_kyn"$\rightarrow$ Parses asHeartbeat - If
"delegation_signature"$\rightarrow$ Parses asAuthorizedKid - If
"manifest"$\rightarrow$ Parses asAuthorizedManifest - If
"host_id"$\rightarrow$ Parses asHostRoutingRecord
- If
- Provider Records Feature:
- Globally disabled to mitigate Case 183 (Provider Spam).
- Attempting to add one instantly returns
kad::store::Error::MaxProvidedKeys.
- VDF Threading:
- Use
put_verified_recordto safely skip VDF checks if the proof was already validated in a separate background thread.
- Use
Open Questions / Things to Revisit
-
JSON Parsing Inefficiency: The system currently takes an incoming Kademlia byte array and parses the entire payload into a generic
serde_json::Valuetree just to check which keys exist. For a maximum-size 80 KB payload, parsing into a DOM-like structure is heavy on CPU and memory allocations. We should consider replacing this with a lightweight regex search over the bytes. Alternatively, we could use a custom streaming JSON parser that aborts early once the identifying key is found. This would allow us to deserialize directly into the typed Rust struct without building the costly generic tree first. -
Fallback Read Latency in State Machine: In
put_record_internal, validating anAuthorizedKidrequires callingget_record_with_fallback. If the record is not in the memory cache, this function hits the Sled database synchronously. Because this code runs directly inside the synchronous Kademlia event loop, a Sled disk I/O read could block the entire routing table if disk latency spikes. Moving this Sled lookup to an asynchronous background task would drastically improve network resilience under load. -
Memory Store Duplication: The two-tier storage system means we are writing the raw byte arrays into the persistent Sled database, and then also storing them in the
self.innerkad::store::MemoryStore. This literally doubles the memory footprint for all DHT records stored by the node. For nodes running on low-memory edge devices, this is suboptimal. A future refactor should implement a fully customRecordStoretrait that queries Sled directly for all reads, eliminating the need for the redundant innerMemoryStore.