Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Store Handlers: Domain Reveals and Heartbeats

Crate: kinetic-network

Stage: 8

Reading time: 60 minutes

Depends on: 02_store_core.md


What Is This?

The handlers.rs file represents the ultimate gatekeeper for the Kinetic network’s decentralized namespace.

It contains the core logic for processing, validating, and persistently storing two of the most critical network operations:

Domain Reveals (NameRecord): This is how a node claims a namespace (such as saif.kyn).

The reveal contains a Verifiable Delay Function (VDF) proof demonstrating that the node has burned computational time to earn the right to the name.

Liveness Pings (Heartbeat): This is how a node proves it is still online, active, and actively defending its claimed name.

It is a cryptographic signature broadcast to the network.

When the peer-to-peer network receives one of these messages via Gossipsub, it does not blindly trust it.

The message is immediately routed to the KineticRecordStore where these handlers live.

The handlers meticulously dissect the message, evaluate its cryptographic validity, check it against the current local state of the namespace, and make a deterministic decision on whether to accept it, reject it, or overwrite an existing claim.

These handlers are where the theoretical rules of the Kinetic network—such as how domains expire, how they can be stolen, and how ties are broken—are practically enforced in code.

If these handlers fail or have logical flaws, the entire namespace collapses into chaos, and nodes will fall out of consensus.

Every single decision made by these handlers must be deterministic so that every node in the world agrees on the current owner of a name without ever talking to each other to coordinate.


Why Kinetic Needs This

Kinetic is designed as a fully decentralized, permissionless namespace.

Unlike traditional DNS, there is no centralized root server, no ICANN, and no human authority to resolve disputes or enforce rules.

In a system where anyone can gossip anything over Libp2p, the network must rely on cryptographic proofs and deterministic consensus math.

Specifically, Kinetic needs these handlers to solve five major existential threats:

Sybil Attacks and Name Squatting:

Without a cost to registration, a single malicious actor could instantly register millions of names, squatting the entire namespace.

By enforcing VDF verification in the handler, Kinetic ensures that claiming a name requires a tangible expenditure of CPU time.

The handler actively checks the VDF proof length against the name’s difficulty.

Domain Abandonment:

If a user registers a name and then loses their private key or shuts down their node forever, that name would theoretically be locked forever.

The handlers implement a “domain stealing” mechanic, where an abandoned name gradually becomes easier for someone else to claim over time.

Simultaneous Claims (The Collision Problem):

In a globally distributed network, two peers might compute a VDF for the exact same name and broadcast it at the exact same time.

The handlers implement a deterministic tie-breaker so that every node independently reaches the exact same conclusion about who won, without needing a voting round.

Denial of Service (DoS) Attacks:

Cryptographic signatures (like Post-Quantum ML-DSA) are computationally expensive to verify.

If an attacker floods the network with garbage heartbeats, nodes would waste all their CPU trying to verify them.

The handlers implement fast-path rejections to drop duplicates before doing any heavy math.

Replay Attacks:

An attacker might record a valid heartbeat from yesterday and rebroadcast it today to make it look like an offline peer is still online.

The handlers use strict monotonic counters (drand_kyn) to reject any historical data.

Without the dense, optimized logic in this file, the Kinetic network would be non-functional.

The P2P layer is just the transport; this file is the brain.


The Anatomy of a Domain Steal

Before diving into the code step-by-step, it is crucial to understand the conceptual model of “Domain Stealing” that these handlers enforce.

Imagine a domain claim as a fortress.

  • When you compute a VDF to claim a name, you are building the walls of that fortress. The more VDF iterations you compute, the higher the walls.

  • When you broadcast a heartbeat, you are paying guards to walk the walls.

  • As long as you keep broadcasting heartbeats (paying the guards), your walls remain at their full height, and no one can easily take your fortress.

However, if you go offline and stop broadcasting heartbeats, the guards go to sleep.

Slowly, over time (measured in drand kyns), your walls begin to crumble.

The ConsensusParams math determines the exact rate of decay.

If someone else wants your name, they must build a siege tower (compute their own VDF).

If your walls are at full height, they need a massive siege tower (more iterations than you).

But if you have been offline for weeks, your walls have crumbled significantly, and they only need a small siege tower (fewer iterations) to breach the walls and steal the domain.

This exact mathematical threshold is calculated and enforced dynamically within handle_record every single time a competing claim is received.


How It Works: The Record Handler (handle_record)

The handle_record function is massive because it orchestrates the entire lifecycle of a domain claim.

It is located at:

-> See: kinetic-network/src/store/handlers.rs — Lines 8 to 263

Here is the exhaustive step-by-step breakdown of how a NameRecord is processed:

Step 1: Premium vs Standard Discrimination

The handler first checks if the record is a Standard reveal (which uses a VDF) or a Premium reveal (which uses an alternative claim mechanism).

-> See: kinetic-network/src/store/handlers.rs — Lines 13 to 16

If it is a Premium name, the VDF-specific expiry checks are bypassed entirely.

Premium domains have special protection and cannot participate in the standard stealing lifecycle.

This branch ensures they are never subjected to VDF threshold checks.

By isolating Premium logic early, the system avoids running unnecessary mathematical calculations for names that are immune to them.

Step 2: VDF Expiry and Network Pauses

For standard records, the handler must ensure the VDF proof is not too old.

-> See: kinetic-network/src/store/handlers.rs — Lines 18 to 36

  • It extracts the drand_kyn (the exact network pulse when the VDF calculation started).

  • It accesses the GLOBAL_GOVERNANCE_STATE (protected by a Mutex lock) to ask: “How many network pauses have occurred since this VDF started?” Network pauses happen during extreme network turbulence.

  • It calculates effective_age using saturating_sub. This is a crucial Rust concept that subtracts numbers but stops at 0 instead of panicking on underflow.

  • effective_age = current_kyn.saturating_sub(proof_kyn).saturating_sub(paused_kyns)

  • If a user computed a VDF 100 kyns ago, but the network was paused for 90 of those kyns, their effective age is only 10 kyns. They are not penalized for the network going down.

  • If this effective_age is greater than the RESQUARING_EPOCH_KYNS, the VDF is considered expired, and the handler throws KineticStoreError::VdfExpired.

Step 3: Cryptographic Verification

Unless the skip_verify flag is true (used internally during fast syncs), the handler passes the reveal to the VDF engine.

-> See: kinetic-network/src/store/handlers.rs — Lines 38 to 50

This delegates to verify_reveal, which runs the Post-Quantum VDF math.

If the proof is invalid, the record is immediately dropped.

The VDF verification proves that the node actually spent the CPU time it claims to have spent.

This is the absolute foundation of Kinetic’s Sybil resistance.

Step 4: The Conflict Resolution Matrix

If the local store already has a record for this name, the handler checks the public keys.

If existing_record.pubkey() != record.pubkey(), this is a hostile takeover attempt (a steal).

-> See: kinetic-network/src/store/handlers.rs — Lines 52 to 77

  • It calculates hb_age: the number of kyns since the current owner last sent a heartbeat. If the heartbeat cache is empty, it falls back to the original reveal kyn.

  • It consults ConsensusParams to calculate the steal_threshold. The formula inside ConsensusParams discounts the required iterations as hb_age grows large.

  • It enforces that Premium domains can neither be stolen nor used to steal. If a Premium domain is involved in a collision, it immediately throws KineticStoreError::TieBroken.

Step 5: The Case 121 Deterministic Tie-Breaker

This is one of the most conceptually advanced parts of the codebase.

If the new claimant has the exact same number of VDF iterations as the existing owner, and the heartbeat age is very recent (< 100 kyns), the network recognizes this as a simultaneous collision.

-> See: kinetic-network/src/store/handlers.rs — Lines 78 to 121

To resolve the tie without a voting round, it calculates an XOR distance (^) between the claimant’s public key and their VDF proof bytes.

  • It iterates over the public key bytes using .iter().

  • It aligns them with the proof bytes using .zip().

  • Because the proof bytes might be shorter than the public key, it uses .chain(std::iter::once(&0)).cycle() to infinitely loop the proof bytes, padding with a zero at the boundary.

  • It maps the pairs to their XOR result, creating a new Vec<u8>.

It does this for both the existing owner and the new claimant.

The one with the lower XOR distance wins the tie.

Because this calculation relies only on the cryptographically secure proof bytes and the public key, it acts as a universally verifiable random lottery.

Every node in the network will calculate exactly the same XOR distance and agree on the same winner, resolving the tie deterministically.

Step 6: Enforcing Steal Thresholds

If it is not a Case 121 tie, the handler simply checks if the new VDF iterations exceed the required steal_threshold.

-> See: kinetic-network/src/store/handlers.rs — Lines 122 to 128

If the attacker’s VDF is too weak (they did not compute enough iterations to overcome the current owner’s active defense), the handler throws KineticStoreError::InsufficientIterations.

Step 7: State Cleanup for Evicted Owners

If the steal is successful, the previous owner is evicted.

However, their old data still lives in the DHT (Distributed Hash Table).

-> See: kinetic-network/src/store/handlers.rs — Lines 130 to 152

The handler uses kinetic_core::types::derive_storage_keys to figure out exactly what DHT keys the old owner was using.

It constructs the sled database keys by prefixing kad_record: to the byte array, and deletes them.

It does the same for the heartbeat keys using derive_heartbeat_keys.

This prevents the database from bloating with ghost records from evicted peers.

Step 8: Payload Updates and Replay Protection

If the public keys DO match, it means the existing owner is just updating their record (e.g., changing their routing payload).

-> See: kinetic-network/src/store/handlers.rs — Lines 153 to 186

The handler checks if the new drand_kyn is older than the one on file.

If so, it rejects it as a replay attack (StaleReveal).

If the payload is genuinely new, it verifies the Post-Quantum ML-DSA signature over the new payload.

Step 9: Sliding Window Rate Limiting

To prevent a node from spamming the database with valid but useless reveals, the handler implements a strict rate limiter.

-> See: kinetic-network/src/store/handlers.rs — Lines 206 to 225

It accesses accepted_reveals_timestamps, which maps domain names to a VecDeque (a double-ended queue) of timestamps.

  • When a new reveal arrives, it records the exact web_time::Instant::now().

  • It then iterates from the front() of the queue (the oldest timestamps).

  • If an old timestamp is older than 3600 seconds (1 hour), it is removed using pop_front().

  • After clearing old entries, it checks the remaining size of the queue. If it is >= max_reveals_per_hour, the reveal is dropped with KineticStoreError::RateLimited.

  • If there is space, the new timestamp is added using push_back().

This ensures a perfect 1-hour sliding window without requiring expensive database lookups.

Step 10: Asynchronous Database Persistence

Finally, the validated record is serialized to JSON and prepared for storage.

-> See: kinetic-network/src/store/handlers.rs — Lines 250 to 260

Because this handler runs inside a synchronous function called by the async networking loop, it cannot perform blocking disk I/O directly.

If it did, it would stall the entire peer-to-peer network executor thread.

Instead, it clones the sled storage handle, creates an async task, and uses spawn_blocking to safely write the bytes to the database on a dedicated blocking thread pool.


The Role of ML-DSA in Liveness

A critical component of this handler is the use of ML-DSA (Module Lattice Digital Signature Algorithm).

This is a Post-Quantum signature scheme.

When an owner broadcasts a heartbeat to keep their domain alive, they are not just saying “I am here.” They are cryptographically proving that they still possess the exact private key that originally claimed the domain.

Because ML-DSA signatures are large and computationally intense to verify, the handle_heartbeat function must be paranoid about when to actually run the verification.

If it verified every signature it saw, a simple script kiddie could take down the entire Kinetic network by flooding it with garbage heartbeats, causing all nodes to lock up doing math.

This paranoia is implemented in the fast-path rejection.


How It Works: The Heartbeat Handler (handle_heartbeat)

The handle_heartbeat function is simpler in scope but critical for network performance, as heartbeats are the most high-volume messages in the system.

-> See: kinetic-network/src/store/handlers.rs — Lines 265 to 345

Step 1: The Fast-Path Rejection (Optimization)

A typical Kinetic node will receive the exact same heartbeat dozens of times from different peers via Gossipsub.

-> See: kinetic-network/src/store/handlers.rs — Lines 270 to 285

The handler looks up the domain in last_heartbeats_by_name.

  • If the incoming latest_drand_kyn exactly matches the cached kyn, it is a duplicate. The handler returns Ok(()) silently, stopping further propagation but avoiding all math.

  • If it is older than the cached kyn, it is a historical replay attack. The handler throws StaleHeartbeat.

This fast-path rejection is what keeps the node’s CPU usage low during high network activity.

Step 2: Extracting the Signable Bytes

To verify the heartbeat, the handler must know what data the owner actually signed.

-> See: kinetic-network/src/store/handlers.rs — Lines 287 to 296

It fetches the existing NameRecord from the database.

If it doesn’t exist, it throws RevealNotFound (you cannot heartbeat a name that hasn’t been revealed).

It then calls heartbeat.signable_bytes(NETWORK_ID) to reconstruct the exact byte payload that the owner’s private key originally signed.

The NETWORK_ID is included to prevent cross-network replay attacks (e.g., replaying a testnet heartbeat on mainnet).

Step 3: ML-DSA Signature Verification

The handler converts the raw bytes of the public key and the signature into the strongly typed structures required by the ml_dsa crate.

-> See: kinetic-network/src/store/handlers.rs — Lines 297 to 317

It uses the Verifier trait to check if the signature is valid for the signable_bytes.

If the verification fails, it throws InvalidSignature.

This step guarantees that only the true owner can generate a valid heartbeat.

Step 4: Future-Dating Bounds Check

A malicious node might try to broadcast a heartbeat with a drand_kyn from a week in the future, hoping to buy themselves a week of idle time.

-> See: kinetic-network/src/store/handlers.rs — Lines 319 to 327

The handler checks if the heartbeat’s kyn is greater than current_drand_kyn + 2.

The + 2 tolerance accounts for minor network propagation delays and clock drift across drand nodes.

Anything further into the future is rejected.

This enforces strict temporal bounds on liveness claims.

Step 5: Database Persistence

Like the record handler, the heartbeat handler updates the in-memory cache and then offloads the actual sled::put database write to a spawn_blocking task to protect the async executor.

-> See: kinetic-network/src/store/handlers.rs — Lines 331 to 342

The heartbeat kyn is converted to big-endian bytes (to_be_bytes()) before storage to ensure consistent cross-platform representation.


Exhaustive Error Handling Breakdown

This file makes extensive use of the KineticStoreError enum.

Understanding these errors is critical for debugging why a record was rejected:

  • VdfExpired: The VDF proof is too old. The user took too long to broadcast it after computing it.

  • TieBroken: The reveal collided with an existing claim, and the Case 121 math determined this peer lost the tie-breaker. The XOR distance was higher than the competitor’s.

  • InsufficientIterations: The peer attempted to steal a domain, but their VDF walls were not high enough to beat the current owner’s heartbeat age. The steal threshold was not met.

  • StaleReveal: A replay attack was detected. A name record payload update was broadcast with an older kyn than the one currently stored.

  • StaleHeartbeat: A replay attack was detected. A heartbeat was broadcast with an older kyn than the one currently stored. The node has already seen a fresher ping.

  • InvalidSignature: The ML-DSA post-quantum signature failed math verification, or the payload was maliciously tampered with in transit.

  • MalformedSignature: The byte array provided was the wrong length or invalid format for ML-DSA. It could not even be parsed into a cryptographic structure.

  • InvalidPublicKey: The public key stored in the database could not be loaded into the ML-DSA verifier. It is corrupted or invalid.

  • RateLimited: The peer submitted more reveals for a single name in an hour than the network allows. The sliding window queue reached maximum capacity.

  • RevealNotFound: A heartbeat was received for a name that has no underlying registration record in the local database. The node must drop the heartbeat because it cannot verify it without the public key stored in the record.


Asynchronous Event Loop Considerations

One of the most important architectural patterns in this file is how it deals with synchronous vs asynchronous code.

The handle_record and handle_heartbeat functions are purely synchronous (fn not async fn).

This is because they are called deep within the libp2p Gossipsub event handlers, which often require synchronous closures or callbacks.

However, writing to a disk-based database like sled is an I/O operation.

It takes time.

If a synchronous function blocks waiting for disk I/O, it freezes the tokio worker thread it is running on.

If enough messages arrive at once, all worker threads freeze, and the entire networking stack collapses (a classic async deadlock).

To solve this, both handlers collect their database writes into a vector, and then use crate::event_loop::utils::spawn_blocking.

This macro takes a closure, sends it to a special tokio thread pool dedicated to blocking operations, and immediately returns.

The networking thread is instantly freed to process the next Gossipsub message, while the disk I/O happens safely in the background.

This pattern is essential for maintaining high throughput in a peer-to-peer network.


Key Pieces

  • handle_record

  • Location: kinetic-network/src/store/handlers.rs:L8

  • Purpose: The primary ingress point for Domain Reveals.

Handles verification, stealing, tie-breaking, rate-limiting, and persistence.

  • Why it matters: This is the consensus engine for the Kinetic namespace.

It decides who owns what.

  • Case 121 Tie-Breaker Logic

  • Location: kinetic-network/src/store/handlers.rs:L80-L107

  • Purpose: Calculates dist_new and dist_existing using bitwise XOR (^) on the public key and VDF proof bytes mapped through complex iterators.

  • Why it matters: Prevents network forks when two peers claim a domain simultaneously.

  • handle_heartbeat

  • Location: kinetic-network/src/store/handlers.rs:L265

  • Purpose: The primary ingress point for Liveness Pings.

  • Why it matters: Keeps domains alive and defends them from being stolen by lowering the hb_age.


How This Connects to the Rest of Kinetic

  • CROSS-CRATE: NameRecord and Heartbeat types — defined and explained in docs/learn/types/01_core_types.md (or equivalent types overview).

  • CROSS-CRATE: verify_reveal and VDF logic — depends on kinetic-verify and the underlying kyn-vdf library.

  • CROSS-CRATE: ConsensusParams (calculating steal thresholds) — defined in kinetic-core.

  • CROSS-CRATE: is_dev_mode — config checks are used to bypass signatures when the node is running in local development mode.

  • Database Storage: The validated data here is passed directly to the sled tree wrapped by KineticRecordStore (explained in 02_store_core.md).

  • Gossipsub Routing: These handlers are the terminus for messages routed through the Libp2p Gossipsub network layer.


Quick Reference

  • Standard vs Premium: Premium domains cannot be stolen or used to steal other domains. Standard domains are fully subject to the stealing lifecycle.

  • Effective Age Formula: current_kyn - proof_kyn - paused_kyns. Used to check VDF expiry.

  • Heartbeat Age Formula (hb_age): current_kyn - last_heartbeat_kyn. This integer directly determines how vulnerable a domain is to being stolen.

  • Case 121 Mechanism: A deterministic XOR distance tie-breaker for simultaneous identical claims. It ensures perfect agreement across the network.

  • Fast-Path Rejection: Heartbeats with a kyn <= existing_kyn are dropped immediately without signature verification to save massive amounts of CPU.

  • Rate Limit Mechanism: Controlled by a VecDeque acting as a sliding window of timestamps over the last 3600 seconds.

  • Persistence Pattern: Handlers are synchronous but offload database writes to spawn_blocking to prevent async deadlocks.


Open Questions / Things to Revisit

  • Database Cleanup Fragility: When a domain is successfully stolen, the code deletes the old kad_record keys manually (lines 130-152). However, it assumes a specific string prefix (kad_record:). If the underlying DHT storage format ever changes, this hardcoded cleanup logic will fail silently, leaving orphaned garbage keys in the database. This should probably be extracted into a shared constant or a helper function inside the storage core.

  • Tie-Breaker Cryptographic Distribution: The Case 121 tie-breaker relies on a ^ b over the pubkey and padded proof bytes. While fully deterministic, it might be worth verifying if this distribution is perfect, or if certain public key prefixes have a microscopic statistical advantage in winning tie-breakers. A formal proof of fairness for this specific XOR construction would add confidence.

  • Future-Dated Heartbeats: The code allows heartbeats up to current_drand_kyn + 2 (line 319). This minor leniency accounts for network propagation delay, but it should be documented whether +2 is a hard requirement for consensus or a flexible heuristic that can be adjusted safely in future versions.

  • Storage Deletion Errors: In lines 140 and 151, the result of self.storage.delete is ignored with a let _ =. If the database deletion fails for any reason, the system will not throw an error or retry, potentially leading to state inconsistencies.

Specific Cryptographic Validations

  • The VDF validation relies fundamentally on standard modulus math.
  • The length of the name string dictates the base difficulty.
  • The iterations are verified using kyn-vdf internals.
  • Every single node on the network validates this independently.
  • If a single node disagrees on the result, it will drop the record.
  • This ensures that malicious peers cannot force bad records into the DHT.
  • ML-DSA is a post-quantum algorithm designed by NIST.
  • It produces signatures that are much larger than traditional ECDSA.
  • This large size is why fast-path rejection is mandatory.
  • It is also why we use spawn_blocking for the subsequent disk writes.

The Importance of Monotonicity

  • Time in Kinetic is not measured in seconds or milliseconds.
  • It is measured purely in drand kyns (pulses).
  • These pulses are provided by an external, decentralized beacon.
  • By using kyns instead of system time, we avoid clock-drift attacks.
  • We also prevent NTP synchronization issues from splitting the network.
  • drand_kyn acts as a monotonic counter for all state changes.
  • A newer record must always have a higher drand_kyn than the old one.
  • If it is lower, it is proven to be a replay attack.
  • If it is equal (for heartbeats), it is a network duplicate.
  • This strict time-bounding is the anchor of the entire consensus system.
  • It allows the network to reach eventual consistency without needing a blockchain.

Step-by-Step Flow Summary

  • 1: Message received from Gossipsub.
  • 2: Basic malformation checks passed.
  • 3: Handed off to handle_record or handle_heartbeat.
  • 4: Local state queried (get_record_with_fallback).
  • 5: Expiry checks enforced.
  • 6: Cryptographic math (VDF or ML-DSA) executed.
  • 7: Consensus rules (Steal Thresholds, Case 121) applied.
  • 8: Old state evicted if necessary.
  • 9: New state cached in memory for rapid reads.
  • 10: New state flushed to persistent disk via blocking thread pool.
  • 11: Return Ok(()) to allow Gossipsub to propagate the message further.

Final Review on Handlers

  • Handlers are the most resource-intensive part of the node.
  • They are the primary defense against network spam.
  • The KineticRecordStore struct must remain lock-free where possible.
  • If a lock is held during ML-DSA verification, the node will stall.
  • This is why the cache uses standard hashmaps inside a single async task owner, rather than globally shared Mutexes.
  • The architecture separates the P2P transport from the cryptographic rules.