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

Kinetic Architecture Documentation

Welcome to the internal documentation for the Kinetic Network.

Use the sidebar to navigate the different modules and crates of the Kinetic codebase. Each document breaks down the internal architecture, reasoning, and Rust implementations step-by-step.

Kinetic: Rust Concepts Glossary

This document serves as a centralized glossary for all the high-level, advanced Rust concepts used extensively throughout the entire Kinetic monorepo (core, network, daemon, node, host, cli, vdf, dns, pac, etc.).

By centralizing these concepts, we avoid documenting the same pattern redundantly across every crate.


1. Concurrency, Async, and Threading

tokio::spawn and tokio::task::spawn_blocking

#![allow(unused)]
fn main() {
// Launch async task
tokio::spawn(async move { handle_request().await; });

// Offload heavy CPU math
tokio::task::spawn_blocking(move || { compute_vdf() }).await;
}
  • What it is: tokio::spawn launches an asynchronous background task on the Tokio runtime, allowing highly concurrent I/O operations (like handling hundreds of HTTP requests). spawn_blocking offloads heavy, CPU-bound computations (like VDF generation, PoW hashing, or disk I/O) to a dedicated OS thread pool.
  • How Kinetic uses it: The kinetic-daemon and kinetic-cli use spawn_blocking extensively for cryptographic tasks. If this math were run on the normal async thread, it would starve the executor and drop incoming network packets.
  • WASM Equivalent: In kinetic-wasm, standard Tokio threading is unsupported. wasm_bindgen_futures::spawn_local is used instead to bridge Rust’s async futures with the browser’s native JavaScript microtask queue.

Note

Standard Tokio threading is unsupported in WebAssembly. Always use wasm_bindgen_futures::spawn_local for WASM targets.

std::sync::Mutex vs tokio::sync::Mutex vs RwLock<T>

TypeThread BlockingBest Use CaseReaders/Writers
std::sync::MutexBlocks OS ThreadUltra-fast updates, no .await1 Writer OR 1 Reader
tokio::sync::MutexYields to RuntimeHeld across .await points1 Writer OR 1 Reader
RwLock<T>Thread BlockingRead-heavy workloadsInfinite Readers OR 1 Writer
  • What they are: Synchronization primitives that safely lock data for multi-threaded access.
  • How Kinetic uses them:
    • std::sync::Mutex is used for ultra-fast, synchronous updates where the lock is never held across an .await point.
    • tokio::sync::Mutex safely yields execution back to the runtime while waiting, which is required if the lock is held across network calls.
    • RwLock (Read-Write Lock) allows infinite simultaneous readers but only one writer. It is used heavily in kinetic-host and kinetic-dns (e.g., caching upstream resolvers) because reads happen thousands of times per second, but writes (config reloads) are rare.

Asynchronous Channels (mpsc, oneshot, broadcast, watch)

  • What they are: Thread-safe queues for passing messages between tasks.
  • How Kinetic uses them:
    • mpsc (Multi-Producer, Single-Consumer): Used to funnel API commands from multiple HTTP threads into the single-threaded Libp2p swarm event loop.
    • oneshot: Bundled inside the mpsc commands to route the exact network response back to the specific foreground API thread that requested it.
    • broadcast: Fans out identical messages (like Gossipsub events) to multiple consumers, such as live SSE streams.
    • watch: Retains only the latest value (like the current_drand_kyn epoch), dropping outdated state instantly so observers never process stale data.

tokio::select! and tokio::join!

#![allow(unused)]
fn main() {
tokio::select! {
    _ = sigterm.recv() => { println!("Shutting down"); }
    packet = socket.recv() => { process(packet); }
}
}
  • What they are: Macros for managing multiple asynchronous futures concurrently.
  • How Kinetic uses them: select! races multiple branches and executes the first one that completes (e.g., racing incoming network packets against a graceful shutdown SIGTERM signal). join! waits for all branches to finish concurrently.

tokio::sync::Semaphore

  • What it is: Limits how many tasks can access a resource simultaneously.
  • How Kinetic uses it: The try_acquire_owned() method acts as a non-blocking rate limiter for gossip messages and VDF evaluations, instantly dropping packets during DDoS attacks instead of halting the executor or exhausting RAM.

std::sync::atomic::AtomicU64

  • What it is: A variable that can be safely mutated across threads at the hardware level without a Mutex.
  • How Kinetic uses it: Used in kinetic-storage to cleanly track database corruption fallback counters without risking deadlocks during panic handling.

2. Memory, Pointers, and Data Types

Box<T> and Arc<dyn Trait> (Dynamic Dispatch)

  • What they are: Heap allocations. Box owns the data. Arc (Atomically Reference Counted) safely shares read-only ownership across multiple threads. dyn Trait hides the concrete type behind a standard interface.
  • How Kinetic uses them: NameRecord::Standard uses Box<Reveal> to prevent the massive Reveal struct from bloating the entire enum’s memory footprint. Arc<dyn VdfEngine> allows the daemon to hot-swap cryptographic engines at runtime without altering the networking code.

&[u8] vs Vec<u8> vs bytes::Bytes

TypeMemory LocationMutabilityOwnershipBest Use Case
&[u8]Pointer to memoryRead-onlyBorrowedCryptographic validation (Zero-copy)
Vec<u8>HeapMutableOwnedBuilding network payloads
bytes::BytesHeap (Ref-Counted)Read-onlyShared OwnershipPassing multi-megabyte streams through pipelines
  • What they are: Different ways to interact with byte arrays in memory.
  • How Kinetic uses them: Kinetic returns &[u8] for cryptographic validations (like checking signatures) to avoid memory allocation overhead. It uses bytes::Bytes in the HTTP proxy to pass multi-megabyte video streams through the pipeline by just incrementing a tiny pointer counter, preventing massive RAM duplication.

Cow<'_, T> (Clone-On-Write)

  • What it is: A smart pointer that holds either borrowed references (fast) or owned data (slow).
  • How Kinetic uses it: In kinetic-network, the record store returns a cheap reference for reads, but dynamically allocates a copy only if the data needs mutation.

Option<T> and Result<T, E>

  • What they are: Rust’s zero-cost replacements for null and Exceptions.
  • How Kinetic uses them: Option forces the compiler to ensure code explicitly handles missing data (like an optional manifest signature). Result safely propagates errors. E.g., Result<Option<T>, E> rigorously maps three states: invalid, missing, or present.

Specialized Collections (BTreeMap, LruCache, HashMap)

  • What they are: Highly optimized data structures.
  • How Kinetic uses them:
    • HashMap provides O(1) lookups for DNS zones.
    • BTreeMap keeps keys sorted, enabling blazing-fast scan_prefix operations for the WASM fallback database.
    • lru::LruCache ejects the Least Recently Used elements, preventing RAM exhaustion from malicious peers overflowing the banned peer list.

3. Macros and Code Generation

thiserror (#[error], #[from], #[source])

  • What it is: An external macro that auto-generates error trait implementations.
  • How Kinetic uses it: Removes hundreds of lines of boilerplate. #[from] allows automatic conversion of system errors (like std::io::Error) into unified KineticError variants via the ? operator.

serde (#[serde(untagged)], #[serde(tag = ...)], #[serde(other)])

  • What it is: Directives for parsing JSON and binary structures.
  • How Kinetic uses it:
    • untagged allows seamless wire transmission of different payload variants without bloated "type" JSON keys.
    • other safely catches unrecognized DNS records sent by future protocol versions without crashing the parser.

#[cfg(...)] and #![no_std] (Conditional Compilation)

#![allow(unused)]
fn main() {
#[cfg(not(target_arch = "wasm32"))]
pub mod native_db;

#[cfg(target_arch = "wasm32")]
pub mod wasm_db;
}
  • What it is: Tells the compiler to include or exclude code based on the target environment.
  • How Kinetic uses it: #[cfg(not(target_arch = "wasm32"))] strips out native Sled database I/O when compiling for browsers. #![no_std] ensures the kyn-vdf core cryptographic math can compile without an operating system.

Important

#![no_std] is an absolute requirement for compiling the pure-Rust cryptographic math into secure WebAssembly environments.

proptest!

  • What it is: A property-based fuzzer.
  • How Kinetic uses it: Generates thousands of randomized, hostile byte arrays to test the DNS parser in kinetic-dns, ensuring it cannot panic regardless of malicious inputs.

4. Systems, Safety, and Advanced Syntax

Filesystem Locks & Atomic Writes

  • What it is: Safe interaction with the host OS.
  • How Kinetic uses it: fs2 uses OS-level flock to ensure only one VDF process runs per machine, preventing cross-daemon CPU saturation. When writing config files (like PAC scripts), Kinetic writes to a .tmp file and calls std::fs::rename() to atomically replace the old file, guaranteeing zero corruption if power is lost mid-write.

Constant-Time Equality (subtle::ConstantTimeEq)

  • What it is: A mathematical comparison that always takes the exact same number of CPU cycles.
  • How Kinetic uses it: Prevents cryptographic timing attacks on the API. Without it, an attacker could guess an API Bearer token character-by-character by measuring microsecond variations in how fast the server rejects the request.

Warning

Never use standard == string comparisons for API tokens. Always use ConstantTimeEq to prevent microsecond timing attacks.

std::process::Command (Process Delegation)

  • What it is: Shells out to execute external OS processes.
  • How Kinetic uses it: kinetic-cli acts as a unified delegator. Instead of running networking code directly, it executes commands like sudo kinetic-dns start and streams the output directly to the user’s terminal.

Extension Trait Pattern

  • What it is: Bypasses Rust’s “Orphan Rule” (which forbids implementing standard methods on external types).
  • How Kinetic uses it: Kinetic creates custom traits (like DnsZoneExt) to bolt heavy validation logic (parse_payload) onto lightweight structs provided by third-party libraries (like hickory-dns).

Differential Fuzzing in Cryptography

  • What it is: Testing two entirely different implementations against each other instead of static answers.
  • How Kinetic uses it: kinetic-vdf tests the pure Rust verification logic (kyn-vdf) against the C++ FFI library (chiavdf) with millions of random inputs, ensuring absolute mathematical parity to prevent network forks.

PhantomData and the Newtype Pattern

  • What it is: Zero-overhead type system strictness.
  • How Kinetic uses it: KineticDid wraps a String privately. The only way to instantiate it is via a constructor that validates the syntax. Thus, any function accepting a KineticDid knows with 100% certainty the DID is valid without ever re-running validation checks.

01 — Overview (kinetic-types)

Crate: kinetic-types Stage: 1 of 10 Reading time: ~5 minutes Depends on: None Source files: All files in kinetic-types/src/


What Is This?

This crate is the zero-dependency structural foundation of the Kinetic network. It defines the exact shapes of the data (structs, enums) that flow through the network. It does not implement heavy logic like consensus, networking, or cryptographic math — it simply defines the contracts that all those other systems must agree on.

Why Kinetic Needs This

Important

Without a centralized, zero-dependency type hub, you end up with “dependency hell.” If the browser extension, the node daemon, and the offline signing tools all defined their own version of a NameRecord, they would eventually drift out of sync. By having kinetic-types stand alone with no dependencies on heavy networking libraries, light clients (like WASM frontends or air-gapped signers) can import these exact schemas without bloating their binaries.

How It Works

This crate acts as a dictionary. When a node receives bytes over the wire, it uses the definitions in this crate to decode those bytes into meaningful Rust objects.

  • It uses serde for JSON and binary serialization.
  • It defines exact byte-layouts for signatures to prevent ambiguity.
  • It organizes types by their domain: time (clock), identity (KIDs), names, VDFs, and governance.

Key Pieces

This documentation is split into 7 deep-dive topic files:

  1. 02_error_and_severity.md: The foundational error taxonomy (Severity) used to categorize failures across the network.
  2. 03_identity.md: AuthorizedKid and AuthorizedManifest — how decentralized identities and capabilities are cryptographically bound to .kin names.
  3. 04_clock.md: KineticTime — the branded time hierarchy (Kyns, Facets, Prisms, etc.) built on network beacons.
  4. 05_vdf_types.md: The structures backing the two-phase commit-and-reveal name registration protocol and VDF proofs.
  5. 06_governance_types.md: Opcodes and structures for root authority actions like halting the network or granting premium names.
  6. 07_name_record.md: The ultimate source of truth for name ownership, DHT redundancy, and heartbeat liveness proofs.
  7. 08_dns_proxy_cdn.md: Decentralized DNS schemas, IPC proxy payloads for the browser extension, and CDN caching types.

How This Connects to the Rest of Kinetic

FORWARD DEPENDENCY: Literally every other crate in the Kinetic workspace imports kinetic-types.

  • kinetic-network uses it to parse incoming P2P messages.
  • kinetic-verify uses it to extract the public keys and signatures from records to validate them.
  • kyn-vdf and vdfrs provide the mathematical engines that populate the VdfProof structures defined here.

Quick Reference

  • Crate purpose: Data schemas and serialization.
  • Dependencies: None (zero internal workspace dependencies).
  • Primary tool: serde (Serialization/Deserialization).
  • Design pattern: Strict separation of data (here) from behavior (elsewhere).

Open Questions / Things to Revisit

Note

  • Are there any types currently in kinetic-types that accidentally pull in heavy dependencies and should be refactored out? (e.g., Does importing thiserror or ml_dsa defeat the zero-dependency goal, or are they lightweight enough?)
  • Should the IPC proxy types live here, or belong in a dedicated kinetic-ipc crate to keep this crate strictly focused on network consensus types?

kinetic-types — Stage 1, Topic 02: Error Taxonomy & Crate Entry Point

Crate: kinetic-types Stage: 1 of N Estimated reading time: 12 minutes Depends on: Topic 01 (KineticTime / clock subsystem) — helpful but not required


What Is This?

error.rs defines Severity — a four-level enum that every domain error type in Kinetic uses to classify how bad a problem is. It is the single source of truth for log filtering, UI alert levels, and automated node decision-making across the entire workspace.

lib.rs is the crate’s front door. It declares every subsystem module — clock, dns, name_record, governance, identity, proxy, cdn, vdf — and documents what the crate is for. Together these two files establish the crate’s contract with the rest of Kinetic.

-> See: kinetic-types/src/error.rs — Lines 1 to 34 -> See: kinetic-types/src/lib.rs — Lines 1 to 29


Why Kinetic Needs This

Kinetic is a distributed network. At any given moment a node is processing DNS lookups, verifying VDF proofs, handling governance votes, streaming CDN chunks, and maintaining heartbeat liveness proofs — all concurrently, all of which can fail in different ways.

Without a shared severity taxonomy, every subsystem would invent its own language for “this is bad.” One module might log a string “fatal”, another might return an integer 2, a third might panic. When a node’s supervisor loop — the code that decides whether to retry an operation, send an alert, or halt cleanly — reads these results, it has no common vocabulary to reason with. It cannot distinguish a transient DNS cache miss (retry in 5s) from a cryptographic signature forgery (halt and alert the operator immediately).

Severity gives the whole network one answer to the question: how urgently does this need a response? Because it lives in kinetic-types — the zero-dependency type hub — every crate that imports kinetic-types gets access to it without pulling in consensus engines, networking stacks, or any heavy machinery.

lib.rs matters for a complementary reason: it is the declaration that kinetic-types exists as a coherent unit. Without it, Rust does not know that clock, dns, error, and the other modules belong to the same crate. Every use kinetic_types::error::Severity statement anywhere in the workspace resolves through the pub mod error; line in lib.rs.


How It Works

The Severity Ladder (error.rs Lines 13-22)

Severity is an enum with exactly four variants, ordered from least to most urgent:

Info — Something happened and it is fine. A DNS record was fetched from cache. A heartbeat arrived on schedule. No action is needed by the caller. Nodes log these at trace level and move on.

Warning — Something is degraded but not broken. A VDF proof took longer than expected. A DNS record is close to its TTL expiry. The correct response is to monitor, maybe retry, but not to alert anyone. The node can continue normal operation.

Error — A specific operation failed and the caller must handle it. A name record signature did not verify. A CDN chunk was unavailable. This needs attention from the client or from the node’s retry logic, but it does not threaten the node’s overall health.

Caution

Critical — A protocol-level, cryptographic, or safety violation occurred. A governance message was signed with a revoked key. A VDF reveal did not match its committed value. A replay-protection counter went backwards. When a node sees Critical, it should stop trusting the source of that error and possibly alert the operator. Continuing blindly past a Critical severity is how security vulnerabilities propagate through distributed systems.

The ladder is intentional. Kinetic is designed so that node operators — and the automated supervisor loops that monitor node health — can filter on Severity without reading the full error message. A monitoring dashboard can count Critical events in a time window and page an operator when that count crosses a threshold, without understanding anything about VDF math or governance opcodes.

How Domain Errors Plug Into Severity

Every domain-specific error enum in Kinetic — DnsError, GovernanceError, VdfError, and others — implements a severity() method that returns a Severity variant for each of its own variants. The signature looks like this:

#![allow(unused)]
fn main() {
fn severity(&self) -> Severity { ... }
}

This means a caller can always ask any Kinetic error “how bad are you?” without knowing what kind of error it is. The Severity type is the bridge between domain-specific error detail and network-wide operational policy. The same supervisor loop code can handle a failed DNS lookup, a VDF rejection, and a bad governance signature — because they all speak the same severity language.

FORWARD DEPENDENCY: DnsError, GovernanceError, VdfError and their severity() methods are defined in other modules of kinetic-types and documented in later topics. When you read those files, look for how each variant maps to a Severity level — the mapping is a design decision that encodes protocol intent directly into the type system.

The Display Implementation (error.rs Lines 24-33)

Severity implements std::fmt::Display, which means when you write {severity} in a format string or log statement, Rust calls this code and prints INFO, WARNING, ERROR, or CRITICAL. This is not automatic — Rust does not know how to turn your enum into a human-readable string unless you explicitly implement it.

-> See: kinetic-types/src/error.rs — Lines 24 to 33

The implementation uses a match block that exhaustively covers all four variants. Rust enforces exhaustiveness at compile time: if someone adds a fifth variant to Severity and forgets to update the Display implementation, the build fails with a clear error. This makes the type system itself a safety net for protocol evolution — you cannot silently omit a case.

The uppercase string format ("CRITICAL" not "Critical") is a deliberate convention. It matches standard syslog severity formats and makes Kinetic log output grep-friendly. A DevOps engineer monitoring Kinetic nodes with standard log tooling will instantly recognize the format.

The Derive Macro Line (error.rs Line 12)

The line above the enum definition reads:

#![allow(unused)]
fn main() {
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
}

This single annotation instructs the Rust compiler to automatically generate implementations of eight different traits.

-> See RUST_CONCEPTS.md for a full breakdown of #[derive(...)].

The Crate Entry Point (lib.rs Lines 20-28)

-> See: kinetic-types/src/lib.rs — Lines 20 to 28

These nine pub mod declarations are Rust’s way of saying “these source files are part of this crate and their public items are accessible from outside.” The keyword pub before mod means other crates in the workspace — kinetic-kid, kinetic-verify, the browser extension bridge — can import from these modules. A mod without pub would be private, visible only within kinetic-types itself.

The module list is also an architectural statement: every concern in Kinetic that produces shared data types has its own module. Clock lives separately from DNS. DNS lives separately from governance. This separation means that kinetic-kid can import only kinetic_types::vdf::VdfProof without being forced to compile or depend on DNS type code. Rust compiles only what you use, and module boundaries make that precise.

Zero-Dependency Philosophy (lib.rs Lines 1-6)

Important

The lib.rs doc comment calls kinetic-types a “zero-dependency, lightweight type hub.” This is a load-bearing architectural decision, not a style preference. Any crate in the workspace that defines or consumes shared types — including offline tools, browser extensions, hardware wallets — must be able to import kinetic-types. If kinetic-types depended on a networking library, every offline tool would be forced to compile that library too. If it depended on a consensus engine, it could never run in a browser WebAssembly context where networking is sandboxed.

The only external dependencies kinetic-types may carry are serialization (serde) and cryptographic primitives — things so fundamental that every possible consumer needs them anyway. This is why Severity derives Serialize and Deserialize from serde rather than implementing a custom wire format: serde is already a cost every consumer accepts.


Key Pieces

Severitykinetic-types/src/error.rs Lines 13-22

The four-level classification enum. Every error in the Kinetic system answers to this type. Choosing the right variant for an error is a protocol design decision: it tells operators and automated systems how to respond without requiring them to understand the domain. Getting this wrong — tagging a signature forgery as Warning instead of Critical — is a security decision embedded in code, invisible until something goes wrong in production.

impl std::fmt::Display for Severitykinetic-types/src/error.rs Lines 24-33

Converts a Severity variant to an uppercase string for logs and error messages. The match is exhaustive — Rust will refuse to compile if a variant is missing from the arm list. The uppercase format matches syslog conventions for grep-friendly log tooling.

pub mod error;kinetic-types/src/lib.rs Line 23

The one line that makes Severity importable as kinetic_types::error::Severity from anywhere in the workspace. Without this declaration, error.rs would be an orphaned file that the compiler ignores entirely.

Nine pub mod declarations — kinetic-types/src/lib.rs Lines 20-28

The module declarations that constitute the crate’s public surface. Each one corresponds to a source file or directory under kinetic-types/src/. The ordering roughly follows dependency direction: error is declared before governance because governance errors reference Severity.


How This Connects to the Rest of Kinetic

Every other crate in the workspace that can produce an error uses Severity from this module. That makes kinetic-types::error the most imported error module in the codebase. It is infrastructure as fundamental as the clock subsystem — arguably more so, because errors happen everywhere.

CROSS-CRATE: Severity — this is the canonical definition. All other crates import it from here. It has no definition anywhere else in the workspace.

FORWARD DEPENDENCY: kinetic-kid — the node identity and key management crate — will import Severity to classify key-loading and signature errors. When a KID (Key Identifier) cannot be loaded, that error’s severity() method will return a variant defined right here.

FORWARD DEPENDENCY: kinetic-verify — the proof verification engine — uses Severity to signal whether a failed VDF proof is a transient computation error (Warning) or a deliberate submission of an invalid proof (Critical). The distinction matters because a Critical here triggers blacklisting logic at the network layer, not just a retry.

FORWARD DEPENDENCY: The browser extension proxy (proxy module declared in lib.rs Line 26) serializes errors over an IPC channel back to the browser. Because Severity derives Serialize/Deserialize, the browser receives a structured severity level it can use to display the right UI alert color without parsing a raw string.

FORWARD DEPENDENCY: The governance module (governance, lib.rs Line 24) produces GovernanceError variants that each carry a Severity. A governance message with an invalid binary opcode gets Warning; a message with a forged signature gets Critical. Node operators can configure severity-gated alerting rules that fire on Critical governance errors specifically, independently of other error streams.


Quick Reference

ItemFileLinesOne-line purpose
Severity enumerror.rs13-22Four-level error classification
Severity::Infoerror.rs15Benign — no action needed
Severity::Warningerror.rs17Transient — retry or monitor
Severity::Errorerror.rs19Operation failed — caller must handle
Severity::Criticalerror.rs21Safety/crypto violation — halt or alert
Display for Severityerror.rs24-33Converts to uppercase string for logs
#[derive(...)] on Severityerror.rs12Enables debug, copy, compare, serialize
pub mod error;lib.rs23Exposes error module to workspace
Nine pub mod declarationslib.rs20-28All subsystem modules declared public
Zero-dependency rulelib.rs1-6No heavy crates — stays WASM-compatible

Severity decision flow for node supervisors:

SeverityDecision Flow
Infolog at trace level, continue normally
Warninglog, schedule a retry or set a monitor interval, continue
Errorlog, return error to caller, let caller’s policy decide
Criticallog at error level, alert operator, consider blacklisting the source

Open Questions / Things to Revisit

1. No severity ordering trait. Severity does not implement PartialOrd or Ord, so you cannot write if severity >= Severity::Error. A supervisor loop that wants to handle “anything Error or worse” must use a match arm or a helper function. The ordering is obvious (Info < Warning < Error < Critical) and the omission may be intentional — forcing explicit match arms prevents accidentally skipping a case — but it is worth revisiting if supervisor logic grows verbose.

2. No KineticError trait definition. error.rs describes (in comments) that domain errors implement a severity() method, but there is no Rust trait that formally requires this. Each domain error type adds severity() by convention, not by compiler enforcement. A future trait KineticError { fn severity(&self) -> Severity; } would make this a compile-time guarantee rather than a code review concern.

3. Wire format for Severity is unspecified. Serialize/Deserialize on Severity means serde knows how to handle it, but the actual wire format (JSON string “Critical”, integer, MessagePack byte) depends on the serializer each subsystem chooses. If the proxy subsystem uses JSON and the VDF subsystem uses MessagePack, a Severity crossing that boundary may need a translation step. Worth confirming all subsystems agree on a single encoding.

4. lib.rs module order is not documented as intentional. The nine pub mod declarations follow a rough dependency order (error before governance, which uses it) but this is not documented anywhere in the file. If a new subsystem is added, the author needs to know where to insert it. A short comment in lib.rs explaining the ordering rule would prevent future confusion.

Stage 1 · identity.rs — Authorization Containers for .kin Names

Crate: kinetic-types Source: kinetic-types/src/identity.rs — Lines 1–97 Estimated reading time: ~12 minutes Depends on: 01_lib.md (crate overview), 02_name.md (.kin name model)


What Is This?

identity.rs defines two authorization containers: AuthorizedKid and AuthorizedManifest. Each one wraps a cryptographic identity document or capability declaration and binds it to a specific .kin name, sealed with the name-owner’s digital signature. This is how Kinetic proves that the person who owns alice.kin is the one who chose to attach a particular key or capability — not a stranger injecting their own.


Why Kinetic Needs This

Without these containers, any node on the network could claim “I am alice.kin” by presenting any key document whatsoever — there would be no proof the name owner agreed. AuthorizedKid provides that proof for identity keys. AuthorizedManifest provides the same proof for capability rules (“this key may only sign name-transfer messages, nothing else”).

There is a second, more subtle problem: a valid cryptographic signature is just bytes. If the signing input is not locked to a specific network context, an attacker could take a signature produced on the production .kin network and replay it on a test network like .corp or .local, where the same key might have different privileges. The signable_bytes methods on both structs solve this by baking the network_id string directly into the bytes that get signed. No network_id → no valid signature.


How It Works

Concept 1 — What Is a KID?

KID stands for Key Identifier. Think of it as a decentralized identity card. It is a structured document that says: “Here are the public keys that represent this identity, here is who controls them, and here is the proof.” KID documents are the Kinetic equivalent of a DID Document in the W3C DID standard.

The actual KidDocument type lives in the kinetic-kid crate (Stage 3). Inside identity.rs it appears as kinetic_kid::document::KidDocument — Kinetic uses the full path because kinetic-types depends on kinetic-kid.

FORWARD DEPENDENCY: KidDocument — defined in kinetic-kid/src/document.rs. Full documentation in docs/learn/kid/ (Stage 3). Conceptually: a structured document containing the public keys and controller metadata for one Kinetic identity.

Concept 2 — What Is a CapabilityManifest?

A CapabilityManifest is a declaration of what a key is permitted to do. Rather than saying “this key can do everything”, Kinetic lets identity owners publish narrow permissions: “this key may only register sub-names”, or “this key may only publish data records, not transfer ownership.” This is capability-based access control applied to a decentralized network.

FORWARD DEPENDENCY: CapabilityManifest — defined in kinetic-kid/src/manifest.rs. Full documentation in docs/learn/kid/ (Stage 3). Conceptually: a signed list of permitted operations scoped to one key.

Concept 3 — AuthorizedKid: Binding an Identity to a Name

-> See: kinetic-types/src/identity.rs — Lines 20 to 57

AuthorizedKid has three fields:

  • name — the .kin name this authorization is for (e.g. "alice.kin").
  • kid_doc — the full KidDocument being attached. Contains public keys.
  • owner_signature — the signature the name-owner produced over signable_bytes(...).

When a node in Kinetic receives an AuthorizedKid, it:

  1. Reconstructs the expected byte sequence using signable_bytes(network_id).
  2. Verifies owner_signature against that byte sequence using the name-owner’s currently-registered public key.
  3. If it checks out, the kid_doc is accepted as the authorized identity for that name.

If any of those three fields is tampered with — the name changed, a different key inserted, the signature swapped — step 2 fails and the document is rejected.

Concept 4 — AuthorizedManifest: Binding Capabilities to a Name

-> See: kinetic-types/src/identity.rs — Lines 60 to 96

AuthorizedManifest has four fields:

  • name — same idea; the .kin name this manifest governs.
  • manifest — the CapabilityManifest declaring permitted operations.
  • kid_doc — an optional KidDocument. Sometimes you publish capabilities alongside the identity document; sometimes you publish them separately. Option<KidDocument> lets both cases exist without duplicating the struct.
  • owner_signature — same role as in AuthorizedKid; the name-owner’s seal of approval.

The Option<T> here is a deliberate design choice: a capability manifest may be published independently of a KID update. Forcing kid_doc to always be present would mean every manifest update re-uploads an unchanged identity document — wasted bandwidth and storage.

Concept 5 — Replay Protection and signable_bytes, Step by Step

This is the most important mechanism in the file. Read it carefully.

Warning

Why replay attacks exist: A cryptographic signature only proves “someone signed this exact sequence of bytes.” It says nothing about which network that signing was intended for. If two networks accept the same key, an attacker can lift a valid signature from one and present it as proof of authorization on the other.

How Kinetic defeats it: Every call to signable_bytes(network_id) begins by embedding the network’s unique identifier string into the byte payload before anything else. Different network, different prefix, different bytes, different signature required.

The byte layout for AuthorizedKid::signable_bytes:

-> See: kinetic-types/src/identity.rs — Lines 42 to 56

Walk through each piece in order:

  1. network_id bytes — e.g. b"kin" for the production network. This is the anti-replay anchor. A .corp node uses b"corp" here, producing a completely different byte sequence even for the same name and document.

  2. b"-auth-kid-v1" — a fixed ASCII tag. Together with step 1 this forms the full prefix kin-auth-kid-v1. The v1 suffix means this layout can be versioned if the format ever needs to change without breaking old signatures.

  3. u32_be(name.len()) — four bytes encoding the byte-length of the name as a big-endian 32-bit unsigned integer. This is called a length prefix. Without it, a parser cannot tell where the name ends and the JSON begins. An attacker could craft a name whose bytes overlap with the start of the JSON, confusing the verifier.

  4. name_bytes — the raw UTF-8 bytes of the .kin name. Length determined by step 3.

  5. u32_be(canon_json.len()) — four bytes encoding the byte-length of the canonical JSON. Same reason as step 3 — unambiguous boundary.

  6. canon_json_bytes — the canonical JSON serialization of the KidDocument. “Canonical” means the JSON is always produced in the same way regardless of how the struct was constructed — same key order, same whitespace rules (none). This is what canonicalize() does (see Concept 6 below).

For AuthorizedManifest::signable_bytes the layout is identical except step 2 uses b"-auth-manifest-v1" and step 6 serializes the CapabilityManifest instead.

-> See: kinetic-types/src/identity.rs — Lines 81 to 95

The implementation uses Vec::with_capacity(...) to pre-allocate the exact number of bytes needed before filling them in — a small but real performance optimization that avoids repeated heap reallocations as data is appended.

Concept 6 — What Does canonicalize() Do?

-> See: kinetic-types/src/identity.rs — Lines 44 and 83

UNVERIFIED — the canonicalize method is defined in kinetic-kid. The following is the expected behavior based on the naming convention and cryptographic context; verify against kinetic-kid documentation when Stage 3 is written.

canonicalize() converts a struct into a canonical JSON string — a JSON representation where key ordering is deterministic and no optional whitespace is included.

The problem it solves: JSON is not unique. The same Rust struct can serialize to {"name":"alice","keys":[...]} or {"keys":[...],"name":"alice"} depending on which serializer ran first. If two nodes produce different byte sequences for the same logical document, their signature hashes will differ and verification will fail randomly. Canonical JSON eliminates this by specifying exactly one valid serialization per document.

The .unwrap_or_default() call after canonicalize() means: if canonicalization somehow fails (e.g. a field contains a value that cannot serialize), produce an empty string rather than crashing. An empty canonical payload would cause signature verification to fail, which is the safe outcome.


Key Pieces

AuthorizedKid

File: kinetic-types/src/identity.rs — Lines 20–57 What it does: Wraps a KID document and proves the .kin name-owner authorized it. Why it matters: Without it, any node could attach any key to any name unchecked.

AuthorizedManifest

File: kinetic-types/src/identity.rs — Lines 60–96 What it does: Wraps a capability manifest and proves the name-owner authorized it. Why it matters: Enables scoped-permission keys; proves the name-owner set the scope.

AuthorizedKid::signable_bytes

File: kinetic-types/src/identity.rs — Lines 42–56 What it does: Produces a deterministic, network-scoped byte sequence for signing. Why it matters: This exact byte sequence is what gets signed and what gets verified. Any mismatch — wrong name, wrong network, wrong document — causes verification to fail.

AuthorizedManifest::signable_bytes

File: kinetic-types/src/identity.rs — Lines 81–95 What it does: Same role as above, for capability manifests. Why it matters: The -auth-manifest-v1 tag prevents manifest signatures and KID signatures from being confused with each other even on the same network.

#[derive(Debug, Clone, Serialize, Deserialize)]

File: kinetic-types/src/identity.rs — Lines 20 and 60 -> See RUST_CONCEPTS.md for an explanation of #[derive(...)].


How This Connects to the Rest of Kinetic

Both structs embed types from kinetic-kid directly:

FORWARD DEPENDENCY: kinetic_kid::document::KidDocument — used in AuthorizedKid.kid_doc (Line 25) and AuthorizedManifest.kid_doc (Line 67). Defined in kinetic-kid. When Stage 3 is documented, cross-reference: docs/learn/kid/XX_document.md.

FORWARD DEPENDENCY: kinetic_kid::manifest::CapabilityManifest — used in AuthorizedManifest.manifest (Line 65). Defined in kinetic-kid. When Stage 3 is documented, cross-reference: docs/learn/kid/XX_manifest.md.

The name field on both structs ties back to the .kin naming model introduced in 02_name.md. An AuthorizedKid is only meaningful if you already understand what a .kin name is and how ownership is established.

The owner_signature field (a Vec<u8> on both structs) is raw bytes. The actual signature verification logic — checking this signature against the name-owner’s key — lives in kinetic-verify (a later stage). kinetic-types only holds the data shape; it does not verify anything itself.

FORWARD DEPENDENCY: Signature verification of owner_signature — handled by kinetic-verify. When that stage is documented, add a cross-reference here.


Quick Reference

ItemWhat it isFile:Line
AuthorizedKidKID doc + name + owner sigidentity.rs:20–28
AuthorizedManifestManifest + name + optional KID + owner sigidentity.rs:60–70
signable_bytes (KID)Produces bytes to sign/verifyidentity.rs:42–56
signable_bytes (manifest)Same, for manifestsidentity.rs:81–95
Byte layout prefix{network_id}-auth-kid-v1 or -auth-manifest-v1identity.rs:43, 82
Length prefix (u32_be)4 bytes before each variable-length fieldidentity.rs:51–53
canonicalize()Deterministic JSON to bytesidentity.rs:44, 83
KidDocumentFORWARD DEPENDENCY — kinetic-kid Stage 3identity.rs:25, 67
CapabilityManifestFORWARD DEPENDENCY — kinetic-kid Stage 3identity.rs:65

Important

The anti-replay rule to remember: network_id is always the first bytes written. Change the network, change the required signature. Period.

The length-prefix rule: Every variable-length field is preceded by 4 bytes (u32, big-endian) saying how long it is. This makes the byte sequence parseable without ambiguity.


Open Questions / Things to Revisit

Note

  1. canonicalize() — UNVERIFIED. The behavior described in Concept 6 is inferred from naming and cryptographic convention. Once Stage 3 (kinetic-kid) is documented, confirm exactly which JSON canonicalization scheme is used (JCS? custom?), whether it sorts keys, and whether it handles nested structures deterministically.

  2. unwrap_or_default() on canonicalize. If canonicalization fails silently (empty string), the resulting signature will also be wrong silently. Is there an error path that surfaces canonicalization failures to callers? Worth checking whether this should return Result<Vec<u8>, Error> rather than Vec<u8>.

  3. Who verifies owner_signature? This file only defines the shape. The verification logic is deferred to kinetic-verify. When that crate is documented, confirm which key (from which field of KidDocument) is used to verify the signature and how the verifier knows which network_id to use.

  4. kid_doc is Option on AuthorizedManifest but required on AuthorizedKid. Was this intentional from the start, or an evolving design? If a manifest is published without a KID, how does a verifier know which public key to check the owner_signature against? This might be answered by looking at how the verifier resolves the name’s current key before checking the manifest.

  5. Version tag v1. The -auth-kid-v1 suffix implies a versioning plan. Is there a migration path documented for what happens when the signing format needs to change? Who increments the version, and how do nodes handle both v1 and v2 during transition?

clock.rs — Kinetic Network Timekeeping (The Crystal Lexicon)

Cratekinetic-types
Stage1 of N
Reading time~10 minutes
Depends onNothing — this module is self-contained

What Is This?

clock.rs defines how Kinetic measures and expresses time on its own terms. Rather than leaning on Unix timestamps (seconds since January 1, 1970), Kinetic has its own named time units called The Crystal Lexicon: Kyn, Facet, Prism, Matrix, Lattice, and Apex. The KineticTime struct holds a decoded snapshot of exactly where the network is in that hierarchy at any given moment.

-> See: kinetic-types/src/clock.rs — Lines 1–12 (module-level doc comment listing all six units)


Why Kinetic Needs This

Without a custom time system, every frontend, explorer, and monitoring tool would independently interpret raw beacon numbers differently. You’d get one dashboard showing Unix timestamps, another showing block heights, and a third showing something else entirely. That confusion is a support nightmare and it makes Kinetic look like a generic chain rather than a purpose-built network.

There is also a practical reason: Kinetic’s consensus engine works in beacon slots — absolute counters that tick every 3 seconds. Those raw counters mean nothing to a human. The Crystal Lexicon translates them into something a user can read: “Prism 14, Facet 3 (Kyn 401)” instead of “412,601 beacons since genesis.” The time system is also a guard against confusion with wall-clock time — a Prism is not a “day” in the POSIX sense; it is exactly 28,800 Kyns, which happens to equal 24 hours of real time. Naming it differently keeps Kinetic network time and system time clearly separated in code and in conversation.


How It Works

The Six Units — sizes and rationale

The Crystal Lexicon is a strict hierarchy. Each unit is a fixed multiple of the one below it:

UnitIn KynsIn Real Time
Kyn13 seconds
Facet1,2001 hour
Prism28,8001 day
Matrix201,6001 week (7 Prisms)
Lattice864,0001 month (30 Prisms)
Apex10,512,0001 year (365 Prisms)

The Kyn is the heartbeat. Every time the network’s consensus engine produces a beacon, one Kyn has passed. At 3 seconds per Kyn, this is a deliberate engineering choice: it is long enough for a message to propagate across the globe, but short enough that the network feels responsive. Everything above a Kyn is just grouping those heartbeats into human-friendly containers.

-> See: kinetic-types/src/clock.rs — Lines 7–12 (the hierarchy comment block)


The KineticTime struct

-> See: kinetic-types/src/clock.rs — Lines 26–36

KineticTime has four fields:

  • total_kyns — the raw count of Kyns elapsed since the network’s genesis point. This is the authoritative number; all other fields are derived from it.
  • prism — how many complete Prisms (days) have passed since genesis. Not the prism-of-the-year; the total prism count from day zero.
  • facet — how many complete Facets (hours) have passed within the current incomplete Prism. This is always in the range 0–23.
  • kyn — how many complete Kyns have passed within the current incomplete Facet. Always in the range 0–1199.

Think of it like a digital clock. The hours hand doesn’t keep counting past 23; it resets at midnight. Same principle here: facet resets every Prism, kyn resets every Facet. The only field that never resets is total_kyns.

The #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] line above the struct tells Rust to auto-generate several standard behaviours. See RUST_CONCEPTS.md — #[derive] for what each of those means.


from_kyn() — converting a raw beacon number into human time

-> See: kinetic-types/src/clock.rs — Lines 44–68

This is the only constructor. It takes two arguments:

  • current_kyn — the absolute beacon number the network is at right now.
  • genesis_kyn — the beacon number at which this particular chain’s counting started. (More on why this matters in a moment.)

Important

The function has a safety check first: if current_kyn is somehow less than genesis_kyn (the clock hasn’t reached genesis yet, or the inputs are mismatched), the function returns a zeroed-out KineticTime rather than panicking or underflowing. This matters because u64 cannot go negative — subtracting a larger number from a smaller one would wrap around to an enormous number and silently corrupt the time. The guard prevents that entirely.

The arithmetic uses basic integer division (/) and remainders (%) in two passes to extract the full Prisms, Facets, and Kyns from the total elapsed kyns.

Concrete example — total_kyns = 50,000:

  • 50,000 / 28,800 = 1 complete Prism.
  • 21,200 Kyns remain.
  • 21,200 / 1,200 = 17 complete Facets.
  • 800 Kyns remain.
  • Display: “Prism 1, Facet 17 (Kyn 800)”

In real time, that 50,000 Kyn mark is 1 day, 17 hours, and 40 minutes after genesis (800 Kyns × 3 seconds = 2,400 seconds = 40 minutes).


Why genesis_kyn exists

The consensus engine counts beacons from the very start of the software’s life. But Kinetic may have multiple environments: the main network, a testnet that launched later, a devnet that launched later still. Each of those networks has its own genesis point within the global beacon stream. Without genesis_kyn, every environment would have to pretend it started at beacon zero, which would make a testnet launched at beacon 1,000,000 appear to be almost 35 days old from the very first moment. By subtracting genesis_kyn, the clock always reads “how long since this network started” rather than “how long since the software was first compiled.”

-> See: kinetic-types/src/clock.rs — Lines 44–52 (the guard and the subtraction)


matrix(), lattice(), apex() — computed, not stored

-> See: kinetic-types/src/clock.rs — Lines 70–83

These three methods return the week, month, and year counts respectively. They are not fields in the struct. Instead, they are calculated on demand from self.prism:

#![allow(unused)]
fn main() {
matrix()  ->  self.prism / 7
lattice() ->  self.prism / 30
apex()    ->  self.prism / 365
}

The reason they are methods instead of stored fields is that storing them would be redundant. If you know prism, you can always derive the others with one integer division. Storing them alongside prism would mean four numbers that must all be kept in sync — and every place that creates or modifies a KineticTime would have to update all four correctly. One source of truth (prism) is simpler, safer, and uses less memory. This pattern — “store the minimum, derive the rest” — is a common design discipline in Rust and in systems programming generally.

These methods will mainly be used by analytics layers: “how many Apexes has this validator been active?” or “show me all events within Lattice 3.”


to_display_string() — human-readable output

-> See: kinetic-types/src/clock.rs — Lines 85–91

This method produces a String formatted as:

#![allow(unused)]
fn main() {
"Prism 1, Facet 17 (Kyn 800)"
}

It uses the format!() macro — Rust’s way of building strings with embedded values. See RUST_CONCEPTS.md — format!() for details.

This string is designed for UIs: block explorers, node dashboards, wallet history screens. It shows the three most human-relevant units (Prism = which day, Facet = which hour, Kyn = sub-hour precision) without overwhelming the user with total_kyns or matrix/lattice/apex counts. Those larger and smaller values are available on the struct and via methods if a UI wants them.


Key Pieces

NameTypeLocationWhat it does
KineticTimestructclock.rs:27–36Holds a decoded snapshot of Kinetic network time in Crystal Lexicon units
from_kyn()methodclock.rs:44–68Primary constructor — converts raw beacon number + genesis offset into a KineticTime
prismfield (u64)clock.rs:29Total Prisms (days) elapsed since genesis — the anchor field everything else derives from
facetfield (u64)clock.rs:31Facets (hours) within the current Prism, always 0–23
kynfield (u64)clock.rs:33Kyns (3-second beats) within the current Facet, always 0–1199
total_kynsfield (u64)clock.rs:35Raw elapsed Kyn count — the single authoritative number
matrix()methodclock.rs:71–73Returns completed weeks since genesis (prism / 7)
lattice()methodclock.rs:76–78Returns completed months since genesis (prism / 30)
apex()methodclock.rs:81–83Returns completed years since genesis (prism / 365)
to_display_string()methodclock.rs:86–91Formats time as branded, human-readable string for UIs

How This Connects to the Rest of Kinetic

KineticTime is purely a data type — it does not talk to the network, read files, or produce side effects. Its job is to be created, carried around, and read. That means it will appear as a field or return value in many other crates.

FORWARD DEPENDENCY: kinetic-kid — the KID node will produce the raw current_kyn value from its consensus engine and pass it to from_kyn() to build a KineticTime for inclusion in block headers and status responses.

FORWARD DEPENDENCY: kinetic-verify — the verifier will likely use KineticTime to timestamp proofs and check whether events fall within expected time windows.

FORWARD DEPENDENCY: Explorer and frontend tooling — to_display_string() exists specifically for this layer. Any dashboard that shows “current network time” will call this method and render its output directly.

The Serialize and Deserialize derives (from the serde crate) mean that KineticTime can be converted to and from JSON automatically. This is how it will travel over the network — a node serializes it to JSON, sends it over HTTP or WebSocket, and the receiving frontend deserializes it back into a KineticTime struct without any custom parsing code.


Quick Reference

1 Kyn     = 3 seconds       (network heartbeat)
1 Facet   = 1,200 Kyns      (1 hour)
1 Prism   = 28,800 Kyns     (1 day)
1 Matrix  = 7 Prisms        (1 week)   — computed via .matrix()
1 Lattice = 30 Prisms       (1 month)  — computed via .lattice()
1 Apex    = 365 Prisms      (1 year)   — computed via .apex()

Constructor: KineticTime::from_kyn(current_kyn, genesis_kyn)
Display:     my_time.to_display_string()  ->  "Prism N, Facet N (Kyn N)"

Fields stored:   prism, facet, kyn, total_kyns
Fields derived:  matrix(), lattice(), apex()

Guard: if current_kyn < genesis_kyn -> returns all zeros, never panics

Open Questions / Things to Revisit

Note

  • Leap seconds and clock drift: Kinetic time is defined purely by beacon count. If the real-world 3-second interval ever drifts (node clock skew, consensus delays), Kinetic time and wall-clock time will diverge silently. There is no correction mechanism in this module. Worth flagging for production monitoring.
  • Lattice is 30 Prisms, not a calendar month: Real months have 28–31 days. Kinetic Lattices are always exactly 30 Prisms. This simplifies math but means “Lattice 1” does not map cleanly to “February 2027.” Explorers should clarify this distinction in their UI copy.
  • apex() denominator is 365, not 365.25: Real years have leap years. Kinetic Apexes are exactly 365 Prisms. Over four years, Kinetic time will fall roughly one day behind the Gregorian calendar. Likely intentional (simplicity), but it should be documented in the network spec.
  • No locale support in to_display_string(): If frontends want to localize (e.g., “Day 2, Hour 5, Beat 800” in another language), they will need to build their own formatter on top of the raw struct fields.
  • #[allow(clippy::absurd_extreme_comparisons)] on line 43: This lint suppression is needed because Clippy misreads the current_kyn < genesis_kyn guard as impossible (both are u64). The guard is meaningful and correct — it prevents a silent underflow wrap. Keep an eye on this if the argument types ever change.

Stage 1 · kinetic-types · VDF Types

Source file: kinetic-types/src/vdf.rs — Lines 1–325 Reading time: ~20 minutes Depends on: docs/learn/types/01_error_types.md (for Severity)


What Is This?

vdf.rs defines every wire type involved in Kinetic’s two-phase name registration protocol. A “wire type” is a data structure that travels across the network — between a name owner’s client, a miner, and the Kinetic validator — and must be serialized, deserialized, and cryptographically verified consistently everywhere.

This file is the backbone of how Kinetic assigns ownership of .kin names. It contains the structures for both phases: the opaque commitment that establishes priority, and the full reveal that proves ownership with a post-quantum signature and a Verifiable Delay Function proof. Nothing about name registration happens without the types defined here.

The file also defines VdfVerifyError, the structured error type returned when post-quantum signature verification fails, and VdfJobRequest, the parameters sent to a miner when asking it to compute a VDF proof.


Why Kinetic Needs This

Important

Kinetic’s name system has one brutal requirement: no one can steal a name you are legitimately registering, and no one can fake the work required to register it.

Without the two-phase commit-reveal protocol defined here, two attacks are trivially possible:

Warning

Attack 1 — Front-running. You broadcast “I want to register alice.kin.” A malicious validator node reads your transaction out of the mempool and immediately submits its own registration for alice.kin with higher priority. You did the work; it gets the name. The Commitment struct stops this because you submit only a hash of your name first, not the name itself. The hash hides the name while still locking in your timestamp priority. Nobody can steal what they cannot see.

Attack 2 — Precomputation. Because VDF computation takes real wall-clock time, an adversary could try to precompute VDF proofs for all possible names ahead of time and then submit instantly when they see a commitment. The drand_kyn field (a randomness beacon tied to real network time) is included in the signed and proofed data so every VDF proof is bound to a specific moment in history. A precomputed proof from yesterday is invalid today.

Without VdfVerifyError and verify_signature(), the network could not reject tampered or forged proofs. Without PreviousProof, name renewals would have no cryptographic continuity — anyone could claim an expiring name by submitting a brand-new registration and pretending they are the incumbent owner.


How It Works

Background — Cryptographic Primitives

VDF (Verifiable Delay Function): Kinetic uses chiavdf to enforce a time delay on name registration. The iterations field controls the delay, and VdfProof wraps the output.

drand (Distributed Randomness): A public randomness beacon. Kinetic includes drand_kyn and drand_signature in proofs to tie the computation to a specific real-world moment, preventing precomputation attacks.

ML-DSA-65: A NIST-standardized post-quantum signature algorithm (FIPS 204). Kinetic uses it to ensure .kin name ownership remains secure against future quantum computers. It requires large public keys (~1,952 bytes) and signatures (~3,309 bytes).

Phase 1 — The Commitment

-> See: kinetic-types/src/vdf.rs — Lines 74–96

Phase 1 begins when a client wants to register a name. It picks a random 32-byte salt, computes SHA-256(name_bytes + salt_bytes), and wraps the result in a Commitment { hash: [u8; 32] }. It then puts the name and commitment into a CommitRequest and submits it to the Kinetic network.

The hash is exactly 32 bytes — always, no matter how long the name is. The network timestamps this commitment and records priority. At this moment, the name itself is invisible to everyone else on the network. Only the submitter knows the name and the salt.

The #[serde(deny_unknown_fields)] attribute on Commitment means that if any extra JSON or binary field arrives that is not hash, deserialization fails immediately. This prevents protocol confusion attacks where an attacker might sneak extra data into a commitment hoping a validator will interpret it differently.

-> See RUST_CONCEPTS.md entry: #[serde(deny_unknown_fields)]

Phase 2 — The Reveal

-> See: kinetic-types/src/vdf.rs — Lines 176–311

After the VDF computation finishes (which takes real time — that is the point), the client submits a Reveal. This is the largest structure in the file and contains everything the network needs to:

  1. Confirm the commitment hash matches the submitted name and salt
  2. Verify the drand randomness is real and from the right epoch
  3. Accept the VDF proof as valid work
  4. Verify the ML-DSA-65 signature over everything above
  5. Optionally verify a chained renewal from a previous registration

Every field in Reveal is part of the signed payload. This means that changing any single field — even the protocol_version byte — invalidates the signature.

The protocol_version field defaults to 1 if not present in the incoming data. This lets the network handle older clients that do not include this field explicitly, without breaking deserialization.

-> See RUST_CONCEPTS.md entry: #[serde(default = "...")]

How signable_bytes() Works

-> See: kinetic-types/src/vdf.rs — Lines 239–310

signable_bytes() is the method that both the signer (the name owner’s client) and the verifier (kinetic-verify) must call and get identical output. If they disagree by even one byte, signature verification fails.

The method builds a flat byte buffer in this exact order:

  1. Network prefix — the string "{network_id}-vdf-reveal-v1" as UTF-8 bytes. This domain-separates the signature so a reveal signed for the testnet cannot be replayed on mainnet.
  2. protocol_version — 1 byte.
  3. name — 4-byte big-endian length, then the UTF-8 name bytes.
  4. payload — 4-byte big-endian length, then the payload bytes.
  5. salt — exactly 32 bytes (no length prefix — it is always 32).
  6. drand_kyn — 8 bytes, big-endian.
  7. drand_signature — 4-byte length, then the hex string bytes.
  8. iterations — 8 bytes, big-endian.
  9. vdf_proof.proof_bytes — 4-byte length, then the proof bytes.
  10. pubkey — 4-byte length, then the ML-DSA-65 public key bytes.
  11. previous_proof option flag — 1 byte: 1 if present, 0 if absent. If present: 4-byte length + the serialized PreviousProof.proof_bytes().
  12. miner_pubkey option flag — 1 byte: 1 if present, 0 if absent. If present: 4-byte length + the miner’s public key bytes.

-> CROSS-CRATE: 03_identity.md — see “Concept 5” for a full breakdown of why we use length-prefixing to prevent boundary ambiguity.

The big-endian byte order is a convention: the network byte order standard (RFC 1700) uses big-endian. Kinetic follows it for all multi-byte integers in wire formats.

How verify_signature() Works Step by Step

-> See: kinetic-types/src/vdf.rs — Lines 208–236

This method is called by the validator when a Reveal arrives. Here is what happens internally, step by step:

Step 1 — Build the canonical bytes. self.signable_bytes(network_id) is called. The result is the exact same byte sequence the name owner’s client signed. If the Reveal fields have been tampered with in any way, this byte sequence will differ from what was originally signed.

Step 2 — Parse the public key. ml_dsa::VerifyingKey::<ml_dsa::MlDsa65>::new_from_slice(&self.pubkey) tries to parse the raw bytes in pubkey as a valid ML-DSA-65 public key. If the bytes are corrupt or wrong length, it returns an error — mapped with .map_err(|_| VdfVerifyError::MalformedPublicKey) to Kinetic’s error type.

-> See RUST_CONCEPTS.md entry: map_err()

Step 3 — Parse the signature. ml_dsa::Signature::<ml_dsa::MlDsa65>::try_from(&self.signature) parses the raw signature bytes. ML-DSA-65 signatures have a fixed structure; if these bytes do not conform, you get VdfVerifyError::MalformedSignature.

Step 4 — Verify the main signature. pubkey.verify(&signable, &sig) runs the ML-DSA-65 mathematical verification. This is the cryptographic core: it checks that the signature was produced by the private key corresponding to pubkey, over exactly the bytes in signable. Any mismatch gives VdfVerifyError::InvalidSignature.

Step 5 — Verify the previous proof’s signature (if present). If self.previous_proof is Some(prev), the method also verifies that prev.signature was produced by the same pubkey over prev.signable_bytes(). This is the continuity check: it proves the same identity that owns the name now also signed the previous registration. You cannot chain in someone else’s old proof.

Step 6 — Return Ok(()). If all checks pass, the method returns Ok(()) — Rust’s idiom for “success with no value.” The caller in kinetic-verify then proceeds to validate the VDF proof itself.

The ? operator is used internally on each fallible step. If any step fails, execution exits immediately and the error is returned to the caller without reaching the next step.

PreviousProof and Renewal Chains

-> See: kinetic-types/src/vdf.rs — Lines 98–173

When a .kin name expires and the owner wants to renew it, they do not just submit a fresh registration as if they were a new registrant. Instead, they include a PreviousProof inside their new Reveal.

PreviousProof contains the salt, drand values, iteration count, VDF proof, and signature from the previous registration of this name. By signing over the previous proof with the same private key used in the new registration, the owner creates a cryptographic chain: “I am the same entity who held this name before, and I am extending my ownership.”

The signable_bytes() method on PreviousProof produces the bytes that were originally signed when that old registration was submitted. The proof_bytes() method produces the same content plus the previous signature itself — this is what gets embedded inside the new Reveal.signable_bytes() so that everything is covered by the new signature.

The prefix "{network_id}-vdf-prev-v1" domain-separates previous proofs from reveal proofs, preventing any cross-type reuse of a signature.

VdfJobRequest — Kicking Off the VDF Computation

-> See: kinetic-types/src/vdf.rs — Lines 313–324

Once the commitment is submitted, the client (or a miner acting on its behalf) needs to compute the actual VDF proof. VdfJobRequest is the message that starts that job. It contains:

  • challenge_hash — the 32-byte commitment hash. The VDF is computed over this input.
  • name_length — the character length of the name being registered. Kinetic uses name length to determine how many iterations are required. Shorter names are more valuable, so they may require more iterations.
  • hashcash_nonce — a proof-of-work nonce, adding an additional spam barrier on top of the VDF time cost.
  • drand_kyn — the drand round number that this VDF computation is anchored to.

FORWARD DEPENDENCY: VdfJobRequest is consumed by the VDF miner service (not in kinetic-types). The miner reads this struct, runs the chiavdf engine, and returns a VdfProof that gets embedded in the Reveal.


Key Pieces

VdfVerifyError

File: kinetic-types/src/vdf.rs — Lines 13–68 What it does: A three-variant enum representing every way that ML-DSA-65 signature verification can fail on a Reveal. Uses thiserror::Error derive so it integrates automatically with the ? operator and the Display trait. Why it matters: Gives the validator precise, actionable signals. A MalformedPublicKey means the submitted key bytes are garbage — a Warning that might indicate a client bug. An InvalidSignature is an Error — someone is submitting a tampered reveal.

Each variant has a machine-readable code() (KIN-VDF-040 through KIN-VDF-042) and a severity() for log routing. is_retryable() is always false — none of these errors can resolve without the client fixing its data. error_type_uri() produces an RFC 7807-compliant URL for API responses.

-> See RUST_CONCEPTS.md entry: thiserror::Error -> See RUST_CONCEPTS.md entry: &'static str

Commitment

File: kinetic-types/src/vdf.rs — Lines 74–80 What it does: Wraps exactly 32 bytes — the SHA-256 hash of name + salt. Why it matters: This is the atomic unit of Phase 1. Its hash: [u8; 32] is a fixed-size array (not a Vec<u8>), meaning the compiler enforces the 32-byte constraint — you literally cannot construct a Commitment with 31 or 33 bytes. deny_unknown_fields keeps deserialization strict.

-> See RUST_CONCEPTS.md entry: [u8; 32] vs Vec<u8>

VdfProof

File: kinetic-types/src/vdf.rs — Lines 82–87 What it does: Wraps the raw output bytes from the chiavdf engine as a Vec<u8>. The proof size is variable depending on the VDF parameters. Why it matters: This struct is embedded in both PreviousProof and Reveal. It is what the validator passes to the chiavdf verifier to confirm that real sequential computation happened. The size is not known at compile time, so Vec<u8> (heap-allocated) is the right choice here.

CommitRequest

File: kinetic-types/src/vdf.rs — Lines 89–96 What it does: The full Phase 1 HTTP/network payload. Bundles the name being registered (as a plain String) with its Commitment hash. Why it matters: The name in CommitRequest is stored by the validator but not yet verified — the validator just records “this name was committed at this time.” The hash is what gets timestamped. Phase 2’s Reveal must match.

PreviousProof

File: kinetic-types/src/vdf.rs — Lines 98–173 What it does: Carries all fields from a prior name registration, including the signature over that prior registration. Why it matters: Makes name renewals cryptographically auditable. The chain of PreviousProof objects forms an unbroken ownership history from the first registration to the current one. Key methods:

  • proof_bytes(&self, network_id) — serializes all 6 fields including the signature. Used as embedded content inside a new Reveal.signable_bytes().
  • signable_bytes(&self, network_id) — serializes the same 5 fields excluding the signature. This is what the original ML-DSA-65 private key signed when the prior registration was submitted.

Reveal

File: kinetic-types/src/vdf.rs — Lines 176–311 What it does: The full Phase 2 payload. Contains all 12 fields needed for a complete name registration: the name, payload, salt, drand anchor, VDF proof, post-quantum signature, and optionally a renewal chain and miner key. Why it matters: This is the centerpiece of the entire VDF module. Every other type in this file either feeds into Reveal or exists to help verify it. Key methods:

  • verify_signature(&self, network_id) — the validation entrypoint. Returns Ok(()) on success or a VdfVerifyError on any failure.
  • signable_bytes(&self, network_id) — produces the exact byte sequence that was signed. Both client and validator call this independently; they must agree.

miner_pubkey — Miner Delegation

The miner_pubkey: Option<Vec<u8>> field on Reveal is how Kinetic accommodates the reality that VDF computation is resource-intensive. Instead of requiring every name owner to run a VDF engine locally for minutes, they can hand off a VdfJobRequest to a miner. The miner computes the proof and returns it. When the owner builds their Reveal, they include the miner’s public key so the network can route compensation to the miner. This is included in signable_bytes(), which means the owner explicitly endorses this specific miner by signing over their public key. The owner cannot be retroactively charged for a different miner’s work.

VdfJobRequest

File: kinetic-types/src/vdf.rs — Lines 313–324 What it does: Packages the parameters needed for a miner to begin VDF computation: the challenge hash, name length, hashcash nonce, and drand round. Why it matters: Decouples the name owner from the VDF computation. The owner can hand off this struct to a miner, go offline, and come back later to receive the completed VdfProof.


How This Connects to the Rest of Kinetic

VdfVerifyError uses Severity from the Kinetic error module. -> See: docs/learn/types/01_error_types.md for the full Severity enum.

The Reveal struct and its verify_signature() method are the primary inputs to the kinetic-verify crate. That crate receives a Reveal off the wire and calls verify_signature() first, before doing any VDF-specific validation.

FORWARD DEPENDENCY: kinetic-verify — takes a Reveal, calls verify_signature(), then passes vdf_proof.proof_bytes to the chiavdf library verifier. Explained in Stage N kinetic-verify docs.

FORWARD DEPENDENCY: kinetic-kid — the name owner client that constructs CommitRequest and Reveal, computes the SHA-256 commitment hash, picks the salt, and generates the ML-DSA-65 key pair. The client side of the protocol.

FORWARD DEPENDENCY: VDF Miner Service — receives VdfJobRequest, runs chiavdf computation, returns a VdfProof to be embedded in the Reveal.

CROSS-CRATE: ml_dsa crate — provides VerifyingKey<MlDsa65>, Signature<MlDsa65>, KeyInit, and the Verifier trait. Referenced directly in verify_signature(). This is the post-quantum cryptography engine.

CROSS-CRATE: serde crate — provides Serialize and Deserialize derives on every struct. Enables JSON and binary encoding for all wire types.

CROSS-CRATE: thiserror crate — provides the #[derive(thiserror::Error)] macro that makes VdfVerifyError implement std::error::Error and Display automatically without boilerplate.


Quick Reference

TypePhaseRole
Commitment132-byte SHA-256 hash of name+salt; establishes priority
CommitRequest1Full Phase 1 submission: name + commitment
VdfProof2Raw chiavdf output bytes proving sequential computation
PreviousProof2Chained prior registration for renewals
Reveal2Full Phase 2 payload; everything signed and verified
VdfJobRequestAsyncParameters sent to a miner to start VDF computation
VdfVerifyError2Structured errors from ML-DSA-65 verification
Error VariantCodeSeverityMeaning
MalformedPublicKeyKIN-VDF-040WarningPublic key bytes invalid
MalformedSignatureKIN-VDF-041WarningSignature bytes invalid
InvalidSignatureKIN-VDF-042ErrorCrypto check failed

signable_bytes() field order (Reveal): network prefix → protocol_version → name → payload → salt → drand_kyn → drand_signature → iterations → vdf_proof → pubkey → previous_proof flag → miner_pubkey flag

signable_bytes() field order (PreviousProof): network prefix → salt → drand_kyn → drand_signature → iterations → vdf_proof

Length-prefix rule: Every variable-length field is prefixed with its length as a 4-byte big-endian u32. Fixed-size fields (salt = 32 bytes, drand_kyn = 8 bytes) have no prefix.

Network domain separation:

  • Reveal: "{network_id}-vdf-reveal-v1"
  • PreviousProof: "{network_id}-vdf-prev-v1"

is_retryable(): Always false for all VdfVerifyError variants. A malformed or invalid submission cannot succeed by retrying — the client must fix its data.


Open Questions / Things to Revisit

Note

1. VDF iteration count determination. VdfJobRequest carries name_length so the miner can derive iterations, but the actual formula mapping name length to iteration count is not in this file. Where does that mapping live? What prevents a miner from using fewer iterations than required and submitting a too-easy proof?

2. Commitment hash re-verification in Phase 2. Reveal contains name and salt, which together should reproduce the original Commitment.hash. The verify_signature() method does not do this check — it only verifies the ML-DSA-65 signature. Somewhere in kinetic-verify, the SHA-256 reproduction check must happen. Confirm where.

3. Miner compensation flow. miner_pubkey is included in the signed reveal so the miner can be paid, but this file has no payment logic. How does the miner actually get compensated? Is there a separate transaction? Is it part of the block reward?

4. Salt storage between phases. The salt is a random 32-byte value picked in Phase 1 but only submitted in Phase 2. Between the two phases, the client must store the salt somewhere safe. If the salt is lost, the commitment cannot be revealed and the name registration is forfeit. This is a user-experience risk worth documenting in the kinetic-kid client docs.

5. payload field semantics. Reveal.payload is described as “arbitrary name metadata or DNS zone record payload bytes.” What is the maximum size? Is there a validator-enforced limit? What is the wire format inside payload for DNS records?

6. No expiry field. Reveal has no expires_at or duration field. Does the network derive name expiry from the drand timestamp, from the number of VDF iterations, or from some external configuration? Renewal via PreviousProof implies names do expire — but the mechanic is opaque from this file alone.

7. hashcash_nonce validation. VdfJobRequest contains a hashcash_nonce described as an “evaluated Hashcash proof-of-work nonce,” but the Hashcash difficulty target and validation logic are not in this file. What makes a valid nonce and who checks it?

Crate: kinetic-types

Stage: 1

Reading Time: 25 minutes

Depends On: 01_kyns.md, 02_errors.md (conceptually)

What Is This?

This file (governance.rs) defines the foundational data structures, precise binary parsing logic, and execution opcodes for governance actions on the Kinetic network. In any distributed system, the rules of the protocol handle 99.9% of interactions automatically. However, there is always a tiny fraction of network operations that require human oversight, privileged authority, or emergency intervention. These are governance actions.

In Kinetic, governance actions bypass the standard Proof-of-Work (PoW) name registration and execution state machines. They are highly privileged commands injected directly into the network stream by an authorized council. This file strictly defines what those commands are, what data they carry, and most importantly, how they are mathematically packed into bytes.

Important

A critical design choice for governance.rs is its absolute isolation. This file is “self-contained,” meaning it deliberately avoids importing large, complex dependencies from the rest of the Kinetic ecosystem. It does not import network sockets, peer discovery, database layers, or even the actual cryptographic verification logic. This is not an accident. It ensures that the governance logic can be compiled onto a tiny, physically air-gapped machine (a computer with its network card physically removed). Network administrators can use this air-gapped machine to safely construct, inspect, and mathematically sign governance proposals without ever exposing their supreme private keys to the internet or to a bloated software supply chain.

Why Kinetic Needs This

To understand why this module exists, you have to look at what would happen to the Kinetic network if it were entirely automated without any governance override. Without the data structures defined in this file, the network would face several existential threats:

  1. The Premium Name Problem: In Kinetic, usernames or application names are extremely valuable commodities. Single-character names (like a, x, 7) or widely recognized brand names (like btc, eth, pay) carry immense prestige and utility. If Kinetic allowed these to be registered via standard Proof-of-Work, massive industrial mining pools would immediately point their hash power at the network the second it launched, sniping every valuable name in existence. By reserving these as “Premium Names”, the network requires a GrantPremiumName action. This allows the network stewards to auction these names equitably, distribute them fairly, or reserve them for long-term ecosystem partners, rather than rewarding whoever has the biggest server farm on day one.

  2. Infrastructure Naming Hijacking: The network relies on hardcoded routing names to function. For example, when a new node joins the network, it might look for a name like seed or api to find its initial peers. These are Category 2 names. If a malicious actor successfully mined the name seed, they could redirect all new nodes joining the network to a malicious server, effectively partitioning the network and executing a massive eclipse attack. The GrantInfrastructureName action ensures that only the authorized governance council can point these critical infrastructure names to verified public keys.

  3. Disaster Recovery and Zero-Day Exploits: No software is perfect. If a critical bug or a zero-day exploit is discovered in the core Kinetic protocol—for instance, a flaw in the Verifiable Delay Function (VDF) math that allows an attacker to bypass time calculations—the network needs an emergency brake. Without an EmergencyHalt action, the attacker could exploit the bug indefinitely while developers scramble to write, test, and release a patch. The EmergencyHalt allows the council to instantly freeze all state transitions, preserving the integrity of the network while a fix is coordinated.

  4. Key Compromise and The Escape Hatch: Security is about assuming the worst. What happens if the governance multi-signature keys themselves are compromised by a highly sophisticated attacker? If there was no way to change the keys, the network would be permanently taken over. The RotateRootKey action provides a built-in escape hatch. If the current council realizes their keys are at risk, they can rapidly sign a rotation command to transfer ultimate authority to a fresh, uncompromised keypair, locking the attacker out.

  5. Deterministic Serialization: If the council decides to act, they need a way to broadcast their action that is absolutely immune to interpretation errors. If they used JSON like {"action":"halt"}, different programming languages might serialize the spacing differently, altering the final bytes and causing the digital signatures to fail validation. Kinetic needs this file to provide a deterministic, byte-level specification so that every computer on earth produces the exact same array of bytes for a given command.

  6. Decentralization vs Pragmatism: In crypto-economic design, purists often argue that there should be zero governance—that the code is the ultimate law. However, real-world experience over the last decade shows that immutable bugs destroy networks. Kinetic takes a pragmatic approach. It builds a governance framework that is mathematically transparent (everyone can see the action and verify the multi-sig on the blockchain) but retains the ability to save the system from existential threats. The transparency of canonical serialization guarantees that governance cannot act secretly; every node logs the event.

How It Works

The core of this module is built around transforming high-level Rust enums into a rigid, sequential byte array, and vice versa. This process is called “canonical serialization.”

The Multi-Signature Structure

Let’s examine how actions are packaged. -> See: kinetic-types/src/governance.rs — Lines 71-154

The SignedGovernanceMessage is the envelope that carries the governance action across the network. Notice that the signatures field is of type Vec<SignatureBytes>. The use of a Vec (Vector/List) instead of a single signature is a profound architectural statement. Kinetic operates on a multi-signature (multi-sig) security model. A single human making a single mistake should not be able to halt the network or give away premium names. The system might be configured to require 3 out of 5, or 5 out of 7 authorized signatures. This struct acts as a carrier, holding all the independent signatures together so that a receiving node can loop through and verify each one against the known governance threshold.

Transforming Commands to Canonical Bytes

When the air-gapped machine is ready to produce a command, it calls the to_canonical_bytes() method. This method does not use any third-party serialization libraries (like serde_json or bincode). It manually constructs the byte array to guarantee absolute determinism.

  1. The Opcode Prefix: The very first byte of the output array is the “opcode” (operation code). This is a single, hardcoded hexadecimal number that identifies the command. For example, if the action is RotateRootKey, the first byte is strictly 0x0B. This allows parsing logic to instantly know what data to expect next.

  2. Payload Serialization (Strings): If the command involves a string (like the name in GrantPremiumName), it uses “length-prefixing”. It first writes the length of the string as a 4-byte integer (u32), encoded in Big-Endian format. Then, it writes the actual UTF-8 bytes of the string. -> See RUST_CONCEPTS.md for Big-Endian Parsing.

  3. Payload Serialization (Public Keys): If the command includes a public key, the raw bytes of that key are appended sequentially. Because ML-DSA-65 keys are large but fixed in context, they are laid out exactly as they exist in memory without needing length prefixes.

Important

4. The Replay-Protection Timestamp: Finally, regardless of what the command is, the system always appends an 8-byte integer (u64) representing the current Unix timestamp in seconds (timestamp_sec). This is arguably the most important part of the serialization. It prevents “Replay Attacks.” If the council signs a command to grant the name test to Bob, and Bob later loses that name, Bob cannot simply take the old, validly signed byte array and broadcast it again to steal the name back. The network validators will look at the timestamp appended to the bytes, see that it is drastically out of date, and reject the transaction.

Parsing Canonical Payload from the Network Wire

When a node receives raw bytes from a peer, it needs to decode them safely using the parse_canonical_payload() function. -> See: kinetic-types/src/governance.rs — Lines 221-333

  1. Size Verification: The function immediately verifies that the byte slice is at least 9 bytes long. Why 9? Because the smallest possible command (like EmergencyHalt) consists of a 1-byte opcode and an 8-byte timestamp. Anything smaller is physically impossible to be a valid command.

  2. Extracting the Timestamp: Because the timestamp is always the last 8 bytes, the parser slices them off the end of the array, reverses the Big-Endian encoding using u64::from_be_bytes(), and stores the timestamp. The remaining bytes are now isolated as the “payload.”

  3. Opcode Switching (The Match Statement): The parser takes the first byte of the payload and uses a match statement (Rust’s powerful switch equivalent) to route the execution logic.

  4. Safe Extraction: If the opcode is 0x0A (GrantPremiumName), it reads the first 4 bytes to find the name length, safely slices that exact number of bytes to construct the string (verifying it is valid UTF-8 along the way), and treats the rest of the payload as the public key. If the bytes run out unexpectedly, or if the string is mangled, the parser safely aborts and returns a specific GovernanceTypeError instead of panicking and crashing the node.

Key Pieces

type Hash256 = [u8; 32]

-> See: kinetic-types/src/governance.rs — Line 24 -> See RUST_CONCEPTS.md for an explanation of Type Aliases. Why it matters: Naming [u8; 32] as Hash256 anchors the concept and makes function signatures instantly readable, distinguishing it from random data or partial keys.

type PublicKeyBytes = Vec<u8>

-> See: kinetic-types/src/governance.rs — Line 26 -> See RUST_CONCEPTS.md for an explanation of Type Aliases. Why it matters: ML-DSA-65 public keys are massive (typically 1952 bytes). PublicKeyBytes represents the raw binary data of these keys as they travel over the wire.

type SignatureBytes = Vec<u8>

-> See: kinetic-types/src/governance.rs — Line 28 -> See RUST_CONCEPTS.md for an explanation of Type Aliases. Why it matters: Just like the public keys, ML-DSA-65 signatures are exceptionally large (~3309 bytes). This alias clarifies the intent of the data.

enum GovernanceAction

-> See: kinetic-types/src/governance.rs — Lines 30-69 -> See RUST_CONCEPTS.md for an explanation of Enum variants with named fields. What it does: This is the master ledger of every permissible governance command, with each variant holding exactly the unique data it requires. Why it matters:

  • GrantPremiumName { name: String, target_pubkey: PublicKeyBytes } (Opcode 0x0A): Used to bypass the standard mining process and allocate a high-value 1-character name to a specific cryptographic identity.
  • RevokePremiumName { name: String } (Opcode 0x0E): Provides a mechanism to strip a premium name from a user. This is critical if a premium name is abandoned, transferred incorrectly, or if the governance council needs to reclaim it.
  • GrantInfrastructureName { name: String, target_pubkey: PublicKeyBytes } (Opcode 0x0F): Functions identically to premium names, but is semantically separated for Category 2 infrastructure names like api, seed, or telemetry.
  • RevokeInfrastructureName { name: String } (Opcode 0x10): Reclaims an infrastructure name, ensuring routing can be updated if an official node goes offline or is compromised.
  • RotateRootKey { new_key: PublicKeyBytes } (Opcode 0x0B): The ultimate security fallback. If the current council multi-sig is compromised, this command replaces the master key authorized to execute all other governance commands.
  • EmergencyHalt (Opcode 0x0C): Carries no payload. When verified, it instructs the state machine to instantly reject all new blocks and registrations. The network freezes in time.
  • EmergencyResume { paused_kyns: u64 } (Opcode 0x0D): Unfreezes the network. The paused_kyns field is mathematically crucial. Kinetic measures network time in “kyns” using a Verifiable Delay Function (VDF). If the network is halted for 48 hours, resuming it without accounting for that missing time would destroy the difficulty adjustment algorithm (the network would think time leaped forward inexplicably). The paused_kyns value tells the network state machine exactly how much “time” to subtract from its ledgers so that calculations remain stable.

struct SignedGovernanceMessage

-> See: kinetic-types/src/governance.rs — Lines 71-154 What it does: This is the primary transport object. It wraps a specific GovernanceAction alongside the timestamp_sec when it was issued, and the signatures array that proves it is legitimate. Why it matters: This struct implements the vital to_canonical_bytes() serialization function. It is the bridge between human-readable Rust structs and the raw, rigid binary needed for the peer-to-peer network.

thiserror and Severity

-> See: kinetic-types/src/governance.rs — Lines 156-219 What it does: The GovernanceTypeError utilizes the thiserror crate via a derive macro. This macro auto-generates the boilerplate required to implement Rust’s standard std::error::Error trait. Why it matters: Instead of manually writing Display implementations for every error variant, developers can simply annotate the enum variants with #[error("...")]. This is crucial for network diagnostics. When a node operators sees KIN-GOV-031 in their logs, they immediately know an UnknownOpcode was received, rather than a generic “Parsing Failed” message. Furthermore, the Severity categorization (Warning vs Error) allows the network layer to decide whether to simply drop the connection to the offending peer (for an Error) or just drop the packet (for a Warning).

enum GovernanceTypeError

-> See: kinetic-types/src/governance.rs — Lines 156-219 What it does: An exhaustive catalog of exactly how the parsing process can fail when attempting to read canonical bytes. Why it matters:

  • BufferTooSmall (KIN-GOV-030): Emitted if the byte array is shorter than 9 bytes. This is flagged as a Warning because it might simply be the result of a dropped TCP packet or a noisy network connection, rather than an active attack.
  • UnknownOpcode(u8) (KIN-GOV-031): Emitted if the first payload byte is not recognized (e.g., 0x99). This is flagged as an Error because it implies a peer is transmitting malicious junk data or is running an incompatible, highly divergent version of the software.
  • InvalidUtf8 (KIN-GOV-032): Emitted if the string parsing encounters bytes that violate the strict rules of UTF-8 text encoding. This is a Warning.
  • InvalidPubkeyLength (KIN-GOV-033): Emitted if the public key slice does not match expected length parameters. Also a Warning.

Crucially, every single one of these errors hardcodes is_retryable() to false. If a governance message is malformed, attempting to parse it again will not magically fix it. It is permanently invalid and should be immediately dropped.

How This Connects to the Rest of Kinetic

Because kinetic-types is a foundational crate at the bottom of the dependency hierarchy, this file does not import logic from higher-level crates. Instead, it provides the strict blueprints that those higher-level crates must follow.

  • FORWARD DEPENDENCY: kinetic-network: When a node is listening to peer gossip traffic, it will receive raw bytes. It uses the kinetic-network crate to route those bytes directly into this module’s parse_canonical_payload function. If the function succeeds, the networking layer knows it has a properly structured message.
  • FORWARD DEPENDENCY: kinetic-verify: This module does absolutely zero cryptographic verification. It only parses the bytes. Once a message is successfully parsed, it is handed off to the kinetic-verify crate. That crate will take the signatures array, load the ML-DSA-65 algorithms, and mathematically prove that the signatures match the root key.
  • FORWARD DEPENDENCY: kinetic-state: After the signatures are verified, the action is passed to the kinetic-state machine. If the action is EmergencyResume, the state machine extracts the paused_kyns integer and manually recalibrates the global network VDF clock before accepting new blocks.

Quick Reference

  • Parsing Logic: Controlled by deterministic byte manipulation to ensure identical cross-platform representation without heavy JSON/Protobuf libraries.
OpcodeActionDescription
0x0AGrantPremiumNameGrants high-value standard names
0x0BRotateRootKeyEmergency replacement of master security key
0x0CEmergencyHaltInstantly freezes the network state
0x0DEmergencyResumeUnfreezes the network, requires paused_kyns
0x0ERevokePremiumNameReclaims a premium name
0x0FGrantInfrastructureNameAllocates routing name like seed
0x10RevokeInfrastructureNameReclaims a routing name
  • Security Posture: Enforces a multi-signature model via Vec<SignatureBytes>.
  • Replay Protection: Strictly enforced by the 8-byte timestamp_sec appended to every single serialized payload.
  • Main Entrypoint: GovernanceAction::parse_canonical_payload(bytes: &[u8]).

Open Questions / Things to Revisit

Warning

  • Hardcoded Length Vulnerabilities in Parsing: The current implementation of parse_canonical_payload reads a 4-byte Big-Endian integer (u32) to determine the length of a name string. A u32 can represent a number up to 4.2 billion. If a malicious node sends a valid opcode but maliciously sets the length prefix to 4 billion, the node might attempt to allocate 4 gigabytes of memory for a string, resulting in an instant Out-Of-Memory (OOM) crash. This parsing function urgently needs a sanity-check limit (e.g., rejecting any name length over 255 bytes) before slicing the array.

  • Cryptographic Agility for Opcodes: Currently, the system implicitly expects public keys and signatures to conform to ML-DSA-65 lengths. If the National Institute of Standards and Technology (NIST) releases a newer standard, and Kinetic wishes to upgrade its cryptography, the parsing logic will break if the new keys are different lengths. We may need to introduce versioned opcodes (e.g., GrantPremiumNameV2) or start prefixing public keys with their byte lengths just like we do for strings.

  • Timestamp Drift Windows: The file includes a timestamp to prevent replay attacks, but it does not dictate how “old” an action can be before it is rejected. It simply parses the timestamp. The upstream consensus layers must rigorously define an expiration window (e.g., “reject governance actions older than 4 hours”) to prevent a leaked, validly signed command from being strategically deployed days or weeks later.

kinetic-types :: Name Records

Stage: 1
Reading Time: ~10-12 minutes
Depends on: 06_vdf.md (for understanding the Reveal struct wrapped inside NameRecord::Standard)


What Is This?

This file (kinetic-types/src/name_record.rs) defines the ultimate source of truth for name ownership on the Kinetic network. It describes exactly what a .kin name record looks like when it is flying across the network as a UDP packet. It also describes how it looks when it is sitting statically in the Distributed Hash Table (DHT). In Kinetic, a NameRecord is the fundamental data structure that answers the question “Who owns this name and what does it resolve to?” They are the fundamental building block that turns a chaotic network of peers into an organized, discoverable domain system.

It also defines Heartbeats, which are cryptographic pings. Owners must periodically broadcast these pings to prove they are still alive and actively maintaining their names. Without both of these structures, Kinetic would not function as a decentralized naming system.


Why Kinetic Needs This

Without a standardized NameRecord, the Kinetic network would have no way to enforce name ownership or route traffic. When a user tries to access mysite.kin, the network has to ask the DHT for the record associated with that name. If that record didn’t exist in a universally understood format, the network couldn’t know whether the name is available. It also wouldn’t know who has the right to update its IP address, or what its current routing payload is.

Furthermore, Kinetic is completely decentralized. There is no central database, and there is no “Kinetic Inc.” to manually clean up abandoned names. If someone registers a name and loses their private key, or dies, that name could be locked forever. Without these mechanisms, the system would rapidly degrade into a graveyard of lost, unreachable names that nobody could manage.

Important

The Heartbeat mechanism solves this massive problem: If a node doesn’t see a valid, freshly signed heartbeat for a name within a specific timeframe, the network considers the name abandoned.

Once abandoned, the network allows it to be reclaimed by someone else. The NameRecord and Heartbeat structures are therefore the absolute foundation of Kinetic’s decentralized domain name system (DNS) replacement.


How It Works

The architecture of name ownership is bifurcated into two parallel tracks that share a common interface.

The Two Classes of Names

#![allow(unused)]
fn main() {
// See: kinetic-types/src/name_record.rs — Lines 45 to 61
}

The NameRecord is an enum with two distinct variants:

  1. Standard: This is how 99.9% of names are registered on Kinetic. A user performs Proof of Work, computes a Verifiable Delay Function (VDF), and publishes a Reveal. The network verifies the cryptography. This variant wraps the Reveal struct (which we look at in vdf.rs).
  2. Premium: These are special, typically 1-character apex names (like a.kin or x.kin). They are excluded from the standard mining process to prevent domain squatters from grabbing the most valuable network assets instantly. Instead of being mined, they are granted by the Governance Root Authority key. The Premium variant holds the grant details, including a timestamp and a direct signature from the authority.

Because both variants are packed into the single NameRecord enum, the rest of the networking code doesn’t have to care whether a name is Standard or Premium. The NameRecord provides uniform methods like .pubkey() or .payload() that automatically pull the right data out of whichever variant is being used. This is a powerful use of Rust’s pattern matching.

Preventing Enum Bloat

#![allow(unused)]
fn main() {
// See: kinetic-types/src/name_record.rs — Line 47
}

Notice that the Standard variant holds Box<crate::vdf::Reveal>.

Note

See RUST_CONCEPTS.md for Box<T> and why it is used here to prevent enum memory bloat.

Borrowing Data Instead of Copying

#![allow(unused)]
fn main() {
// See: kinetic-types/src/name_record.rs — Lines 64 to 94
}

The NameRecord implements several methods that return data, like pubkey(), payload(), and signature(). Notice that these methods return &[u8] instead of Vec<u8>.

Note

See RUST_CONCEPTS.md for &[u8] (Borrowed Byte Slice) and how it achieves zero-cost, read-only data access without expensive memory copies.

Signature Verification

#![allow(unused)]
fn main() {
// See: kinetic-types/src/name_record.rs — Lines 97 to 124
}

A name record is completely useless if someone can forge it. The verify_signature method ensures the data hasn’t been tampered with by a man-in-the-middle. For a Standard name, this simply delegates to the Reveal struct’s internal verification logic. For a Premium name, it does something more manual: It takes the name bytes, the zone payload, and the network ID. It packs them together into a single byte array. It then verifies the ML-DSA-65 post-quantum signature against the public key stored in the record. If the signature doesn’t match the bytes exactly, the record is rejected as fraudulent.

Important

By including the network_id in the signature, it prevents a record from one Kinetic testnet from being replayed on the mainnet.

Name Normalization

#![allow(unused)]
fn main() {
// See: kinetic-types/src/name_record.rs — Lines 133 to 139
}

When a user types MYSITE.KIN. or mysite.kin, they fundamentally mean the same thing. The normalize_name function aggressively converts the string to lowercase. It also strips all trailing dots. If Kinetic didn’t do this, a malicious actor could register MYSITE.KIN as a separate record from mysite.kin. This would fracture the network, confuse users, and enable phishing attacks. Normalization guarantees a single canonical form before any hashing occurs. It ensures that everyone is talking about the exact same sequence of bytes.

Storage Keys vs Heartbeat Keys

#![allow(unused)]
fn main() {
// See: kinetic-types/src/name_record.rs — Lines 145 and 169
}

To store a name in a Distributed Hash Table (DHT), you need a key. This key is essentially an address in the network where the data lives. Kinetic stores every name at 32 different addresses to ensure extreme fault tolerance. If 20 nodes go offline, the name is still safely reachable on 12 others.

  1. derive_storage_keys generates 32 keys by hashing the normalized name, an index i (0 to 31), the network ID, and the string -dht-v1.
  2. derive_heartbeat_keys generates 32 different keys by hashing the network ID, the string -hb-v1, the normalized name, and the index i.

The input fields are intentionally fed into the SHA-256 hasher in a completely different order. This guarantees that a storage key and a heartbeat key for the exact same name will never collide. If they did collide, a network node might accidentally overwrite a NameRecord with a Heartbeat packet in its local database.

Warning

By strictly separating the hashing domains (using -dht-v1 and -hb-v1), Kinetic prevents database poisoning.


Key Pieces

NameRecord

#![allow(unused)]
fn main() {
// See: kinetic-types/src/name_record.rs — Line 45
}

The central enum that dictates ownership across the entire platform. It is marked #[serde(untagged)], which means when it is serialized to JSON or binary, it doesn’t include an artificial "type": "Standard" field. Instead, Serde attempts to parse incoming bytes as Standard first. If that fails, it tries to parse them as Premium. This keeps the network wire format lean, self-describing, and bandwidth-efficient.

Heartbeat

#![allow(unused)]
fn main() {
// See: kinetic-types/src/name_record.rs — Line 18
}

A struct that proves the owner of a name is still alive and cares about the name. The owner signs it with their ML-DSA-65 private key. Without these heartbeats flowing through the network, the DHT would eventually drop the NameRecord. This frees it for someone else to register, preventing dead names from cluttering the namespace forever.

signable_bytes() (Inside Heartbeat)

#![allow(unused)]
fn main() {
// See: kinetic-types/src/name_record.rs — Line 28
}

This method prepares the exact sequence of bytes that the owner must sign to produce a valid heartbeat. It carefully packs the network ID, the literal string -heartbeat-v1, the length of the name, the name itself, and the drand kyn number. This strict, deterministic layout guarantees that the signature is completely locked to this exact context. An attacker cannot take a signature meant for a heartbeat and try to reuse it to authorize a payload update, because the prefix -heartbeat-v1 will not match the other format.

latest_drand_kyn (Inside Heartbeat)

#![allow(unused)]
fn main() {
// See: kinetic-types/src/name_record.rs — Line 22
}

A specific field inside the Heartbeat that references a time pulse from the drand network. This ties the heartbeat to an exact moment in real-world time. If this wasn’t here, an attacker could record an old heartbeat from three years ago. They could then replay it forever, preventing the name from ever expiring even if the owner died. This provides absolute replay protection.

M_REDUNDANCY

#![allow(unused)]
fn main() {
// See: kinetic-types/src/name_record.rs — Line 129
}

A constant set to 32. It defines the replication factor across the DHT. When you register a name or send a heartbeat, you aren’t sending it to one central server. You are sending it to 32 independent nodes to ensure high availability and censorship resistance.

normalize_name

#![allow(unused)]
fn main() {
// See: kinetic-types/src/name_record.rs — Line 133
}

A critical utility function that squashes case differences and removes trailing dots. MyName.kin. becomes myname.kin. This is executed before any keys are derived. It ensures canonical uniqueness across the entire global state.

payload()

#![allow(unused)]
fn main() {
// See: kinetic-types/src/name_record.rs — Line 81
}

A helper method on NameRecord that returns the raw byte slice (&[u8]) of the zone data. This payload contains the actual DNS records. These are the IP addresses, IPFS hashes, or Tor onion addresses that the name should resolve to. Notice it returns a borrowed &[u8], providing a read-only window into the data without triggering expensive memory copies.


How This Connects to the Rest of Kinetic

  • FORWARD DEPENDENCY: kinetic-dht: The 32 keys generated by derive_storage_keys and derive_heartbeat_keys are directly consumed by the routing layer in the kinetic-dht crate. The DHT uses these 32 hashes to figure out exactly which physical IP addresses need to receive the NameRecord packet.
  • CROSS-CRATE: crate::vdf::Reveal: The Standard variant tightly couples with the VDF crate. A standard name is entirely defined by the cryptographic proof that someone burned CPU time to claim it. The NameRecord is essentially just a transport wrapper around that Reveal.
  • FORWARD DEPENDENCY: kinetic-node (Liveness tracking): The background workers in kinetic-node use the Heartbeat struct to update local timestamps in their state maps. If the local timestamp for a name falls too far behind the current time, the node will aggressively purge the NameRecord from its local memory.
  • FORWARD DEPENDENCY: kinetic-client: The client-side CLI tools construct the Heartbeat objects by filling out latest_drand_kyn and generating the ML-DSA-65 signature before transmitting them to the network.

Quick Reference

ConceptDescription
NameRecordThe core data structure representing absolute name ownership.
Standard VariantRegistered via competitive mining (PoW + VDF). Wraps a heap-allocated Box<Reveal>.
Premium Variant1-character apex names granted manually by governance. Contains direct ML-DSA-65 signatures.
HeartbeatA cryptographic ping proving the owner still controls the name and is online.
Replay ProtectionHeartbeats include a latest_drand_kyn to prevent attackers from maliciously reusing old pings.
M_REDUNDANCY32. The exact number of DHT nodes that store a redundant copy of the name.
Key SeparationStorage keys and heartbeat keys are derived using different hashing orders to strictly prevent database collisions.
NormalizationAll names are forced lowercase and stripped of trailing dots to maintain a single, unbroken canonical hash.
payload()Provides a zero-cost, read-only view (&[u8]) into the actual DNS zone routing instructions.
signable_bytes()Generates the exact deterministic layout that a name owner must sign to prove liveness.

Open Questions / Things to Revisit

  1. Governance Key Rotation: The Premium names rely on a “Governance Root Authority key”. If that key is ever compromised or needs rotation, how does the network update all existing Premium names? This might need an explicit revocation or update mechanism built into a future network upgrade.
  2. Payload Size Limits: The NameRecord carries the payload (DNS zone data). However, there is no explicitly defined maximum size limit enforced anywhere in this specific file. A malicious user could theoretically attach a 500MB payload and overwhelm the DHT if bounds checking isn’t strictly enforced at the network ingress layer.
  3. Heartbeat Frequency: How often must a Heartbeat be sent? This file defines what a heartbeat is, but the actual TTL (Time To Live) is handled elsewhere. We should ensure the TTL math perfectly aligns with the Drand network’s pulse frequency to avoid edge cases where valid names are dropped.
  4. Signature Verification Overhead: For Premium names, we are concatenating strings and byte arrays (signable.extend_from_slice(...)) every single time we verify a signature. This involves dynamic memory allocation (Vec::new()) on the hot path. We might want to pre-allocate this vector or use a streaming hashing interface that takes parts individually to improve node throughput.

08_dns_proxy_cdn.md

Crate: kinetic-types Stage: 1 Reading Time: 15 minutes Depends On: vdf.rs, name_record.rs


What Is This?

This document extensively covers three highly interconnected modules: dns.rs, proxy.rs, and cdn.rs. Together, they fundamentally define how the Kinetic network translates .kin human-readable names into actual underlying network primitives. They also define exactly how those translations are safely, reliably, and efficiently routed across isolated processes and caching layers. At a high, conceptual level, this document is examining the structural foundation for decentralized naming within the Kinetic ecosystem. It forms the bedrock of identity resolution. It also serves as the core infrastructure for content delivery. By establishing the exact memory layout for network records, it guarantees uniformity across the network. Furthermore, by providing zero-copy message structures, it ensures that the user-facing browser extension can talk to the background core node at blazing fast speeds.


Why Kinetic Needs This

If the Kinetic network only resolved .kin names to standard, traditional IP addresses, it would fail its core mission. It would not be a true peer-to-peer network. Instead, it would essentially just operate as an alternative version of ICANN, heavily reliant on central points of failure. The network absolutely requires a highly flexible way to link names directly to cryptographic identities. This is achieved using Key Identifiers (KID). It also requires the ability to link names to distributed decentralized storage. This is achieved using InterPlanetary File System (IPFS) content identifiers. Finally, it must link names directly to libp2p network nodes using PeerIds. All of this must happen without ever relying on centralized servers or authorities.

Furthermore, the architectural design of Kinetic places the user-facing browser extension and the underlying Kinetic network node in completely separate operating system processes. Because they are isolated for security reasons, they desperately need a blisteringly fast way to communicate with one another. Without specialized, zero-copy Inter-Process Communication (IPC) structures (like those carefully designed in proxy.rs and cdn.rs), the system would grind to a halt.

Warning

Every single network request would incur massive, unnecessary memory overhead. Constantly allocating new memory and deeply copying request strings across process boundaries is computationally expensive.

Similarly, deeply copying heavy response payloads (like large images or video buffers) across boundaries would cripple overall network performance. It would immediately spike CPU and memory usage, making the extension unusable for end users.


How It Works

DNS Resolution Pipeline

When a user attempts to resolve a decentralized domain, such as mysite.kin, a complex process begins. The Kinetic network initiates a traversal of the Distributed Hash Table (DHT). Its goal is to locate a NameRecord mathematically corresponding to that specific domain. Once the network successfully finds this record, it extracts the raw byte payload embedded within it. This raw payload is fundamentally a serialized DnsZone structure.

#![allow(unused)]
fn main() {
// See: kinetic-types/src/dns.rs — Lines 11 to 15
}

The DnsZone struct is designed to hold a standard HashMap. This HashMap actively maps subdomain labels (for example, “www”, “api”, or simply “@” for the root domain) to a dynamically sized list of DnsRecords. The network node looks up the user’s requested subdomain in this exact map. This lookup directly determines where the network traffic should actually be routed.

#![allow(unused)]
fn main() {
// See: kinetic-types/src/dns.rs — Lines 17 to 27
}

The DnsRecord enum is where the core decentralization magic actually happens in the codebase. It fully supports standard internet routing mechanisms. For example, it supports A and AAAA records for traditional IP addresses. It also supports CNAME for standard domain aliases. But more importantly, it introduces custom, Kinetic-specific variants meticulously tailored for Web3 interactions:

  • PeerId(String): This critical variant routes traffic directly to a specific libp2p peer. Instead of resolving to a fragile IP address that might change dynamically or sit invisibly behind a strict NAT firewall, it resolves to a permanent, persistent network identity. This directly enables true decentralized peer discovery without requiring a central matchmaking server.
  • KID(String): This points directly to a Key Identifier document on the network. This allows anyone traversing the network to rigorously and cryptographically verify the exact identity and public keys of the name owner.
  • IPFS(String): This variant links directly to an IPFS content identifier (often called a CID). By utilizing this, users can seamlessly host fully decentralized, static websites that are linked directly to a .kin name.

High-Performance IPC (Inter-Process Communication)

The browser extension cannot read the DHT directly. This is because it runs inside a highly restricted, isolated browser sandbox environment. Instead, to fetch data, it must send an HTTP-like request to the local background Kinetic node using IPC.

#![allow(unused)]
fn main() {
// See: kinetic-types/src/proxy.rs — Lines 15 to 22
}

The ProxyRequest struct mathematically bridges this process gap. It captures all the standard elements of an HTTP request: the method, the path, the headers, and the body. Crucially, the various string components inside this request exclusively use Arc<str>.

Note

Consider a scenario where 1,000 concurrent proxy worker threads are handling rapid requests. If they all share the exact same standard “User-Agent” header string, Arc<str> ensures a massive optimization. It guarantees the string is only actually allocated in system memory a single time.

The worker threads simply pass around ultra-lightweight reference counts instead of duplicating the exact same string 1,000 times on the heap.

#![allow(unused)]
fn main() {
// See: kinetic-types/src/proxy.rs — Lines 25 to 33
}

Similarly, the ProxyResponse structure encapsulates all returning data. To intelligently avoid copying massive web assets (like uncompressed images, heavy video files, or large WASM executable bundles) across operating system process boundaries, it employs a trick. It mandates the use of the bytes::Bytes type for the payload body. This functions practically as a zero-copy byte buffer. When the massive response body needs to be rapidly sent to multiple different parts of the system, it is cloned extremely cheaply. It does this simply by incrementing an atomic reference counter, outright avoiding the need to deeply copy every individual byte. Additionally, a custom serialization wrapper called serde_bytes_wrapper is utilized.

Important

This explicitly ensures these bytes are encoded efficiently as raw binary over the wire. If we relied on standard Rust serialization, it would encode the bytes as a highly inefficient sequence of JSON numbers, drastically increasing the network serialization overhead.

CDN Caching Layer

Fetching a NameRecord directly from the DHT every single time is far too slow for a seamless web browsing experience. Kinetic nodes actively mitigate this frustrating latency by caching records locally in memory. The CDN module elegantly provides the IPC structures needed for this direct, rapid cache access layer.

#![allow(unused)]
fn main() {
// See: kinetic-types/src/cdn.rs — Lines 14 to 21
}

The CdnRequest struct simply queries a domain name using a hyper-efficient, zero-copy Arc<str>. The CdnResponse responds dynamically with an Option<Vec<u8>>. The use of the Option type is absolutely crucial and intentional here. Returning None clearly and unambiguously signals a “cache miss” to the system. This immediately prompts the system to confidently fall back to a full, standard DHT lookup. Returning Some(bytes) explicitly means the cache was successfully hit. It instantly provides the fully serialized NameRecord without network delay.

Dynamic Host Routing

Sometimes, a peer’s physical network location or public IP address changes rapidly due to network topography. However, their logical, overarching host_id stays exactly the same.

#![allow(unused)]
fn main() {
// See: kinetic-types/src/dns.rs — Lines 30 to 45
}

The HostRoutingRecord struct exists explicitly to handle this dynamic mapping problem gracefully. Unlike a rigid, static DnsZone which maps subdomains to addresses in a fairly permanent, unmoving way, the HostRoutingRecord is fluid. It maps a constant, logical host_id directly to a current, potentially ephemeral libp2p current_peer_id. To guarantee security, this record is actively and securely signed by the owner. This cryptographic proof is securely stored in the signature field. It is also mathematically tied directly to the current verifiable drand round. This specific round number is stored securely in the drand_kyn field. This powerful cryptographic combination directly prevents dangerous replay attacks. It ensures that network routing stays fresh and accurate.

Important

A malicious attacker cannot successfully broadcast an old, outdated routing record. The cryptographic signature inextricably binds the record to a specific, unalterable point in time.


Key Pieces

  • DnsZone (dns.rs:11) The structural root of any .kin network payload. It acts as a primary collection logically mapping string subdomains directly to decentralized network records. This is precisely what you receive when you successfully decode the raw payload of a NameRecord.
  • DnsRecord (dns.rs:17) The absolute core routing primitive within the network. It bridges legacy internet protocols (like A and AAAA records) with Web3 specific protocols (like IPFS and PeerId).
  • HostRoutingRecord (dns.rs:30) A dynamic, cryptographically signed mapping system. It continuously tracks roaming peers across the global network, ensuring you can always successfully find a host even if their underlying ISP dynamically changes their IP address.
  • ProxyRequest (proxy.rs:15) A heavily optimized, zero-copy IPC payload. It allows the sandboxed browser extension to completely and accurately describe an HTTP request to the underlying node without ever allocating massive amounts of unnecessary memory.
  • ProxyResponse (proxy.rs:25) The direct structural counterpart to ProxyRequest. It acts as the vehicle for returning HTTP status codes, parsed headers, and efficient zero-copy byte buffers (bytes::Bytes) back to the client cleanly and swiftly.
  • CdnRequest (cdn.rs:14) A lightweight, ultra-efficient, zero-copy query payload. It is specifically used to ask the local node’s caching layer if it has a specific domain name securely recorded in memory.
  • CdnResponse (cdn.rs:18) An elegant wrapper type explicitly leveraging Rust’s powerful Option enum. It allows the system to gracefully handle unexpected cache misses without relying on expensive exceptions or throwing heavy system errors.

How This Connects to the Rest of Kinetic

  • CROSS-CRATE: NameRecord This is fundamentally defined and thoroughly explained in the dedicated documentation for name_record.rs. The physical byte payload of a NameRecord is strictly stored within the network DHT, but it is seamlessly and logically decoded into a readable DnsZone exclusively by this module.
  • FORWARD DEPENDENCY: kinetic-kid The specific KID(String) variant located inside the DnsRecord enum actively relies heavily on the kinetic-kid crate. It needs this forward dependency to actually resolve Key Identifier documents efficiently across the network.
  • FORWARD DEPENDENCY: kinetic-verify The signature field meticulously found within the HostRoutingRecord is rigorously and securely validated by specific cryptographic functions. These highly complex functions will be extensively explained in the upcoming kinetic-verify crate documentation.
  • FORWARD DEPENDENCY: kinetic-drand The critical drand_kyn field explicitly ties active routing records to verifiable randomness beacons. This connection is fundamental for explicitly ensuring strict, mathematically sound time-bound validity for all network paths.

Quick Reference

ConceptDescription
What is a DnsZone?A critical map of subdomains to records, physically living inside a NameRecord payload.
What is a PeerId record?A direct, unbroken pointer to a libp2p peer, enabling fully decentralized networking without centralized DNS servers.
What is a KID record?A secure cryptographic identity link ensuring you know exactly who you are talking to at all times.
What is an IPFS record?A permanent link to decentralized file storage for hosting robust, un-censorable static web content.
Why Arc<str> in IPC?It significantly prevents dangerous and redundant heap memory allocations when identical strings span across multiple concurrent system requests.
Why bytes::Bytes?It allows an entire, massive response body to be cloned and shared across isolated threads simply by bumping a tiny reference count. This completely avoids expensive deep copies of large files.
Why Option<Vec<u8>> for CDN response?To explicitly, safely, and unambiguously model both successful cache hits (Some) and failed cache misses (None) at the strict compiler level.
What is HostRoutingRecord?A dynamic, mathematically time-bound network registry meticulously tracking the current libp2p address of a specific host on the network.

Open Questions / Things to Revisit

  • The HostRoutingRecord signature method actively requires a mysterious network_id for proper signing. How exactly is this network_id securely distributed and flawlessly agreed upon by all clients without accidentally introducing a highly vulnerable centralization vector?
  • HostRoutingRecord inherently and permanently ties its entire validity to the drand_kyn field. If the local system clock drifts severely, or the external drand synchronization gets heavily delayed, could perfectly valid routing updates be erroneously rejected by the network node?
  • Both ProxyRequest and ProxyResponse serialize active HTTP headers as a completely flat Vec<(Arc<str>, Arc<str>)>. Searching for a specific HTTP header therefore always requires a potentially slow linear scan (O(N)). If application header lists get particularly large, this architectural choice might become a noticeably painful performance bottleneck compared to using a traditional HashMap.
  • The #[serde(other)] attribute currently silently and implicitly absorbs unrecognized DNS record types directly into the generic Other variant. Because it carelessly throws away the actual incoming type value, we permanently lose the critical ability to log or safely inspect what the unrecognized type string actually was. Should this implementation be modified immediately to actively capture unknown types for much better network telemetry?

01 — Overview

Crate: kinetic-kid Stage: 3 of 10 Reading time: ~5 minutes Depends on: kinetic-types, kinetic-verify


What Is This?

kinetic-kid handles Kinetic Identity Documents (KIDs). It provides the self-sovereign identity layer for the network. In the Kinetic ecosystem, a KID acts as a decentralized identifier (DID) anchored to the .kin network. This crate defines exactly how these identities are created, signed, cryptographically verified, and extended using capability manifests.


Why Kinetic Needs This

Important

In a decentralized network, there is no central database of users. Without this crate, there would be no way to securely prove who owns a specific .kin name or what their keys are authorized to do. By using ML-DSA-65 post-quantum signatures and RFC 8785 JSON Canonicalization, kinetic-kid guarantees that identities are cryptographically tamper-proof across the network.


How It Works

The crate exposes the core types required for identity management. The main entry point is lib.rs (208 lines), which publicly exports the modules.

  • It heavily leverages the JCS (JSON Canonicalization Scheme) to ensure that the byte representation of an identity document is exactly identical across all platforms (Windows, Linux, WASM in browser) before it gets signed.
  • It provides a robust, bounded parsing mechanism to defend against malicious peers sending massive payloads.

Key Pieces (Topic File Breakdown)

This crate is composed of ~981 total lines of code, broken down into the following topics:

  1. 02_document.md — Covers KidDocument and ControllerKey. This is the root identity document structure and signature verification logic.
  2. 03_manifest.md — Covers CapabilityManifest and ServiceEntry. Explains how identities advertise web endpoints securely.
  3. 04_error.md — Covers KidError. The robust 16-variant error taxonomy for the identity layer.
  4. 05_did.md — Covers KineticDid. The strict parsing logic for did:kin:<hash> identifiers.
  5. 06_bounded.md — Covers OOM defense mechanisms (BoundedVecVisitor) to stop network memory-bomb attacks.

How This Connects to the Rest of Kinetic

  • CROSS-CRATE: Relies heavily on the ML-DSA-65 algorithms and structures from kinetic-types and kinetic-verify.
  • FORWARD DEPENDENCY: The DHT/P2P network (kinetic-network) and the database layer (kinetic-storage) will ingest these structs. Specifically, they rely on KidDocument::is_authorized_update to manage key rotations securely across the network.

Quick Reference

  • Total Lines: ~981
  • Signature Algorithm: ML-DSA-65 (Post-Quantum)
  • Canonicalization: JCS (RFC 8785)
  • Bounds: Max 20 controller keys, max 50 services.

Open Questions / Things to Revisit

Warning

The tests module inside lib.rs contains hardcoded key generation for MlDsa65. In a production environment, test fixtures should ideally be isolated to prevent testing dependencies from bleeding into the compiled WASM binary.

Crate: kinetic-kid

Stage: 02

Reading Time: 25-30 minutes

Depends On: 01_did.md (Concept of KineticDid)


What Is This?

The KidDocument is the core cryptographic root of identity in the Kinetic network. It is a highly structured data format, heavily inspired by the W3C Decentralized Identifier (DID) specification. It mathematically binds a did:kin:<hash> identifier to a specific, rotating set of public keys. Defined entirely within document.rs, it acts as the self-certifying anchor for all user interactions. It proves, without relying on any centralized server, that “I am the owner of this identity.” It also proves “these are my current keys, and here is how you can verify my signatures.” Unlike traditional X.509 certificates used on the web, a KidDocument does not require a Certificate Authority to issue or validate it. Every single node in the Kinetic network can independently and deterministically verify the authenticity of this document. This forms the foundational layer for all peer-to-peer trust, messaging, and access control in the Kinetic ecosystem. It allows individuals, devices, and automated agents to prove who they are with absolute mathematical certainty. Without this core structure, the network would have to fall back on vulnerable centralized registries. Instead, identity is sovereign, portable, and entirely controlled by the user.


Why Kinetic Needs This

In a truly decentralized, peer-to-peer network like Kinetic, there is absolutely no central database. There is no root certificate authority, and no centralized user registry to look up who a user is. There is no centralized service to look up what their current public key is. When Alice wants to send a secure message to Bob, she needs his exact, current keys. When Alice wants to authorize a sensitive transaction on the network, the network nodes need her keys to verify it. Peers need a rigorous way to verify identity independently and deterministically without trusting a middleman or corporate server. Kinetic solves this fundamental problem by making the identity document explicitly self-certifying. The KidDocument contains the ML-DSA-65 post-quantum public keys required to verify cryptographic signatures. Because the DID identifier itself is derived directly from a one-way cryptographic hash of the document’s genesis key, anyone can verify it. Anyone can fetch the document from the network. Anyone can hash the first key themselves. Anyone can check that it perfectly matches the DID URI embedded in the document. This mathematically proves that the document is authentic and originally created by the owner of that key. Furthermore, digital identities are not static entities; they must evolve. Users lose mobile devices, and hardware security tokens get compromised or stolen. Network capabilities, service endpoints, and routing requirements change constantly over time. The document provides a standardized, machine-readable format to dynamically list authorized keys. It provides a standardized way to point to off-chain capability manifests for heavier profile data. It provides an extremely robust mechanism to facilitate secure key rotation and recovery. All of this is done without ever changing the underlying, permanent did:kin identifier. This allows a user’s identity to persist securely through compromises, hardware changes, and protocol upgrades.

Important

It fundamentally ensures that the kinetic network maintains incredibly high security standards against future post-quantum computing threats. This guarantees that Kinetic identities will remain mathematically secure for decades.


How It Works

The KidDocument lifecycle involves several distinct operational phases. Each phase heavily relies on strict cryptographic checks and bounds limits to maintain trust in a trustless environment. Let’s break down each crucial step of how the document functions under the hood.


1. Document Structure and Bounds

#![allow(unused)]
fn main() {
Option<T>
crate::bounded::deserialize_max_20
}

The document is a JSON-serializable struct containing metadata, authorized keys, and signatures. -> See: kinetic-kid/src/document.rs — Lines 38 to 63

-> See RUST_CONCEPTS.md for Option<T> (used here for optional fields like signature and ManifestPointer).

Warning

To prevent Denial of Service (DoS) memory exhaustion attacks, we use custom bounded deserialization (crate::bounded::deserialize_max_20). This ensures vectors never exceed 20 items at the parsing stage, rejecting inflated payloads before they reach expensive cryptographic logic.


2. Canonicalization (Determinism)

#![allow(unused)]
fn main() {
self.canonicalize()
serde_jcs
}

Before we can cryptographically sign the document, we must have a perfectly predictable representation of its bytes. If a JSON serializer adds a single extra space somewhere in the payload, the entire hash changes. If it uses different indentation or line endings, the hash changes completely. If it reorders the dictionary keys arbitrarily, the resulting hash will be radically different. Any of these tiny variations will cause the cryptographic signature verification to completely fail. -> See: kinetic-kid/src/document.rs — Lines 65 to 77 The canonicalize() method takes an immutable reference to the document to prepare it for signing or verification. First, it clones the struct to create a temporary, mutable copy in local memory. Next, it explicitly strips out the signature field by aggressively setting it to None. This is logically required because you mathematically cannot include a signature in the very payload that is currently being signed. Then, it uses serde_jcs (JSON Canonicalization Scheme, RFC 8785) to turn the struct into a highly deterministic JSON string. This strict canonicalization scheme guarantees totally predictable byte output regardless of the system environment. Whether the document was originally generated in Rust, a browser running TypeScript, or a backend Go service, the byte representation will match perfectly. The final bytes fed into the ML-DSA-65 signing algorithm will be exactly identical bit-for-bit across all operating systems and platforms. This prevents incredibly subtle cross-language compatibility bugs from accidentally breaking network consensus. This exact methodology is what allows disparate, heterogeneous clients to all agree on the validity of a single identity.


3. Verification (Stateless check)

#![allow(unused)]
fn main() {
verify()
.ok_or(KidError::MissingSignature)?
kinetic-kid-v1\0
.is_ok()
Ok(())
}

When a Kinetic node receives a new KidDocument over the wire, the absolute first thing it does is a stateless verification. This is the front-line, high-speed defense against invalid or intentionally malicious data. -> See: kinetic-kid/src/document.rs — Lines 79 to 170 The verify() method is termed purely “stateless” because it absolutely does not look at any network history. It does not check any ledger, DHT state, or blockchain state to figure out if the document is allowed. It only analyzes the exact bytes of the document currently loaded in memory in front of it. It performs a rigorous sequence of checks to ensure the document is internally self-consistent.

Warning

First, it performs extensive bounded fields validation on all internal collections. It explicitly checks that there aren’t too many controller keys. It checks that there aren’t too many revocation keys or manifest URLs. It carefully checks that string lengths do not exceed strict memory limits (e.g., public keys must be smaller than LIMITS_KID_MAX_PUBLIC_KEY_BYTES).

Next, it performs robust signature extraction and decoding. It safely extracts the base64url-encoded signature using the idiomatic Rust pattern .ok_or(KidError::MissingSignature)?. It then decodes the base64url string into raw binary bytes. It parses those raw bytes into a strongly typed ML-DSA-65 signature object native to the ml-dsa crate. If the bytes are malformed, it throws an InvalidSignature error immediately. Next, it reconstructs the original signed payload. It calls self.canonicalize() to get the deterministic JSON string representation. It deliberately prepends a kinetic-kid-v1\0 prefix to the JSON bytes before verification. This domain-separation prefix is an absolutely critical protocol security feature. It aggressively prevents cross-protocol signature reuse attacks. It totally stops an attacker from tricking a user into signing a DID document when the user genuinely thought they were signing a private chat message. Finally, it runs the core cryptographic verification loop. It iterates sequentially through the controller_keys list. If the document is marked as deactivated, it instead iterates sequentially through the revocation_keys list. The closure leverages .is_ok() to gracefully and silently ignore any keys that fail verification without crashing. It successfully returns Ok(()) the very moment any valid signature match is found in the authorized list. Notice carefully that this stateless method does not check if the did:kin string matches the public keys. This is an extremely intentional design decision that very often confuses new developers reading the code. Because keys can and must be rotated over the lifetime of a long-lived identity, the keys will drift and change. A perfectly valid, completely current document in year 5 might not contain the original genesis key from year 1 anymore. This strict separation of stateless structure checks and stateful logic is core to Kinetic’s architecture.


4. Genesis Binding (The Anchor)

#![allow(unused)]
fn main() {
verify_genesis()
did:kin:<hex(SHA256(primary_key_bytes))>
}

If verify() intentionally doesn’t check the DID string, how do we know a DID belongs to this document in the first place? How do we mathematically stop Alice from simply claiming Bob’s DID by uploading a document? This is exactly where the genesis check acts as the foundational root anchor for the entire network. -> See: kinetic-kid/src/document.rs — Lines 172 to 211 The verify_genesis() method is the absolute anchor of the whole decentralized identity system. It is strictly called only once when the document is first published to the network. It extracts the very first key in controller_keys, which is permanently designated as the primary genesis key. It hashes its raw public bytes using the industry-standard SHA-256 cryptographic algorithm. It systematically formats the resulting 32-byte digest as a lowercase hex string. Finally, it rigorously verifies that the resulting string exactly matches the suffix of the did:kin identifier in the document.

Important

This creates an unbreakable, mathematical binding at the very moment of creation. You cannot claim or squat on a DID unless you demonstrably possess the private key that generates the exact public key that hashes to that exact DID. Because SHA-256 is universally collision-resistant, it is practically impossible to find two different keys that hash to the exact same DID.

This brilliantly ensures a globally unique identity namespace without relying on any central registrar or DNS server. Once the genesis check passes, the DID is irrevocably bound to that user’s cryptosystem forever.


5. Authorized Updates (The Chain of Trust)

#![allow(unused)]
fn main() {
is_authorized_update(&self, previous_doc)
.iter().any(...)
}

When Alice wants to selectively rotate her keys because she bought a new laptop, she publishes a new KidDocument. This totally new document contains the newly generated key in the controller_keys list. How does the peer network know this isn’t an active attacker trying to hijack her DID by publishing a fake update? -> See: kinetic-kid/src/document.rs — Lines 213 to 261 The is_authorized_update(&self, previous_doc) method checks the new document’s signature against the authorized keys stored in the old, proven document (previous_doc).

-> See RUST_CONCEPTS.md for Closures & Iterators (used here via .iter().any(...) to efficiently verify the signature).

This guarantees that the new document was signed by an entity already recognized as an authorized controller in the previous state, maintaining an unbroken chain of trust back to the genesis key.


Key Pieces

This section deeply breaks down the core data structures and methods that define the identity subsystem. These are the foundational building blocks you will interact with most frequently when working on Kinetic identity features.


KidDocument (Struct)

#![allow(unused)]
fn main() {
KidDocument
}

-> See: kinetic-kid/src/document.rs — Line 38 This is the primary main identity container that wholly defines a Kinetic user. It safely holds the immutable DID string identifier for the user. It conservatively holds the Unix creation timestamp to formally establish the document’s age. It contains the strictly ordered lists of controller and revocation keys. It cleanly holds the Base64url signature that definitively authenticates the entire package. It is the absolutely fundamental data model actively passed around the Kinetic peer-to-peer network to establish digital identity. The vital deactivated boolean flag is also located here. It explicitly marks an identity as permanently burnt or compromised, permanently disabling standard signing operations forever.


ControllerKey (Struct)

#![allow(unused)]
fn main() {
ControllerKey
"ML-DSA-65"
}

-> See: kinetic-kid/src/document.rs — Line 9 This crucial struct tightly represents a specific public key authorized to act on behalf of the DID. It explicitly includes an id field, which is a fragment URI like #key-1 heavily used for relative referencing within the document itself. It includes the key_type field, which currently must absolutely always be set to the exact string "ML-DSA-65". It securely includes the raw Base64url-encoded public key bytes that will actively be used for mathematical verification.


ManifestPointer (Struct)

#![allow(unused)]
fn main() {
ManifestPointer
CapabilityManifest
}

-> See: kinetic-kid/src/document.rs — Line 21 Identities in the Kinetic network aren’t just used for basic signing; they possess vast network capabilities. They have complex profile data, avatars, and custom network routing endpoints. To fiercely keep the base KidDocument extremely small and cheap to verify across the network, these heavy operational details are removed. They are completely pushed out to a separate, much larger CapabilityManifest document. This struct safely provides a secure cryptographic hash of that external manifest payload. It also provides a reliable array of network URLs (like HTTPS or IPFS) to securely locate and download that off-chain manifest data. By deliberately separating the lightweight identity anchor from the heavy capability manifest, we ensure the core identity layer remains incredibly fast and cheap.


verify() (Method)

#![allow(unused)]
fn main() {
verify()
self.deactivated
revocation_keys
}

-> See: kinetic-kid/src/document.rs — Line 87 This critically important method performs the rigorous internal consistency check of the document structure and signature.

Note

Crucial note for Saif: Pay particularly close attention to exactly how this method checks the self.deactivated flag. If the document is officially marked as revoked, standard controller keys are immediately stripped of their signing authority. From that exact point forward, only dedicated, high-security revocation_keys can authorize any further operations or final updates. This intelligently ensures that if your daily-driver device is totally compromised, the attacker still cannot maliciously un-revoke the identity.


verify_genesis() (Method)

#![allow(unused)]
fn main() {
verify_genesis()
}

-> See: kinetic-kid/src/document.rs — Line 188 This fiercely enforces the mandatory, one-time initial binding of the DID string to the precise SHA-256 hash of the primary key. This mathematically prevents “squatting” on DIDs by early adopters or attackers. It absolutely guarantees that you cannot claim an identity string that rightfully belongs to another user on the network.


is_authorized_update() (Method)

#![allow(unused)]
fn main() {
is_authorized_update()
}

-> See: kinetic-kid/src/document.rs — Line 222 This is the absolute most structurally crucial method for all complex state transition logic in Kinetic. It ensures that any and all changes to an identity’s keys or manifest are cryptographically authorized by the immediately previous state of that identity. It forms an unbroken, verifiable chain of trust spanning all the way back to the original genesis key.


sign() (Method)

#![allow(unused)]
fn main() {
sign()
SigningKey
}

-> See: kinetic-kid/src/document.rs — Line 268 This is a highly developer-friendly utility designed to seamlessly sign the document during initial creation or later update. It fully automates the complex JCS canonicalization of the struct. It rapidly signs the resulting bytes using the provided ML-DSA-65 SigningKey. It then neatly injects the resulting encoded Base64url string back into the struct’s signature field. It conveniently returns the fully signed, ready-to-publish document directly to the caller.


How This Connects to the Rest of Kinetic

This file absolutely does not exist in a vacuum; it has profound and critical dependencies across the broader Kinetic codebase. CROSS-CRATE DEPENDENCIES:

  1. kinetic-did: The vital kid field internally uses the robust KineticDid struct from the kinetic-did crate. The document.rs module safely assumes that the provided string has already been strictly validated as a structurally correct did:kin URI at parsing time. This avoids performing redundant string regex validations during heavy cryptographic operations.
  2. ml-dsa: This entire crate heavily relies on the post-quantum ml-dsa crate for absolutely all cryptographic operations. It specifically utilizes standard cryptographic traits like Signer and Verifier from the broader Rust crypto ecosystem. These specific traits define the generic, highly standardized interface for digital signatures in Rust. This smart abstraction fundamentally allows us to seamlessly swap algorithms in the future if a critical vulnerability in ML-DSA is ever discovered.
  3. Storage Layer (Forward Dependency): The stateful is_authorized_update method strongly implies a complex future storage layer is being built somewhere else. This could eventually be a Distributed Hash Table (DHT), a global verified blockchain, or a local verified database. That external storage layer is entirely responsible for securely fetching the proven canonical previous_doc and passing it directly to this method. The KidDocument itself is purely functional and purely stateless; it absolutely does not possess the network context to know how to fetch previous states from remote peers.

Quick Reference

Here is a highly scannable, rapid summary of the strict rules enforced by KidDocument:

  • Algorithms Supported: Exclusively ML-DSA-65 (NIST Post-Quantum standard).
  • Max Keys Limits: Hardcoded protocol safety limit of exactly 20 controller keys.
  • Max Revocation Limits: Hardcoded protocol safety limit of exactly 20 revocation keys.
  • Max Location Limits: Hardcoded protocol safety limit of exactly 20 manifest locations.
  • Prefix Isolation: Signed data is always forcibly prepended with kinetic-kid-v1\0 to absolutely prevent replay attacks across entirely different cryptographic subsystems.
  • Genesis Binding Formula: did:kin:<hex(SHA256(primary_key_bytes))>
  • Serialization Standard: RFC 8785 JSON Canonicalization Scheme (JCS).
  • Encoding: All binary keys and raw signatures are strictly Base64url encoded with NO padding characters whatsoever.

Open Questions / Things to Revisit

The current KidDocument implementation is incredibly robust, but there are several rough edges and deep architectural questions that critically need to be revisited as the network radically scales.

  • Algorithm Agility Implementation: The current code heavily hardcodes "ML-DSA-65" string checks widely throughout the entire module. If we ever urgently need to migrate to a totally new post-quantum standard, we will need to introduce massive algorithm agility here. We will have to rigorously handle versioning extremely carefully to thoroughly prevent catastrophic downgrade attacks where an attacker forces a weak algorithm. This will likely require a v2 document schema to cleanly implement without breaking existing nodes.
  • Key Rotation Race Conditions: If a user rapidly publishes two perfectly valid, conflicting updates to their document simultaneously, they might accidentally branch the entire chain of trust. The external storage and consensus layers will desperately need strict monotonic ordering or robust conflict resolution rules to precisely determine which specific update is mathematically canonical. Without this ordering, the network could split geographically on which key is the true controller.

Warning

  • Manifest Fetching Guarantees: The document securely points to a manifest via URLs, but absolutely does not actively fetch it itself. We critically need to strongly ensure that the external network layer that actually fetches the manifest strictly verifies the downloaded payload against the exact hash provided in ManifestPointer. If the network layer accidentally forgets to check this hash, the entire critical separation of identity and capabilities is fatally compromised, opening the door to devastating spoofing attacks.
  • Revocation Semantics: What exactly mathematically happens to historical signatures and capabilities permanently after a document is formally deactivated? The higher application layer needs exceptionally clear rules on whether past historical signatures remain definitively valid if a key is much later revoked. We must urgently decide if revocation retroactively invalidates all past data, or only prevents future new signatures from being formed. There is currently no explicit timestamp signaling when a revocation formally occurred.
  • Key Expiry: Currently, the defined controller keys have absolutely no expiration date natively embedded in them. We may need to rigorously evaluate whether controller keys should optionally support intrinsic valid_until Unix timestamps. This feature would automatically and systematically force regular key rotation best practices for all users across the entire network, significantly reducing the blast radius of old leaked keys.
  • State Proofs: As the continuous chain of document updates grows significantly over years, verifying a single identity from genesis to present could become extremely computationally expensive. We may actively need to aggressively investigate SNARKs, STARKs, or other zero-knowledge state proofs to radically compress this massive verification chain in the near future. This will become extremely relevant for light clients running on constrained mobile devices.
  • Error Granularity: The KidError::TooManyKeys error variant is heavily and sloppily overloaded in the verify() method. It is bizarrely returned for too many keys, too many URLs, and even raw string lengths exceeding protocol limits. We should absolutely, urgently split this into much more explicitly granular error types (e.g., MaxUrlsExceeded, PublicKeyTooLong, TooManyControllerKeys). This granular split will significantly aid in debugging rejected documents for downstream external developers actively building on Kinetic. If they get a vague error, they will waste hours blindly guessing what limit they violated.

Manifest


title: CapabilityManifest & ServiceEntry crate: kinetic-kid stage: 3 reading_time: 15 mins depends_on: 01_did.md, 02_document.md

What Is This?

The CapabilityManifest is an optional, modular addition to a Kinetic Identity Document (KID). While the main KID document acts as your core identity and cryptographic root of trust, the manifest functions as your decentralized public bulletin board. It provides a structured, standardized way to advertise services associated with your identity. For example, you might list a personal website, a REST API endpoint, a decentralized storage gateway, or a messaging inbox. The network relies on DID identifiers which are effectively just cryptographic hashes. These hashes alone cannot tell a peer how to dial a WebSocket or where to make an HTTP request. The CapabilityManifest closes this gap by securely associating networking routing data with the mathematical identity. By storing this in a completely separate manifest structure, you actively avoid bloating or needlessly rotating your primary identity document. The ServiceEntry struct represents the individual, addressable services listed within this array.

Why Kinetic Needs This

In a decentralized network like Kinetic, users and nodes must have a standard, interoperable way to broadcast: “Here is exactly how you can contact me across the network” or “Here is precisely where my external data lives.” If we decided to put all of this volatile, fast-changing routing information directly into the core KidDocument, we would immediately face significant architectural and performance drawbacks. Any minor operational change—such as simply updating a domain name, changing an API port, or rotating cloud providers— would brutally force the user to issue a full identity rotation and generate a completely new core document revision. This process would unnecessarily stress the network’s consensus mechanisms, flood gossiping channels, and incur computational overhead. Without a separate manifest, Kinetic would either have to enforce rigid, unchangeable service definitions, or force users to abandon their long-term identities whenever their web hosting provider changes. Both options are terrible for user experience.

By strictly separating the CapabilityManifest from the KidDocument, we achieve a highly modular and robust architecture. The core identity remains inherently small, static, unchanging, and maximally secure. Simultaneously, the manifest effectively handles the dynamic, fast-changing, messy reality of real-world service endpoints. Crucially, the manifest remains cryptographically bound to the core identity at all times. It absolutely must be signed by one of the authorized controller keys explicitly listed in the corresponding KidDocument. This strict separation of concerns ultimately ensures that your core identity remains unconditionally stable, while your practical network capabilities remain highly flexible, scalable, and remarkably easy to update.

How It Works

The complete lifecycle workflow for creating, parsing, and verifying a manifest involves several distinct, rigorous steps:

  1. Definition of Services: The process naturally begins by building an ordered list of ServiceEntry objects. Each discrete entry explicitly specifies a unique fragment ID (like "#website"), a broad service category classification, the underlying transport protocol required to connect, and the actual, fully-qualified string endpoint URI. -> See: crates/kinetic-kid/src/manifest.rs — Lines 9 to 21

  2. Constructing the Manifest: These individual service entries are then securely bundled together into a single CapabilityManifest record. This overarching structure includes vital metadata parameters such as an incrementing version number and strict validity time periods. These specific fields are essential to proactively prevent replay attacks and intelligently manage the lifecycle of the advertised endpoints. -> See: crates/kinetic-kid/src/manifest.rs — Lines 29 to 49

  3. Canonicalization: Before applying any post-quantum cryptographic signature, the manifest must be deterministically converted into bytes. The canonicalize() method uses JCS (JSON Canonicalization Scheme) to strictly serialize the entire manifest. It intentionally, necessarily, and completely omits the signature field (since it is mathematically impossible to sign an empty signature). -> See: crates/kinetic-kid/src/manifest.rs — Lines 51 to 63

  4. Signing: The authentic document owner securely uses a valid ML-DSA-65 private key to sign the canonicalized bytes. This specific private key must correspond perfectly to a public key already authorized in their root KidDocument. We programmatically prepend a specific, hardcoded context prefix (b"kinetic-manifest-v1\0") directly to the message bytes. This aggressive prefixing strategy explicitly prevents devastating cross-protocol signature reuse attacks across the network. -> See: crates/kinetic-kid/src/manifest.rs — Lines 138 to 152

  5. Verification: When an arbitrary peer node receives the gossiped manifest, they must thoroughly verify it against the publisher’s root KidDocument. The internal verify() method runs the data through an incredibly rigorous gauntlet of uncompromising security checks. First, it firmly ensures that the manifest’s declared kid strictly matches the parent document’s decentralized identifier (DID). Next, it meticulously evaluates time constraints, guaranteeing the manifest isn’t currently expired based on local time. It also verifies that its valid_from start time isn’t impossibly far in the future, guarding against malicious clock skew injection. Then, it firmly imposes strict mathematical size bounds: a strict maximum of 50 services, and very tight byte-length limits on all internal strings. This specific defense mechanism actively prevents memory exhaustion attacks where a malicious peer intentionally uploads a gigabyte-sized manifest. Finally, it properly decodes the Base64url signature and exhaustively checks it mathematically against every valid ML-DSA-65 controller key listed in the core document. -> See: crates/kinetic-kid/src/manifest.rs — Lines 75 to 136

Key Pieces

ServiceEntry

What it does: Completely represents a single, independently addressable way to interact with the owner’s identity over the network. Where it is: crates/kinetic-kid/src/manifest.rs — Line 11 Why it matters: It elegantly structures volatile endpoint data cleanly and predictably for the rest of the application. Fields like id purposefully allow referencing specific services unambiguously via fragments. The protocol and endpoint fields directly provide the actionable routing information necessary for executing peer-to-peer connections. This explicit structural separation completely prevents ambiguous, error-prone parsing logic across different network node implementations.

CapabilityManifest

What it does: The primary root parent structure directly holding the dynamic array of services, temporal validity bounds, and the final digital signature. Where it is: crates/kinetic-kid/src/manifest.rs — Line 30 Why it matters: It purposefully acts as the definitive cryptographic envelope for decentralized network capability communication. The doc_type string strongly ensures parsers are correctly interpreting the right schema version before attempting deserialization. The monotonically increasing version integer gracefully allows seamless conflict resolution if multiple overlapping manifests are ever found gossiping in the network. The critical valid_from and expires_at fields directly provide essential temporal scoping constraints. This strict temporal scoping is absolutely critical for long-term security in a loosely connected distributed network, as it mathematically ensures that old, outdated, or maliciously compromised routing data cannot continually resurface.

CapabilityManifest::canonicalize

What it does: Securely converts the struct into a deterministically ordered, fully predictable JSON string, specifically skipping the empty signature field. Where it is: crates/kinetic-kid/src/manifest.rs — Line 57 Why it matters: All modern cryptographic signature algorithms fundamentally require an exact, byte-for-byte matching input array. Standard, naive JSON serializers will frequently reorder dictionary keys arbitrarily depending on compiler-specific internal hash map layouts. Arbitrarily reordered keys would instantly invalidate the signature mathematically for any downstream receiving peer. Using the serde_jcs crate safely enforces strict, standardized alphabetical ordering to completely eliminate this issue.

CapabilityManifest::verify

What it does: Thoroughly, safely, and completely validates all internal parameters of the manifest explicitly against its parent KidDocument. Where it is: crates/kinetic-kid/src/manifest.rs — Line 75 Why it matters: This specific method effectively represents the absolute primary defense line against invalid, malformed, or blatantly malicious data ingestion. It is actively designed to be highly paranoid by default, trusting absolutely nothing. It tightly enforces arbitrary, hardcoded limits on total authorized key counts and embedded array lengths. It meticulously checks for excessive system clock skew to proactively prevent complex time manipulation network attacks. It validates all struct string sizes individually to aggressively guard against buffer overflows or intentional memory bloat attempts. Ultimately, it securely maps the provided signature bytes back to the core identity keys for cryptographic authorization validation.

How This Connects to the Rest of Kinetic

FORWARD DEPENDENCY: The CapabilityManifest architecture is inherently, intentionally, and irrevocably linked to the foundational KidDocument. You simply cannot cryptographically verify a manifest without first successfully fetching, parsing, and verifying the corresponding parent document. The parent document strictly acts as the sole cryptographic source of truth for the necessary controller_keys required for validation.

CROSS-CRATE: In the broader ecosystem of the entire Kinetic network (such as the Kademlia DHT implementation or peering protocol layers), participating nodes will constantly, asynchronously, and frequently gossip these manifests among themselves. The underlying network transport layer relies incredibly heavily on the strict structural bounds enforced here. The hard 50 service limit and precise string constraints confidently ensure that gossiped network payloads will perpetually and comfortably fit within standard, acceptable UDP/TCP MTU (Maximum Transmission Unit) sizes. They also formally guarantee highly predictable, bounded memory usage across all network peers, protecting small constrained nodes from malicious exhaustion.

Quick Reference

-> See RUST_CONCEPTS.md for explanations of:

  • web_time::SystemTime (used for WASM compatibility)
  • #[serde(skip_serializing_if = "Option::is_none")] (used to omit empty optional fields from the canonical JSON)
  • #[serde(deserialize_with = "crate::bounded::deserialize_max_50")] (used to prevent memory exhaustion attacks by capping service arrays)

Open Questions / Things to Revisit

  • The internal time skew allowance during timestamp validation is currently hardcoded to exactly 300 seconds (5 minutes).
  • Is a 300 second threshold sufficiently robust for highly distributed global nodes with potentially wildly drifting hardware clocks?
  • Currently, the ML-DSA-65 signature verification iteratively loops linearly over all valid controller_keys in the document.
  • If a primary document has 20 unique keys, and the manifest happens to be signed by the 20th key, verification does a massive amount of unnecessary cryptographic math.
  • Should we strongly consider including a lightweight key_id hint parameter directly in the manifest to natively optimize this lookup time?
  • We presently only verify specific MlDsa65 key variants during the rigorous verification step.
  • If Kinetic successfully adds new post-quantum signature schemes later, this specific verify block will immediately need substantial algorithmic modification or abstract trait abstraction.
  • The b"kinetic-manifest-v1\0" context byte string is currently hardcoded inline within the signing methods.
  • It would definitively be significantly better architecture to formally move this to a shared, public constant inside src/constants.rs.
  • Centralizing this critical context string would definitively prevent accidental copy-paste typos in future refactoring efforts.

Error


crate: kinetic-kid stage: 4 reading_time: 15 mins depends_on: [01_did.md, 02_document.md, 03_manifest.md]

What Is This?

This file (kinetic-kid/src/error.rs) defines the comprehensive error handling system for the entire kinetic-kid crate. It introduces the KidError enum, which strictly categorizes every single way that parsing, validating, or verifying a Kinetic Identity Document (KID) or a Capability Manifest can fail. Instead of using generic Rust errors or raw strings, Kinetic defines 16 highly specific failure modes with stable protocol error codes. The file also provides automated error conversion rules for third-party libraries (like serde_json and base64). Finally, it maps these protocol-level errors to appropriate security severity levels, ensuring the network daemon knows how to react to failures. This is the single source of truth for all identity-related failures in the Kinetic ecosystem. When anything breaks in kinetic-kid, it returns a KidError.

Why Kinetic Needs This

In a decentralized peer-to-peer network, error handling is not just about developer debugging; it is a critical security boundary. When a Kinetic node receives a KID document from an untrusted peer on the network, it must process it carefully and deterministically. If the document is invalid, the node needs to know exactly why it is invalid before taking action.

Consider the difference in intent between two failures. If a document fails because a JSON bracket is missing or a timestamp field is misspelled, that might just indicate a buggy client or an outdated peer broadcasting malformed data. But if a document fails because the cryptographic signature is actively invalid (InvalidSignature), or it contains too many keys (TooManyKeys), that changes things entirely. That failure could indicate an active Denial of Service (DoS) attack designed to exhaust node memory. It could be an attempted cryptographic forgery. It could be a malicious actor trying to hijack an identity they do not own. By explicitly categorizing errors into 16 distinct variants, Kinetic can safely apply different severities to different failures, protecting the node.

Furthermore, we need stable error codes (like KIN-KID-015). Cross-language clients (like an Android app, a web dashboard, or a third-party wallet application) must reliably handle network rejections. They cannot rely on parsing fragile error strings, which might change in a future Rust update. A stable error code allows the frontend application to immediately show a localized, user-friendly message. It also allows automated bots to trigger specific recovery flows, relying on a guaranteed, unchanging API contract that bridges the Rust backend and external systems.

How It Works

The error system is built around the thiserror crate, which is the standard ecosystem pattern for defining custom library errors in Rust. Let’s break down how the file is structured and how the different features interlock to provide a robust boundary.

1. The KidError Enum Definition

-> See: kinetic-kid/src/error.rs — Lines 4 to 56

The core of the file is the pub enum KidError definition. -> See RUST_CONCEPTS.md for an explanation of the #[derive(Error)] macro (from the thiserror crate).

The #[error("...")] attributes attached to each variant define exactly how the error should be formatted when converted to a string. This is what shows up in the node’s server logs or terminal output when something fails.

The 16 variants are carefully designed to cover every possible failure in the lifecycle of a KID:

  • Formatting & Parsing:
    • InvalidDidPrefix, InvalidDidFormat, InvalidDidHexLength, InvalidDidHexCharacters.
    • These ensure the DID string strictly matches our required format before the CPU even attempts expensive cryptographic operations. It acts as a cheap, early filter.
  • Serialization & Encoding:
    • JsonParseError, CanonicalizationError, Base64Error.
    • These handle the mechanics of moving between raw network bytes, base64url strings, and structured JSON objects in memory.
  • Cryptography & Security:
    • InvalidSignature, MissingSignature, KeyParseError, UnauthorizedManifestSignature.
    • These represent core security failures where ML-DSA-65 post-quantum verification fails entirely, or the keys themselves are completely malformed.
  • Protocol Bounds & State:
    • TooManyKeys (which acts as DoS protection preventing massive JSON files from crashing the parser).
    • InvalidValidFrom, ManifestExpired (which handle time-based validity failures, ensuring old manifests cannot be replayed).
  • Identity Lifecycle & Authority:
    • DidKeyMismatch (genesis binding failure when a new identity is first created, ensuring the DID hash matches the key).
    • UnauthorizedKidUpdate (update authorization failure when modifying an existing document without permission).

2. Automatic Error Conversion (The ? Operator Magic)

-> See: kinetic-kid/src/error.rs — Lines 58 to 68

-> See RUST_CONCEPTS.md for an explanation of the ? operator and impl From<T> for U.

This standard trait implementation keeps the main validation logic clean by automatically translating serde_json::Error and base64::DecodeError into our custom KidError variants.

3. Stable Protocol Codes

-> See: kinetic-kid/src/error.rs — Lines 81 to 100

The code() method uses a match statement to assign a hardcoded, static string prefix to every single variant of the enum. -> See RUST_CONCEPTS.md for an explanation of match and exhaustive pattern matching.

4. Severity Mapping for Network Operations

-> See: kinetic-kid/src/error.rs — Lines 107 to 117

The severity() method maps errors into either Severity::Warning or Severity::Error. Notice the specific syntax used here: Self::InvalidSignature | Self::UnauthorizedManifestSignature | ... => Severity::Error. This is advanced pattern matching at work, grouping multiple conditions together into a single logical branch. It declares “if the internal error state matches any of these specific, security-critical variants, return an Error severity level.” For absolutely everything else (which is handled by the _ => wildcard catch-all at the very bottom), it gracefully returns a Warning. This explicit separation is vital for building a resilient, autonomous network architecture. It allows the higher-level network layers to easily distinguish between a harmless client typo (which just needs a retry) and a serious cryptographic breach (which requires immediate isolation). Without this, the node would have to parse strings to guess if the error was dangerous.

Key Pieces

The KidError Enum

  • What it does: The central error registry containing 16 specific failure modes for identity operations.
  • File/Line: kinetic-kid/src/error.rs — Lines 4-56
  • Why it matters: It forces the developer to explicitly account for every specific way identity validation can fail, rather than relying on lazy, generic “it broke” strings that provide absolutely no context.

The impl From<T> for U Trait Implementations

  • What it does: Standard Rust trait implementations that automatically convert external, third-party errors into internal KidError types.
  • File/Line: kinetic-kid/src/error.rs — Lines 58-68
  • Why it matters: This trait is the hidden machinery that powers the ergonomic ? operator. It makes the core validation logic concise by completely hiding the error translation boilerplate.

The Severity Enum

  • What it does: A simple binary categorization enum (Warning, Error) used to flag the danger level of KidError variants.
  • File/Line: kinetic-kid/src/error.rs — Lines 70-77
  • Why it matters: Provides an immediate, high-level signal to the caller (like the networking daemon or the peer-to-peer layer) about whether an error represents a malicious security threat or just malformed data.

The KidError::code() Method

  • What it does: Returns a static string prefix like “KIN-KID-015” deterministically based on the specific error variant.
  • File/Line: kinetic-kid/src/error.rs — Lines 81-100
  • Why it matters: These alphanumeric codes form the permanent API contract with external clients. They are guaranteed not to change, ensuring frontends do not unexpectedly break even if the internal Rust logic completely shifts.

How This Connects to the Rest of Kinetic

CROSS-CRATE: kinetic-network and kinetic-node When the kinetic-node or kinetic-daemon receives a gossip message containing a KID document from another peer, it will invoke the validation functions found elsewhere in this crate. Those functions will invariably return a Result<(), KidError>. The node will then inspect the error’s severity by evaluating the output of err.severity(). If it encounters a Severity::Error (like InvalidSignature or TooManyKeys), the node instantly knows the peer sending the document is either severely compromised or actively malicious. In response, the network layer can immediately drop the peer TCP connection and heavily penalize their network reputation score. This aggressive pruning protects the local node from further abuse and isolates bad actors. Conversely, if it receives a Severity::Warning (like a JsonParseError), it might just ignore the message and send a soft rejection. It assumes the peer is merely running outdated software or experiencing a bit flip, rather than acting maliciously.

FORWARD DEPENDENCY: JSON-RPC and REST API Error Responses The error_type_uri() and user_message() methods are specifically designed for the JSON-RPC or REST API boundary exposed to developers. When an external application, such as a command-line interface or a web wallet, submits an invalid identity update via the Kinetic API, the daemon will catch the resulting KidError. It will then serialize these string fields into a standard RFC 7807 Problem Details JSON HTTP response. This provides the external developer with exact, actionable frontend feedback—like “The DID method-specific ID is malformed”—rather than dumping a raw, incomprehensible Rust stack trace to their console.

Quick Reference

Here are the critical stable protocol codes generated by this module and what they represent in practice:

  • KIN-KID-001 to KIN-KID-004: DID formatting failures (wrong scheme prefix, invalid hex characters, incorrect length).
  • KIN-KID-005 to KIN-KID-006: Serialization and canonicalization failures (unable to parse JSON, or unable to serialize to JCS).
  • KIN-KID-007 to KIN-KID-008: Core signature verification failures or missing signature fields entirely.
  • KIN-KID-009 to KIN-KID-010: Base64url decoding failures or ML-DSA-65 cryptographic key parsing errors.
  • KIN-KID-011: A capability manifest was explicitly signed by a key that isn’t authorized in the parent KID document.
  • KIN-KID-012: DoS protection triggered (too many keys or endpoints embedded in a single document, exceeding hard memory bounds).
  • KIN-KID-013 to KIN-KID-014: Time-based failures (the manifest valid_from timestamp is in the future, or the manifest has already expired).
  • KIN-KID-015: Genesis binding failure (the DID hash does not mathematically match the primary controller key at creation time).
  • KIN-KID-016: Unauthorized identity update (the new document version was not signed by a key specifically authorized in the previous version).

Open Questions / Things to Revisit

  • Retry Logic and Transience:
    • Currently, the is_retryable() method always strictly returns false (Line 120).
    • This behavior makes perfect sense for strict cryptographic validation (if a signature is mathematically bad, trying it again won’t magically fix it).
    • However, as the Kinetic network evolves, will we ever introduce transient KID errors into the system?
    • For example, if validating a document eventually requires checking a decentralized revocation registry that might be temporarily unreachable, we might genuinely need a distinct Retryable error state to instruct the client to try again later without permanently failing the operation.
  • Extensibility for Cryptographic Agility:
    • If we add new signature schemes (like an Ed25519 fallback) in the future, we might need significantly more granular key parsing errors.
    • Currently, the KeyParseError variant assumes an ML-DSA-65 post-quantum failure.
    • As we expand, we may need to distinguish between ML-DSA failures, Ed25519 failures, and ECDSA failures explicitly for better developer debugging.
  • Validation Granularity in Parsing:
    • Should we attempt to expose the exact JSON field that failed parsing in the JsonParseError variant?
    • Currently, the error completely relies on serde_json’s raw string output.
    • While this is helpful for backend developers reading logs, it is not always perfectly structured for programmatic frontend handling where a UI might want to highlight a specific invalid text input box.

Crate: kinetic-kid

Stage: 1 - Core Identity and Parsing

Reading Time: 15 minutes

Depends On: error.rs (for KidError)


What Is This?

This file defines KineticDid, a custom Rust type that wraps a standard String to represent a Decentralized Identifier (DID) specifically for the Kinetic network. It ensures that any string claiming to be a Kinetic DID strictly adheres to the format did:kin:<64-character-lowercase-hex>. Because of the way this struct is built, it is literally impossible to have a KineticDid in your program that contains a malformed identifier. If the incoming data is invalid, it is caught and rejected at the system boundary before the struct is even allowed to exist. This strictness gives us immense confidence when passing it around the Kinetic codebase. The identifier consists of two main parts:

  1. The scheme prefix, which is always expected to be did:kin:.
  2. The method-specific identifier, which is a 64-character, all-lowercase hexadecimal string. This 64-character string is typically the hex-encoded SHA-256 hash of a public key or another underlying identity primitive in the Kinetic system.

Why Kinetic Needs This

You might wonder: why go through the trouble of creating a whole struct just to hold a single string? Why not just pass a normal String around in our functions?

If a Kinetic networking function takes a String as an argument representing a DID, it has no idea if that string is a valid DID, an empty string, or complete gibberish sent by a malicious peer. The function would either have to blindly trust the caller or re-validate the string itself, over and over again in every part of the codebase. If a developer forgets to validate it just once, invalid data poisons the system and could corrupt the Kinetic network state. This helps us avoid entire classes of bugs (like network spoofing) just by using a strong type.

Note

See RUST_CONCEPTS.md for an explanation of the “Newtype” pattern and the “Parse, don’t validate” philosophy used here. By wrapping a standard type in a custom struct (KineticDid), we force all parsing and validation to happen at the system boundary before the struct is constructed.


How It Works

The core logic of did.rs happens at the boundaries where a String tries to become a KineticDid.

#![allow(unused)]
fn main() {
// See: kinetic-kid/src/did.rs — Lines 6 to 9
}

Notice the #[derive(Debug, Clone, PartialEq, Eq, Hash)] macro above the struct.

Note

See RUST_CONCEPTS.md for #[derive(...)]. This ensures our custom struct can be copied, compared for equality, and used as a key in HashMaps (e.g., mapping a DID to a peer connection).

#![allow(unused)]
fn main() {
// See: kinetic-kid/src/did.rs — Lines 20 to 45
}

The new function is the gatekeeper. It performs four distinct, sequential checks, returning a specific variant of KidError the moment one fails:

  1. First, it pulls in the required prefix using the env!("KINETIC_DID_PREFIX") macro. This macro actually reads an environment variable at compile time and bakes it into the binary. It verifies the string starts with this prefix.
  2. It uses string slicing (&id_str[expected_prefix.len()..]) to safely extract the method-specific ID part of the string, and checks if it’s empty. String slices are fast, zero-copy views into the original string.
  3. It explicitly checks that the method-specific ID is exactly 64 characters long, ensuring it matches the expected SHA-256 hex length.
  4. Finally, it validates that every single character in the ID is correct.

Let’s look at the character validation closely:

#![allow(unused)]
fn main() {
// See: kinetic-kid/src/did.rs — Lines 35 to 40
}

The code method_specific_id.chars().all(|c| c.is_ascii_hexdigit() && !c.is_ascii_uppercase()) performs the check efficiently.

Note

See RUST_CONCEPTS.md for Closures & Iterators (used here via .chars().all(...) to validate each character with a short-circuiting check).

#![allow(unused)]
fn main() {
// See: kinetic-kid/src/did.rs — Lines 69 to 78
}

Another crucial part of how this works is the custom Deserialize implementation. By default, if you use a #[derive(Deserialize)] macro on a Newtype, Rust would blindly put the incoming network string directly into the private id field, bypassing our validation entirely. We override this behavior by implementing the Deserialize trait manually. When JSON or binary data arrives over the Kinetic network, our custom Deserialize block intercepts it. First, it uses the standard deserializer to parse the raw bytes into a normal, unverified String. Then, it immediately passes that unverified String to our strict KineticDid::new() function. If new() succeeds, we get our safe KineticDid. If new() fails, the entire network payload parsing is rejected and an error is thrown (serde::de::Error::custom).

Important

This guarantees that invalid DIDs cannot even enter the running application memory space. This makes debugging network payloads incredibly reliable.


Key Pieces

ComponentDescription
KineticDid struct (Lines 6-9)The actual Newtype struct. It contains a single private field: id: String. The privacy here is the most critical part—because id is private, no other code anywhere in Kinetic can manually construct this struct or modify the string inside it, preserving the validity guarantee.
Derived Traits (Line 6)Clone for copying, PartialEq/Eq for checking equality, and Hash so DIDs can be used as keys in network routing tables or peer lists.
KineticDid::new (Lines 20-45)The constructor and sole validation boundary. It returns a Result<Self, KidError>, forcing the caller to proactively handle the possibility that the input string is invalid.
as_str method (Lines 48-50)A safe way to read the underlying valid string as a reference without consuming or destroying the struct.
Display implementation (Lines 53-57)This wires up the struct to Rust’s standard formatting macros. It allows a KineticDid to be easily printed using println!("{}", did) or converted back into a string, outputting the raw internal string transparently without wrapping it in quotes or struct syntax.
Serialize and Deserialize (Lines 60-78)The active shields at the network boundaries, ensuring no serialized payload goes out malformed, and no deserialized payload comes in unverified.
proptests module (Lines 80-91)Uses property-based testing (proptest!) to generate random garbage data (like the regex pattern "\\PC*") and feeds it into the parser to guarantee the code will never panic or crash on unexpected input.

How This Connects to the Rest of Kinetic

FORWARD DEPENDENCY: Any component in the Kinetic network dealing with user identities, peer nodes, or cryptographic signatures will rely heavily on KineticDid. For example, when building the P2P networking layer or the underlying database schema, functions and structs will accept a KineticDid type instead of a String. This completely removes the need to clutter their business logic with string format validation, making the system much safer and easier to read.

CROSS-CRATE: Network payloads defined in other crates (like peer discovery messages or consensus blocks) will use KineticDid as a field type. Because of our custom Deserialize logic, the network parsing layer will automatically enforce our DID rules before the application logic even sees the incoming message. If a peer sends a malicious DID, the network layer drops it before the core node logic is even aware of it. By doing this, we guarantee that all network communication relies entirely on strictly verified DIDs.


Quick Reference

Action / ConceptDetails
To create a DIDlet my_did = KineticDid::new("did:kin:a1b2c3d4...")?; (Note the ? to handle the KidError if it fails).
To safely get the string back outlet raw_str = my_did.as_str();
To print it to the consoleprintln!("Connected to peer: {}", my_did);
Serialization boundaryKineticDid guarantees it will reject invalid peer data eagerly at the network edge during JSON or binary parsing.
Underlying structureA private string wrapper leveraging the Newtype pattern for compiler-enforced safety.

Open Questions / Things to Revisit

  • Compile-time environment variable: Using env!("KINETIC_DID_PREFIX") means the did:kin: prefix is baked into the binary at compile time. Is there any scenario where we’d want this to be configurable at runtime, or is did:kin: totally permanent for the lifespan of the project?
  • Proptests Expansion: The file currently includes a basic property-based test (Lines 80-91) ensuring the parser doesn’t panic or crash on garbage input strings. We should likely expand these proptests to ensure specific edge-case boundary conditions (like exactly 63 characters vs 65 characters) explicitly return the correct KidError variants, rather than just checking for crashes.
  • Memory Allocation: Currently, KineticDid wraps a heap-allocated String. If we pass these around millions of times per second during consensus, the memory allocations could add up. We might want to revisit this later and explore using a fixed-size byte array (like [u8; 32]) internally to avoid heap allocations entirely while retaining the exact same Newtype safety guarantees.

Kinetic-KID: bounded.rs

Crate: kinetic-kid Stage: 06 Reading Time: 15 minutes Depends On: serde (external)


What Is This?

This module provides custom Serde deserialization helpers designed to strictly limit the maximum number of items in a deserialized array or vector. Rather than relying on standard Serde routines that happily ingest arrays of unlimited length, this code enforces a hard, unyielding cap at the exact moment of processing. It acts as an essential security gatekeeper during payload parsing in the Kinetic network. If a network peer tries to send a payload that exceeds these explicitly defined array lengths, the parser rejects it instantly instead of allocating unbounded memory. This file contains the primitive types and logic required to seamlessly integrate these bounded checks into standard Rust structs without writing custom parsers from scratch for every new network message struct.


Why Kinetic Needs This

In a decentralized network like Kinetic, we process un-trusted data sent from random peers over the open, unauthenticated internet. A classic attack vector against network nodes in systems programming is a “memory exhaustion attack” (often referred to as a memory bomb or resource exhaustion DoS). If we define a struct with a standard Vec<T>, a malicious actor could deliberately construct a payload with hundreds of millions of empty or tiny elements. Standard Serde arrays try to allocate memory to match the size of the incoming data before completely failing. If an attacker sends a JSON or binary document containing a massive array, Serde might attempt to allocate a multi-gigabyte vector on the heap right away.

Warning

This can easily crash our node by triggering a fatal Out Of Memory (OOM) panic in Rust, causing a catastrophic Denial of Service (DoS) across the network.

We absolutely must defend the node against this vector, as crashing nodes could partition the network. By wrapping list deserialization in explicit bounded caps, we guarantee our parsing logic aborts the exact moment an attacker exceeds the configured threshold. This approach prevents memory allocation attacks at the serialization boundary, ensuring Kinetic remains robust, predictable, and highly resilient under hostile network conditions.


How It Works

The standard deserialize provided by Serde is fully generic, meaning it has no inherent sense of maximum array bounds. To modify this core behavior, we implement our own deserialization logic using the Visitor trait.

Note

See RUST_CONCEPTS.md for an explanation of the serde::de::Visitor trait.

#![allow(unused)]
fn main() {
// See: kinetic-kid/src/bounded.rs — Lines 9 to 59
}

The custom behavior is driven by the internal BoundedVecVisitor<T> struct. When Serde detects a sequence, the visitor starts by defensively checking the size_hint() provided by the incoming sequence inside the visit_seq method.

#![allow(unused)]
fn main() {
// See: kinetic-kid/src/bounded.rs — Lines 37 to 41
}

If the underlying data format provides an upfront size (like knowing the array length in advance because of a binary prefix) and that size exceeds our hard max, we immediately return an invalid_length error.

Important

We do not allocate a single byte of heap memory for the vector yet. This is the optimal fast path for rejection, saving CPU cycles.

However, some formats (like streaming JSON) do not provide a size upfront. In those cases, we must allocate a vector to start storing the parsed elements, but we do so defensively and carefully.

#![allow(unused)]
fn main() {
// See: kinetic-kid/src/bounded.rs — Line 43
}

The capacity allocation is strictly limited to either the size hint or the max allowed limit, preventing us from pre-allocating an excessively large heap buffer. Then we begin to iterate through the sequence using seq.next_element()?, parsing and instantiating one element at a time.

#![allow(unused)]
fn main() {
// See: kinetic-kid/src/bounded.rs — Lines 46 to 55
}

For every single element popped off the wire and deserialized, we check if our running count has reached the max threshold. If it hits the ceiling, we immediately abort the while loop and return a custom Serde error indicating the boundary was crossed. This drops the partially built vector, freeing the memory instantly, and terminates the connection context via the bubbled-up Error. If the sequence ends before hitting the max, the returned vector is safely bounded and passed back into the struct being populated.


Key Pieces

BoundedVecVisitor<T>

#![allow(unused)]
fn main() {
// See: kinetic-kid/src/bounded.rs — Lines 9-21
}

This is the core struct driving the defensive parsing strategy. It holds a max value of type usize that determines the absolute ceiling for the array length.

Note

See RUST_CONCEPTS.md for PhantomData (used here to bind the generic type T without actually allocating it).

visit_seq

#![allow(unused)]
fn main() {
// See: kinetic-kid/src/bounded.rs — Lines 33-59
}

This is a required method on the Visitor trait where the actual bounded iteration logic lives. It is extremely important because this is the exact boundary layer where un-trusted network bytes are converted into Rust heap allocations. It correctly handles both early-abort checks using size hints, and mid-stream aborts if the incoming data stream deliberately hides its true length.

deserialize_max_20

#![allow(unused)]
fn main() {
// See: kinetic-kid/src/bounded.rs — Lines 66-72
}

A public helper function that sets up a visitor with a hard limit of exactly 20 elements. This is used across Kinetic when we know an array should be relatively small, such as a list of recent cryptographic signatures or short peer discovery lists.

deserialize_max_50

#![allow(unused)]
fn main() {
// See: kinetic-kid/src/bounded.rs — Lines 79-85
}

Similar to the above, but sets a hard limit of exactly 50 elements. Used for slightly larger payload arrays, such as transaction batches or mempool gossips, while still maintaining a strict memory bounds check against runaway allocations.


How This Connects to the Rest of Kinetic

FORWARD DEPENDENCY: You will see these exact helper functions heavily utilized across Kinetic’s core payload structures in other modules. Any struct that needs to be deserialized from a network payload and contains a Vec<T> should be explicitly annotated with #[serde(deserialize_with = "deserialize_max_20")] (or deserialize_max_50). Without this explicit annotation, the struct would silently fall back to Serde’s default, unbounded Vec deserialization, immediately re-opening the memory exhaustion attack vector.

CROSS-CRATE: This specific file acts as a foundational security primitive for the larger kinetic-network crate. When the peer-to-peer layer ingests raw bytes from a TCP/UDP socket, these bounds act as the absolute first line of defense, ensuring that maliciously crafted messages are dropped entirely without affecting the node runtime’s memory footprint or stability.


Quick Reference

ConceptDetail
ProblemNode crashing due to OOM attacks via unbounded JSON/binary arrays in network payloads.
SolutionImplement a custom BoundedVecVisitor that stops parsing the exact moment the length limit is hit.
UsageApply #[serde(deserialize_with = "deserialize_max_20")] to Vec struct fields in Kinetic payloads.
Fail StateReturns a Serde error which bubbles up to the network layer to drop the bad peer’s message immediately.
Key ConceptPhantomData is used to artificially carry type information about T without actually storing the type.

Open Questions / Things to Revisit

  • Currently, we only have hardcoded limits of 20 and 50 exposed as public helpers. Should we implement a Rust macro to generate generic bounded functions (e.g. 100, 500), or is this rigid approach safer for auditing? Having a small, fixed set of limits makes it easier to verify that developers aren’t accidentally allowing arrays of size 10,000 somewhere in the network crate.
  • Is there a way to computationally penalize peers (e.g., lower their reputation score) at the network layer if they trigger the bound limit early in the connection phase to save bandwidth? We need to ensure we don’t accidentally penalize honest peers running outdated client software.
  • Does the serde_json memory footprint still scale poorly with highly nested object structures (like { "a": { "b": { ... } } }) even if the child arrays themselves are strictly bounded? We might need a structural depth check in the future to defend against stack-overflow attacks.
  • If we ever migrate from JSON payloads to a purely binary format like Bincode, do we still need this specific visitor, or does the binary format naturally prevent this type of resource exhaustion? Bincode often allocates based on size prefixes, so this vulnerability might actually be worse in binary if not handled correctly.
  • Should we expose the BoundedVecVisitor directly so that other developers can use it in custom implementations, or keep it strictly encapsulated behind the deserialize_max_* macros?

01 — Overview and Errors

Crate: kinetic-verify
Stage: 2 of 10
Reading time: ~6 minutes
Depends on: kinetic-types
Source files: kinetic-verify/src/lib.rs (15 lines), kinetic-verify/src/error.rs (20 lines)


What Is This?

kinetic-verify is a highly specialized, ultra-lightweight cryptographic verification library for the Kinetic network. It acts as the mathematical bouncer for the network, ensuring that signatures and VDF proofs are valid before any heavy processing or storage occurs.


Why Kinetic Needs This

Why separate verification from kinetic-types or kinetic-network?

  • Decoupling: P2P networking code is heavy and requires an operating system (sockets, threads). Verification is pure math. By keeping it separate and no_std compatible, you can compile the verification logic into an offline hardware wallet or a browser extension that doesn’t need to run a full node.
  • Security: When a node receives a message, it needs to verify the cryptographic signatures before spending CPU cycles processing it. Having a dedicated verify crate ensures this critical boundary is cleanly enforced.

How It Works

Currently, this crate is extremely minimal (35 lines of code total). Its primary job right now is establishing the boundary and exporting the types needed for verification.

  • It defines constants that all nodes must agree on (like MAX_PAYLOAD_SIZE).
  • It exports the VDF types (from kinetic-types) so that downstream crates can just import kinetic-verify to get both the types and (eventually) the verification functions.
  • It defines VerifyError to categorize exactly how a verification can fail.

Key Pieces

RESQUARING_EPOCH_KYNS

#![allow(unused)]
fn main() {
RESQUARING_EPOCH_KYNS
}
  • What it does: A constant set to 5,256,000.
  • Location: lib.rs:8
  • Why it matters:

    Note

    At 3 seconds per Kyn, 5.25 million Kyns equals exactly 182.5 days (half a year). This is the network’s resquaring epoch interval. In VDF cryptography, proofs get larger over time, so the network must periodically “resquare” or reset the baseline to prevent proofs from taking too long to verify.


MAX_PAYLOAD_SIZE

#![allow(unused)]
fn main() {
MAX_PAYLOAD_SIZE
}
  • What it does: A constant set to 65,536 (64 KB).
  • Location: lib.rs:10
  • Why it matters:

    Important

    This is the hard limit on the size of a name reveal payload. Without this, a malicious actor could submit a 10 GB payload, causing all nodes on the network to run out of memory trying to store or verify it.


VerifyError

#![allow(unused)]
fn main() {
VerifyError
}
  • What it does: An enum with three variants: InvalidSignature, MalformedPublicKey, MalformedSignature.
  • Location: error.rs:6-19
  • Why it matters:

    Warning

    It is critical to distinguish why verification failed. A MalformedPublicKey might mean network corruption (a byte got flipped in transit). An InvalidSignature means the math doesn’t check out, which usually implies deliberate forgery or a malicious actor.


How This Connects to the Rest of Kinetic

  • CROSS-CRATE: This crate re-exports the VDF types (Commitment, Reveal, VdfProof, etc.) which are defined in docs/learn/types/05_vdf_types.md.
  • FORWARD DEPENDENCY: kinetic-kid and kinetic-core will consume this crate to validate incoming registrations and capability manifests.
  • FORWARD DEPENDENCY: vdfrs and kyn-vdf (the actual math engines) will be hooked into this crate to provide the deep algebraic verification.

Quick Reference

  • Crate Size: ~35 lines of code.
  • no_std: Designed to run without an operating system.
  • Epoch: 182.5 days (RESQUARING_EPOCH_KYNS).
  • Max Payload: 64 KB (MAX_PAYLOAD_SIZE).

Open Questions / Things to Revisit

  • Missing implementation: Right now, this crate only defines the errors and exports types, but it doesn’t contain any actual verify() functions (like the ML-DSA-65 signature checks). Are those checks currently living directly inside kinetic-types (which violates the separation of concerns), or are they planned to be moved here?
  • Severity mapping: VerifyError currently does not implement a severity() method returning Severity::Critical or Severity::Warning like the errors in kinetic-types. This should be added so network nodes know how to handle these failures automatically.

01 — Overview

Crate: kyn-vdf Stage: 6 of 10 Reading time: ~5 minutes Depends on: kinetic-vdf (conceptually)


What Is This?

kyn-vdf is Kinetic’s pure-Rust implementation of Verifiable Delay Functions. It implements Imaginary Quadratic Class Groups, Shanks’ composition algorithms, and Wesolowski verification entirely from scratch in safe Rust.

Warning

(Note: While the core math is pure-Rust, the crate lacks the #![no_std] attribute and relies on thiserror, which inherently pulls in std).

Why Kinetic Needs This

In Stage 5 (kinetic-vdf), you saw that generating a VDF proof requires the heavy C++ chiavdf library. However, a major goal of Kinetic is to allow web browsers, mobile phones, and IoT devices to connect to the network as light clients without installing C++ toolchains. By writing the verification logic in pure Rust, kyn-vdf can be compiled directly to WebAssembly (WASM). This allows any web page to instantly verify 30 days of CPU time cryptographically, ensuring light clients do not have to blindly trust central servers.

How It Works

At its core, this crate performs math on large numbers (1024-bit integers).

  1. It deterministically derives a prime number (the Discriminant $D$) using SHA-256 and Miller-Rabin tests.
  2. It parses a compressed 100-byte BQFC network payload into a Quadratic Form $(a, b, c)$.
  3. It performs sub-quadratic Euclidean exponentiation using Shanks’ NUCOMP and NUDUPL algorithms.
  4. It checks the Wesolowski equation $\pi^B \cdot x^r = y$ to guarantee the proof is valid.

Key Pieces (Topic File Breakdown)

This crate is composed of ~1,480 total lines of code, broken down into the following dense mathematical topics:

  1. 02_math_core.md — Defines the Form struct and the Gauss reduction algorithms to keep numbers canonical.
  2. 03_math_comp.md — The complex Shanks’ NUCOMP and NUDUPL algorithms for fast algebraic composition.
  3. 04_chia_prime.md — HashPrime and Discriminant derivation via the Miller-Rabin primality test.
  4. 05_chia_bqfc.md — Binary Quadratic Form Compression (BQFC) to squish 130 bytes of data into exactly 100 bytes for optimal network MTU routing.
  5. 06_verify.md — The Wesolowski verification equation that makes the entire time-lock system possible.

How This Connects to the Rest of Kinetic

  • FORWARD DEPENDENCY: The network daemon (kinetic-daemon) will embed this crate to verify every single transaction.
  • CROSS-CRATE: The C++ wrapper from Stage 5 (kinetic-vdf) uses this crate to perform continuous differential fuzzing to ensure the C++ and Rust engines never fork.

Quick Reference

  • Total Lines: 1,480
  • Math Type: Imaginary Quadratic Class Groups
  • Primary Dependency: num-bigint
  • WASM Compatible: Yes (though restricted by std usage via thiserror)

kyn-vdf: Imaginary Quadratic Class Groups Math Core

Crate: kyn-vdf Stage: 2 Reading Time: 40 minutes Depends On: 01_vdf_overview.md (Conceptual overview of VDFs)

What Is This?

This document breaks down the mathematical foundation of the Verifiable Delay Function (VDF) implementation in the Kinetic network. Specifically, it provides an explanation of the mathematical operations over Imaginary Quadratic Class Groups. These class groups provide the non-parallelizable, deterministic time-delay necessary for our network’s consensus mechanism to function safely. This file specifically documents the core operations found in kyn-vdf/src/math.rs (lines 1-226). It acts as the strict foundational bedrock for the entire cryptographic pipeline. It defines binary quadratic forms, encompassing arbitrary precision arithmetic, Shanks NUCOMP threshold calculations, partial Euclidean steps, and Gauss reduction algorithms. All of this is implemented in purely deterministic Rust code to ensure cross-network consensus stability and bit-for-bit accuracy. Without the precise mathematics in this file, Kinetic would not be able to securely elect block producers. It would also fail to verify elapsed time in a decentralized manner, which is the entire premise of the Kinetic network. This math forms the strict computational wall that prevents attackers from rushing or manipulating the network’s internal clock. It is the heart of the Kinetic VDF and the absolute source of truth for the entire sequence of squarings. Understanding this file is for anyone working on the consensus layer. It marries advanced algebraic number theory with low-level systems engineering.

Why Kinetic Needs This

At the very heart of Kinetic’s network security model is the Verifiable Delay Function (VDF). A VDF is a cryptographic primitive that guarantees that a certain amount of sequential computational time has unequivocally elapsed. This verifiable proof of elapsed time is crucial for preventing malicious actors from rushing the consensus process. In a proof-of-stake system like Kinetic, unpredictable randomness is required to elect leaders safely and fairly. If the randomness generation is instant, attackers can simulate thousands of forks and pick the one where they win the election. A VDF acts as an “randomness beacon” for the entire network. It prevents attackers from predicting or manipulating future block producers by forcing a , unavoidable time delay on the randomness generation.

To achieve this sequential delay, we need a mathematical structure known as a “group of unknown order”. In a group of known order, an attacker can use shortcut calculations (like Euler’s theorem) to compute the final result instantly. By bypassing the sequential delay entirely, the VDF loses its primary property and the network becomes vulnerable to immediate takeover. While standard RSA groups are indeed of unknown order, they come with a fatal flaw: they require a trusted setup.

Warning

A trusted setup means a trusted party must generate the prime factors and then securely destroy them. This represents a centralization risk and a single point of failure that Kinetic refuses to accept.

Imaginary Quadratic Class Groups offer a elegant, trustless alternative to RSA. We can generate a class group of unknown order simply by using a large negative prime number. This number is called a fundamental discriminant, commonly denoted as $D$. Because $D$ is just a prime number (which can be deterministically derived from a block hash), this entirely eliminates the need for a trusted setup phase. This preserves the true, permissionless decentralization of the Kinetic network from the genesis block onwards.

However, this mathematical elegance comes with heavy, engineering requirements. First, we must deal with integers to maintain robust cryptographic security against modern hardware and ASICs. For a standard 128-bit cryptographic security level, our discriminant $D$ needs to be a negative prime number that is thousands of bits long. Typically, in the Kinetic protocol, this means a 1024-bit or 2048-bit negative prime integer. Standard Rust primitive types like u64 or u128 simply cannot hold values of this enormous magnitude. This is exactly why we depend on the num-bigint crate to handle all values as heap-allocated BigInt objects. Without BigInt, we would not be able to store our parameters, let alone perform multi-million iteration squarings on them. These objects dynamically allocate memory to store as many bits as required for the calculation.

Second, the system demands absolute, bit-perfect determinism across the entire global network. Kinetic is a decentralized network running on heterogeneous hardware, from cloud servers to desktop PCs. When a node computes a VDF proof, every other node in the network must be able to verify it independently. They must arrive at the exact same mathematical conclusion down to the final bit, without exception. This means we cannot rely on floating-point arithmetic for any part of the calculation. Standard operations like f64::sqrt() are forbidden in our consensus-critical code. Floating-point precision, rounding behavior, and NaN handling can vary subtly between different CPU architectures (such as x86 vs ARM). Furthermore, different compiler optimizations can alter the order of operations, changing the final result of floating-point math. If nodes disagree on the result of a math operation by even a single bit, the network would instantly fork and fail to reach consensus. Therefore, every single calculation in math.rs must be , integer-based.

Finally, we need extreme, performance. VDFs are evaluated continuously by nodes, requiring billions of sequential squaring operations to prove elapsed time. If our BigInt math is inefficient, the baseline delay becomes artificially inflated. The sequential delay would be caused by memory allocations and garbage collection rather than actual cryptographic operations. This is why you will see a heavy reliance on passing variables by reference (e.g., &BigInt) rather than taking ownership. Taking ownership causes cloning, which means allocating new space on the heap, copying bytes, and eventually dropping the memory. Minimizing these costly heap allocations during the billions of additions and multiplications is a top architectural priority for kyn-vdf. Every single microsecond saved in the math core translates directly to a faster, more secure network. By passing immutable and mutable references , we allow the compiler to reuse memory allocations.

How It Works

The entire mathematical mechanism of the class group operates on structures known as “Binary Quadratic Forms”. A binary quadratic form is a homogeneous polynomial of degree two in two variables. Specifically, it takes the form $f(x, y) = ax^2 + bxy + cy^2$. In our codebase, we typically represent this polynomial simply as a tuple of its coefficients: (a, b, c). For a given negative discriminant $D$, these forms operate as the group elements of our cryptographic class group. The equivalence classes of these forms under modular transformations form a finite abelian group. This mathematical group provides the exact algebraic structure needed to chain squaring operations for the VDF.

In kyn-vdf/src/math.rs, the code is structured into a tight pipeline that supports two main operations. The first operation is composing two forms together (or squaring a form with itself). The second operation is reducing a form back to its canonical, minimal representation. Because the coefficients grow fast when composing forms, we use a sophisticated algorithm. This algorithm is called Shanks NUCOMP, and it keeps the coefficients as small as possible throughout the intermediate steps of the computation. Let’s walk through the exact flow of the code from lines 1 to 226 in extensive detail to understand this pipeline.

1. Calculating the NUCOMP Threshold

-> See: kyn-vdf/src/math.rs — Lines 10 to 45

When we compose two quadratic forms , the resulting coefficients can grow exceptionally large. Without optimization, they can easily grow to the size of $D^2$, which ruins cache locality and dramatically reduces performance. Shanks NUCOMP is a algorithmic optimization that performs a partial reduction during the composition step itself, rather than waiting until the end. By reducing early, it prevents the intermediate numbers from ever getting that large in memory, speeding up the overall calculation.

To know exactly when to stop this partial reduction, the NUCOMP algorithm requires a specific threshold value, usually denoted as $L$. This threshold is defined as the fourth root of the absolute value of the discriminant: $L = |D|^{1/4}$. In lines 10-45, we implement the isqrt_fourth(n) function to calculate this critical threshold. Because we are forbidden from using floating point math for consensus, this function computes the integer square root twice in succession. Applying the integer square root twice yields the integer fourth root exactly. It uses a purely deterministic Newton-Raphson method adapted for integer arithmetic. The algorithm starts with an initial, conservative guess and iteratively refines it using integer division and averaging. By staying entirely in integer space, we guarantee perfect consistency across all platforms. We ensure that every single Kinetic node calculates the exact same threshold $L$, down to the last bit. This is true regardless of whether the node is running on a high-end Intel server or a low-power ARM device. The function is written carefully to manage memory by reusing local mutable variables during the Newton-Raphson loop, avoiding excess allocations. It ensures that the fourth root calculation, while slightly slower than a hardware floating point equivalent, is 100% deterministic. This function is a great example of rewriting standard math primitives exclusively for cryptographic contexts.

2. The Partial Extended Euclidean Algorithm

-> See: kyn-vdf/src/math.rs — Lines 47 to 95

The standard Extended Euclidean Algorithm (XGCD) is a foundational algorithm in computational number theory. It finds the greatest common divisor of two integers and simultaneously computes the coefficients of Bézout’s identity. However, for the Shanks NUCOMP optimization, we do not want to run the XGCD all the way to completion. Instead, we run a specialized, modified version known as a “Partial” XGCD.

In the xgcd_partial function, we pass in two BigInt values and the specific threshold $L$. This threshold $L$ is exactly the value we just computed using the isqrt_fourth function. The loop inside this function executes the standard Euclidean division steps: dividing, finding the remainder, and shifting variables. But crucially, it includes a strict early-exit condition that defines the “partial” nature of the algorithm. It halts the moment the remainder drops below the threshold $L$.

Notice how heavily this specific section of code relies on Rust references and borrowing syntax. Mathematical operations like a = &b - &q * &r are written with explicit borrows on every single operand. If we were to blindly clone these 1024-bit integers on every single iteration of the Euclidean algorithm loop, performance would tank immediately. The memory allocator would thrash, and the VDF would become far too slow to run on consumer-grade hardware. By using references, the num-bigint crate can perform the arithmetic using the existing backing vectors. This minimizes the stress on the global system allocator and keeps the CPU cache piping hot for maximum instruction throughput. This pattern of utilizing references for num-bigint arithmetic is a hallmark of Kinetic’s cryptographic optimizations. Understanding lifetime scopes and borrow checking is critical to making sense of these dense mathematical loops.

3. Representing the Quadratic Form

-> See: kyn-vdf/src/math.rs — Lines 97 to 120

In this section, we define the central data structure of the entire crate: the Form struct. It is defined simply as a struct holding exactly three BigInt fields, named a, b, and c. These three fields map directly to the coefficients of our binary quadratic form $ax^2 + bxy + cy^2$. The quadratic form has deep connections to ideals in quadratic fields, making it the perfect algebraic structure for our group operations. There is a fundamental, mathematical invariant that this struct must always maintain at rest. The discriminant of the form must always equal the network’s globally chosen $D$. In equation terms, this means that the relation $b^2 - 4ac = D$ must always hold true for any valid form. This relation directly mirrors the standard quadratic formula discriminant, and it ties the specific form back to the global class group. If a form ever violates this invariant, it is invalid, cryptographically useless, and represents a critical consensus failure.

The struct provides various initialization methods to safely construct forms, ensuring the invariant holds upon creation. It carefully initializes the identity element of the class group, which is required to start the VDF sequence. More importantly, it provides the target receiver for our next, and most crucial, operation: reduction. The Form struct also implements traits like Clone and PartialEq. However, you must be careful: equality checking is only meaningful if both forms are in their reduced state. Comparing two unreduced forms directly via PartialEq is a logical error, as they may represent the exact same mathematical element in the class group but have wildly different coefficients. The system relies on strict reduced-form comparisons during the verification step to ensure consensus agreement. To avoid bugs, equality comparisons should always be preceded by a call to reduce().

4. Gauss Reduction Algorithm

-> See: kyn-vdf/src/math.rs — Lines 122 to 226

When operations like composition or squaring are performed on forms, they naturally become “unreduced”. An unreduced form represents the exact same mathematical equivalence class as a properly reduced form. However, its coefficients $a$, $b$, and $c$ are unnecessarily large and non-canonical. In order to deterministically compare forms (which the verifier must do to check the VDF proof), they must be standardized. They must be converted back into a unique, canonical state known as the “reduced” state. The reduce() method implements the classical Gauss reduction algorithm for this exact standardization purpose.

Gauss reduction is conceptually similar to the Euclidean algorithm, but it operates on two-dimensional quadratic forms instead of one-dimensional integers. A binary quadratic form is considered properly, reduced if and only if two strict conditions are met. First, the coefficients must satisfy the primary inequality: $-a < b <= a < c$. Second, if $a$ happens to exactly equal $c$, then $b$ must be greater than or equal to $0$. The reduce() function uses a mutable while loop that repeatedly applies two core normalization steps until these precise conditions are met.

The first step is logically called “Normalize B”. It shifts the value of the $b$ coefficient so that it falls precisely into the mathematical range $(-a, a]$. This step involves calculating a shift factor $q$ via a careful integer division. It then updates both the $b$ and $c$ coefficients accordingly using this calculated shift factor, preserving the overall discriminant invariant throughout the transformation. The preservation of the discriminant invariant is verified via robust unit testing, but logically it is guaranteed by the algebraic shifts applied. This step acts as a sliding window that bounds the size of $b$.

The second step is logically called “Minimize A”. If the coefficient $a$ is greater than $c$, the form is fundamentally unbalanced and clearly violates the reduction condition. To fix this imbalance, the algorithm dynamically swaps the values of $a$ and $c$, and simultaneously negates the value of the $b$ coefficient. This elegant mathematical swap immediately makes the new $a$ coefficient smaller than the new $c$ coefficient. This step acts as the primary driver of reduction, constantly pushing the maximum values downwards with every loop iteration.

The main loop in lines 122-226 alternates continuously between these two critical normalizations. Because the coefficient $a$ decreases every single time a swap operation occurs, the algorithm is guaranteed to eventually terminate. The heavy use of BigInt mutable references in this section is perhaps the single most performance-critical code in the entire repository. Gauss reduction runs iteratively after every single composition step in the main VDF prover loop. If this specific reduction loop is slow, the entire network’s baseline VDF delay increases, harming network throughput. Any future optimizations to Kinetic’s underlying math library will undoubtedly start right here in reduce().

Key Pieces

This section breaks down the critical components you need to understand in this file.

isqrt_fourth(n: &BigInt) -> BigInt

#![allow(unused)]
fn main() {
isqrt_fourth(n: &BigInt) -> BigInt
}
  • What it does: Computes the deterministic integer fourth root of a number $n$.
  • File:Line: kyn-vdf/src/math.rs — Lines 10-45.
  • Why it matters: It calculates the foundational $L$ threshold required by the Shanks NUCOMP optimization.

Important

If this value is computed incorrectly by even a single bit, the partial XGCD will stop at the wrong time. Stopping at the wrong time corrupts the intermediate state and invalidates the entire VDF output, rendering the proof useless. The strict integer-only implementation is the bedrock that guarantees network-wide consensus determinism.

xgcd_partial(a: &BigInt, b: &BigInt, limit: &BigInt) -> (BigInt, BigInt, BigInt)

#![allow(unused)]
fn main() {
xgcd_partial(a: &BigInt, b: &BigInt, limit: &BigInt) -> (BigInt, BigInt, BigInt)
}
  • What it does: Runs the Extended Euclidean Algorithm but forces a strict early stop when the remainder is less than or equal to the limit.
  • File:Line: kyn-vdf/src/math.rs — Lines 47-95.
  • Why it matters: It is the true computational engine of the Shanks NUCOMP optimization strategy. By strategically stopping the Euclidean algorithm early, we extract the exact intermediate Bézout coefficients needed for composition. These precise coefficients allow us to compose two forms without letting their intermediate values explode in system memory. This partial step is the crux of how Kinetic maintains memory-efficient group operations at scale.

struct Form

#![allow(unused)]
fn main() {
struct Form
}
  • What it does: Represents the abstract mathematical object $ax^2 + bxy + cy^2$ using three arbitrary-precision BigInt fields.
  • File:Line: kyn-vdf/src/math.rs — Lines 97-120.
  • Why it matters: This simple struct is the atomic unit of the entire VDF architecture. The entire VDF computation is functionally just a long, sequential chain of squaring Form objects millions of times. It securely holds the cryptographic state that is ultimately passed between the prover and the verifier over the network. Without a tight, efficient Form abstraction, the code would be a sprawling mess of loose variables.

Form::reduce(&mut self)

#![allow(unused)]
fn main() {
Form::reduce(&mut self)
}
  • What it does: Canonicalizes a quadratic form in-place so its internal coefficients satisfy the strict Gauss reduction inequality conditions.
  • File:Line: kyn-vdf/src/math.rs — Lines 122-226.
  • Why it matters: Without this mandatory reduction step, the coefficients $a$, $b$, and $c$ would grow with every single squaring operation. They would quickly consume gigabytes of memory and exhaust all available RAM on the node, fatally crashing the system. Reduction guarantees that the coefficients stay tightly bounded safely by the size of the discriminant $D$. It also ensures that every single node arrives at the exact same canonical representation for final verification. Verification hinges entirely on forms matching identically after reduction.

How This Connects to the Rest of Kinetic

This mathematical foundation is not an isolated academic exercise. It is deeply, inextricably wired into the cryptographic layers of the network and directly drives the consensus engine.

  • FORWARD DEPENDENCY: kyn-vdf/src/prover.rs The prover module takes a starting Form and squares it $T$ times consecutively to generate the delay. To achieve this sequential squaring, it continuously and repeatedly calls both xgcd_partial and Form::reduce in a tight loop. The execution efficiency of the code in math.rs directly dictates how fast the prover can physically run on given hardware. This hardware speed, in turn, structurally defines the network’s minimum possible block delay.

  • FORWARD DEPENDENCY: kyn-vdf/src/verifier.rs The verifier module uses the exact same mathematical operations defined here. It uses them to check the Wesolowski proof provided by the remote prover node. The verifier requires absolute, exact deterministic agreement on the reduce() outputs to accept a proof as valid and safe. Any discrepancy in math.rs means the verifier will falsely reject a valid proof.

  • CROSS-CRATE: kinetic-core The core consensus mechanism in the kinetic-core crate relies heavily on the VDF as an , trustless clock. It treats the finalized, reduced Form output as a secure source of pseudo-randomness for electing the next block producer. If the underlying math in this file is flawed, the random seed generation for leader election breaks down, stalling the chain. kinetic-core trusts math.rs to provide the randomness needed for Proof of Stake.

Quick Reference

Here are the essential mathematical facts you need to memorize about this module.

  • Primary Group Operation: Composition and Squaring of Binary Quadratic Forms.
  • Discriminant ($D$) Size: Typically 1024 to 2048 bits (must be a negative prime number).
  • Shanks NUCOMP Threshold ($L$): Defined as the fourth root of the absolute value of $D$ ($|D|^{1/4}$).
  • Gauss Reduction Condition 1: The coefficients must satisfy $-a < b <= a < c$.
  • Gauss Reduction Condition 2: Alternatively, if $a = c$, then $b >= 0$.
  • Data Types: All heavy computation exclusively relies on num-bigint::BigInt for arbitrary precision.

Caution

  • Determinism Constraint: NO FLOATING POINT math is allowed under any circumstances, ever, to prevent consensus forks.
  • Memory Strategy: Aggressive use of references (&BigInt) and mutable in-place operations (&mut self) to avoid expensive heap allocations.
  • Fundamental Invariant: The equation $b^2 - 4ac$ must always equal $D$ at all resting states.

Open Questions / Things to Revisit

There are several rough edges, architectural questions, and areas for future optimization in this file.

  • Memory Allocations in reduce: Even with careful use of references, num-bigint occasionally reallocates internal backing vectors during division and modulo operations. We need to profile the reduce() function under heavy, sustained load to identify these hidden allocations. We should investigate if we can pre-allocate scratch space buffers to eliminate the remaining heap allocations and stabilize the garbage collector. This could provide a speedup for the prover.

  • Alternative Math Backends: Currently, we use the num-bigint crate because it provides pure Rust compatibility, safety, and ease of auditing. However, GMP (GNU Multiple Precision Arithmetic Library) is widely known to be significantly faster for numbers of this specific 1024-bit size. Should we seriously consider a C-FFI wrapper to GMP for high-performance nodes that want to run optimized provers? If we do make this architectural shift, we must prove that determinism is preserved across the FFI boundary, which is risky.

  • Constant Time Execution Considerations: Classical Gauss reduction is fundamentally a variable-time algorithm by its mathematical nature. Its actual execution time depends heavily on the specific coefficients being reduced during the loop. In the context of a VDF prover, this is generally acceptable because we actually want the prover to run as fast as natively possible. Furthermore, VDF proofs are inherently evaluated on public data, meaning secrecy is not the primary concern. However, we need a thorough, professional security audit to ensure this doesn’t open up any obscure timing side-channel attacks on the verifier side of the protocol.

  • NUCOMP vs Standard Composition Crossover: For very small discriminants, the computational overhead of calculating xgcd_partial might actually be worse than just doing standard Arndt composition and then a full reduction. We should establish comprehensive benchmarks to find the exact performance crossover point where NUCOMP becomes superior to standard composition. This might allow us to switch algorithms dynamically based on the discriminant size for optimal performance.

  • WebAssembly Compatibility: If we eventually want light clients or browser wallets to verify VDFs natively in the browser, this math must compile cleanly and efficiently to Wasm.

Important

We need to verify that our aggressive use of num-bigint does not introduce any Wasm-incompatible instructions or performance regressions when compiled to WebAssembly targets.

  • Parallel Reduction Exploration: While the VDF itself is sequential by cryptographic design, could micro-parts of the reduction algorithm be parallelized safely using SIMD instructions? We need to deeply investigate if modern CPU vector extensions (like AVX-512) can accelerate the normalization steps without breaking the strict bit-level determinism requirement.

kyn-vdf: Math Composition (Stage 3)

Reading Time: 35 minutes Depends On: kyn-vdf/src/form.rs, kyn-vdf/src/xgcd.rs

What Is This?

This file contains the optimized mathematical foundation for multiplying and squaring elements within an imaginary quadratic class group. It fundamentally implements the exact algebraic rules required to advance the Verifiable Delay Function (VDF) state securely. Specifically, it provides Shanks’ NUDUPL algorithm for efficiently squaring a single form in the class group space. It also provides Shanks’ NUCOMP algorithm for efficiently composing two different forms together . Finally, it includes a fast binary exponentiation method to raise a given form to an arbitrary, integer power. These complex algorithms take one or two quadratic forms as inputs to begin the computational process. They perform structured polynomial transformations on these specific input variables. They then securely return a canonical reduced product form representing the final outcome. The internal logic heavily leverages the extended Euclidean algorithm to prevent intermediate numbers from growing too large. This careful, deliberate bounding of coefficient sizes ensures that memory allocations remain small and predictable throughout execution. Ultimately, this specific file serves as the absolute core cryptographic engine that guarantees the un-forgeable delay. This un-forgeable delay is what the entire Kinetic network consensus relies upon to function properly. Without this precise implementation, the network would collapse due to forged timestamps. It acts as the strict timekeeper for a globally distributed, trustless environment. Every line of code here represents a carefully considered tradeoff between mathematical purity and bare-metal performance.

Why Kinetic Needs This

Kinetic relies entirely on a Verifiable Delay Function to cryptographically prove that real-world time has elapsed between blocks. This cryptographic proof is actively generated by sequentially computing an immense number of repeated squarings. These sequential squarings happen inside a specialized mathematical structure called an unknown-order group. In our unique architecture, this unknown-order group is defined as an imaginary quadratic class group. This class group uses a carefully selected, cryptographically secure large prime discriminant. The standard, academically pure way to multiply two class group forms is widely known as naive Gaussian composition. However, naive Gaussian composition presents an insurmountable performance challenge for a high-throughput network like Kinetic. During naive Gaussian composition, the intermediate coefficients (a, b, c) balloon to large, unmanageable sizes. These sizes naturally occur immediately before the final standard reduction step is finally permitted to clean them up. Specifically, they can rapidly grow up to the exact mathematical square of the absolute size of the underlying network discriminant. If Kinetic uses a standard, secure 2048-bit discriminant, naive composition would routinely generate intermediate numbers exceeding 4096 bits. Performing raw mathematical arithmetic on 4096-bit numbers is more computationally expensive than operating on 2048-bit numbers. It is also significantly more memory-intensive across the entire hardware execution pipeline. It demands vastly more CPU cycles per individual multiplication, addition, or division instruction. It also forces the underlying system allocator to handle , unpredictable heap allocations continuously. Since generating a VDF proof fundamentally requires many millions of sequential squarings, this performance overhead compounds . Even a microscopic microsecond slowdown per single squaring operation destroys the absolute reliability of the global delay guarantee. This cascading failure breaks the core consensus timing assumptions entirely. Furthermore, it would slow down all other honest nodes on the broader network. These honest nodes must verify the block accurately before safely accepting it into their local decentralized chain. To resolve this critical computational bottleneck, Kinetic mandates the exclusive use of Shanks’ algorithms. Specifically, it enforces the NUDUPL and NUCOMP algorithms for all class group operations without exception. These specialized algorithms intercept the intermediate components before they are ever allowed to explode in memory size. They apply a partial extended greatest common divisor (XGCD) routine directly mid-calculation. This mid-calculation intervention actively reduces the internal values dynamically and predictably. This specific methodology guarantees that the intermediate BigInt structures stay bounded. They generally remain very close to the exact original bit-size of the discriminant itself throughout the entire execution lifetime. bounded sizes logically mean that dynamic memory allocations remain entirely predictable. They are remarkably cache-friendly and significantly faster to process directly at the hardware CPU level. By relying on these specific, tuned optimizations, Kinetic levels the computational playing field. This creates a fundamentally fair network for all disparate participants regardless of hardware budgets. If the operation were intentionally memory-hard instead of purely sequential, attackers could easily utilize specialized memory architectures. This would allow them to calculate the VDF faster and maliciously manipulate the network timestamp mechanism. Hardware advantages like parallel ALUs or expensive custom FPGAs become bottlenecked. They are constrained by the inherently sequential, step-by-step nature of this exact bounded arithmetic design. Without math.rs and its tuned, memory management, Kinetic’s consensus mechanism would be slow and insecure. It would be instantly vulnerable to well-funded participants leveraging superior, specialized computing hardware. Therefore, the density and performance of this single file arguably define the security perimeter of the entire Kinetic ecosystem.

How It Works

The overarching execution flow within this file is an absolute masterclass in managing arbitrary-precision BigInt memory allocations. It manages these allocations safely while performing dense, cryptography over complex algebraic structures. The entire mathematical processing pipeline is divided into three primary algorithms. Each of these algorithms serves a distinct, irreplaceable, and designed role in the full VDF lifecycle. The overall engineering objective here is to ensure that no single operation causes an unexpected garbage collection pause. It is equally critical that no operation forces the host CPU to catastrophically stall on predictable cache misses.

1. Shanks’ NUDUPL (Squaring)

-> See: kyn-vdf/src/math.rs — Lines 227 to 310 NUDUPL is unequivocally the absolute core of the prover’s execution loop. It easily ranks as the most frequently executed single function anywhere in the entire Kinetic network stack. It is a specialized variant of Shanks’ composition and exclusively tailored for squaring a single mathematical form. When a cryptographic form is composed with itself, many algebraic terms gracefully cancel each other out. This symmetric cancellation and permanently reduces the initial processing overhead. This is heavily contrasted with the much slower process of composing two entirely different forms together. The algorithm starts execution by cleanly extracting the a, b, and c coefficients directly from the input form’s state. It carefully avoids unnecessary copying during this initial coefficient extraction phase. It then immediately triggers the optimized partial extended Euclidean algorithm. This trigger occurs via a strict, direct call to the dedicated xgcd_partial function module. Instead of running naively until a full, final zero remainder is predictably reached, xgcd_partial is instructed to stop early. It deliberately halts its internal iterative loop exactly when the computed remainders cross a specific mathematical threshold. This strict threshold is precisely the fourth root of the absolute mathematical value of the network discriminant. This deliberate premature termination yields a specialized 2x2 mathematical transition matrix. This matrix is composed of remarkably small, easily manageable integer coefficients. The algorithm carefully takes this small transition matrix and cross-multiplies it iteratively with the original form components. This specific matrix cross-multiplication yields a brand new set of mathematical coefficients . Crucially, these newly minted coefficients are naturally, already partially reduced by design. By performing this precise matrix multiplication sequence, NUDUPL skips the dangerous computational phase entirely. This skipped phase is exactly where naive coefficients would have normally, disastrously doubled in bit length. This section heavily and intentionally utilizes Rust’s powerful num_integer::Integer trait to execute precise division semantics. Specifically, it relies on div_floor and mod_floor rather than using the basic native operators built into the Rust language. In strict cryptographic mathematics over class groups, taking a modulo of any negative number must universally wrap around. It must always wrap directly to a positive remainder to maintain structural integrity. Rust’s standard % operator simply truncates the division lazily toward zero. This default truncation yields destructive negative remainders for all negative inputs. Using % here would instantaneously corrupt the deep structural validity of the class group form permanently. This would result in invalid VDF proofs being broadcast to the network. After the transition matrix is successfully and safely applied, NUDUPL always triggers a standard, final reduction step. This final reduction logically guarantees that the newly squared form is canonical, unique, and minimal. Memory management dynamically here requires meticulous, manual oversight to ensure high performance without memory leaks. Because BigInt structures reside exclusively on the heap by design, they naturally drop their memory when fully consumed by an operator. The codebase heavily utilizes cleanly borrowed references (e.g., &a + &b) to securely execute arithmetic. It does this without erroneously taking permanent ownership of the transient variables. It manually triggers an explicit clone() only when a specific intermediate value must be preserved. This preservation only happens across volatile mutable boundaries for a subsequent, necessary calculation step.

2. Shanks’ NUCOMP (General Composition)

-> See: kyn-vdf/src/math.rs — Lines 312 to 420 NUCOMP brilliantly generalizes the fast composition logic to handle two different mathematical forms safely. Because the raw inputs are inherently distinct and asymmetric, the algorithm simply cannot rely on the symmetric mathematical cancellations. It cannot use the elegant shortcuts that NUDUPL heavily exploits for raw speed. Instead, it must painstakingly compute the full, unadulterated greatest common divisor . It computes this GCD specifically between the respective a coefficients of both individual input forms. This heavy initial phase involves safely calculating specific intermediate structural values correctly. These values are consistently referred to as s and m in deep academic cryptographic literature. Once these foundational values are cleanly found, the algorithm proceeds carefully to solve a series of strict linear congruences. The ultimate, necessary objective of these complex, interconnected congruences is to discover a single unified structural variable. This critical variable is typically denoted simply as V in the codebase. This specific variable V must uniquely and simultaneously satisfy two rigid, independent modular constraints. These precise constraints actively involve the original, unmodified b coefficients of the parent forms. V essentially acts as the critical mathematical bridge required to merge the independent states. It merges the states of the two distinct input forms into a single cohesive mathematical entity. If the protocol naively combined the forms directly using V right away, disaster would strike. The resulting leading A coefficient would instantaneously explode in size directly to the product of a1 * a2. NUCOMP elegantly and robustly prevents this impending, memory explosion . It does this by immediately feeding the discovered V and the original coefficients cleanly into xgcd_partial. Exactly like the NUDUPL implementation, it reliably retrieves a specialized transition matrix directly based exclusively on early-stopping GCD remainders. The various mathematical components of the two parent forms are then carefully cross-multiplied with this specific transition matrix. This cross-multiplication produces the final, safely bounded coefficients representing the merged state. This complex phase necessarily involves an high volume of temporary, volatile BigInt allocations in memory. These allocations are immediately, ruthlessly dropped from local scope immediately after their singular use. Managing these precise lifetimes safely during these dense, chaotic matrix multiplications is critical. It is the only reliable way to avoid devastating, compilation-halting borrow-checker errors in Rust. Finally, a standard, rigorous reduction pass is reliably applied to definitively yield the canonical product. This ensures the final representation of the two composed forms is minimal and unique. Because of the heavy initial GCD computations and complex, demanding congruence solving, NUCOMP is naturally much slower than NUDUPL. Therefore, the Kinetic network protocol and ensures it never, ever uses NUCOMP for the intensive sequential squaring loop. Instead, it is , unapologetically reserved exclusively for the verifier protocol execution phase. The verifier uses it sparingly but crucially to conclusively validate the final mathematical structure of the incoming proof.

3. Fast Exponentiation (fast_pow)

-> See: kyn-vdf/src/math.rs — Lines 422 to 502 This robust function implements a efficient, tightly controlled algorithmic loop. It precisely computes f^n for an arbitrarily large integer n with remarkable efficiency. It fundamentally and heavily utilizes the classic left-to-right binary exponentiation technique universally known in cryptography. This elegant technique is often colloquially called the square-and-multiply algorithm by cryptographic engineers. The strict algorithm carefully evaluates the absolute binary bit representation of the exponent n. It securely evaluates this representation sequentially, starting securely from the absolute most significant bit down to the absolute least. It first carefully initializes a mutable accumulator variable securely in memory. This accumulator typically, reliably starts directly as the mathematical identity form of the defined class group. For every single, distinct bit successfully evaluated in the binary string, a specific action occurs unconditionally. The accumulator form is unconditionally, reliably, and forcefully squared using the fastest available method. This critical, repetitive squaring operation is executed directly using the optimized nudupl function described extensively above. If the current binary bit being evaluated currently happens to be exactly a binary 1, an additional operational step occurs. This asymmetric, additional step is and securely triggered by the internal logic branch. The running accumulator is multiplied cleanly by the exact original base form f. It executes this multiplication using the versatile, generalized nucomp function for distinct forms. This deliberate, mathematical strategy dramatically reduces the required total operations required to finish safely. It drops the complexity from a purely, disastrously linear O(n) to an fast, manageable logarithmic O(log n). A , critical performance optimization nested deeply inside this hot loop is opportunistic reduction. During continuous, heavy mathematical composition, small, subtle inefficiencies can gradually, inevitably compound. This compounding causes the underlying form coefficients to slowly, consistently creep upward in raw size over time. The carefully designed algorithm actively intercepts this subtle, growing threat proactively. It does this by continuously, checking the absolute bit length securely of the accumulator’s internal a coefficient. If this specific, critical bit length unexpectedly exceeds exactly half the total bit width of the network discriminant, action is taken. The smart algorithm instantly, forcefully intervenes directly in the execution flow. It forces a rigid, standard reduction pass safely on the accumulator form. It successfully executes this reduction before allowing the loop to proceed cleanly to the next exponent bit. This brilliant, preemptive safeguard eliminates the , very real risk of a performance cliff. It prevents this cliff from occurring dangerously near the very end of very large, long-running exponentiations. The hot loop constantly, safely overwrites the main accumulator with newly generated forms continuously. This constant overwriting ensures old memory is cleanly, freed by the Rust garbage collector .

Key Pieces

The following structural components represent the most critical moving parts of the mathematical engine safely:

fn nudupl(f: &Form, d: &BigInt) -> Form

#![allow(unused)]
fn main() {
fn nudupl(f: &Form, d: &BigInt) -> Form
}

-> See: kyn-vdf/src/math.rs — Lines 227-310 This is undoubtedly the single, absolute most critical mathematical function for the overall performance of the entire VDF prover. It directly, consistently provides the high-speed squaring capability that fundamentally drives the entire verifiable delay sequence securely. Its raw, unadulterated performance characteristics directly, fundamentally dictate the exact real-world delay duration. This duration is inherently, required to maintain Kinetic’s target block intervals accurately. It prudently, intelligently receives the target form immutably to actively prevent unnecessary, expensive heap cloning. It then heavily internally manages its own transient, temporary memory allocations with absolute precision. Any regressions internally in this function directly, immediately weaken the economic security of the entire Kinetic network architecture significantly.

fn nucomp(f1: &Form, f2: &Form, d: &BigInt) -> Form

#![allow(unused)]
fn main() {
fn nucomp(f1: &Form, f2: &Form, d: &BigInt) -> Form
}

-> See: kyn-vdf/src/math.rs — Lines 312-420 This crucial function exclusively provides the generalized, robust composition logic for safely handling distinct, non-identical forms. It is an absolute, unavoidable requirement for the Wesolowski verification process executed securely by all honest network nodes. The network verifier must reliably, accurately multiply a standard cryptographic base form directly against a proof form safely. This proof form is raised to a dynamic, unpredictable challenge power derived from network state safely. It safely navigates complex linear congruences continuously without risking disastrous integer overflows . It does this while also avoiding precision loss or fatal division panics during runtime.

fn fast_pow(base: &Form, exp: &BigInt, d: &BigInt) -> Form

#![allow(unused)]
fn main() {
fn fast_pow(base: &Form, exp: &BigInt, d: &BigInt) -> Form
}

-> See: kyn-vdf/src/math.rs — Lines 422-502 This is the absolute primary orchestrator designated for safely raising any given mathematical form to an arbitrary BigInt power. It relies fundamentally, heavily on intelligently alternating sequenced calls to both nudupl and nucomp safely. It bases these specific calls entirely on strict, sequential binary bit evaluation securely. It matters intensely for rapid proof generation, where the prover must cleanly, accurately compute a quotient power dynamically. It effectively, permanently guarantees that even large, complex exponentiations reliably complete rapidly. They complete consistently in a tiny fraction of a single second rather than dangerously hanging indefinitely and stalling consensus.

BigInt Re-borrowing Patterns (&(&a + &b)) The Rust BigInt type definitely does not magically implement the standard Copy trait effortlessly. This is exactly because it relies heavily, consistently on dynamic, variable-length heap allocations exclusively. Consequently, utilizing any standard mathematical operators natively like + or * actively consumes the underlying values permanently by default. To prevent the strict Rust compiler from , prematurely destroying variables that are needed later, you must borrow them securely. You achieve this safely using the explicit & operator cleanly. Understanding exactly when to properly use &a, when to let the compiler safely consume a directly, and when to intentionally call a.clone() is . This careful, deliberate juggling of transient reference lifetimes is exactly what allows the cryptographic math to safely run at blazing speeds. It does this continuously without allocating endlessly and predictably crashing the entire host node gracefully. It is arguably the absolute most difficult aspect of writing -performant, memory-safe cryptography in pure Rust securely.

Caution

num_integer::Integer methods (mod_floor, div_floor) This sensitive module and universally requires correct Euclidean division rules when handling any negative numbers safely. Native arithmetic operators trivially like % in standard Rust simply, dangerously truncate toward zero continuously. This actively produces results that actively, break the underlying cryptographic structures securely. You must , use mod_floor consistently to ensure any negative dividends always wrap cleanly into positive remainders reliably. Failing to do so accurately will silently, dangerously yield invalid mathematical forms safely. This will permanently, irrevocably ruin the entire consensus proof validity .

How This Connects to the Rest of Kinetic

FORWARD DEPENDENCY: kyn-vdf/src/prover.rs The prover entirely, depends on nudupl to reliably, predictably execute the many millions of sequential squarings required. These squarings are required for the fundamental delay phase safely. Its core architectural execution loop is fundamentally, beautifully just a very tight, optimized continuous sequence . It is a sequence of continuous nudupl invocations executing flawlessly. Without optimized nudupl, the prover simply cannot logically securely generate a valid proof of elapsed time reliably.

FORWARD DEPENDENCY: kyn-vdf/src/verifier.rs The verifier relies structurally, on nucomp and fast_pow to independently securely validate proofs. It validates the pure mathematical integrity of all incoming proofs efficiently. It safely ensures that the block producer actually spent the exact required computational time reliably. It ensures they did not attempt to artificially forge the delay securely. This prevents malicious, aggressive nodes from endlessly spamming the secure network with instantly generated blocks safely.

CROSS-CRATE: kinetic-consensus/src/engine.rs The global, authoritative consensus engine actively, intelligently monitors the exact real-world time required . It monitors the time to successfully securely execute these specific mathematical functions reliably. It intelligently, dynamically uses this historical benchmarking data constantly to dynamically adjust . It cleanly adjusts the required VDF target iterations accurately for future upcoming network epochs safely. The pure algorithmic efficiency internally of math.rs is intrinsically, deeply, and directly permanently tied purely securely. It is tied to the absolute stability of the global, decentralized block interval reliably across all global continents safely.

Quick Reference

  • Need to sequentially square a form? Always reliably use nudupl(f, D) directly.
  • Why avoid nucomp for squaring? Do not use nucomp for simple squaring safely as it is computationally slower securely.
  • Need to securely combine two distinct, separate cryptographic forms? Always securely confidently use nucomp(f1, f2, D) reliably.
  • Need to securely precisely raise a specific base form smoothly to a very large power? Invoke fast_pow(f, exponent, D) reliably for lightning-fast explicit binary exponentiation securely.
  • Doing complex group math safely with potential negative BigInts? Never, ever reliably use % natively. Always heavily rely on mod_floor securely from the trusted Integer trait .
  • Compiler complaining about erroneously moved values abruptly? Check your mathematical expressions very carefully structurally.
  • How to fix moved values securely? Ensure all critical mathematical operands are properly safely borrowed with & reliably.
  • Why is exponentiation safely inexplicably slowing down over time? You likely mistakenly removed the opportunistic check.
  • What got removed? You probably accidentally fully disabled the critical opportunistic standard reduction check deeply buried safely in fast_pow .

Open Questions / Things to Revisit

Warning

Timing Side-Channels in Exponentiation The fast_pow function branches its execution path based on the individual bits of the given exponent. It uses a strict square-and-multiply branching logic that is predictable. While VDF exponents are inherently public data in the Kinetic protocol, we must be careful. We must deeply analyze if this predictable branching exposes any subtle hardware vulnerabilities. We must investigate if it makes the network susceptible to CPU cache timing attacks. If a malicious node can optimize execution based on timing data, it ruins the delay proof.

Memory Allocation Overheads During Composition Both nucomp and nudupl rapidly allocate and cleanly deallocate dozens of temporary BigInt structures. They do this during every single mathematical execution loop. Running this continuously for millions of consecutive iterations places unrelenting stress on the system allocator. The default Rust allocator struggles to keep up with the continuous heap fragmentation. We should investigate implementing a custom slab allocator. Alternatively, we could build a pre-allocated BigInt memory pool specifically for this math module. This could reduce garbage collection pauses and speed up proof generation.

Important

WebAssembly Stack Limits for Light Clients If Kinetic nodes are ever compiled down to WASM for browser-based light clients, we face risks. The internal xgcd_partial routine must be thoroughly and audited. If it relies heavily on deep recursive function calls, it will fail. Recursive calls easily and reliably blow up the limited WASM call stack in browsers. An iterative rewrite might be necessary for broader, stable client compatibility. We must test this logic in a WASM sandbox before deploying light nodes.

Discriminant Pointer Indirection Overhead We are currently actively passing the discriminant D directly by reference. We pass it into every single mathematical function repeatedly across the entire codebase. This causes constant, unnecessary pointer indirection and cache misses. We should investigate encapsulating D securely inside a persistent ClassGroupContext struct. This would immediately eliminate this constant pointer passing natively. It would also and permanently clean up the external API surface.

Crate: kyn-vdf

Stage: 4

Reading Time: 45 minutes

Depends On: kinetic-core (hashing primitives, seed generation), num-bigint (arbitrary precision integer arithmetic), sha2 (SHA-256 implementation)

What Is This?

This document provides an , detailed conceptual breakdown of the deterministic prime generation logic used by the Kinetic network. The code resides in the kyn-vdf crate, specifically within kyn-vdf/src/chia.rs covering lines 1 to 235. Its sole responsibility is to generate a , 1024-bit negative prime number known as a discriminant for the Verifiable Delay Function (VDF). Without a valid, deterministically generated prime discriminant derived exactly from a network seed, the Kinetic VDF cannot operate securely. This document systematically maps the Rust implementation details directly to the dense mathematical requirements of the cryptographic protocol. It is critical for any core developer touching the consensus rules to thoroughly understand every single step of this exact file. A single bit of difference here will immediately cause a hard fork of the entire Kinetic blockchain. This is the single source of truth for the most sensitive cryptographic parameter in the system.

Why Kinetic Needs This

To fully appreciate the immense complexity of this file, you must first understand the fundamental “clock problem” in decentralized consensus systems. In a centralized database, a single authoritative server timestamps every transaction and event. This ensures that everyone agrees on the precise order of events. In a decentralized blockchain like Kinetic, there is no central authority to tell time. Furthermore, nodes fundamentally cannot trust each other’s internal system clocks. If nodes blindly trusted local timestamps, malicious actors could effortlessly spoof them. They would do this to strategically manipulate block rewards, reorder transactions, or rush the network’s block production schedule. Kinetic solves this existential threat by employing a cryptographic primitive called a Verifiable Delay Function (VDF). The VDF serves as a decentralized, cryptographic clock.

The Role of the VDF

A VDF acts as a strict, cryptographic clock that ticks predictably for the entire decentralized network. It is an algorithm designed to require a verifiable, unavoidable amount of sequential computational time to execute. Crucially, VDF computations are inherently and purposefully sequential by design. They cannot be sped up by throwing more CPU cores at them. They cannot be parallelized across a GPU cluster or an ASIC farm. Once the VDF finally finishes its long, arduous computation, the final mathematical proof it generates must be fast for other network nodes to verify. Our specific implementation of a VDF in Kinetic relies on a complex mathematical process known as “repeated squaring”. This squaring takes place within a rigorous structure known as an ideal class group of an imaginary quadratic field.

The Math of the Ideal Class Group

The absolute security, integrity, and operational safety of this ideal class group depend entirely on the properties of its core parameter. This parameter is universally known as the discriminant, which is denoted as $D$. The discriminant fundamentally and structurally defines the specific geometric and algebraic properties of the class group that the VDF operates within. The foundational mathematical laws governing the class group strongly require $D$ to be a strict negative prime number. This requirement ensures the group behaves exactly as expected cryptographically, without any underlying structural weaknesses.

Caution

If $D$ is inadvertently generated as a composite number (meaning it can be factored into smaller primes), the system fails. The entire security model collapses instantly and catastrophically in this scenario. An attacker who knows the secret prime factors of a composite discriminant $D$ can instantly leverage specialized mathematical shortcuts. One such devastating shortcut is the Chinese Remainder Theorem. These mathematical shortcuts allow the attacker to instantly compute the VDF result in a fraction of a millisecond. This and bypasses the required time delay that the VDF is supposed to enforce. This effectively grants the attacker the god-like ability to time-travel within the consensus protocol. They could effortlessly rewrite history, steal block rewards, and permanently destroy the blockchain’s integrity.

The Determinism Dilemma

Therefore, ensuring that $D$ is prime is a matter of life and death for the Kinetic blockchain. However, we face a severe, paradoxical cryptographic dilemma. We desperately need a trusted setup to generate this prime, but we cannot trust any single human entity or server to perform it for us.

Warning

If the core developers arbitrarily generated the prime $D$ and hardcoded it into the Kinetic software, they could secretly retain its factors. This would happen if they deliberately, maliciously chose a composite number that merely looked prime to standard, shallow tests. Doing so would create a , fully undetectable backdoor into the heart of the network.

To prevent this doomsday scenario, the decentralized network itself must logically generate $D$ dynamically for every single block that is produced. The discriminant must be generated deterministically from publicly verifiable, unpredictable data. Specifically, this data is the current block’s cryptographic seed. Because the seed changes randomly with every newly minted block, the generated discriminant changes constantly alongside it. This relentless changing ensures that no attacker ever has the necessary time to pre-compute the factors before the block becomes obsolete. But standard prime generation algorithms heavily and inherently rely on random number generators (RNGs). They use RNGs to pick candidate numbers to test. They also use RNGs to heavily select the testing bases for the probabilisitic primality checks. We cannot use random number generators in a strict blockchain consensus protocol under any circumstances whatsoever. If we foolishly did, Node A might roll a different random number than Node B. This would inevitably result in two entirely different prime discriminants being generated from the exact same block state. The network would immediately fork, shatter, and fail to agree on the state of the VDF clock. This specific file, chia.rs, is painstakingly and purposefully engineered to solve this exact, complex determinism dilemma. It and surgically strips all randomness out of standard prime generation algorithms. It replaces this randomness with strict, deterministic, seed-based derivation pipelines. It ensures that the generation of this critical cryptographic parameter is reproducible. It ensures it is fully verifiable and secure across the entire decentralized network.

How It Works

The intricate, relentless process of forging a tiny 32-byte seed into a colossal 1024-bit negative prime discriminant is rigorous. It is a multi-stage pipeline specifically designed for absolute determinism and absolute maximum performance. Every single step is carefully calibrated to ensure that given identical input, the output is always identical. This holds true down to the exact, precise bit level. The logic heavily handles raw memory management, low-level raw bit manipulation, fast-path divisional math, and deep cryptographic exponentiation sequentially.

Step 1: The Input Space and The Iterative Counter

-> See: kyn-vdf/src/chia.rs — Lines 70-85 The primary entry point for this dense mathematical logic is the critical hash_prime function. It takes exactly two defined arguments. The first argument is a 32-byte seed (derived cryptographically from the current block state). The second argument is a target bit length (which is and immutably fixed to exactly 1024 for our specific VDF implementation). We cannot simply hash the seed once and expect to magically receive a prime number. Finding a prime of this incredible magnitude is a slow process of relentless, iterative trial and error. Therefore, we initialize a specific u64 64-bit integer counter to exactly 0 before beginning the search. Using a full 64-bit counter securely provides over 18 quintillion potential iterations. This guarantees we will eventually locate a prime, no matter how unlucky the starting seed is. This counter is the core deterministic mechanism that safely allows us to systematically iterate through an infinite sequence of candidate numbers cleanly. If our first candidate number thoroughly fails the rigorous primality test, we simply increment this counter by exactly 1. We then boldly start the entire hashing process over again using the incremented counter. Because the starting seed is identical across the network, the sequence of tested candidates is flawlessly synchronized. The counter increments predictably across all running nodes, ensuring perfect consensus.

Step 2: The SHA-256 Concatenation Pipeline

-> See: kyn-vdf/src/chia.rs — Lines 86-115 A single execution of the standard SHA-256 algorithm via the sha2 crate only ever produces 256 bits of raw output data. We urgently require a 1024-bit number to successfully meet the strict cryptanalytic security bounds of the ideal class group. To effectively construct a number of this extreme magnitude, the function enters a specialized, high-speed hashing loop. It uses the Digest::update trait methods from the sha2 crate to dynamically append the data. It appends the current numerical state of our 64-bit counter directly to the 32-byte seed in memory. It then hashes this concatenated data using the SHA-256 engine. This generates the very first foundational block of 256 bits for our mathematical candidate. But we are still short of our required mathematical goal; we are exactly 768 bits short. The code then carefully appends an inner loop index (or modifies the counter slightly) and calls Digest::finalize. It hashes the seed once again, obtaining the very next sequential block of 256 bits. This specific, relentless concatenation process continues iteratively, hashing over and over again in a tight, hot loop. The resulting raw bytes are appended sequentially into a large, continuously growing Vec<u8> memory buffer. This memory buffer is pre-allocated in RAM to avoid resizing overhead. This intense loop continues unabated until we have accumulated exactly enough raw bytes to form a full, uninterrupted 1024-bit integer. The meticulous use of a pre-allocated buffer is critical here. It minimizes expensive, slow memory reallocations during this high-frequency, CPU-intensive execution loop.

Step 3: Byte-to-BigInt Conversion

-> See: kyn-vdf/src/chia.rs — Lines 116-130 Once our memory buffer is stuffed full of deterministic, pseudo-random bytes painstakingly derived from the SHA-256 hashes, we must proceed. We must carefully convert this raw buffer into a usable mathematical object for processing. Kinetic purposefully utilizes the popular, optimized num-bigint crate for actively managing these numbers. These numbers vastly exceed the capabilities of standard 64-bit or 128-bit CPU limits. The raw bytes sitting in our buffer are read using the BigInt::from_bytes_be function. This specific function parses them in strict big-endian format. In big-endian architecture, the most significant, largest byte always comes first at exactly index 0 of the array. Big-endian is specifically chosen to guarantee absolute cross-platform compatibility across the entire decentralized network. It ensures different CPU architectures (like Intel x86 versus Apple ARM) read the raw memory in exactly the same way. At this exact moment in the execution flow, the candidate is merely a random-looking 1024-bit integer residing in the heap. Statistically speaking, it is overwhelmingly likely to be a composite number. This means it is not yet prime and must be relentlessly, tested.

Step 4: The Bit Twiddling (Forcing Oddness and Exact Sizing)

-> See: kyn-vdf/src/chia.rs — Lines 131-150 Before we foolishly waste valuable CPU cycles testing the raw candidate for primality, we intervene. We apply strict bit-level constraints to forcibly shape it . First, a prime number (greater than the number 2) , unconditionally must be an odd number. Instead of wastefully testing if the candidate is even and discarding it if it is, we forcefully modify the number’s bits. We manipulate the bits directly to permanently make it odd, significantly saving execution time. We achieve this by calling the set_bit(0, true) method on the BigInt structure. This directly and instantly sets the very least significant bit in the binary representation to a value of 1. Second, we must and cryptographically guarantee that the candidate is exactly 1024 bits long. It must not be a single bit more, nor a single bit less. If the top bytes generated from our SHA-256 concatenation pipeline happened to contain leading zeros, a problem arises. Our final number might artificially and disastrously shrink to only 1000 bits or 1010 bits long. A discriminant that is inadvertently too small reduces the cryptanalytic security of the VDF. It makes the entire network vulnerable to devastating mathematical attacks. To preemptively prevent this fatal flaw, we forcefully set the most significant bit. Since bits are always 0-indexed in Rust, we specifically call set_bit(1023, true). This locks the 1024th bit to a permanent, unchangeable value of 1. This raw, low-level bitwise manipulation heavily guarantees the candidate permanently operates at the exact geometric scale. This is the precise scale required by the underlying class group security proofs.

Step 5: The Fast Path (Deterministic Small Prime Filtering)

-> See: kyn-vdf/src/chia.rs — Lines 15-25 (inside is_probable_prime) Rigorous primality testing via complex modular exponentiation is computationally agonizing. It is brutally slow for the CPU to perform repeatedly. We urgently want to avoid it entirely if the candidate number is obviously and trivially not prime. The code implements a optimized, lightning-fast mathematical filter to act as an bouncer. It and checks if the candidate is cleanly divisible by the first few hundred small prime numbers. These are primes such as 3, 5, 7, 11, 13, 17, and so forth. It does this with extreme mathematical efficiency by computing the mathematical modulo of the candidate. It calculates this against a single, pre-computed product of all these small primes combined together. This product is usually established early, perhaps even at compile time. This optimization ensures that a single, solitary modular division operation can implicitly check hundreds of prime factors at once. If the modulo operation yields exactly zero, the candidate is definitively cleanly divisible by at least one of these tiny small primes. In this case, we instantly and ruthlessly reject it as a composite number. We then immediately increment our main u64 iteration counter from Step 1. We restart the entire SHA-256 hashing pipeline from scratch. This elegantly simple mathematical division step definitively eliminates a vast majority of composite candidates. It does this in a mere fraction of a millisecond, dramatically increasing overall block validation speed across the node.

Step 6: The Miller-Rabin Mathematical Setup

-> See: kyn-vdf/src/chia.rs — Lines 26-40 If a fortunate candidate successfully survives the brutal small prime division filter, it progresses to the next stage. It is subsequently passed directly to the core Miller-Rabin probabilistic test logic. The acclaimed Miller-Rabin algorithm requires a very specific, strict mathematical decomposition of the candidate number. This decomposition must occur exactly before the actual testing logic can begin. Specifically, it requires writing the candidate minus one (expressed as $n - 1$). It must be written in the form of $2^r \cdot d$. In this formulation, the variable $d$ must be an odd number. The Rust code achieves this complex decomposition by utilizing fast binary methods. Methods like trailing_zeros() are used to rapidly and instantly identify the exact powers of 2. It systematically and shifts the binary representation rightward. It cleanly factors out the powers of 2 (carefully and counting them as the variable $r$). It continues this shifting until an unambiguously odd number definitively remains. This final odd number is then assigned to the variable $d$ for further calculations in the next step. This exact, precise decomposition is , fundamentally necessary. It is required for the complex modular exponentiation steps that form the beating heart of the Miller-Rabin test.

Step 7: The Deterministic Miller-Rabin Loop

-> See: kyn-vdf/src/chia.rs — Lines 41-69 This specific, intense step is the absolute cryptographic heart of the entire primality verification process. It is where the CPU predictably and unavoidably spends the vast majority of its execution time. Standard, textbook Miller-Rabin algorithms haphazardly choose random numerical bases. They test against the candidate using these random bases to determine primality probabilistically. As we have firmly and repeatedly established, we cannot use random bases in the Kinetic consensus protocol. Under no circumstances whatsoever is randomness permitted here. Instead, we use a fixed, hardcoded static array containing exactly the first 25 consecutive prime numbers. Specifically, these deterministic bases are exactly 2, 3, 5, 7, 11, 13, and so on up to the 25th prime. These serve as our purely deterministic, unvarying testing bases. The function resolutely enters a large, overarching loop specifically labelled 'outer. It sequentially iterates through these 25 bases one by one without fail, in exact order. For each individual base, we compute the heavy modular exponentiation equation. We use the modpow function to compute: $base^d \pmod n$. If the resulting computed value is exactly 1 or exactly $n - 1$, the candidate survives. It has successfully passed the rigorous test for that specific base, and we cautiously move to the very next base in the array. If the initial result is neither 1 nor $n - 1$, we inevitably enter a secondary, intensive inner squaring loop. We repeatedly square the intermediate mathematical result exactly $r - 1$ times. We continually and obsessively check if the intermediate value becomes exactly $n - 1$ at each step of the squaring. If the candidate tragically fails these strict, mathematical conditions for even a single deterministic base, it is over. It is definitively, proven to be a composite number. We safely reject it immediately, mercilessly halting the test and saving CPU cycles. However, if it successfully passes the rigorous mathematical conditions for all 25 specific deterministic bases, a mathematical guarantee is reached. The mathematical probability of it being a composite number is astronomically, practically exactly zero. We officially accept the hardened candidate as a valid, sound, securely derived prime number.

Step 8: Constructing the Final Negative Discriminant

-> See: kyn-vdf/src/chia.rs — Lines 180-235 The grueling, intense hash_prime function eventually returns our successfully tested, verified 1024-bit prime number. But our complex cryptographical work is not quite complete yet. The create_discriminant function confidently takes this raw prime and processes it further. It shapes it further to meet the specific geometric requirements of the ideal class group. Class groups , unequivocally require a negative discriminant to function correctly as an imaginary quadratic field. Positive discriminants will fail and fundamentally break the VDF math. Furthermore, the mathematical group structure and non-negotiably mandates an additional rule. The discriminant must satisfy a specific modular congruence: $D \equiv 1 \pmod 4$. The code applies careful modular arithmetic to gently adjust the raw prime accordingly. It does this specifically without accidentally breaking its hard-won primality. It first negates the prime directly using the standard negation operator to deliberately make it negative. It then computes the modulo against the number 4 using the Remainder operations provided by Rust. If necessary, it applies a carefully calculated mathematical offset. This offset might be a simple subtraction or addition, to satisfy the strict modulo condition. It simultaneously ensures the underlying number itself remains a valid, untouched prime. The final, resulting BigInt structure is the tested, negative discriminant $D$. It is deterministically derived and ready to empower the VDF clock.

Key Pieces

Function: is_probable_prime(candidate: &BigInt) -> bool

#![allow(unused)]
fn main() {
is_probable_prime(candidate: &BigInt) -> bool
}
  • What it does: Executes the exceptionally heavy Miller-Rabin primality test. It uses a fixed, predefined deterministic set of exactly 25 small prime bases to eliminate randomness. It first efficiently filters out obvious composites using raw numerical division via a pre-computed product. Then, it systematically runs 25 rigorous mathematical rounds of intense modular exponentiation via the heavy modpow function to achieve near-absolute probabilistic certainty.
  • Location: kyn-vdf/src/chia.rs — Lines 10-69
  • Why it matters: This crucial function is the ultimate, arbiter of mathematical truth for all VDF parameters in Kinetic. Because it uses deterministic bases instead of unpredictable random ones, it is safe. It ensures that every single node in the global decentralized network invariably reaches the exact same conclusion about a candidate number’s primality. This and fully preserves critical network consensus.

Function: hash_prime(seed: &[u8], length_bits: usize) -> BigInt

#![allow(unused)]
fn main() {
hash_prime(seed: &[u8], length_bits: usize) -> BigInt
}
  • What it does: Comprehensively orchestrates the entire, complex seed hashing sequence. It manages the raw byte accumulation pipeline, precise bit manipulation logic, and the overarching primality testing loop. It manages the vital u64 incrementing counter that flawlessly guarantees we will eventually find a valid prime, regardless of what the initial starting seed was.
  • Location: kyn-vdf/src/chia.rs — Lines 70-179
  • Why it matters: This essential function is the raw, unbridled engine of our network’s determinism. It takes wildly unpredictable block state (the seed) and deterministically maps it safely to a specific, secure mathematical parameter (a 1024-bit prime). It does this in a way that is universally reproducible by anyone on any hardware setup. It forms the most crucial bridge between standard cryptographic hashing and pure, unadulterated number theory.

Function: create_discriminant(seed: &[u8], length_bits: usize) -> BigInt

#![allow(unused)]
fn main() {
create_discriminant(seed: &[u8], length_bits: usize) -> BigInt
}
  • What it does: Serves as the primary, accessible public interface for the entire, complex discriminant generation module. It calls hash_prime to securely obtain the foundational base prime. It then applies the final, strict mathematical constraints required by imaginary quadratic fields. Specifically, it applies the strict negativity constraint and the unyielding modulo 4 rules.
  • Location: kyn-vdf/src/chia.rs — Lines 180-235
  • Why it matters: This precise function returns the exact, final mathematical parameter that the heavy VDF evaluator and the network’s lightweight verifiers will dependently use to run the cryptographic clock. Its absolute correctness is totally . If this function ever fails to consistently produce a valid, correctly formed discriminant, the security of the entire proof-of-time layer is fundamentally and irrevocably compromised.

Rust Concept: Loop Labels and continue 'outer

  • What it does: Rust safely allows developers to label specific loops with a distinctive tick mark. For example, writing 'outer: for i in .... When you are buried deep inside a nested inner loop, you can use the explicit, powerful command continue 'outer;. This immediately and abruptly halts the inner loop and jumps straight to the very next iteration of the labelled outer loop.
  • Location: kyn-vdf/src/chia.rs — Heavily and strategically used inside the core Miller-Rabin primality test loops. This is specifically between Lines 41-69.
  • Why it matters: Saif, you will see this specific, idiomatic pattern used extensively and deliberately in the primality tests. When testing a specific base in Miller-Rabin, if the inner squaring loop finds the critically required value $n-1$, the candidate has successfully passed for that specific base. We do not need to wastefully finish executing the remainder of the inner squaring loop. We use continue 'outer; to jump straight out and immediately begin testing the very next base in the sequence. This avoids the painful need for complex, messy, stateful boolean tracking variables (like let mut passed = false;). It effectively keeps the code fast, remarkably readable, and idiomatic. It is an exceptionally powerful control flow tool for organizing complex, heavily nested mathematical algorithms like this without losing track of state.

Rust Concept: Bit Manipulation on BigInt

  • What it does: The standard, widely used num-bigint library provides direct, efficient bit-access methods. One key method is the set_bit(index, value) command. This allows developers to directly and instantly modify the raw binary representation of a integer sitting in system memory. It does this without resorting to performing outrageously costly mathematical arithmetic operations.
  • Location: kyn-vdf/src/chia.rs — Used extensively and purposefully inside hash_prime. This occurs when forcefully guaranteeing oddness and enforcing strict length constraints, specifically around Lines 131-150.
  • Why it matters: Saif, if we logically wanted to forcibly make a candidate number odd, we could theoretically do complex modulo math to check if it’s currently even. If so, we could perform a sluggish addition operation to precisely add 1. But standard mathematical arithmetic on gigantic 1024-bit numbers is , painfully slow for a CPU to perform repeatedly. Because BigInts are ultimately just simple, raw arrays of bits stored sequentially in memory, calling candidate.set_bit(0, true) instantly forces the very least significant bit in the array to a 1. In standard binary representation, any number ending in a 1 is unconditionally, odd. This guarantees oddness almost instantly with near-zero CPU overhead. We intelligently use this exact same blindingly fast technique to flip the top bit to 1 via set_bit(1023, true). This ensures the number is exactly, 1024 bits long. It is a brilliant, low-level memory optimization that is crucial for the high-speed performance of the block validator.

Rust Concept: std::cmp::max and std::cmp::min

  • What it does: These are optimized, universally relied upon standard library functions. They precisely take two values of the exact same data type and safely, predictably return the larger or specifically smaller of the two, respectively. They do this without requiring custom if-else branching logic.
  • Location: kyn-vdf/src/chia.rs — Used continuously and carefully during the delicate byte slicing and precise buffer management phases of the frantic SHA-256 pipeline.
  • Why it matters: Saif, when we are actively and rapidly slicing the tiny 32-byte SHA-256 outputs to cleverly fit them exactly into our target byte length buffer, we have to be careful. We must painstakingly ensure not to accidentally read or write out of the allocated memory bounds. Doing so would instantly cause a severe, unrecoverable runtime panic that would crash the node immediately. std::cmp::min is used repeatedly and elegantly to calculate exactly how many bytes we can safely and legally copy. It copies them from the small hash output buffer directly into our main, candidate buffer without ever overflowing the dynamically allocated memory. It is a clean, safe, and idiomatic Rust way to elegantly handle delicate boundary logic. It does this without writing horribly verbose, intensely error-prone if-else blocks that would mercilessly clutter the codebase.

How This Connects to the Rest of Kinetic

  • CROSS-CRATE (kinetic-core Consensus): The core, foundational consensus mechanism residing centrally in the kinetic-core crate , undeniably relies on the create_discriminant function. It relies on it for the strict, unforgiving validation of every single block traversing the network. When a node successfully receives a valid proof of space from the broader network, it immediately uses the challenge hash of that exact proof as the cryptographic seed. It uses this seed to generate this exact discriminant. If the generation fails or produces a complete mismatch compared to the block, the block is , brutally rejected by the node. This firmly halts malicious actors in their tracks.
  • CROSS-CRATE (kinetic-cli Timelord): The Timelord is the specialized, powerful network node running the actual heavy VDF computation. It uses this exact same codebase and specific file. It must generate the exact same prime discriminant as all the validating nodes scattered on the network. If the Timelord’s chia.rs ever behaves even slightly differently due to a subtle software bug or a bizarre hardware quirk, disaster strikes. The Timelord will stubbornly compute a VDF over the wrong class group entirely. The network will swiftly and mercilessly reject its final proof. This wastes amounts of the Timelord’s precious CPU work, time, and expensive electricity.
  • FORWARD DEPENDENCY (VDF Squaring Engine): The actual core VDF squaring engine implements the repetitive, intense squaring logic loop. It and blindly assumes that $D$ is a valid, uncompromised prime number. It does not attempt to wastefully verify the primality of $D$ itself before beginning the gruelling squaring process. Doing so on every single squaring step would be totally for overall performance, instantly bringing the network to a halt. The rigorous, unbreakable security of the entire squaring engine rests entirely on the unshakeable foundational assumption that this specific file did its job flawlessly.

Quick Reference

  • Primary Goal: Transform a wildly unpredictable 32-byte seed into a valid, securely derived, undeniably 1024-bit negative prime discriminant.
  • Primary Algorithm Pipeline:
  1. Carefully Initialize 64-bit Iteration Counter for Determinism
  2. Execute High-Speed SHA-256 Concatenation Pipeline
  3. Execute Safe Big-Endian Memory Buffer to BigInt Conversion
  4. Force Absolute Oddness and Strict 1024-bit Target Bit Length Restrictions
  5. Execute Blindingly Fast Pre-computed Small Primes Division Check
  6. Execute Strict Miller-Rabin Mathematical Decomposition ($2^r \cdot d$)
  7. Execute Heavy Miller-Rabin Test (Using exactly 25 predefined deterministic bases)
  8. Execute Precise Modulo 4 Structural Adjustment and Explicit Negation
  • The Golden Determinism Rule: The exact same starting seed must always, unconditionally, flawlessly yield the exact same prime number. This must hold true across all computer architectures, operating systems, and node implementations without exception.
  • Strict Mathematical Requirements: The successfully generated discriminant $D$ must fundamentally be a negative prime of exactly 1024 bits in length. It must also satisfy the strict mathematical condition $D \equiv 1 \pmod 4$.
  • Performance Criticality: High. This complex, heavy code runs repeatedly and intensively during continuous block validation. Any inefficiencies or memory leaks here will slow down the entire network’s block propagation times.

Open Questions / Things to Revisit

Performance Bottlenecks

  • Miller-Rabin Round Optimization: We are currently mandating a full 25 rounds of the heavy deterministic Miller-Rabin test. Current advanced cryptographic literature clearly suggests that for numbers over 1024 bits, far fewer rounds might provide entirely sufficient probabilistic certainty. We should , scientifically investigate if we can safely and responsibly reduce this to 15 or 20 rounds. This would dramatically speed up block validation without inadvertently opening up terrifying edge-case vulnerabilities to sophisticated attackers who might pre-compute composite edge cases.
  • BigInt Memory Allocation Churn: The intricate, repetitive process of constantly accumulating SHA-256 hashes into a dynamic Vec<u8> is intense. Carefully converting it into a BigInt inevitably involves several heavy heap allocations directly inside the tight looping structure. In a high-throughput scenario, such as a brand new node rapidly syncing thousands of blocks from genesis, this is problematic. This will cause , unsustainable memory allocation churn and trigger the garbage collector or allocator overhead far too frequently. We should deeply explore pre-allocating these buffers globally once. Alternatively, we could use a specialized, stack-based big integer library , flawlessly optimized for this specific 1024-bit size constraint to bypass the heap entirely.

Security Audits

  • Seed Entropy Source Audit: The foundational, absolute security of this entire file assumes that the input seed genuinely possesses 256 bits of true cryptographic entropy. It assumes this entropy is , irrecoverably unpredictable to all miners. We critically need to and independently audit the complex kinetic-core seed derivation path. If a clever attacker can successfully manipulate the block state to rapidly grind for favorable seeds, they might be able to easily find a weak discriminant. This weak discriminant makes the VDF easier to solve, fundamentally, irreparably compromising the blockchain’s clock mechanism and allowing them to steal rewards.
  • Hardware Acceleration Extensibility: The heavy modular exponentiation required in the Miller-Rabin test is currently CPU-bound. It is done entirely in pure, unaccelerated software. As network difficulty inevitably and continuously increases, we may desperately want to selectively expose complex hooks. These hooks would allow powerful nodes with specialized, custom GPUs or FPGAs to offload the excruciating primality testing entirely. However, the strict, deterministic requirement must be maintained flawlessly and across wildly varying hardware implementations. This is a significant, terrifying, and daunting engineering challenge that we must confidently solve before scaling the network further.

Crate: kyn-vdf

Stage: 05

Reading Time: 25 minutes

Depends On: 04_chia_forms.md, 03_vdf_basics.md

What Is This?

This document explores the Binary Quadratic Form Compression (BQFC) implementation inside kyn-vdf/src/chia.rs (lines 237-597). At a high level, BQFC is a sophisticated mathematical and engineering technique for taking a large mathematical object—a binary quadratic form—and squeezing it down into the smallest possible representation without losing any critical information. In the Kinetic network, we are constantly passing around VDF (Verifiable Delay Function) proofs, which are primarily composed of these binary quadratic forms. A raw 1024-bit form, if just serialized directly as its constituent components a, b, and c, takes about 130 bytes of space. Using BQFC, we are able to compress this down to exactly 100 bytes. This document will walk you through exactly how the CompressedForm struct represents this compressed state. We will examine how bqfc_compr takes a raw form and compresses it. We will also see how bqfc_decompr reverses that process. Finally, we will cover how serialize_form and deserialize_form translate the mathematical data into raw bytes for network transmission. When a developer first looks at this file, the immediate question is usually: “Why are we doing so much math just to serialize an object?” The answer is that a binary quadratic form is not just a random collection of bytes; it has strict internal mathematical relationships. BQFC exploits these relationships. Because the values of a, b, and c are bound together by the discriminant equation b^2 - 4ac = d, sending all three is redundant. Sending a and b is better, but still redundant because b itself has internal symmetries we can exploit. BQFC is the process of stripping away every single bit of redundant data until we are left with the absolute theoretical minimum representation. This mathematical compression is fundamental to the scalability of the Kinetic blockchain, ensuring that our verifiable delay functions do not choke the peer-to-peer layer.

Why Kinetic Needs This

You might look at the difference between 130 bytes and 100 bytes and think, “Is this really worth the complexity?” The answer is an emphatic yes. In the Kinetic network, VDF proofs are not just calculated once and stored; they are constantly gossiped across the peer-to-peer (P2P) network. Every single node that participates in consensus needs to receive, verify, and potentially re-broadcast these proofs. When you have thousands of nodes broadcasting proofs every few seconds, that extra 30 bytes per form adds up . Let’s break down exactly why this compression is a feature for the Kinetic architecture.

First, consider MTU and Packet Fragmentation. Network packets have a Maximum Transmission Unit (MTU), which is typically around 1500 bytes for standard Ethernet. If a message exceeds this, it gets fragmented into multiple packets. Fragmentation increases latency and the chance of packet loss. VDF proofs are often bundled with other block headers and signatures. By keeping our VDF proofs as small as possible, we maximize the number of proofs we can fit into a single unfragmented UDP or TCP packet. A 100-byte footprint gives us crucial breathing room in our packet budgets. If we were to use the uncompressed 130-byte forms, a single broadcast containing a handful of proofs could easily spill over the MTU threshold.

Second, we must address Blockchain State Bloat. Over time, these VDF proofs end up being embedded into the historical record of the Kinetic blockchain. 30 extra bytes per block, extrapolated over years and millions of blocks, turns into gigabytes of pure waste. Storage is not free, and one of Kinetic’s primary goals is to keep the node requirements low enough that anyone can run an archival node on consumer hardware. By compressing forms at the network layer and persisting them in this compressed state, we are significantly reducing the long-term storage requirements. This has a direct impact on the decentralization of the network.

Third, Bandwidth Costs are a major concern. For light clients or nodes running in bandwidth-constrained environments (like mobile devices, rural internet connections, or satellite links), bandwidth is expensive and limited. Shaving off ~23% of the size of the most commonly transmitted data structure is a win for network inclusivity. Kinetic aims to be a globally accessible network, and every byte saved lowers the barrier to entry. When syncing from genesis, a node must download millions of these forms; a 23% reduction in size translates to hours saved during the initial sync process.

Fourth, there is an interesting dynamic regarding Processing Overhead and Cache Locality. While decompression takes a small amount of CPU work to perform the mathematical recovery, transmitting less data over the wire and reading fewer bytes from disk often results in an overall performance gain. Modern CPUs are fast, but memory bandwidth and cache misses are slow. Reading 100 bytes into the L1/L2 cache is faster than reading 130 bytes. The CPU overhead of running the math in bqfc_decompr is often hidden by the time saved not waiting for RAM or network I/O. This illustrates the principle of trading cheap compute cycles for expensive bandwidth and I/O.

Finally, consider DDoS Mitigation. Smaller packet sizes mean that an attacker trying to flood the network with fake proofs has to expend relatively more effort to saturate a node’s bandwidth. While BQFC wasn’t designed purely as a security feature, dense packing of data structurally hardens the P2P layer. An attacker must generate valid or pseudo-valid compressed forms, meaning they cannot just spray random large bytes at the network and expect them to be processed as VDF proofs.

How It Works

The BQFC process relies on a core insight from number theory: a binary quadratic form (a, b, c) of a known discriminant d has redundant information. Because the relationship b^2 - 4ac = d always holds true, if we know a and b (and d, which is a global network constant), we can easily calculate c. However, BQFC takes this even further. It turns out you don’t even need all of b. You can compress a and a specialized representation of b into a smaller space, and then reconstruct b on the other side. This process is broken down into several stages: structural representation, mathematical compression, byte serialization, byte deserialization, and finally mathematical decompression.

Step 1: The CompressedForm Structure

-> See: kyn-vdf/src/chia.rs — Lines 237-250

When we compress a form, we don’t just truncate bytes arbitrarily. We transform the standard (a, b, c) coordinate system into a specialized CompressedForm structure. This structure contains four key fields: a, t, g, and b0. The a field remains the exact same value as in the original form. It is the foundational pillar of the form and cannot be compressed away; its full precision is required. The t field is a partial quotient derived from running the Extended Euclidean Algorithm (XGCD) on a and b. The g field is the greatest common divisor extracted during that same XGCD step. The b0 field is a small remainder value, essentially a mathematical parity check. By storing these specific elements instead of the integer b, we encapsulate enough mathematical “breadcrumbs” to recreate the original b value. It is a form of lossy compression where the “lost” data can be deterministically recalculated on the other side without having to send the entire integer over the network. This structure is the cornerstone of our ability to hit the 100-byte target.

Step 2: Compression (bqfc_compr)

-> See: kyn-vdf/src/chia.rs — Lines 255-320

The bqfc_compr function is where the heavy lifting of compression occurs. It takes a raw, fully inflated binary quadratic form and produces a CompressedForm. The function first initiates a bounded version of the Extended Euclidean Algorithm on the inputs a and b. The Euclidean algorithm is typically used to find the greatest common divisor of two numbers, but here it is used to find a smaller representation of the relationship between a and b modulo a certain base. The algorithm runs until it reaches a specific bound, at which point it extracts the variables t and g. Once the XGCD yields t and g, the function calculates b0. Because the reconstruction process later will involve calculating square roots, and because square roots have multiple valid solutions in modular arithmetic, b0 acts as a crucial parity bit or remainder to resolve ambiguities later. The beauty of this algorithm is that it takes a number b that normally requires a amount of bits and shatters it into these smaller mathematical components (t, g, b0) that pack tightly together. This mathematical decomposition is what allows us to discard the bulk of b’s bits.

Step 3: Serialization (serialize_form)

-> See: kyn-vdf/src/chia.rs — Lines 325-390

Once we have a CompressedForm sitting in memory, we need to turn it into a flat array of exactly 100 bytes so it can be sent over a TCP or UDP socket. This is where serialize_form comes in. The function allocates a Vec<u8>, which is a dynamically sized, owned array of bytes on the heap. It then begins packing the fields a, t, g, and b0 into it. Because these fields are large, multi-precision integers (typically BigInts), it extracts the raw bytes of each number and concatenates them in a strict, deterministic order.

When writing this in Rust, the distinction between a Vec<u8> and a [u8] is . A Vec<u8> is an owned, growable array allocated on the heap. When serialize_form creates a new Vec<u8>, it is taking ownership of new memory. It populates this memory, and then returns ownership of this vector to the caller. The caller is then responsible for sending it over the network or dropping it, at which point the memory is freed. Conversely, the data it reads from the BigInts often comes out as a slice [u8]. A slice is just a borrowed view into memory owned by someone else. To get the data from the slice into the vector, the code uses copy_from_slice. This function performs a fast, safe memcpy under the hood. It ensures the vector has its own independent copy of the bytes, preventing any data races or use-after-free errors. This explicit copying is why the return type of serialize_form is an owned Vec<u8>.

Critically, it also packs metadata. To save space, it uses bit flags. For example, the sign of b (whether it is positive or negative) is essential for reconstruction. Allocating a whole byte just for a true/false value is wasteful. Instead, the sign is packed into a single bit of a single byte using a bitwise shift, specifically BQFC_B_SIGN = 1 << 0. This means we take the binary number 1 and shift it left by 0 positions, which just equals 1. If the sign is negative, we bitwise OR this value into our byte using the | operator. This technique allows us to cram up to 8 different boolean flags into a single byte of overhead. It is this aggressive, bit-level micromanagement that keeps the final serialized size capped at 100 bytes.

Step 4: Deserialization (deserialize_form)

-> See: kyn-vdf/src/chia.rs — Lines 395-460

On the receiving end, a Kinetic node receives a 100-byte packet from a peer. It uses deserialize_form to parse this raw byte array back into a usable CompressedForm structure in memory. This function takes a slice &[u8], which is a borrowed view into the bytes, rather than an owned Vec<u8>.

The parser is passed a &[u8], a read-only borrowed slice. This means the parser does not own the memory; it is just looking at the bytes received from the network socket. Because it’s a slice, it has a known length, and Rust will automatically bounds-check our reads to prevent buffer overflows. This built-in safety net is why Rust is chosen for these critical network parsers over C or C++. To track our progress through this slice, we use an &mut usize representing an offset. Why a mutable reference? If we just passed usize by value, the helper functions would get a copy of the number. When the helper function reads 4 bytes, it would add 4 to its own copy of the offset, and the main parser would be stuck at 0. By passing &mut offset, the helper function directly modifies the parser’s offset variable. This ensures that every sequential read picks up exactly where the last one left off, moving safely through the 100 bytes.

It reads the bit flags we packed earlier by taking a byte and using bitwise AND operations. By executing byte & BQFC_B_SIGN, the parser masks out all other bits and isolates exactly the boolean value it needs. This cleanly translates network bytes back into structured Rust logic.

Step 5: Decompression (bqfc_decompr)

-> See: kyn-vdf/src/chia.rs — Lines 465-597

Once the node has safely parsed the bytes into a CompressedForm, it still needs the actual, full (a, b, c) form to verify the VDF proof. The bqfc_decompr function performs this mathematical inflation. It takes a, t, g, and b0 and runs the algorithm to reconstruct the original b. This is a computationally intensive process. It involves calculating modular inverses over large finite fields, and eventually calculating modular square roots. Calculating a modular inverse means finding a number x such that (num * x) % mod = 1. Because square roots can have multiple valid answers in modular arithmetic, the algorithm reaches a fork in the road where it must decide which root is the correct one. This is exactly why the b0 value and the sign flags were packed earlier. They act as a tie-breaker, forcing the algorithm to select the exact root that corresponds to the original b. If we did not have these flags, we would have a 50/50 chance of picking the wrong root, which would invalidate the VDF proof. Finally, once a and b are fully recovered and verified, c is trivially computed using the fundamental discriminant equation: c = (b^2 - d) / (4a). The form is now fully restored and ready for VDF verification. The CPU overhead here is non-trivial, but as mentioned earlier, it is well worth the bandwidth savings.

Key Pieces

Here is a detailed breakdown of the critical types and functions you need to understand in this file. When modifying this file, these are the components you must be careful with.

CompressedForm

  • What it does: A structured payload holding a, t, g, and b0.
  • File & Line: kyn-vdf/src/chia.rs — Lines 237-250.
  • Why it matters: This is the intermediate state between a full mathematical form and raw bytes. It defines exactly what mathematical components are necessary to achieve the 100-byte compression target. If you ever need to change the compression algorithm, this struct will be the first thing to change. It acts as the bridge between pure math and network serialization.

bqfc_compr

  • What it does: Compresses a standard binary quadratic form into a CompressedForm.
  • File & Line: kyn-vdf/src/chia.rs — Lines 255-320.
  • Why it matters: This is the engine of the BQFC protocol. It uses the Extended Euclidean Algorithm to discard redundant bits of b and generate the minimal t and g values. It is optimized and any bugs here will result in forms that cannot be decompressed by peers. A bug here would effectively create a network fork, as nodes running the buggy code would produce forms that other nodes would reject as invalid.

bqfc_decompr

  • What it does: Takes a CompressedForm and inflates it back into a standard (a, b, c) form.
  • File & Line: kyn-vdf/src/chia.rs — Lines 465-597.
  • Why it matters: Without this, the VDF proofs received from the network are useless. It performs complex modular arithmetic (inverses and square roots) to reconstruct the dropped data. Performance is critical here, as this runs directly during block validation. Any inefficiency here will slow down the entire node’s ability to sync with the network.

serialize_form

  • What it does: Flattens a CompressedForm into a Vec<u8> of exactly 100 bytes.
  • File & Line: kyn-vdf/src/chia.rs — Lines 325-390.
  • Why it matters: This function bridges the gap between abstract Rust memory structures and physical network sockets. It handles byte-order (endianness) and tight bit-packing for boolean flags. It must be deterministic; the exact same mathematical form must always serialize to the exact same 100 bytes. If it is not deterministic, block hashes will diverge, causing consensus failures.

deserialize_form

  • What it does: Parses a 100-byte slice [u8] back into a CompressedForm.
  • File & Line: kyn-vdf/src/chia.rs — Lines 395-460.
  • Why it matters: This is the entry point for all incoming network data related to VDFs. It safely validates that the incoming data is exactly 100 bytes and decodes the packed bit flags. It must be robust against malformed or malicious inputs to prevent panics. Because it touches untrusted network data, it is a prime target for fuzzing and security audits.

How This Connects to the Rest of Kinetic

CROSS-CRATE: kyn-network The kyn-network crate is the primary consumer of serialize_form and deserialize_form. When the network layer needs to broadcast a block containing a VDF proof, it calls serialize_form. This ensures the payload is as small as possible before constructing the final P2P protocol message. Conversely, when kyn-network receives a proof from a peer, it immediately calls deserialize_form. This step is required to validate the byte structure before passing the message further up the protocol stack. Without this tight integration, our P2P layer would be overwhelmed by large proof sizes.

CROSS-CRATE: kyn-consensus The kyn-consensus crate relies entirely on the successful output of bqfc_decompr. The consensus rules dictate that a block is only valid if the VDF proof checks out. The consensus engine must take the decompressed form, feed it into the VDF verification algorithm, and ensure it matches the expected difficulty. It also ensures the iterations match for that specific block height. If bqfc_decompr fails, or if it produces a structurally invalid form, the block is instantly rejected by consensus. There is no fallback mechanism; BQFC decompression must succeed for the chain to progress.

FORWARD DEPENDENCY: VDF Verification Engine The output of bqfc_decompr feeds directly into the squaring loop of the VDF verification process. The values of a and b generated here must be accurate down to the very last bit. If decompression yields even a single bit of error in b, the subsequent squaring operations will immediately diverge. They will stray from the prover’s original mathematical path. This leads to a failed verification, and usually results in the peer that sent the form being banned for providing invalid data.

Quick Reference

  • Original Form Size: ~130 bytes (for a standard 1024-bit discriminant).
  • Compressed Form Size: Exactly 100 bytes.
  • CompressedForm fields:
  • a: The primary coefficient. - t: The partial quotient from the XGCD run. - g: The greatest common divisor from the XGCD run. - b0: The mathematical remainder/parity check used to break ties.
  • Compression Method: Partial Extended Euclidean Algorithm (XGCD) to discard redundant bits of b.
  • Decompression Method: Modular inverses and modular square roots to recover the dropped bits of b.
  • Serialization Format: Big-endian byte arrays combined with bit-packed boolean flags for maximum data density.
  • Network Goal: Keeping VDF proofs well within standard MTU limits and reducing node storage bloat.

Open Questions / Things to Revisit

  • BigInt Allocation Overhead: The bqfc_decompr function currently performs several large BigInt allocations when computing modular inverses and square roots. We should investigate if we can reuse a pre-allocated memory pool or bump allocator. This would reduce garbage collection and allocator pressure during heavy blockchain syncing. Currently, syncing from genesis allocates and deallocates millions of BigInts, which is not optimal for CPU caches.

  • Constant Time Execution guarantees: Is bqfc_decompr currently constant-time? Almost certainly not, due to the nature of the XGCD and square root algorithms. Since this code is used for verification and not for signing or key generation, side-channel attacks (like timing attacks) are less of a concern. However, it is worth documenting so future developers do not mistakenly use these functions in a cryptographic context where constant-time execution is required. If we ever expose these functions to smart contracts, this could become a severe vulnerability.

  • Error Handling on Malicious Packets: If an attacker sends garbage 100-byte payloads filled with random noise, does deserialize_form fail gracefully? More importantly, does bqfc_decompr waste excessive CPU cycles trying to calculate impossible square roots before realizing the form is invalid? We need to ensure we have strict, early exit paths to prevent asymmetric denial-of-service (DoS) vectors. An attacker could theoretically burn our CPU with cheap random bytes if we are not careful to abort early on impossible states.

  • Memory Safety Review: While deserialize_form leverages Rust’s safe slices, we still need to audit the bounds checking logic manually. A panic here from an out-of-bounds slice access would crash the network layer. We need to guarantee that every single slice indexing operation is guarded by a length check before deployment.

  • Future Optimizations: Can we compress these forms even further? Recent research in cryptography suggests that 96-byte compressions might be possible for 1024-bit discriminants. However, this would require a hard fork and rewriting both bqfc_compr and bqfc_decompr. For now, 100 bytes is the absolute sweet spot between implementation complexity, CPU verification time, and network bandwidth.

Crate: kyn-vdf

Stage: 06 (Verification)

Reading Time: 25 minutes

Depends On: kyn-vdf (Squaring / Proof Generation), kyn-types

What Is This?

This document covers the culminating step of the kyn-vdf crate: the Wesolowski verification phase. While earlier stages deal with the brutally slow and computationally intense process of generating a Verifiable Delay Function (VDF) proof through repeated squarings, this stage is entirely about validating that proof almost instantly. Specifically, this documentation explores the derivation of the Fiat-Shamir challenge, the fast verification mathematics, the pure-Rust entry point verify_chia_vdf in lib.rs, and the comprehensive error handling taxonomy defined in error.rs. In any system utilizing VDFs, the prover does all the heavy lifting over a sustained period of time, while the verifier must be able to confirm the work rapidly. This file explains how the Kinetic network achieves that critical asymmetry using pure Rust, enabling seamless execution across servers, mobile devices, and web browsers.

Why Kinetic Needs This

In a decentralized network like Kinetic, time must be objectively verifiable by anyone. Traditional consensus mechanisms often rely on raw hashing power (Proof of Work) or capital lockups (Proof of Stake). However, to prevent certain types of network manipulation— such as long-range attacks where an adversary tries to rewrite history— we need a cryptographic guarantee that a specific, unavoidable amount of sequential wall-clock time has passed. We use a VDF to prove this passage of time. A node generating a proof might spend 30 days of continuous CPU time computing billions of sequential operations. However, if verifying that proof also took 30 days, the entire network would grind to a halt. The nodes would never be able to synchronize. The entire system relies on an asymmetry: proof generation is intentionally slow and un-parallelizable, but verification must be almost instantaneous. Wesolowski’s protocol provides this exact mathematical property. It reduces a time-intensive operation into a quick cryptographic check, allowing the network to agree on the passage of time without having to re-compute the entire sequence.

But why do we need this specific implementation in pure Rust? Why not just use the existing, optimized C++ libraries provided by projects like Chia? The Kinetic architecture requires maximum portability and decentralization. We envision a network where a full node or a capable light client can run directly inside a standard web browser using WebAssembly (WASM). Traditional C++ cryptography libraries often rely heavily on underlying operating system APIs, custom memory allocators, and hardware-specific assembly instructions. These dependencies make them notoriously difficult and sometimes impossible to compile reliably to WASM. By rewriting the validation logic—specifically the Wesolowski verifier—in pure Rust, we leverage Rust’s powerful cross-compilation toolchains. More importantly, by utilizing the #![no_std] capability, we guarantee that this code does not depend on the standard C-like operating system underneath. It requires only a basic memory allocator. This means our VDF verifier can be deployed to any environment, including a browser, ensuring that users do not have to trust a centralized server to verify network state. It is lightweight, secure, and universally portable, which is a hard requirement for the frontend accessibility of the Kinetic ecosystem.

How It Works

The magic of Wesolowski verification lies in how it condenses billions of operations into a single equation: pi^B * x^r = y. To understand how this works in our codebase, we need to break down the environment, the variables, the math, and the exact code path. To prevent the prover from taking parallel shortcuts, the VDF operates within an Imaginary Quadratic Class Group. The crucial property of this mathematical group is that its “order” (the number of elements it contains) is unknown. Because the order is unknown, the prover cannot use shortcuts like Fermat’s Little Theorem to skip ahead in the computation. They are forced to perform every single squaring operation sequentially: x, x^2, x^4, x^8... up to x^(2^T).

When a prover finishes computing y = x^(2^T) (where T is the number of squarings, representing the time elapsed), they don’t just broadcast y. They also calculate and broadcast a proof element, pi. The verifier (any Kinetic node) must check if pi correctly proves that y was derived from x after exactly T squarings.

-> See: kyn-vdf/src/chia.rs — Lines 599 to 615 The first step in verification is determining the challenge, B. We cannot let the prover simply choose B, otherwise they could forge a valid-looking proof without doing the actual work. Instead, we use the Fiat-Shamir heuristic to make the process non-interactive and secure. We hash the public inputs together to deterministically generate a prime number. In our implementation, the function get_b takes the generator element x and the final output element y. It runs them through a secure cryptographic hash function to produce a 264-bit prime challenge. Because both the prover and verifier use the exact same deterministic hashing process on the identical inputs, they both arrive at the exact same prime B. The prover could not know B before they finished computing y, locking them into the honest computation.

-> See: kyn-vdf/src/chia.rs — Lines 616 to 640 Once the challenge B is derived, we must calculate the remainder r. Imagine dividing the total number of squarings 2^T by our challenge prime B. You would get some quotient q and a remainder r. , this is expressed as: 2^T = q * B + r. The verifier needs the remainder r. Fortunately, r is trivial to calculate quickly using modular arithmetic because we don’t actually need to compute the number 2^T. We simply compute r = 2^T mod B. Since B is a 264-bit prime, this modular exponentiation is fast, executing in microseconds.

-> See: kyn-vdf/src/chia.rs — Lines 641 to 670 Now we enter the core execution of verify_wesolowski. The prover has provided the proof pi. Under the hood, pi is actually the generator x raised to the quotient q (so, pi = x^q). The prover had to calculate this during the proof generation phase. We need to prove that the output equals the generator after T squarings: y = x^(2^T). We substitute our division formula for 2^T into the equation: y = x^(q * B + r). Using basic exponent rules, we can split this into two parts: y = (x^q)^B * x^r. Since the prover gave us pi (which represents x^q), we substitute pi in: y = pi^B * x^r. This is exactly the equation that verify_wesolowski calculates and checks. It takes the proof pi, raises it to the power of our derived prime challenge B, and multiplies it by the generator x raised to our calculated remainder r. If the result equals the claimed output y, the proof is cryptographically valid. Raising elements to the power of B and r only takes O(log B) operations using square-and-multiply algorithms. Since B is a 264-bit number, it takes roughly 264 squarings to verify the proof. It does not matter if T was a thousand or a billion; verification always takes roughly 264 operations. This is the breakthrough of Wesolowski’s protocol: compressing 30 days of computation into less than 100 milliseconds of verification time.

-> See: kyn-vdf/src/lib.rs While chia.rs handles the complex underlying mathematics, lib.rs provides the clean, unified entry point for the rest of the application: verify_chia_vdf. It bridges the gap between the raw bytes received over the Kinetic peer-to-peer network and the complex mathematical objects required by the verifier. It takes the serialized byte arrays for the discriminant, x, y, and the proof pi. It deserializes them, handles instantiation of the class group elements, and then cleanly orchestrates the call into verify_wesolowski. It acts as a strict abstraction boundary, shielding the rest of the Kinetic network daemon from the mathematical complexity, presenting a simple function that returns a successful Result or an error.

Key Pieces

get_b (Function)

-> See: kyn-vdf/src/chia.rs This function is responsible for executing the Fiat-Shamir transformation. It takes the serialized forms of the input generator and the output element, concatenates them, and hashes them repeatedly until it finds a valid 264-bit prime number. This matters tremendously because the entire security model of Wesolowski verification rests on B being a prime number that the prover could not predict or manipulate. By deriving it directly from the hash of the output y, we force the prover to finish computing the time-delay y before they can even know what B is, rendering forgery impossible.

verify_wesolowski (Function)

-> See: kyn-vdf/src/chia.rs This is the core mathematical engine of the crate. It is the direct implementation of the pi^B * x^r = y validation logic. It takes the generator x, the output y, the proof pi, the prime challenge B, and the remainder r. It performs the necessary imaginary quadratic class group operations— specifically squarings and multiplications— to evaluate the left side of the equation. This function relies on an underlying big-integer arithmetic library to handle the numbers involved. It is deeply optimized because a node syncing to the Kinetic network from scratch might need to verify thousands of these proofs in rapid succession.

verify_chia_vdf (Function)

-> See: kyn-vdf/src/lib.rs This is the primary public API of the crate. It is the integration and orchestration point. It accepts raw byte slices (&[u8]) for the inputs and the iteration count T. It safely parses these bytes into the mathematical ClassGroupElement structures, diligently handles any potential deserialization errors or malformed data, and coordinates the execution of verify_wesolowski. This is the function that the higher-level Kinetic daemon, the block validator, or the WASM wrapper will actively call.

KynVdfError (Enum)

-> See: kyn-vdf/src/error.rs This file defines the comprehensive taxonomy of everything that can possibly go wrong during the validation process. Using the thiserror crate, it defines clear variants such as:

  • InvalidProofLength: Triggered if the byte array for pi is the wrong size.
  • InvalidDiscriminantIdentity: Triggered if the prime discriminant doesn’t meet the strict cryptographic requirements for the class group.
  • VerificationFailed: Triggered if the core math equation simply doesn’t balance out. This matters deeply because in an untrusted decentralized network, a node will constantly receive garbage data or malicious payloads. You must reject bad data with precise, typed, and understandable reasons to prevent cascading network failures and to allow developers to debug complex peer-to-peer data transmission issues effectively. The thiserror crate shines here by automatically deriving the Display and Error traits, eliminating hundreds of lines of tedious boilerplate code.

How This Connects to the Rest of Kinetic

CROSS-CRATE: This entire module acts as a vital, foundational cryptographic primitive for both the kinetic-verify and kinetic-node crates. When a Kinetic node receives a new block over the network or a time-challenge response, it cannot trust the data blindly. It extracts the raw VDF bytes from the block header and passes them directly to verify_chia_vdf exposed by lib.rs. If the function returns an error, the block is instantly dropped and the peer is penalized.

FORWARD DEPENDENCY: Because this verification logic is written in pure Rust and avoids standard library operating system calls (utilizing #![no_std]), it directly enables the kinetic-wasm crate. The kinetic-wasm crate will simply import kyn-vdf, wrap the verify_chia_vdf function in wasm-bindgen bindings, and output a Javascript-compatible WebAssembly module. This architecture guarantees that a Kinetic light client running in a web browser can verify time proofs locally and securely, without ever having to trust a centralized RPC server. This keeps the network genuinely decentralized down to the consumer frontend.

Quick Reference

  • The Core Equation: pi^B * x^r == y
  • T: The number of iterations (squarings), representing the exact amount of time elapsed.
  • x: The initial generator value (the public input).
  • y: The output value after precisely T squarings.
  • pi: The proof element provided by the block generator.
  • B: The Fiat-Shamir prime challenge, securely derived by hashing x and y.
  • r: The remainder of 2^T mod B.
  • Time Complexity: Proving requires O(T) sequential steps. Verifying requires only O(log B) steps.
  • Portability Factor: Written in Pure Rust with zero C++ dependencies. no_std compatible, suited for WebAssembly compilation.

Open Questions / Things to Revisit

  • Class Group Arithmetic Optimization: Are the underlying multiplications and squarings within verify_wesolowski as heavily optimized as possible? While the overall verification complexity is a minimal O(log B), implementing constant-time optimizations or advanced algorithms (like NUDUPL) in the underlying large integer library could yield measurable sync-time improvements during initial block download.
  • WebAssembly Bundle Size: While pure Rust guarantees WASM compatibility, pulling in robust large integer math libraries might significantly inflate the final .wasm binary size. We should actively monitor and measure the compiled bundle size of kinetic-wasm to ensure it remains lightweight enough for instantaneous browser loading.
  • Error Granularity and Telemetry: Should the generic VerificationFailed error variant be split into more specific granular variants? For example, separating a “mathematical mismatch” from an “invalid group element structure” might vastly improve debugging capabilities when analyzing edge cases in peer-to-peer data transmission or identifying new attack vectors on the node software.

01 — Overview

Crate: kinetic-vdf (Folder: vdfrs) Stage: 5 of 10 Reading time: ~4 minutes Depends on: kinetic-core, kinetic-types


What Is This?

The kinetic-vdf crate is the primary Verifiable Delay Function (VDF) wrapper for the Kinetic network. It acts as the bridge between Kinetic’s pure-Rust architecture and the optimized C++ chiavdf library.


Why Kinetic Needs This

A VDF is a cryptographic function that takes a predictable, sequential amount of time to compute (like solving a puzzle on a single CPU core) but can be verified almost instantly. Kinetic uses VDFs to enforce time-locked grace periods on .kin domain registrations and transfers. This forces an attacker to spend significant time (e.g., 30 days of CPU time) before claiming a name, neutralizing domain sniping and hostile takeovers.


How It Works

Important

Because VDF calculations are heavy, kinetic-vdf uses OS-level file locks to ensure that no two VDF computations can ever run simultaneously on the same machine. This prevents CPU starvation, which would otherwise crash the entire P2P node daemon. It also houses the critical “Differential Fuzzer” — a test suite that ensures the C++ implementation agrees with Kinetic’s pure-Rust verifier (kyn-vdf).


Key Pieces (Topic File Breakdown)

This crate is composed of 725 total lines of code, broken down into the following topics:

  1. 02_engine.md — Covers ChiaVdfEngine, the core FFI wrapper, and the OS-level file locking defense mechanisms.
  2. 03_tests.md — Covers the Differential Cryptography test suite. Explains how bitwise XORs and hardcoded byte anchors protect the network from accidental forks.
  3. 04_binaries.md — Covers benchmark.rs and prove_timing.rs, the CLI tools used to calibrate node difficulty based on actual CPU hardware speeds.

How This Connects to the Rest of Kinetic

  • CROSS-CRATE: Implements the VdfEngine trait defined in kinetic-core.
  • FORWARD DEPENDENCY: The transaction mempool and consensus engine (kinetic-daemon) will invoke this engine whenever they need to construct or validate a time-locked registration.
  • FORWARD DEPENDENCY: The test suite in this crate continuously cross-checks itself against the kyn-vdf crate (Stage 6).

Quick Reference

  • Total Lines: 725
  • Underlying Engine: C++ chiavdf (via FFI bindings)
  • DoS Protection: Single OS-level fs2 lock.
  • Max Iterations Bound: 400 Billion.

Open Questions / Things to Revisit

Warning

  • Mobile/Web Fallbacks: The engine currently hard-errors with UnsupportedPlatform on Android and WASM because the C++ bindings can’t compile there. If Kinetic ever requires mobile nodes to generate proofs, a pure-Rust prover will be needed. (Currently, mobile nodes can only verify proofs using kyn-vdf).

Crate: kinetic-vdf (Engine)

Stage: 02 Reading Time: 40 mins Depends On: kinetic-core


What Is This?

This file provides the primary implementation of the Verifiable Delay Function (VDF) engine for the entire Kinetic network architecture. It formally defines the ChiaVdfEngine struct within the Rust ecosystem. This struct acts as a robust, safe Rust wrapper around the external C++ chiavdf library. By wrapping this specific external library, Kinetic can harness production-ready, battle-tested cryptographic primitives. This eliminates the need for Kinetic developers to reinvent complex class group arithmetic natively in Rust, which is error-prone. Class group arithmetic involves deeply complex mathematical structures where the exact order of the group is unknown. This “unknown order” is the foundational security assumption for Wesolowski Verifiable Delay Functions. Without an unknown order, an attacker could trivially bypass the sequential delay by computing the order directly, defeating the entire mechanism. This wrapper code handles the crucial memory bridge between Kinetic’s internal Rust data structures (such as Commitment and VdfProof). It maps these clean Rust types to the low-level Foreign Function Interface (FFI) bindings provided by the chiavdf C++ codebase. The engine abstracts away the notorious complexity of C++ memory management, manual pointer casting, and cryptographic initialization configuration. It ensures that proper type conversions happen smoothly before bytes cross the language barrier. Ultimately, it presents a clean, memory-safe Rust trait (the VdfEngine trait) to the rest of the application. This ensures that all the unsafe C++ boundaries, memory allocations, and FFI idiosyncrasies are neatly and safely contained within this single file. Containing this unsafety prevents widespread memory leaks or segmentation faults from ever creeping into the rest of the node’s architecture.


Why Kinetic Needs This

In a decentralized, permissionless peer-to-peer network like Kinetic, time is inherently difficult to prove and agree upon across untrusted nodes. Unlike centralized servers, there is no master clock that all nodes trust implicitly. If a user wants to register or transfer a high-value domain name, malicious actors pose a severe and constant threat. Bots, predatory miners, or domain snipers might continuously monitor the unconfirmed transaction mempool, looking for valuable opportunities. When they see a valuable domain transfer pending, they can attempt to front-run the transaction by submitting a duplicate request but paying higher gas fees to miners. Imagine Alice wants to legitimately transfer alice.kin to a new wallet address to secure her assets. If she broadcasts the transaction normally, a sniper could instantly observe it, copy the payload, and submit it with a higher fee to steal the domain before Alice’s block is officially confirmed by the network. To prevent this unfair advantage, Kinetic enforces a strict, verified “time-locked grace period” on all domain transfers. A Verifiable Delay Function (VDF) is a specialized cryptographic algorithm designed specifically for this exact purpose. It requires a specific, predictable, and measurable amount of sequential computational work (the delay) to evaluate correctly. Crucially, it is designed to be fast to verify once the heavy computation is finally complete. By forcing a user to compute a VDF proof before their domain transfer transaction is considered valid, Kinetic proves to the entire network that a certain amount of real-world time has elapsed. Because the computation is sequential (meaning each calculation step depends on the exact output of the previous step), an attacker cannot speed it up. They cannot cheat the delay by throwing more CPU cores, parallel threads, ASICs, or server clusters at the problem; it must be done sequentially, step by step. However, this VDF evaluation is intensely CPU-heavy by explicit design. If an attacker deliberately submits hundreds of invalid or concurrent VDF evaluation requests to a single Kinetic node, the node’s CPU cores would all become fully saturated immediately. The node would rapidly exhaust its computational resources trying to evaluate the fake proofs simultaneously. This represents a Denial-of-Service (DoS) vulnerability that could easily crash the node, desync it from the network consensus, or trigger operating system out-of-memory (OOM) kills. Thus, Kinetic desperately needs this engine not only to compute the mathematical delays but also to enforce strict, operating-system-level resource management. This vital resource management is achieved via specialized filesystem locks. These file locks ensure that the node remains lightweight and responsive to the rest of the P2P network, carefully throttling the heavy delay computations to a manageable queue.


How It Works

1. Structure and Default Elements

#![allow(unused)]
fn main() {
-> See: src/lib.rs — Lines 30 to 52
}

The primary core type of the file is the ChiaVdfEngine struct, which acts as the main entry point for the module. It has no internal state fields, meaning it holds no variables, pointers, or flags. In standard Rust terminology, it is formally referred to as a zero-sized struct (ZST). This is because it merely orchestrates function calls to the external C++ library rather than storing persistent data in memory itself. The default_element() function is a crucial helper method that generates the identity element for the mathematical class group used by chiavdf. It constructs and returns a fixed 100-byte array where only the very first byte (index 0) is deliberately set to 0x08. The remaining 99 bytes of the array are initialized to zero during construction. This specific, hardcoded byte sequence is required by the underlying Wesolowski VDF implementation to function correctly. It serves as the standardized, universally agreed-upon starting point for the millions of repeated squaring operations that actually define the computational delay function. By standardizing this element, all nodes in the network agree on the exact mathematical starting line. The struct also implements the standard Default trait, allowing it to be instantiated ergonomically across the codebase without needing complex initialization parameters or factories.

2. Bounding the Execution to Prevent Infinite Loops

#![allow(unused)]
fn main() {
-> See: src/lib.rs — Lines 63 to 67
}

Inside the evaluate function, the very first operation executed is a critical safety check: if iterations > 400_000_000_000. A VDF’s total execution time is directly and linearly proportional to the number of iterations requested by the input challenge. > [!IMPORTANT]

If a malicious actor crafts a challenge with an artificially iteration count, the node could easily get stuck evaluating it for years. This would effectively lock up the node’s thread indefinitely, acting as a targeted DoS attack against specific peers. The arbitrary limit of 400 billion iterations equates roughly to 30 days of continuous computation on standard, modern hardware (assuming a relatively fast modern CPU architecture). By bounding this upper limit with a hardcoded ceiling, the node guarantees it will always eventually return from the function, even under adverse conditions. If the requested iterations exceed this hardcoded cap, the engine refuses to run at all. It immediately rejects the request with a safely typed VdfError::ProofGenerationError, sparing the node from useless computation.

3. Preventing CPU Starvation (The System Lock)

#![allow(unused)]
fn main() {
-> See: src/lib.rs — Lines 68 to 110
}

Important

This section implements the most critical defensive security mechanism in the entire file. Since the chiavdf C++ library is heavily optimized and computationally aggressive, running multiple instances concurrently will cause severe CPU thrashing. This aggressive thrashing would bring a typical server to its knees, starving all other critical background processes (like consensus validation and mempool management) of necessary CPU time. A standard Rust Mutex or RwLock would theoretically only lock threads within the currently running application process, which is insufficient. To enforce absolute mutual exclusion across the entire operating system (protecting against multiple Kinetic processes or isolated CLI tools running simultaneously), the engine leverages physical filesystem locks. It commands the OS to create a physical lock file on the disk uniquely named kinetic_vdf.lock. First, it intelligently attempts to find a suitable parent directory using the kinetic_core::config::get_base_dir() utility. If that primary directory creation fails, or if disk permissions are unexpectedly denied, it gracefully falls back to a Unix-specific temporary directory structurally bound to the active user’s UID (e.g., /tmp/kinetic-<uid>). Next, it securely configures standard file access options (specifically read, write, and create capabilities) using Rust’s std::fs::OpenOptions builder API. On Unix-based operating systems, it conditionally utilizes the std::os::unix::fs::OpenOptionsExt trait to set the libc::O_NOFOLLOW flag during file creation. This is a critical, subtle security measure designed to prevent devastating symlink attacks. Without this specific flag, a malicious user operating on a shared, multi-tenant server could secretly symlink the expected lock file path to a critical system file (such as /etc/shadow or a root binary). This would cause the Kinetic node (especially if inadvertently running with elevated privileges) to inadvertently corrupt or overwrite the system file with binary lock data, destroying the host operating system. Finally, after safely opening the file descriptor, it formally calls .lock_exclusive() provided by the external fs2::FileExt trait. This function call directly triggers a kernel-level OS file lock (such as flock on Linux/macOS or LockFileEx on modern Windows). If another local process already physically holds the active lock, this call will safely block the current thread until the lock is formally released by the kernel. This robustly guarantees that only one VDF evaluation happens on the physical machine at any given nanosecond, fully preserving vital system resources.

4. Generating the Proof via FFI

#![allow(unused)]
fn main() {
-> See: src/lib.rs — Lines 112 to 123
}

Once the OS lock is firmly and safely acquired by the thread, the engine confidently proceeds to call chiavdf::prove. It systematically passes the challenge.hash as the initial cryptographic random seed, the default_el identity bytes, the discriminant size (which is hardcoded securely to exactly 1024 bits), and the requested iterations. This specific action represents a complex Foreign Function Interface (FFI) call, crossing the strict memory boundary from safe Rust into unsafe, unmanaged C++. The underlying C++ library dynamically allocates the necessary heap memory, computes the Wesolowski proof through millions of repeated squaring steps, and finally returns the resulting byte array back to the Rust caller. Notice deeply that the lock file is never, ever unlocked via manual code (there is no unlock() method call written in the source). Rust elegantly utilizes the RAII (Resource Acquisition Is Initialization) memory management pattern to handle this . When the lock_file variable naturally goes out of scope at the absolute end of the evaluate function’s execution block, Rust’s Drop trait automatically engages in the background. The Drop trait cleanly and safely closes the underlying file descriptor tied to the lock. Closing the file descriptor automatically and natively instructs the operating system kernel to release the physical lock immediately across the whole system. This architectural design ensures no deadlocks occur, even if a runtime panic happens mid-computation or if the function returns early via the Rust ? operator.

5. Proof Verification and Size Bounds

#![allow(unused)]
fn main() {
-> See: src/lib.rs — Lines 131 to 140
}

Verification is routinely performed by all network validators whenever they receive a newly propagated block containing a time-sensitive domain transfer. The verify function must logically be fast, efficient, and lightweight to avoid significantly slowing down block propagation across the global network consensus layer. The very first check executed is a rudimentary basic sanity filter: if proof.proof_bytes.len() > 1024. Wesolowski proofs, by their fundamental mathematical nature, are guaranteed to be very small in byte size, regardless of the total iteration count processed. If a malicious peer intentionally sends a 50MB or 1GB proof over the network wire, it is undeniably an active attack. The attacker is obviously attempting to maliciously exhaust the node’s limited RAM during deserialization or trigger a forced OOM kill during verification processing. The engine rejects oversized proofs immediately, fully protecting the delicate C++ engine from ever allocating memory or processing malformed, intentionally bloated data.

6. Asymmetric Discriminant Derivation

#![allow(unused)]
fn main() {
-> See: src/lib.rs — Lines 142 to 149
}

This specific section highlights a slightly quirky and asymmetrical API design detail of the underlying chiavdf library bindings. When invoking the prove function earlier in the file, the C++ code natively takes the raw challenge hash and dynamically derives the 1024-bit prime discriminant entirely internally without user intervention. However, when invoking verify_n_wesolowski, the C++ API forces the Rust caller to provide the pre-derived discriminant as an argument. Therefore, Kinetic must manually allocate a 128-byte array buffer ([0u8; 128]) on the stack. It then manually calls the FFI function chiavdf::create_discriminant to derive the prime, operating symmetrically to how prove operates silently under the hood. A Discriminant is a vital mathematical parameter that defines the specific structure of the class group utilized for the sequential VDF operations. If this complex prime derivation fails (for instance, if the hash cannot be mapped to a valid, secure prime number), the engine gracefully bails and returns a DiscriminantError.

7. Calling the Verifier

#![allow(unused)]
fn main() {
-> See: src/lib.rs — Lines 151 to 162
}

The engine carefully bundles the freshly derived discriminant, the hardcoded 100-byte default element, the incoming untrusted proof bytes, and the original iteration count limit. It passes all these arguments directly into the chiavdf::verify_n_wesolowski FFI function to validate the math. The final argument passed is the recursion limit, which is hardcoded and permanently to 0. Some advanced VDF architectural configurations dynamically chunk their proofs into smaller sub-segments to heavily optimize specific edge cases or parallelize the verification process across multiple cores. However, Kinetic deliberately utilizes simple, small, single-segment Wesolowski proofs to minimize network overhead and consensus complexity. Because of this fundamental architectural choice, recursive segment checking is disabled entirely by passing 0 to the verifier. The C++ engine verifies the underlying mathematical proof against the inputs and returns a simple boolean indicating whether the proof is valid for the given challenge and iteration parameters.

8. Handling Unsupported Platforms (Mobile and Web)

#![allow(unused)]
fn main() {
-> See: src/lib.rs — Lines 165 to 195
}

Warning

The broader Kinetic ecosystem fully intends to extensively support lightweight client applications running natively on Android devices and within web browsers utilizing WebAssembly (WASM). However, the chiavdf library relies on intensely heavy C++ dependencies, deeply nested standard template libraries, and the GMP (GNU Multiple Precision Arithmetic Library). These complex, legacy dependencies simply do not cross-compile neatly, reliably, or natively to heavily constrained mobile or secure web environments. To elegantly handle this severe architectural limitation without bifurcating the entire codebase into separate repos, the file utilizes advanced Conditional Compilation via the #[cfg(...)] attribute macro. For specific unsupported compilation targets (namely target_os = "android" and target_arch = "wasm32"), the Rust compiler strips out the main C++ FFI implementation entirely during the build step. Instead of failing to compile, it neatly injects dummy implementations of the VdfEngine trait specifically tailored for these unsupported platforms. If a user mistakenly tries to evaluate or verify a VDF natively on a mobile phone or directly inside a browser extension, these lightweight dummy implementations engage safely. They immediately and safely return Err(VdfError::UnsupportedPlatform) without panicking, leaking memory, or crashing the host application.


Key Pieces

ChiaVdfEngine (Struct)

  • What it does: Represents the primary, centralized VDF computation backend for the Kinetic daemon. It intentionally contains no state or fields.
  • Where it is: src/lib.rs — Lines 30-31
  • Why it matters: It is the concrete, strongly-typed implementation of the generic VdfEngine interface. This strict modularity allows Kinetic developers to swap out the underlying C++ library in the future (e.g., swapping for a pure Rust implementation) simply by writing a brand new struct that adheres to the identical core trait without rewriting consensus logic.

evaluate (Method)

  • What it does: Computes the actual sequential delay function over time. It binds execution limits and securely enforces a global filesystem OS lock before safely calling into the C++ bindings.
  • Where it is: src/lib.rs — Lines 63-123
  • Why it matters: This is the core computational heavy lifter of the network. It protects the host node from DoS CPU starvation attacks via the robust OS-level fs2 lock. Furthermore, it ensures the computational time delay actually occurs physically as specified by the strict network consensus rules, protecting the domain registry.

verify (Method)

  • What it does: Cryptographically checks a provided, untrusted VDF proof against a specific network challenge hash. It derives the required mathematical discriminant and formally delegates the intensive mathematical checks to the C++ verifier.
  • Where it is: src/lib.rs — Lines 131-162
  • Why it matters: This is the mission-critical fast path for block consensus. Network validators use this specific function to rapidly and decisively confirm that other peer nodes actually spent the necessary real-world time computing the VDF, securing the domain transfer mechanism against opportunistic front-runners.

fs2::FileExt::lock_exclusive()

  • What it does: Safely obtains an exclusive, blocking file lock on the kinetic_vdf.lock file directly from the operating system kernel API.
  • Where it is: src/lib.rs — Lines 104-110
  • Why it matters: prevents multiple independent Rust threads, or entirely separate Kinetic daemon processes operating on the same physical machine, from fighting over CPU resources during intensive VDF generation, ensuring deeply stable node performance over long periods.

Conditional Compilation Attributes (#[cfg(...)])

  • What it does: Directly instructs the Rust compiler to include or exclude specific blocks of implementation code based on the target operating system or hardware architecture provided at compile time.
  • Where it is: src/lib.rs — Lines 54, 165, 181
  • Why it matters: allows the entire unified Kinetic codebase to compile successfully on restricted mobile and web targets where the bulky C++ chiavdf library simply cannot be built, ensuring the rest of the application (like essential wallet management and basic peer discovery) still functions properly on those constrained platforms without issue.

How This Connects to the Rest of Kinetic

  • CROSS-CRATE DEPENDENCY: This specific crate relies exceptionally heavily on kinetic-core for its foundational core types (specifically the Commitment and VdfProof structs) and its standardized network error handling enum (VdfError). It implements the VdfEngine trait originally defined in that core crate, adhering to the software dependency inversion principle to decouple logic.
  • FORWARD DEPENDENCY: The primary network consensus engine (which is likely located in a downstream kinetic-consensus or kinetic-node crate architecture) will reliably instantiate ChiaVdfEngine during startup. It will then call the verify method sequentially during routine block validation to secure the chain. Additionally, the mempool or transaction builder module will call the evaluate method when an end-user formally initiates a time-locked domain transfer on the live network.

Quick Reference

  • Max Iterations Hard Limit: 400,000,000,000 (allows up to approximately 30 days of continuous single-core CPU time before erroring out).
  • Lock File Primary Search Location: kinetic_core::config::get_base_dir() (Standard application directory).
  • Lock File Fallback Search Location: /tmp/kinetic-<uid> (This specific fallback is exclusively Unix specific).
  • Lock File Standardized Name: kinetic_vdf.lock (Hardcoded string).
  • Max Permitted Proof Byte Size: 1024 bytes (A strict 1 KB sanity check specifically against malicious memory exhaustion).
  • Required Discriminant Size: 1024 bits (Equating to exactly 128 bytes of array data on the stack).
  • Unsupported Platforms: Native android operating systems and wasm32 browser architectures.

Open Questions / Things to Revisit

  • Mobile Verification Gap: Currently, native Android and WASM clients cannot verify VDF proofs locally because of the strict C++ dependency limitation. This realistically means light clients operating on mobile devices are forced to implicitly trust full remote nodes regarding the absolute validity of domain transfers. We may urgently need to find or build a pure-Rust VDF verifier in the near future to enable fully trustless, decentralized mobile clients without compromising security.
  • Lock File Cleanup Strategies: The OS file lock is released gracefully and safely via Rust’s RAII drop mechanics, but the kinetic_vdf.lock file itself is permanently left lingering on the filesystem. Should there be an explicit, structured cleanup mechanism triggered if the daemon shuts down cleanly to prevent unnecessary clutter in the system’s temporary directories over the years?
  • Hardcoded Default Element Assumptions: The default class group element (an array with 0x08 at the first byte) is hardcoded directly into the source code structure. If the mathematical class group specification ever changes in future network upgrades, this hardcoded array will need to be updated manually to match the new mathematical consensus rules.
  • Fallback Lock Directory Behavior Risks: The fallback file path pointing to /tmp utilizes the running process ID on non-Unix systems. This could potentially lead to multiple unused, stale lock directories heavily piling up over time if the application forcefully crashes frequently or restarts unexpectedly often under heavy load.
  • Error Handling Granularity Loss: The underlying C++ FFI layer returns a very simple Option (Some/None) for proof generation or a basic boolean for mathematical verification. We lose the diagnostic granularity of knowing exactly why the C++ engine actually failed during computation. It might be significantly beneficial to heavily modify the FFI wrapper to expose more detailed error codes directly from the chiavdf C++ bindings in future architectural updates to aid debugging.

Detailed Threat Model (The DoS Vector)

In blockchain networks, resource exhaustion is a primary attack vector. Because VDFs are inherently designed to be slow, they are a double-edged sword. If an attacker sends 10,000 invalid transactions to a node, standard signature verification might take a few milliseconds per transaction. The node can easily handle that throughput and ban the peer. However, if an attacker sends 100 transactions, each requiring a 5-minute VDF evaluation, the node is presented with 500 minutes of computation. If the node attempts to process these asynchronously using a thread pool, it will spawn 100 heavy threads. These threads will instantly lock up every CPU core on the machine at 100% utilization. During this time, the node cannot process legitimate blocks. It cannot respond to peer ping requests, causing it to be dropped from the network. It cannot serve RPC requests to the local user. The OS-level file lock (fs2) mitigates this by forcing a serial queue. Only one VDF is evaluated at a time, leaving the other CPU cores free for critical node operations. If the queue gets too long, the mempool can simply reject new VDF requests, preserving node stability.


Under the Hood: What is a Discriminant?

In the context of the Wesolowski Verifiable Delay Function, the mathematics operate within an imaginary quadratic number field. The “discriminant” is a negative prime number that uniquely defines this specific mathematical field. It is the cryptographic parameter that ensures the “unknown order” property holds true. To generate a secure VDF challenge, we must derive a unique discriminant for every single proof. Kinetic does this by taking the SHA-256 hash of the transaction commitment data. It then uses a deterministic algorithm (chiavdf::create_discriminant) to hunt for the nearest valid prime number to that hash. Because it is derived from the transaction hash, the discriminant is bound to that specific transaction. An attacker cannot reuse a VDF proof from a previous transaction because the hashes, and therefore the discriminants, will be entirely different. The size of this discriminant is fixed at 1024 bits (128 bytes). This size provides a cryptographic security margin against modern supercomputers. Even with parallelization, factoring a 1024-bit discriminant to discover the group order is computationally infeasible today.


Rust FFI Safety Practices

Integrating C++ code into a Rust project introduces memory safety risks. Rust’s compiler guarantees memory safety, but only for pure Rust code. The moment we cross the FFI boundary into chiavdf, those guarantees are temporarily suspended. If the C++ library has a buffer overflow or a double-free bug, it will crash the entire Rust application. To minimize this risk, this wrapper controls the data flowing into C++. It uses fixed-size arrays (like [u8; 100] for the default element and [0u8; 128] for the discriminant). By using fixed-size stack arrays, we avoid dynamic heap allocation issues on the Rust side. We pass these arrays to C++ as raw pointers (handled safely by the chiavdf binding crate). Furthermore, the sanity check if proof.proof_bytes.len() > 1024 acts as a crucial firewall. It prevents malformed, infinitely large byte arrays from the network from ever touching the C++ parsing logic. By keeping the unsafe FFI boundary as small and typed as possible, Kinetic maintains a high degree of overall system reliability.

03_tests.md - VDF Differential Cryptography Tests

Crate: kinetic-vdf Stage: 6 Reading Time: 25 minutes Depends on: lib.rs main implementation, kyn-vdf crate, chiavdf C++ bindings


What Is This?

This documentation covers the comprehensive internal test suite for the kinetic-vdf crate. Specifically, it focuses on the code located at the bottom of src/lib.rs (Lines 197-600). In Rust, it is a common idiom to place tests in the same file as the source code they are testing. These are usually wrapped in a mod tests block. They are conditionally compiled only during the testing phase using the #[cfg(test)] attribute. This ensures that the test code and its dependencies do not bloat the final release binary.

While standard unit tests usually just check if a single function returns an expected value, the tests contained in this file are far more advanced and critical to the network’s safety. This suite implements a series of rigorous Differential Cryptography Tests. Differential testing is an advanced software engineering and cryptography technique. It involves taking two entirely separate, independent implementations of a complex algorithm, feeding both implementations the exact same input data, and verifying that they produce the exact same outputs every single time.

In the context of this crate, the test suite acts as a bridge and an impartial referee. It compares two distinct VDF (Verifiable Delay Function) engines: 1. The original C++ chiavdf implementation, accessed via FFI (Foreign Function Interface) bindings. 2. The new pure-Rust kyn-vdf implementation, designed specifically for memory safety and strict parsing.

The tests in this file prove unequivocally that these two different codebases agree on the fundamental mathematical rules of the Kinetic network. They ensure that both engines recognize valid mathematical proofs as valid. More importantly, they guarantee that both engines will reject maliciously crafted, corrupted, or invalid proofs in the exact same manner, leaving no room for consensus discrepancies.


Why Kinetic Needs This

In a decentralized blockchain network like Kinetic, the consensus mechanism is everything. This consensus relies entirely on deterministic, unyielding mathematics. Every single node in the peer-to-peer network must independently verify the blockchain’s history. > [!WARNING]

If two nodes in the network disagree on whether a specific block’s VDF proof is valid, the network will instantly suffer a consensus failure. This disagreement will cause the blockchain to split into a hard fork.

Kinetic currently utilizes a dual-engine architecture for its Verifiable Delay Functions:

1. The Timelords:2. The Validator Nodes:
These are specialized, high-performance prover nodes.These are the regular network participants.
They generate the intensive cryptographic proofs using the optimized C++ chiavdf library.They verify the proofs generated by the Timelords as they arrive over the gossip network.
This C++ library is fast and efficient at sequential squaring operations.They use the memory-safe, pure-Rust kyn-vdf engine to perform this verification. This protects the regular nodes from potential C++ memory vulnerabilities like buffer overflows.

Because the proofs are created in C++ but verified in Rust, they must have absolute alignment. Their mathematical processing and binary parsing must be identical down to the very last byte. Consider a scenario where the C++ Timelord generates a proof that it believes is valid. If the Rust validator node rejects that exact same proof due to a slight parsing difference, or a minor mathematical rounding error, the entire blockchain halts immediately. Conversely, consider if a malicious attacker broadcasts a flawed, carefully crafted proof. If the C++ engine would correctly reject it, but the Rust engine accidentally accepts it due to a parser bug, the attacker could forge the blockchain history and hijack the network.

Differential testing is Kinetic’s primary, safety net against these apocalyptic scenarios. Instead of just hoping the two implementations match, this test suite guarantees it. It achieves this by throwing an , punishing battery of tests at both engines simultaneously. It uses valid proofs, invalid proofs, randomly corrupted proofs, proofs with mismatched parameters, and truncated byte arrays to verify that parity is absolute in every conceivable edge case.

Furthermore, this test suite acts as a powerful guarantee for strict Backwards Compatibility. When a blockchain relies on upstream C++ libraries, a silent version update can be deadly. An update might slightly alter how cryptographic seeds (called discriminants) are generated. If a new version silently changes the underlying math, all older blocks would suddenly fail validation. These tests hardcode known, historically expected outputs to act as immutable anchors in the codebase. If the underlying math ever changes silently, these tests will loudly fail during the local build process. This prevents an accidental network fork before the breaking code is even merged into the repository.


How It Works

The Baseline: Happy Path Verification

#![allow(unused)]
fn main() {
-> See: src/lib.rs — Lines 227 to 261
}

The test_kyn_vdf_verifies_kinetic_proof function establishes the fundamental differential baseline. It proves that the two distinct engines can communicate and fundamentally understand each other’s data formats. The test begins by initializing a new C++ ChiaVdfEngine instance. It creates a mock challenge hash, which is simply a structured array of thirty-two 1 bytes. It then instructs the C++ engine to perform exactly 1,000 iterations to generate a real, valid VDF proof.

Once the proof is successfully generated, the test enforces a strict, wire-format check: assert_eq!(proof.proof_bytes.len(), 200) Kinetic relies on the standard Chia wire format for all VDF proofs transmitted over the network. This standard requires the serialized proof to be exactly 200 bytes in length. The first 100 bytes represent the y coordinate of the proof. The second 100 bytes represent the pi structural proof.

Finally, the test takes those exact 200 bytes, straight from the C++ memory allocator, and passes them directly into the pure-Rust kyn_vdf::verify_chia_vdf() function. When the pure-Rust verifier successfully processes them and returns Ok(true), it establishes a critical fact. It proves that the core mathematics and the strict binary serialization format are aligned.

Concurrency and Thread Safety

#![allow(unused)]
fn main() {
-> See: src/lib.rs — Lines 263 to 286
}

The test_concurrent_evaluate function is crucial for testing the integrity and safety of the FFI boundary. C++ libraries are notoriously prone to severe thread-safety issues, especially when called from Rust. Rust is concurrent by design and expects fearless concurrency from all of its underlying dependencies. This test wraps the C++ engine in a Rust Arc (Atomic Reference Counted pointer). It then rapidly spawns multiple native OS threads using the thread::spawn API.

Each spawned thread independently and simultaneously attempts to generate a new VDF proof. They all use the exact same shared C++ engine concurrently. If the underlying C++ code had any unsafe global state, this test would catch it immediately. If it had hidden race conditions, or lacked proper thread-local memory isolation, this concurrent access would definitively trigger a segmentation fault or a memory corruption panic. By passing this test consistently, we guarantee that the kinetic-vdf crate is fully thread-safe. It is safe to use in a parallel, asynchronous environment like a high-throughput blockchain node.

Anchoring the Discriminant for Backwards Compatibility

#![allow(unused)]
fn main() {
-> See: src/lib.rs — Lines 321 to 340
}

The test_discriminant_consistency_across_versions function is our primary safeguard against dependency drift. In VDF cryptography, the “discriminant” is a number securely derived from the initial challenge hash. This discriminant defines the overarching mathematical group used for the sequential squaring operations. The test provides a specific, deterministic challenge hash: [42u8; 32]. It then asks the C++ engine to generate the 128-byte discriminant for this exact hash.

Crucially, the test then compares the first 16 bytes of the newly generated discriminant array against a hardcoded byte array that lives directly in the test source code: [237, 89, 165, 1, 5, 76, 207, 152, 207, 134, 182, 117, 254, 184, 124, 248] > [!IMPORTANT]

This hardcoded array acts as an immutable, unbreakable cryptographic anchor. If a future update to the chiavdf C++ library silently changes its internal hash algorithm, or if it subtly tweaks the derivation logic for the discriminant, the output will no longer match this array. The test will fail immediately and loudly during local development or in the CI pipeline. This warns the core developers that integrating the new C++ version will catastrophically break compatibility. It ensures that we never accidentally deploy a change that would invalidate historical blocks and fork the live network.

The Differential Fuzzer: Testing Failure Modes

#![allow(unused)]
fn main() {
-> See: src/lib.rs — Lines 343 to 598
}

The test_chiavdf_and_kyn_vdf_differential_compatibility function is the absolute crown jewel of the test suite. It acts as an , multi-step, relentless differential fuzzer. It runs a punishing gauntlet of seven rigorous checks over multiple different deterministic challenge hashes. Each of these mathematical challenges is evaluated for a realistic 10,000 iterations to ensure mathematical depth.

Step 1: Proof Generation The test uses the native C++ engine to generate a brand new, valid 200-byte proof. This generated proof is specific to the current deterministic challenge hash in the fuzzer loop.

Step 2 & 3: Dual Verification The test passes the newly generated proof back into the C++ verifier engine. This acts as a basic sanity check, ensuring the C++ engine actually accepts its own valid work. Then, it passes the exact same proof byte array into the pure-Rust kyn-vdf verifier engine. This ensures the Rust engine also accepts the valid proof, re-establishing the differential baseline.

Step 4: Single Bit Corruption -> See: src/lib.rs — Lines 427 to 470 The test intentionally and surgically corrupts the proof to ensure both engines detect the tampering. It clones the original proof bytes into a new, mutable array. It then cleverly uses the bitwise XOR assignment operator (^=) to flip exactly one single bit in the array: corrupted_bytes[50] ^= 0x01; Using XOR to selectively flip a bit in a valid proof is a standard, effective cryptographic testing technique. An attacker can easily provide purely random garbage data to a node. However, robust parsers often reject pure garbage data instantly, before ever reaching the complex mathematical logic. By flipping a single bit in an otherwise perfect and structurally sound proof, we ensure that the test bypasses these early, superficial parsing checks. This forces the verification engine to run the deep mathematical validation algorithms on corrupted data. The mathematical validation must ultimately and decisively fail. The C++ engine is expected to simply return a boolean false. The Rust engine (kyn_vdf) uses a Rust match statement to handle the rejection safely and gracefully. It correctly considers both Ok(false) (the math failed) and Err(_) (deserialization failed) as valid rejections. Both engines must universally reject the corrupted proof without crashing or panicking.

Step 5: Mismatched Iteration Counts -> See: src/lib.rs — Lines 472 to 508 The test takes the valid proof, but it lies to the verifiers about the required computational work. It passes iterations + 1 to both the C++ and Rust engines instead of the correct value. VDF proofs are , purposefully sensitive to the exact iteration count. Even being off by a single iteration means the resulting y coordinate should be , entirely different. This step ensures that both engines correctly and enforce the iteration parameter during verification. They must ruthlessly reject the proof if the parameter does not match the computational work encapsulated in the proof bytes.

Step 6: The Wrong Challenge Hash -> See: src/lib.rs — Lines 510 to 551 The test specifically mutates the original challenge hash. It again uses the XOR operator to flip the very first bit of the hash: wrong_hash[0] ^= 0x80;. However, it presents the original, valid proof bytes to the verifiers alongside this new, incorrect challenge. The challenge hash dictates the discriminant, which alters the mathematical group for the VDF. Therefore, a valid proof for Block A is meaningless garbage when evaluated for Block B. Both engines must firmly and immediately reject this attempt to replay a valid proof in the wrong context.

Step 7: Array Truncation -> See: src/lib.rs — Lines 553 to 590 The test removes the final byte of the serialized proof array. This artificially shrinks the array from the required 200 bytes down to an invalid 199 bytes. This step specifically tests the memory safety and boundary checking of the binary parsers in both languages. In C++, reading beyond the bounds of an array can easily cause a fatal segmentation fault or read uninitialized memory. The test verifies that the chiavdf C++ engine gracefully handles the truncated input array without any issues. It must return false cleanly without crashing the host process or corrupting the heap. For the Rust kyn-vdf engine, it verifies that the deserializer is enforcing the rigid wire format. It must return an explicit Err rather than attempting to pad the array with zeros or triggering a panic.


Key Pieces

The #[cfg(test)] Attribute

  • What it is: A conditional compilation attribute directive built directly into the Rust compiler.
  • Location: src/lib.rs — Line 197
  • Why it matters: It tells the Rust compiler to ignore this module during standard release builds. The code is only compiled and included in the binary when the cargo test command is run. This ensures that heavy testing dependencies, mock data, and extensive fuzzing logic are never shipped to production nodes.

test_kyn_vdf_verifies_kinetic_proof

  • What it does: Generates a real VDF proof using C++ and verifies it immediately using the pure Rust verifier.
  • Location: src/lib.rs — Lines 228 to 261
  • Why it matters: It proves that the “happy path” operates flawlessly across language boundaries and execution environments. It guarantees that the 200-byte binary serialization format translates between the C++ prover and the Rust verifier. It proves that the C++ prover and the Rust verifier speak the exact same mathematical language.

test_discriminant_consistency_across_versions

  • What it does: Generates a discriminant from a known challenge and asserts an exact, byte-for-byte match.
  • Location: src/lib.rs — Lines 321 to 340
  • Why it matters: This is the primary, immovable defense mechanism against upstream dependency drift. It ensures that silent, undocumented changes to the C++ cryptographic library cannot inadvertently fork the blockchain.

test_chiavdf_and_kyn_vdf_differential_compatibility

  • What it does: An extensive, multi-step, rigorous differential fuzzing test loop.
  • Location: src/lib.rs — Lines 343 to 598
  • Why it matters: It proves that the C++ and Rust engines have identical security profiles. They do not just agree on what a valid proof is when everything is correct and well-formed. They agree on exactly why and how invalid, malicious, or corrupted proofs should be rejected.

Bitwise XOR Mutation (^=)

  • What it does: The bitwise XOR (exclusive OR) assignment operator, a staple in low-level programming. It compares the bits of a byte and flips the bit if they differ. For example, corrupted_bytes[50] ^= 0x01; takes the 50th byte and precisely flips its lowest bit.
  • Location: src/lib.rs — Lines 438 and 518
  • Why it matters: In cryptographic testing, providing invalid arrays is often entirely insufficient. Parsers will reject malformed arrays immediately without ever exercising the core mathematical validation logic. By using XOR to flip a single bit in a valid proof, the data looks legitimate enough to bypass the parser. This securely forces the engine to evaluate the complex math, ensuring the deep validation logic correctly catches the error.

The Commitment Struct

  • What it is: A struct from kinetic_core::types used to represent a 32-byte hash.
  • Location: src/lib.rs — Lines 205, 231, 323
  • Why it matters: It encapsulates the challenge hash in a type-safe manner, ensuring that raw byte arrays are not accidentally passed around as valid challenge seeds.

How This Connects to the Rest of Kinetic

CROSS-CRATE DEPENDENCY: Validating kyn-vdf

This entire rigorous test suite is located inside the kinetic-vdf crate (which handles the legacy C++ FFI bindings). However, its true architectural purpose is to bless and validate the new kyn-vdf crate. When the main Kinetic consensus engine inside the kinetic-node crate calls the Rust verifier, it relies entirely, with full trust, on the absolute guarantees provided by this exact differential test suite. Without these tests passing consistently, kyn-vdf cannot be trusted to verify mainnet blocks.

FORWARD DEPENDENCY: Planned Consensus Hard Forks

If the Kinetic network ever decides to purposefully upgrade its cryptography in the future, such as switching to a significantly faster VDF class group or changing the underlying hashing algorithm, the hardcoded discriminant anchors within this file will intentionally and immediately fail. A core protocol developer will be required to manually calculate the new expected byte arrays. They will then have to purposefully update the hardcoded arrays in the source code themselves. This failure acts as a mandatory, unavoidable review gate for all developers. It ensures that any consensus hard fork is planned, thoroughly documented, and publicly communicated to node operators.

FORWARD DEPENDENCY: The Timelord Network Operations

In the live, production Kinetic network, Timelords will constantly execute the C++ chiavdf code. They will generate thousands of proofs every single day to secure the network. Every single validator node on the network will concurrently run the kyn-vdf Rust code to verify them. This test suite directly simulates and validates that exact, critical network interaction. Passing these tests is the primary indicator that the live network will be able to reach and maintain secure consensus.


Quick Reference

  • Differential Testing: A strict testing methodology that runs identical inputs through two entirely distinct implementations. It guarantees identical outputs across different languages (C++ and Rust).
  • Wire Format Requirement: Both engines must adhere to the 200-byte format constraint. This consists of exactly 100 bytes for the y coordinate and exactly 100 bytes for the pi proof structure.
  • Discriminant Anchor: The 128-byte mathematical seed derived from a challenge hash is deeply anchored. It is anchored to hardcoded byte arrays to prevent silent upstream mathematical changes from causing accidental forks.
  • Robust Failure Modes: The fuzzer tests how both engines react to adversarial network conditions. These conditions include specific bit corruption, incorrect iteration counts, mismatched challenge hashes, and array truncation.

Open Questions / Things to Revisit

  • Test Execution Time Constraints: The differential fuzzer currently runs 10,000 iterations. It runs this heavy computational workload for multiple different challenge hashes in a loop. While thorough, this might significantly slow down execution times in debug builds. It could become a major bottleneck in continuous integration (CI) environments for every commit. We should strongly consider conditionalizing the iteration count based on the #[cfg(debug_assertions)] flag. This would allow local unit tests to run blazing fast with fewer iterations for developers. Meanwhile, the heavy 10,000 iteration test would be preserved for release builds and nightly CI runs.
  • Property-Based Fuzzing Vectors: Currently, the bit corruption test only flips one single, specific bit. Specifically, it reliably executes corrupted_bytes[50] ^= 0x01;. We should deeply evaluate integrating a robust property-based testing framework like proptest. This would allow us to randomly and corrupt bits and bytes across the entire 200-byte proof payload. This advanced fuzzing would ensure that there are no specific, undiscovered byte offsets that could trigger a panic, segmentation fault, or unhandled exception in either the C++ or Rust parsers.
  • Zero Iterations Differential Edge Case: The suite currently includes a standalone test_edge_cases function. This function (at Line 306) specifically verifies the C++ engine correctly rejects a proof with zero iterations. We should expand the main differential fuzzer suite to include this zero-iteration scenario as well. This will definitively ensure that the pure-Rust kyn-vdf engine handles this specific edge case identically to C++.
  • Handling of C++ chiavdf Panics: The differential suite operates under a critical, optimistic assumption. It assumes that the C++ chiavdf library will gracefully return false or an error when presented with severe garbage. If the C++ library were to encounter a state that causes a hard segmentation fault or process abort, the Rust test runner would simply crash instantly without a clear Rust-level error trace or stack. This heavy reliance on the C++ library’s internal safety and error handling is precisely the reason why the Kinetic node architecture is actively migrating to the pure-Rust kyn-vdf engine. The Rust engine provides much stronger guarantees against fatal process crashes when handling untrusted remote network data.

Stage 4: VDF Utility Binaries (benchmark & prove_timing)

Crate: kinetic-vdf Reading Time: ~10-15 minutes Depends on: 03_engine.md (ChiaVdfEngine)


What Is This?

These are standalone utility programs compiled directly from the kinetic-vdf crate’s src/bin/ directory. They do not provide core network functionality to the Kinetic daemon itself. They are never imported as library code by other modules. Instead, they provide critical tooling used by developers and node operators.

The first tool, benchmark.rs:The second tool, prove_timing.rs:
- Calibrates the baseline consensus difficulty for a Kinetic network.- Evaluates the exact millisecond performance differences.
- Bases this calibration on your specific CPU’s execution speed.- Compares generating a VDF proof (proving) versus validating one (verifying).

Together, they form the hardware evaluation suite for Kinetic’s Verifiable Delay Function implementation.


Why Kinetic Needs This

Kinetic’s consensus security depends heavily on precise, hardware-dependent VDF timing. The core of a VDF is that it takes a verifiable, non-parallelizable amount of time to execute. You cannot throw more CPU cores at a VDF to make it go faster. It relies purely on the single-thread execution speed of the hardware.

If the required VDF iteration counts hardcoded into the network are too low:

  • Blocks will be produced far too fast.
  • The network will fork constantly.

If they are too high:

  • The network grinds to a halt.
  • Block production stalls for minutes at a time.

We need a concrete way to ask: “On this specific machine, how many VDF iterations equal exactly 60 seconds of computation?” That is precisely what benchmark.rs answers.

Similarly, Kinetic nodes spend a percentage of their CPU cycles verifying proofs generated by others. If verifying took as long as proving, the network would collapse under its own weight. We need empirical, real-world evidence to ensure that verification remains fast (logarithmic) compared to the slow, linear process of proof generation. prove_timing.rs acts as a developer sanity check. It guarantees the math holds up in the real world when interacting with the underlying C++ wrapper.


How It Works

The bin/ Directory Structure

In Rust, the src/bin/ directory is special. Cargo (Rust’s build system) automatically detects any .rs files placed in this directory. It builds them as standalone executable programs. This happens independently of the main library output of the crate.

They still have full access to the main library’s code. For example, they can import kinetic_vdf::ChiaVdfEngine. However, they get their own independent main() function entry point.

This pattern is why you don’t see these CLI tools polluting the core Kinetic daemon’s codebase. They sit cleanly alongside the VDF library code they test. This ensures the library remains lightweight while the tools are easily accessible via cargo run --bin.

Benchmark Routine

#![allow(unused)]
fn main() {
-> See: kinetic-vdf/src/bin/benchmark.rs — Lines 29 to 54
}

The benchmarking tool runs an infinite loop evaluating small chunks of the VDF. Instead of trying to run one evaluation that might overshoot the 60-second window, it executes the evaluate function in chunks of 10,000 iterations.

Step-by-step: 1. It instantiates the ChiaVdfEngine. 2. It creates a dummy 32-byte zeroed challenge hash. 3. It starts a high-precision timer using std::time::Instant::now(). 4. It enters a while loop, asking the ChiaVdfEngine to run exactly 10,000 iterations. 5. Every time a chunk finishes, it checks start.elapsed().as_secs() < 60. 6. If 60 seconds have passed on the monotonic clock, it breaks out of the loop. 7. It looks at the total iterations performed. 8. It then normalizes the result exactly to a 60.0-second window.

Normalization is fundamentally necessary. The very last 10,000-iteration chunk almost certainly pushed the clock slightly past the exact 60.000-second mark (e.g., stopping at 60.05 seconds). Normalization prevents timing drift.

This entire process repeats sequentially for 10 rounds. This smooths out random CPU spikes, thermal throttling, or operating system background tasks. The result is a stable, reliable hardware average.

Normalizing the Hardware Timing

#![allow(unused)]
fn main() {
-> See: kinetic-vdf/src/bin/benchmark.rs — Lines 62 to 72
}

Once the CPU’s average iterations-per-minute are averaged across all 10 rounds, the script fetches kinetic_core::constants::TARGET_MINUTES. This core constant determines what the target block time is supposed to be for the entire Kinetic network (usually a few minutes).

The script multiplies the hardware’s 1-minute baseline average by this network target. The result is the exact integer number that the user should copy and paste. It belongs in the network.json configuration file under the key benchmark_base_iterations.

Timing the Proofs vs Verification

#![allow(unused)]
fn main() {
-> See: kinetic-vdf/src/bin/prove_timing.rs — Lines 21 to 42
}

This script runs a preset array of iteration targets (T). These range from a trivial 100 all the way up to a 500,000 iterations.

For every single target T: 1. It first runs evaluate (proof generation) using a dummy 0x42 challenge hash. 2. It records the raw milliseconds elapsed. 3. Then, it takes the generated proof output. 4. It immediately runs verify on it. 5. It records that time separately.

Crucially, it runs assert!(valid) on the verification result. If the C++ library somehow generates an invalid proof, the script will panic and crash immediately rather than printing false data.

It prints the side-by-side comparison in a structured ASCII table. This proves that verification remains fast (usually well under 100ms). It holds true even as proof generation times scale linearly into the hundreds or thousands of milliseconds.


Key Pieces

std::time::Instant (Rust Standard Library)

#![allow(unused)]
fn main() {
-> See: kinetic-vdf/src/bin/benchmark.rs — Line 9
}
  • What it does: Provides a monotonically non-decreasing hardware clock for the benchmark loop.
  • Why it matters: Conceptually, Instant::now() is not asking the OS for the current date and time (like SystemTime). It queries a hardware counter. This guarantees that system clock changes (like NTP syncs or daylight savings adjustments occurring during a benchmark) do not suddenly warp the elapsed times. It purely measures absolute time passed.

chunk_size

#![allow(unused)]
fn main() {
-> See: kinetic-vdf/src/bin/benchmark.rs — Line 24
}
  • What it does: Determines the stepping size for the evaluation loop (hardcoded to 10,000).
  • Why it matters: If the chunk size was 1, the overhead of crossing the C++ FFI boundary would dwarf the actual VDF mathematical work. This would invalidate the benchmark entirely. If the chunk size was 1,000,000, we might wildly overshoot the 60-second timer window. We would be waiting minutes for the final chunk to finish before we can stop the clock.

total_iterations_per_minute (Vector)

#![allow(unused)]
fn main() {
-> See: kinetic-vdf/src/bin/benchmark.rs — Line 27
}
  • What it does: A Vec that accumulates the normalized iterations across all 10 rounds.
  • Why it matters: Storing these in a dynamically sized array allows us to calculate the true average at the very end of the run. In the future, this vector could easily be used to calculate standard deviation. It could also be used to discard statistical outliers if the CPU spiked randomly during one specific 60-second round.

t0.elapsed().as_secs_f64()

#![allow(unused)]
fn main() {
-> See: kinetic-vdf/src/bin/prove_timing.rs — Line 27
}
  • What it does: Extracts the elapsed time from an Instant as a high-precision 64-bit float instead of an integer.
  • Why it matters: For timing measurements in the microsecond or millisecond range, integer seconds are totally useless. They would just round down to 0. The float conversion lets us accurately scale down to exact fractional milliseconds for the output table by multiplying the result by 1000.0.

assert!(valid, ...)

#![allow(unused)]
fn main() {
-> See: kinetic-vdf/src/bin/prove_timing.rs — Line 36
}
  • What it does: Instantly aborts the program if the verify function returns false.
  • Why it matters: > [!IMPORTANT]

Silent failures in cryptography benchmarking are dangerous. If the engine somehow produces garbage bytes, assert! ensures the developer is notified via a loud panic. This is far better than silently logging 0ms verify times.


How This Connects to the Rest of Kinetic

CROSS-CRATE:

Both of these binaries directly pull in kinetic_core::traits::VdfEngine and kinetic_core::types::Commitment. This ensures they are using the exact same interface, traits, and data structures the main daemon uses.

FORWARD DEPENDENCY:

The console output of benchmark.rs forms the literal bedrock of Kinetic’s consensus difficulty. The benchmark_base_iterations value generated here is ingested by the networking stack upon node startup. It is used by the consensus layer to determine valid proof lengths during block generation. If this calibration is wrong, the network’s first epoch will have wildly unstable block intervals.


Quick Reference

  • Command cargo run --release --bin benchmark - Find your machine’s optimal benchmark_base_iterations.
  • Always run with --release otherwise debug overhead ruins the timing.
  • Command cargo run --release --bin prove_timing - See the performance differences between generating and verifying proofs on your architecture.
  • Both scripts rely entirely on the ChiaVdfEngine implementation binding to the underlying C++ libraries via FFI.
  • The std::time::Instant type is crucial for all reliable sub-second hardware clock measurements in Rust.

Open Questions / Things to Revisit

  • The benchmark script currently hardcodes the chunk size to exactly 10,000.
  • Is this optimal for all hardware architectures?
  • What if a future CPU is so fast that 10k iterations take less time than the Rust-to-C++ FFI context switch?
  • The timing script notes at the bottom that pure-Rust kyn-vdf verify times are measured separately.
  • It seems the pure-Rust verification might not be fully integrated into this automated test matrix yet, meaning the script only tests the C++ verify path.
  • The benchmark runs for exactly 10 rounds of 60 seconds (10 full minutes total).
  • This is a very long wait for a developer just trying to quickly spin up a local testnet.
  • Perhaps we need to introduce a --fast command-line flag for quick-and-dirty calibrations that only run 1 or 2 rounds.
  • The dummy challenge hash uses [0u8; 32] in the benchmark but [0x42u8; 32] in the timing script.
  • This inconsistency has no functional impact, but it might be worth standardizing to avoid developer confusion.

Crate: kinetic-core

Stage: 7

Reading Time: 120 mins

Depends On: kinetic-types, kinetic-vdf, kinetic-storage, kinetic-network


What Is This?

While kinetic-types defines the data structures and kinetic-network handles peer-to-peer gossip, kinetic-core contains the business logic and consensus rules. It defines what makes a name valid, how governance proposals are executed, how cryptographic random beacons are fetched, and what happens when the network encounters an error.

The kinetic-core crate sits squarely in the middle of the stack. It orchestrates the flow of data between the networking layer and the storage engine.


Key Pieces

  1. Types (src/types/): Defines the internal representations of names, identities, DNS zones, and infrastructure constants.
  2. Errors (src/error/): An exhaustive taxonomy of every single failure mode across the network, mapped to stable, RFC-7807 compliant API codes (e.g. KIN-REG-001).
  3. Governance (src/governance/): The on-chain logic that determines how the network upgrades itself, handles proposals, and counts votes based on whether it is running as a SovereignEngine or a PermissionlessEngine.
  4. Drand (src/drand.rs): Connects to the League of Entropy to fetch unbiasable, distributed randomness beacons which act as the cryptographic seed for all VDF computations.
  5. Config (src/config.rs): Handles loading and parsing the node operator’s TOML configurations safely.
  6. Consensus Math (src/consensus_math.rs): Calculates the dynamic difficulty retargeting curves for the VDFs to prevent name squatting and Sybil attacks.

Why Kinetic Needs This

Without kinetic-core, the Kinetic network would just be a generic Kademlia DHT passing random data around. kinetic-core injects the specific rules of the protocol:

  • A .kin name must only contain lowercase alphanumeric characters.
  • A VDF must be computed for a specific number of iterations based on the name length.

Important

Only the offline Root key can execute a GovernanceAction::EmergencyHalt governance proposal in Sovereign mode (council signatures are explicitly ignored).

By abstracting this into a distinct core crate, Kinetic cleanly separates the rigid consensus rules from the TCP connections and HTTP servers.


How to Read This Stage

Due to the sheer size of kinetic-core (over 6,000 lines of source code), we have broken this down into 18 highly specific, dense files.

Begin with the Types to understand the shapes of the data. Move to the Errors to understand how the system fails safely. Then, dive into the Governance and Drand modules to see the state machine in action. Finally, explore the Config, Traits, and Build sections for deep-level systems architecture.

Types Names


crate: kinetic-core stage: 2 reading_time: 15 mins depends_on:

  • crate::error::NamesError
  • crate::constants::TLD_SUFFIX
  • crate::types::infrastructure

What Is This?

This document breaks down the domain name validation module in kinetic-core/src/types/names.rs. It acts as the foundational authority for all .kin domain name registrations on the network.

Because Kinetic operates as a decentralized, permissionless ecosystem, there is no central authority or human-in-the-loop to review domain applications. Instead, the network relies entirely on deterministic, strictly codified rules running simultaneously on every node.

This file acts as the gatekeeper for the naming system, strictly defining what a valid domain name looks like and how it must be formatted before entering the state machine.

Because these rules are baked directly into the core Rust crate, every peer acts identically. Every peer parses incoming string requests in the exact same way. This ensures that the network state remains perfectly synchronized across thousands of nodes. It achieves this consensus without requiring any external or centralized validation services.

The logic within this module operates entirely statelessly. It does not check the blockchain state to see if a name is already owned or registered. Rather, it exclusively checks the syntactic validity of the requested name. It guarantees that no malformed data, hidden characters, or dangerous system names are ever submitted to the blockchain. By handling this at the lowest level, all higher-level smart contracts can trust the data implicitly.


Why Kinetic Needs This

The necessity of this module comes down to three primary pillars of decentralized network design:

1. State Consistency and Consensus Failures In a decentralized blockchain architecture, network consensus is extremely fragile. If two nodes disagree on how to interpret a piece of data, the network forks immediately. Consider a scenario where a user attempts to register the name EXAMPLE.KIN. Node A might decide to store the name exactly as written, in full uppercase. Node B, however, might run a normalization pass and convert it to lowercase example.kin. If a second user then attempts to register example.kin, the nodes will diverge in their validation. Node A would allow the registration, thinking it is distinct from the uppercase EXAMPLE.KIN. Node B would reject it, correctly identifying it as a duplicate registration attempt. This file completely eliminates that class of fatal consensus bugs. It forces a unified normalization step before any name is ever processed by the chain. Every peer that imports the kinetic-core crate will yield the exact same byte string. This guarantees cryptographic determinism across the entire decentralized network.

2. Defending Against Phishing and Homograph Attacks Decentralized networks and Web3 ecosystems are prime targets for scammers and phishers. If the naming system allowed unrestricted unicode characters, attackers would thrive instantly. Attackers could register domains that look visually identical to popular wallet services. For example, they could replace the standard Latin letter ‘a’ with a Cyrillic ‘a’. This Cyrillic letter has a completely different unicode byte value under the hood. However, it renders identically on a user’s screen in most modern web browsers. This is known in the cybersecurity world as a homograph attack. It is a major vector for wallet draining, phishing, and widespread credential theft.

Important

By strictly enforcing the standard DNS “LDH” rule (Letters, Digits, Hyphens), Kinetic stops this dead in its tracks. The system completely strips out any unicode, emojis, or complex international characters. This dramatically reduces the attack surface for visual spoofing against everyday users. Users can trust that what they see on their screen maps uniquely to a single ledger entry.

3. Infrastructure and System Protection Traditional operating systems, routers, and network stacks rely heavily on reserved domain names. Names like localhost, test, or invalid are deeply embedded into the fabric of the web. They exist in DNS resolvers, web browsers, and local routing configurations. If a malicious actor on Kinetic could register localhost.kin, the results would be disastrous. They could trick client applications into routing sensitive traffic improperly across the web. They could force web wallets or node software to talk to external IP addresses instead of local ones. Kinetic categorizes these globally understood system names as “Category 1” names. These are public utility names drawn directly from internet standards like RFC 2606 and RFC 6761.

Warning

By hardcoding a permanent blocklist of these Category 1 names, the network remains safe. It immunizes itself against a massive class of routing exploits and DNS rebinding attacks. Furthermore, the network protects its own future internal architecture by reserving “Category 2” names. Names such as seed, explorer, and docs are locked down by the protocol. This prevents opportunistic domain squatters from holding core infrastructure hostnames hostage.


How It Works

The lifecycle of domain name processing in Kinetic flows through a strict, multi-stage pipeline. Every user input string must pass through normalization, validation, and reservation checking.

The Normalization Pipeline

Before a name can be judged, it must be comprehensively cleaned and standardized. This prevents malicious actors from bypassing security checks using weird formatting tricks. -> See: kinetic-core/src/types/names.rs — Lines 19 to 28

The normalize_name function executes this critical cleaning process step-by-step:

  • Lowercase Conversion: First, it calls .to_lowercase() on the raw input string. This ensures the entire Kinetic naming ecosystem is completely case-insensitive at the protocol level.
  • Trailing Dot Removal: Next, it enters a while loop that continually pops off trailing dot characters (.). In legacy DNS architecture, a trailing dot signifies the absolute root of the internet. Kinetic strips it away to ensure canonical uniqueness.
  • Suffix Appending: Finally, the function checks if the cleaned string already ends with the official network TLD. If the user only submitted “mywallet”, it automatically appends the TLD_SUFFIX. This provides a massive UX benefit for frontend developers, allowing them to submit partial names safely.

The Validation Pipeline

The is_valid_apex_name function is the primary and final gatekeeper for the network. -> See: kinetic-core/src/types/names.rs — Lines 75 to 124

Step 1: The Suffix Requirement Before any heavy processing occurs, it explicitly checks if the raw lowercase input ends with the TLD_SUFFIX. If it does not, it immediately throws an InvalidTLD error. This early exit ensures users cannot accidentally register .com or .eth domains on Kinetic.

Step 2: Total Length Limits The full string is measured against the strict, standard RFC 1035 length limits. The total length of the domain (including the suffix) cannot exceed 253 characters. Additionally, if the string is completely empty after normalization, it is instantly rejected.

Step 3: Label-by-Label Verification A standard domain name is made up of individual “labels” separated by dots. The function splits the normalized string by the . character and iterates through each label. The length of any single label cannot exceed a maximum of 63 characters. If a label is totally empty (like in the malformed string my..name.kin), it throws a LabelTooLong error.

Step 4: Character Allowlist (LDH) The function checks every single character residing within the current label. It heavily enforces the standard LDH rule: Letters, Digits, Hyphens only. If a character is not an ASCII lowercase letter, an ASCII digit, or a hyphen, it fails immediately. It throws an InvalidCharacter error, completely blocking emojis, spaces, underscores, and special symbols.

Step 5: Structural Edge Cases Even with allowed characters, their specific placement within the label heavily matters. A label is strictly forbidden from starting with a hyphen. A label is strictly forbidden from ending with a hyphen. Crucially, a label is also absolutely forbidden from starting with a digit. This specific rule prevents names from being confused with raw IP addresses or backend system flags.

Step 6: Apex Enforcement The Kinetic protocol currently only supports the registration of apex domains. Users are not permitted to register subdomains directly at the base protocol level. The validator extracts the apex of the string using the extract_apex_name helper function. If the normalized name does not perfectly match the extracted apex, it fails. The user’s request is safely rejected with a NotAnApexName error message.

Step 7: The Reservation Gauntlet The final step checks the normalized apex against the restricted reservation lists. It first calls is_reserved_name to check for collisions with Category 1 locked names. Then, it queries the external infrastructure module for Category 2 locked names. If the name hits either list, the validation fails and the transaction is aborted.

Extracting the Apex

To facilitate the apex enforcement rule, the system must isolate the base domain. -> See: kinetic-core/src/types/names.rs — Lines 126 to 137

The extract_apex_name function takes a fully normalized name string. It splits it into a standard vector of string slices (Vec<&str>). If this vector has 2 or more segments, it securely isolates the very last two segments. It joins them back together with a dot character and returns the newly formed string. If the vector has less than two segments, it simply returns the string exactly as it is.


Key Pieces

TLD Constant

  • Name: TLD
  • Type: &str
  • Location: kinetic-core/src/types/names.rs — Line 4
  • What it does: Defines the absolute primary top-level domain for the network.
  • Why it matters: It acts as the central anchor for all domain-related logic in the crate. Hardcoding it here ensures no other module attempts to guess or reinvent the TLD.

PUBLIC_NAMES Constant Array

  • Name: PUBLIC_NAMES
  • Type: &[&str]
  • Location: kinetic-core/src/types/names.rs — Lines 45 to 59
  • What it does: A statically allocated slice of string slices representing Category 1 reserved names.
  • Why it matters: These are globally recognized reserved domains defined by RFC 2606 and RFC 6761. Kinetic locks these names on-chain so internal network operations and Tor hidden services are never compromised.

normalize_name Function

#![allow(unused)]
fn main() {
pub fn normalize_name(name: &str) -> String
}
  • Name: normalize_name
  • Signature: pub fn normalize_name(name: &str) -> String
  • Location: kinetic-core/src/types/names.rs — Lines 19 to 28
  • What it does: Sanitizes messy user input into a canonical .kin formatted string.
  • Why it matters: It ensures the complex validation logic only ever deals with perfectly clean data. It returns an owned String because it often needs to mutate the data by allocating memory to append the suffix.

is_reserved_name Function

#![allow(unused)]
fn main() {
pub fn is_reserved_name(name: &str) -> bool
}
  • Name: is_reserved_name
  • Signature: pub fn is_reserved_name(name: &str) -> bool
  • Location: kinetic-core/src/types/names.rs — Lines 34 to 40
  • What it does: Checks if a given name collides with the PUBLIC_NAMES array.
  • Why it matters: It acts as an incredibly fast check for Category 1 collisions. It iterates through the array, appends the TLD suffix using a format macro dynamically, and checks for a direct match.

is_valid_apex_name Function

#![allow(unused)]
fn main() {
pub fn is_valid_apex_name(name: &str) -> Result<(), crate::error::NamesError>
}
  • Name: is_valid_apex_name
  • Signature: pub fn is_valid_apex_name(name: &str) -> Result<(), crate::error::NamesError>
  • Location: kinetic-core/src/types/names.rs — Lines 75 to 124
  • What it does: The master security gate and final authority for the naming system.
  • Why it matters: It deliberately returns a Result instead of a simple boolean. This allows the Kinetic RPC servers to display highly specific error messages like NamesError::InvalidCharacter instead of a generic, unhelpful failure message to the end user.

extract_apex_name Function

#![allow(unused)]
fn main() {
pub fn extract_apex_name(name: &str) -> String
}
  • Name: extract_apex_name
  • Signature: pub fn extract_apex_name(name: &str) -> String
  • Location: kinetic-core/src/types/names.rs — Lines 126 to 137
  • What it does: Strips away arbitrary subdomains to return just the base registered apex string.
  • Why it matters: When routing a transaction to api.wallet.kin, the network needs to accurately resolve the true owner of wallet.kin. This function provides a robust, fast way to find that base domain every time.

Property-Based Testing

  • Location: kinetic-core/src/types/names.rs — Lines 259 to 281 -> See RUST_CONCEPTS.md for an explanation of proptest!. Property tests throw thousands of random, malformed strings at the functions to ensure they never trigger a Rust panic.

How This Connects to the Rest of Kinetic

This module serves as an essential foundational primitive sitting near the very bottom of the dependency graph.

FORWARD DEPENDENCY: crate::error::NamesError The primary validation function relies intimately on the NamesError enum defined elsewhere. It returns specific variants like NamesError::InvalidCharacter and NamesError::NotAnApexName. These rich error variants flow upward through the entire application stack. They eventually get translated into human-readable JSON responses by the RPC API endpoint.

FORWARD DEPENDENCY: crate::constants::TLD_SUFFIX Rather than hardcoding the raw string ".kin" inside the validation loop, the module imports TLD_SUFFIX. This is a highly critical architectural decision for the network’s future. It means developers can easily spin up private testnets that use a different suffix like ".test". They can accomplish this by changing one single constant, and all validation automatically adapts without rewrites.

CROSS-CRATE: crate::types::infrastructure::is_infrastructure_name This particular file handles Category 1 (internet-wide public utility) locked names. However, it explicitly delegates Category 2 names to the infrastructure module. Category 2 names are highly specific to Kinetic’s own internal architecture and deployment. By calling out to the infrastructure module, the naming layer maintains a beautifully clean separation of concerns.


Quick Reference

If you are developing a client application or wallet, you must adhere to these absolute rules:

  • Canonical Suffix: The name must fundamentally end with the network suffix.
  • Case Sensitivity: Names are totally case-insensitive on-chain; they always process as lowercase.
  • Maximum Total Length: The complete domain name cannot ever exceed 253 characters.
  • Maximum Label Length: Any single dot-separated segment cannot ever exceed 63 characters.
  • Minimum Label Length: A segment cannot be completely empty; consecutive dots will fail.
  • Allowed Character Set (LDH):
    • Lowercase ASCII letters (a through z).
    • ASCII digits (0 through 9).
    • Hyphens (-).
  • Positional Rules:
    • A label MUST NOT begin with a hyphen.
    • A label MUST NOT end with a hyphen.
    • A label MUST NOT begin with a digit (e.g., 1wallet.kin is rejected).
  • Subdomain Restriction: You can only register an apex domain. Third-level domains are heavily blocked.
  • Reserved Names List: You cannot register any of the following restricted names: test, example, invalid, localhost, local, onion, arpa, null, none, zero, corp, lan, or internal.

Open Questions / Things to Revisit

There are several design limitations that the Kinetic engineering team will need to address over time.

1. Lack of Internationalized Domain Names (IDN) The validation pipeline is locked down to standard ASCII characters. This is the absolute safest choice for a decentralized network launch. It entirely avoids the immensely complex landscape of unicode homograph attacks. However, users globally will demand the ability to register names using non-Latin alphabets. To safely support this feature, the validation logic will need to fully integrate Punycode. This will add significant computational complexity and risk to the validation rules.

2. Second-Level TLD Extraction Vulnerability The extract_apex_name function blindly grabs the last two string segments of the domain. This works flawlessly right now under a single-level TLD architecture like .kin. However, if the network ever supports second-level TLDs (like .co.kin or .gov.kin), it will break. It would incorrectly identify user.co.kin as being the apex co.kin, destroying routing logic. If hierarchical TLDs are introduced, this function must be replaced by a Public Suffix List parser.

3. Performance of Normalization Allocations The normalize_name function is called constantly during network routing and block validation. Currently, it returns a brand new, heap-allocated String every single time it runs. In a high-throughput scenario processing thousands of transactions per second, this is a severe bottleneck. It is highly worth investigating whether this can be optimized using Rust’s Cow<'_, str> type. This Copy-on-Write pointer would gracefully avoid memory allocations when the input is perfectly formatted.

4. Protocol-Level Subdomain Support Currently, is_valid_apex_name explicitly blocks the base registration of any subdomains. If a user owns company.kin, they can configure local routing for blog.company.kin on their own node. However, they cannot trade blog.company.kin as an independent entity on the distributed ledger. If Kinetic wants to support granular subdomain leasing, a massive secondary validation path is needed. This new path would deliberately bypass the apex restriction while still strictly enforcing all other rules.

5. The Concept of Registration Expiration This entire module deals purely with syntactic validity. It asks a very simple question: “Is this string formatted correctly according to the rules?” It does not ask, “Is this name currently active, owned, or expired on the blockchain?” Passing the is_valid_apex_name check only means the string is syntactically allowed to exist. State-level checks and database lookups are always required before finalizing any registration transaction.

6. Penalizing Malicious Registration Attempts The validation logic quickly rejects bad names, but it is currently a free operation. If a malicious actor spams the network with thousands of invalid name registrations, the nodes must process them. Should the protocol implement a slashing mechanism or a minimum fee just for submitting a name? This would prevent denial-of-service attacks aimed specifically at the names.rs validation pipeline.

Types Identity


crate: kinetic-core stage: 3 reading_time: 45 mins depends_on: [kinetic_kid::document::KidDocument, kinetic_kid::manifest::CapabilityManifest, ml_dsa::MlDsa65]

What Is This?

This document covers the loading, derivation, and persistent storage of post-quantum ML-DSA-65 signing keypairs in identity.rs. The identity file serves as the root of trust for any node participating in the network. Without a functioning identity, a node cannot authorize namespaces, sign gossip messages, or establish authenticated P2P connections.

It outlines the authorization structures (AuthorizedKid and AuthorizedManifest) that bind cryptographic identities to human-readable network names (like .kin domains).

This module manages reconstructing post-quantum signature matrices from 32-byte seeds or BIP-39 mnemonic phrases. It secures these seeds via AES-256-GCM encryption on the filesystem, wipes intermediate memory using the zeroize crate, and enforces filesystem access controls to defend against local privilege escalation. -> See RUST_CONCEPTS.md for explanations of Zeroize, AES256Gcm, pbkdf2_hmac, and Nonce.


Why Kinetic Needs This

In a fully decentralized, self-sovereign environment like the Kinetic network, a node’s entire identity, authority, and reputation are completely defined by its cryptographic key material. Unlike traditional web systems where identity is tethered to and validated by a central certificate authority (like TLS certificates), Kinetic nodes must prove their identity autonomously. There is no central server to reset a password or recover an account. The node operator bears full sovereign responsibility for their cryptographic keys. However, Kinetic is built explicitly for the post-quantum era. It operates under the assumption that traditional cryptography will eventually fall to quantum computers. Post-quantum ML-DSA-65 (formerly known as Dilithium) keypairs are extremely large compared to classic Elliptic Curve Cryptography (ECC) or RSA keys. A single ML-DSA-65 public key is over 1 kilobyte in size. The private key matrix is substantially larger than the public key. Their sheer physical size makes them incredibly cumbersome to handle in everyday operations. They are difficult to transmit efficiently. They are effectively impossible to backup securely on physical paper without risk of transcription errors. They are complicated to store safely in environment variables.

Kinetic elegantly addresses this usability hurdle by taking a philosophical stance: it never persists or requires node operators to backup full, expanded ML-DSA-65 keypairs. Instead, it relies entirely on highly compressed, maximal-entropy 32-byte seeds as the root of all cryptographic derivation. These tiny 32-byte seeds can be seamlessly encoded as 24-word BIP-39 mnemonic seed phrases. This borrows a highly familiar, battle-tested, and user-friendly paradigm from cryptocurrency hardware wallets. This specific module implements the vital computational bridge. It expands these massive post-quantum keys from those tiny 32-byte seeds dynamically at runtime every single time the node starts up.

Furthermore, node operators frequently run their infrastructure on shared virtual private servers (VPS) or heavily multi-tenant cloud environments. Because of this, the local private key material must be aggressively protected. This protection must actively prevent side-channel filesystem extraction attacks. It must stop malicious co-tenants from stealthily accessing the keys via directory traversal. This strict threat model necessitates strict POSIX file permission enforcement at the operating system level. It also requires military-grade AES encryption for node operators who choose to encrypt their local disk state. Critically, it requires rapid intermediate memory zeroization (via the zeroize trait implementation) during key generation. This ensures that the raw seed phrase does not linger in RAM where a subsequent core dump could expose it to attackers.

Lastly, the Kinetic network scales across multiple distinct topologies. This includes the public mainnet, isolated developer testnets, and private corporate subnets. The system requires foolproof, unforgeable mechanisms to prevent cross-network replay attacks. An attacker must never be able to take a valid identity authorization signature generated harmlessly on a local testnet and broadcast it maliciously on the mainnet. Such an attack could allow malicious actors to hijack namespaces or impersonate legitimate services across network boundaries. The tightly controlled, network-scoped signable byte structures implemented in this module provide the cryptographic guarantees that completely neutralize this attack vector at the lowest serialization layer.


How It Works

The lifecycle of an identity in Kinetic is meticulously broken down into distinct, heavily guarded operational phases. These phases include secure generation and safe persistence from a human-readable phrase. They include highly protected retrieval and decryption at runtime. They include the deployment of that loaded key to authorize network-specific data structures.

1. Secure Mnemonic Key Generation and Derivation

The critical process of translating a human-readable string into a post-quantum signing key relies on aggressive cryptographic key stretching. This is explicitly designed to mathematically thwart offline dictionary and brute-force permutation attacks.

-> See: kinetic-core/src/types/identity.rs — Lines 150 to 186

When the save_keypair_from_mnemonic function is invoked by the node bootstrapping process, it executes a rigid pipeline:

  1. Strict Mnemonic Validation: The function immediately parses the 24-word string using the bip39 crate. It strictly validates the mnemonic against the standard English wordlist. It also verifies the embedded checksum derived from the original entropy. If a user mistypes a single character or word, it immediately errors out with KIN-IDN-004. If the checksum is invalid, it errors out. This prevents the generation of an unrecoverable phantom key that the user cannot later restore.
  2. Raw Entropy Extraction: The successfully validated mnemonic is converted into its raw 64-byte entropy seed form. At this stage, no additional passphrase is used for the BIP-39 algorithm itself. The empty string "" is explicitly passed to the derivation function.
  3. Network-Specific Deterministic Salting: To ensure that the exact same seed phrase intentionally produces completely different mathematical keys across different network architectures, a salt is constructed dynamically at runtime. The salt format is exclusively format!("{}-seed-key-v1", network_id). This guarantees perfect cryptographic domain separation across networks. This means a testnet key derived from a phrase fundamentally cannot sign mainnet transactions, even if the node operator makes a configuration error.
  4. Iterative Key Stretching (PBKDF2): The system heavily stretches the entropy using the pbkdf2_hmac algorithm. This algorithm is combined with the robust Sha512 hashing primitive. In release builds (triggered via the #[cfg(not(debug_assertions))] compiler flag), this runs through an astronomical 5,000,000 iterations. This extreme iteration count is critical for security against offline attacks. It ensures that even if a highly resourced attacker attempts to rapidly brute-force variations of a partially recovered seed, the sheer computational latency of millions of SHA-512 hashes per guess renders the attack mathematically infeasible on modern GPU clusters.
  5. Post-Quantum Expansion: The resulting 32-byte derived output from the PBKDF2 operation is still not the final usable key. It is explicitly fed into the ml_dsa::SigningKey::<ml_dsa::MlDsa65>::from_seed constructor. The internal ML-DSA-65 algorithms then mathematically expand this minimal, high-entropy seed. They expand it into the massive, multi-kilobyte matrix structures required for post-quantum signature generation.

Important

6. Aggressive Memory Zeroization: This is a crucial, non-negotiable security guarantee against memory introspection. Immediately after the ML-DSA-65 key is successfully populated, the intermediate 64-byte raw mnemonic seed must be wiped. The 32-byte derived PBKDF2 output must also be wiped. They both have the .zeroize() method explicitly called upon them. This action physically overwrites the specific memory buffers in the process RAM with null bytes (0x00). If a malicious process attempts to execute a core dump a microsecond later, the seed material is already permanently destroyed in volatile memory. If an attacker uses a debugger to inspect the running process, they will only find null bytes.

2. Atomic File Persistence

Writing the 32-byte seed to the physical storage medium prioritizes data corruption prevention, transaction atomicity, and local operating system access control.

-> See: kinetic-core/src/types/identity.rs — Lines 187 to 210

Kinetic writes the newly derived seed to a temporary file with a .tmp extension alongside the intended destination path instead of overwriting existing key files directly.

Crucially, it utilizes the standard library’s OpenOptions::new() to apply strict 0o600 POSIX permissions upon file creation. This specific UNIX permission integer is passed to the OS kernel. It instructs the operating system kernel that only the exact user executing the node process is allowed to read or write to this file. All group members are hard-blocked from reading the file contents at the OS filesystem layer. All other generalized system users are also hard-blocked.

Once the exact 32-byte sequence is written to the operating system’s internal filesystem cache buffer, a vital stabilization step occurs. The system call file.sync_all() is immediately invoked on the open file handle. This system call forcefully bypasses the OS write cache. It forces the operating system block layer to perform a synchronous hardware-level flush to the physical disk platter or NVMe SSD storage controller. Only after the hardware controller explicitly acknowledges that the data is physically committed to non-volatile storage does the module proceed.

Finally, it utilizes the fs::rename operation to atomically swap the .tmp file over the active, primary key file path. By leveraging the atomic guarantees of POSIX rename operations, Kinetic guarantees absolute state consistency. It guarantees that a sudden data center power loss during the bootstrap sequence can never result in a corrupted identity file. It guarantees that a kernel panic during saving will never leave an empty file. It guarantees that an abrupt node termination will not result in a half-written file. The identity state on disk is always strictly binary: completely successfully written, or entirely absent.

3. Hydrating the Identity at Runtime

When the Kinetic node initiates its startup sequence, it must rapidly locate this persistent seed on disk. It must then re-hydrate the ML-DSA-65 signing key matrix into operational memory. The module provides dual support for reading both raw plaintext seed files and highly secure AES-encrypted seed files.

Loading Plaintext Keys

-> See: kinetic-core/src/types/identity.rs — Lines 45 to 65

For a standard node runtime leveraging load_keypair, the system first probes the filesystem. It does this by evaluating the ENV_KEY_PATH environment variable override (crate::constants::ENV_KEY_PATH). If this override is absent, it elegantly falls back to utilizing the default configuration base directory. It then appends the provided filename to this base path.

Upon successfully opening and reading the file, it enforces an uncompromising structural boundary check before processing the bytes. If the file is not exactly 32 bytes in length, the operation is immediately rejected entirely. It yields a CorruptedIdentityFile error (KIN-IDN-002) detailing the unexpected length. This specific, rigid constraint prevents the accidental ingestion of full-sized key files that a user might mistakenly paste in. It prevents the ingestion of malformed mnemonic string backups saved in text files. It prevents failures caused by randomly corrupted filesystem sectors. It strictly guarantees that only perfectly formed 32-byte seeds enter the complex ML-DSA-65 algorithms. After validation, it simply copies the bytes into a fixed-size [0u8; 32] array. Finally, it passes this array to the ml_dsa::SigningKey::<ml_dsa::MlDsa65>::from_seed() method.

Loading Encrypted Keys

-> See: kinetic-core/src/types/identity.rs — Lines 81 to 123

For highly security-conscious node operators utilizing load_encrypted_keypair, the underlying file layout is significantly more complex. It expects a rigid, concatenated binary payload sequence rather than raw bytes:

  • 16 bytes: A cryptographically secure random salt generated exclusively for the PBKDF2 stretching phase during the initial encryption pass.
  • 12 bytes: A unique, cryptographically secure random Nonce (Number used once) required by the AES-GCM cipher algorithm logic.
  • Remaining bytes: The actual AES encrypted ciphertext natively alongside the implicitly attached 16-byte GCM MAC (Message Authentication Code).

When hydrating an encrypted key, the plaintext password supplied by the node operator must be aggressively stretched. This utilizes the exact same 5,000,000 iteration PBKDF2-HMAC-SHA512 process used in mnemonic generation. The password is mathematically hashed against the dynamically extracted 16-byte salt parsed from the file header block. This complex, highly intensive computational process deterministically derives the final 32-byte AES symmetric key required for decryption.

The Aes256Gcm cipher struct is then instantiated using this freshly derived key. The 12-byte nonce slice is aligned and passed into the decrypt method. The ciphertext payload is finally submitted for decryption. AES-GCM is an authenticated encryption cipher, which is a critical security property for Kinetic. This means it implicitly mathematically verifies the 16-byte MAC during the single decryption pass. Consequently, any malicious tampering with the file bytes on disk will result in a hard, immediate decryption failure. Any arbitrary bit-flipping caused by cosmic rays or failing SSD sectors will also result in immediate failure. This yields a DecryptionFailed error. It will absolutely never silently output garbage, unpredictable decrypted data into the highly sensitive ML-DSA-65 constructor. This strictly prevents undefined cryptographic behavior at the core node level.

4. Fortified Cross-Network Replay Protection

When a user’s cryptographic identity is successfully loaded into memory, it is utilized to authorize network primitives. A common, foundational example is securely linking a mathematical public key to the highly valuable saif.kin namespace document. A critical security invariant in this decentralized architecture is that an authorization signature generated on a specific network layer must absolutely only be mathematically valid on that exact intended network. It must never cross boundaries.

-> See: kinetic-types/src/identity.rs — Lines 35 to 55

Both the AuthorizedKid and AuthorizedManifest wrapper structures implement a crucial .signable_bytes(network_id) method. Rather than naively passing the raw JSON string to the signature algorithm, they construct a tightly controlled binary payload. Rather than using a loosely concatenated payload, they enforce a rigidly framed binary format:

  1. network_id prefix (e.g., the raw UTF-8 bytes of the string kin-mainnet-v1 or kin-testnet-v2)
  2. A literal protocol suffix byte string array: exactly -auth-kid-v1 or -auth-manifest-v1
  3. A 32-bit big-endian length prefix precisely encoding the length of the namespace string (via u32::to_be_bytes())
  4. The exact UTF-8 bytes of the associated network name string itself
  5. A 32-bit big-endian length prefix encoding the exact byte length of the internal payload’s canonical representation
  6. The canonical, perfectly ordered byte string of the core payload document itself

By permanently hardcoding the environment’s network_id string at the very absolute start of the binary serialization, signatures are explicitly scoped. A signature produced specifically for a local development network like kin-testnet structurally and mathematically cannot ever validate on the production kin-mainnet. The raw byte payload that the ML-DSA-65 signature natively wraps will intrinsically and catastrophically differ due to this prefix boundary.

Furthermore, the strategic use of strict length prefixing ensures that a sophisticated malicious actor cannot creatively exploit byte boundaries to forge signatures. Length prefixing is robustly achieved via the standard u32::to_be_bytes(). Without length prefixes, an attacker might attempt to creatively merge the trailing characters of a name with the leading characters of a payload. This could theoretically maliciously forge a completely different structural context that happens to hash to the exact same overall byte value. Length prefixes categorically eliminate this entire class of subtle boundary manipulation and semantic ambiguity attacks.


Key Pieces

load_keypair

  • What it does: Reads a raw 32-byte seed sequence directly from the designated filesystem path.
  • Validation: Strictly validates its exact physical byte length before any cryptographic processing begins.
  • Expansion: Mathematically expands the 32-byte seed into a fully operational post-quantum ML-DSA-65 matrix keypair ready for rapid signature operations.
  • Location: kinetic-core/src/types/identity.rs:39
  • Why it matters: This function represents the primary initialization path for all standard Kinetic nodes to acquire their operational cryptographic identity.
  • Error Codes: Can return KIN-IDN-001 (IO error), KIN-IDN-002 (Corrupted File), or KIN-IDN-003 (Not Found).
  • Failure Impact: If this function fails due to filesystem errors, missing paths, or file corruption, the node is rendered cryptographically inert. It cannot sign internal gossip messages. It cannot participate in network consensus. It cannot authorize any namespace transactions.

load_encrypted_keypair

  • What it does: Securely parses a concatenated AES-GCM encrypted binary payload from the local disk.
  • Decryption Flow: It dynamically unpacks the embedded cryptographic salt and nonce headers from the unencrypted prefix bytes of the file.
  • Key Stretching: It aggressively stretches the node operator’s provided plaintext password through 5,000,000 rounds of PBKDF2 to derive a symmetric AES decryption key.
  • Finalization: It authenticated-decrypts the internal ciphertext using the MAC. It finally feeds the resulting raw 32 bytes into the ML-DSA-65 expansion algorithm.
  • Location: kinetic-core/src/types/identity.rs:69
  • Why it matters: This function provides vital, defense-in-depth security for high-value node runners (such as foundational relay nodes, critical infrastructure providers, or high-value namespace owners).
  • Threat Model: These high-value nodes might be specifically targeted by sophisticated server breaches or zero-day OS exploits designed to steal key files. The extreme 5-million iteration PBKDF2 stretch makes offline dictionary cracking operations of these stolen files computationally bankrupting for the attacker.

save_keypair_from_mnemonic

  • What it does: Thoroughly validates and parses a standard 24-word BIP-39 mnemonic string provided by the user.
  • Derivation: It algorithmically derives a network-specific cryptographic salt and heavily stretches the resulting entropy via the PBKDF2 algorithm.
  • Persistence: It persistently saves the resulting 32-byte output atomically to disk hardware using temporary files and POSIX rename operations.
  • Security: It enforces tight 0o600 POSIX access control lists (ACLs) before explicitly and aggressively wiping intermediate RAM buffers containing the raw mnemonic entropy using the .zeroize() method.
  • Location: kinetic-core/src/types/identity.rs:150
  • Why it matters: This function is the sole, highly secure mechanism by which human-readable backups (which can be written on physical paper) are converted into operational, machine-readable post-quantum signature keys.
  • Hardware Safety: The stringent atomic filesystem write operations guarantee absolute data safety against sudden hardware power interruptions or kernel panics during the highly vulnerable initial node provisioning sequence.

AuthorizedKid

  • What it does: A comprehensive wrapper struct logically pairing a baseline KidDocument with a specific .kin network string name.
  • Binding Mechanism: It is crucially bounded by a mathematically unforgeable, network-scoped ML-DSA-65 owner signature verifying the precise structural attachment of the name to the public keys.
  • Fields:
    • name: The .kin string literal representing the human-readable namespace.
    • kid_doc: The embedded raw KID document containing the node’s public keys.
    • owner_signature: The raw bytes of the ML-DSA-65 post-quantum signature proving ownership.
  • Location: kinetic-types/src/identity.rs:15
  • Why it matters: Within the Kinetic architectural ecosystem, floating decentralized identifiers (KIDs) constitute the base, anonymous identity layer.
  • Identity Elevation: This specific struct elevates an isolated, floating KID by cryptographically anchoring and binding it to a highly specific, easily human-readable namespace. This transformation makes the identity highly routable. It makes it globally discoverable. It makes it socially relevant within the P2P overlay network.

AuthorizedManifest

  • What it does: A structural mirror to the KID authorization logic, this struct firmly pairs a complex CapabilityManifest with a network namespace.
  • Composition: It optionally appends the parent contextual KID document. It binds all of these tightly together by the exact same network-scoped authorization signature.
  • Fields:
    • name: The .kin string literal representing the human-readable namespace.
    • manifest: The embedded raw capability manifest document outlining node permissions and services.
    • kid_doc: Optional inclusion of the parent identity document for immediate verification context.
    • owner_signature: The raw bytes of the ML-DSA-65 post-quantum signature proving capability ownership.
  • Location: kinetic-types/src/identity.rs:59
  • Why it matters: This struct enables abstract network capabilities (such as localized service hosting, sub-protocol participation rights, or highly restricted data access scopes) to be demonstrably and publicly owned by a specific .kin name.
  • Consistency: The internal byte layout and structural serialization design perfectly mirror the AuthorizedKid implementation. This maintains cognitive and structural homogeneity across the codebase’s core authorization mechanics.

How This Connects to the Rest of Kinetic

  • FORWARD DEPENDENCY: The fully hydrated ML-DSA-65 keys generated by this module are heavily consumed globally by the network transport layer. -> See: kinetic-core/src/network/peer.rs These deeply derived keys are specifically utilized to digitally sign all internal network handshakes. This ensures every single P2P TCP connection is mutually authenticated using post-quantum cryptographic primitives.
  • FORWARD DEPENDENCY: The heavily scrutinized AuthorizedKid and AuthorizedManifest serialization structs represent the primary, foundational payload structures physically transmitted over the global gossip network. -> See: kinetic-core/src/validation.rs The mathematically sound .signable_bytes() method is heavily invoked downstream by the consensus logic. This is done to independently verify the structural integrity of floating namespaces broadcast by untrusted remote network peers.
  • CROSS-CRATE: This entire identity subsystem fundamentally relies on the isolated kinetic-kid crate for the baseline mathematical definitions of the KidDocument and CapabilityManifest internal structures. The logic defined here effectively serves as a higher-level organizational wrapper. It introduces the critical “name binding” and precise “network scope” validation metadata rigorously demanded by the active Peer-to-Peer operational layer.

Quick Reference

  • Core Cryptographic Standard: ML-DSA-65 (NIST Post-Quantum Cryptography Standardization, formerly Dilithium).
  • Physical Disk Footprint: Exactly 32 bytes strictly for plaintext seed operations.
  • Encrypted Disk Footprint: Exactly 60 bytes for AES-encrypted payloads (16 byte Salt + 12 byte Nonce + 32 byte Ciphertext and appended MAC).
  • Key Stretching Configuration: A strict requirement of 5,000,000 PBKDF2 iterations utilizing the SHA-512 hashing algorithm on compiled release builds. (1,000 iterations for debug builds).
  • Operational Filesystem Permissions: Hardcoded to strictly enforce 0o600 access (Read and Write by file owner exclusively) on all UNIX-like operating systems.
  • Cryptographic Replay Protection: Foundational protection implemented intrinsically via rigid network-prefixing at the raw binary serialization layer prior to any signature application.
  • Memory Security Posture: Highly vulnerable intermediate RAM allocations are aggressively destroyed using the explicit zeroize operational trait to neutralize sophisticated memory dump extraction exploits.
  • Key Derivation Path: Mnemonic Phrase -> 64-byte raw entropy -> PBKDF2-HMAC-SHA512 with network salt -> 32-byte derived seed -> ML-DSA-65 Expanded Key Matrix.

Open Questions / Things to Revisit

  • PBKDF2 vs Argon2 Migration: The network is currently hardcoded to utilize PBKDF2-HMAC-SHA512. While this algorithm is highly robust, time-tested, and officially NIST approved, modern cryptographic research indicates that Argon2id provides fundamentally superior, memory-hard resistance against highly optimized ASIC (Application-Specific Integrated Circuit) and GPU-cluster cracking attempts. We should deeply evaluate and potentially schedule migrating the core key derivation function for the encrypted file loading sequence to provide superior resistance against offline brute-forcing.

Warning

  • Windows File Permissions Parity: The explicit, strict 0o600 permission constraint is currently rigidly scoped via the #[cfg(unix)] compilation flag in Rust. We urgently need to investigate and ensure that native Windows OS compiled implementations correctly translate this requirement by aggressively restricting file Access Control Lists (ACLs) exclusively to the executing service user. Failure to do so risks trivial privilege escalation local reads on compromised Windows servers.

Important

  • Browser WebAssembly (WASM) Ecosystem Support: Currently, the entire disk-based key loading and mnemonic saving logic is completely gated behind the #[cfg(not(target_arch = "wasm32"))] compilation flag due to its reliance on standard library file I/O operations (std::fs). If the long-term architectural goal dictates that lightweight web browser clients need to natively generate or securely load keypairs, we must meticulously implement a highly secure storage adapter. This could utilize IndexedDB or asynchronous Web Crypto API backed storage that strictly adheres to the exact same stringent hardware-level security guarantees currently provided by the native POSIX filesystem adapter.
  • Cryptographic Key Rotation APIs: There is currently absolutely no public API surface exposed in this file to gracefully rotate an operationally compromised 32-byte seed into a completely new seed. Simultaneously, the node must safely maintain the exact same historical identity bindings and network reputation. This missing feature is a major friction point for long-term node sustainability and requires immediate architectural design to prevent node reputation loss upon key compromise.
  • Password Strength Enforcement: The load_encrypted_keypair logic relies entirely on the user providing a “strong password” during the initial encryption pass. There is currently no API enforcement during the creation phase of an encrypted keypair to ensure the password meets standard entropy requirements (length, character complexity). We should seriously consider adding an entropy validation pass using a library like zxcvbn before allowing encryption to proceed to disk.

Deep Dive: ML-DSA-65 vs Classical Keys

To truly understand why Kinetic architected the system around 32-byte seeds instead of storing the keys directly, one must understand ML-DSA-65. ML-DSA-65 is the NIST standardization of the Dilithium algorithm.

AlgorithmProperty
ML-DSA-65Unlike RSA which relies on integer factorization, ML-DSA-65 relies on the hardness of finding short vectors in a lattice.
ML-DSA-65Unlike ECC which relies on the discrete logarithm problem on elliptic curves, ML-DSA-65 uses module learning with errors (MLWE).

Because quantum computers running Shor’s algorithm can trivially break RSA and ECC, those algorithms are obsolete for future-proof networks. However, the mathematical structures (matrices and polynomials) required for ML-DSA-65 are massive.

Key TypeSize Details
ed25519A standard ed25519 private key is exactly 32 bytes.
ML-DSA-65 Private KeyAn ML-DSA-65 private key is approximately 4,032 bytes.
ML-DSA-65 Public KeyAn ML-DSA-65 public key is approximately 1,952 bytes.
ML-DSA-65 SignatureAn ML-DSA-65 signature is approximately 3,309 bytes.

If Kinetic forced users to back up 4,032 bytes of random private key data, physical paper backups would be impossible. It would require printing QR codes that are extremely dense and prone to scanning failures. By deterministically generating the 4,032 bytes from a 32-byte seed at runtime, the physical backup remains a simple 24-word phrase.


Byte Serialization Example

The replay protection mechanism requires strict byte layout adherence. Consider a node authorizing the name saif.kin on kin-testnet. The .signable_bytes("kin-testnet") method constructs the exact array passed to the signer.

  1. kin-testnet -> 11 bytes ([107, 105, 110, 45, 116, 101, 115, 116, 110, 101, 116]).
  2. -auth-kid-v1 -> 12 bytes ([45, 97, 117, 116, 104, 45, 107, 105, 100, 45, 118, 49]).
  3. Length of name saif.kin -> 8 bytes. As a 32-bit big-endian integer, this is [0, 0, 0, 8].
  4. The name itself saif.kin -> 8 bytes ([115, 97, 105, 102, 46, 107, 105, 110]).
  5. Length of canonical JSON -> [0, 0, 1, 144] (assuming 400 bytes).
  6. The canonical JSON bytes. If a malicious actor captures this signature and broadcasts it to kin-mainnet, the validation logic will reconstruct the required bytes starting with [107, 105, 110, 45, 109, 97, 105, 110, 110, 101, 116]. Because the very first bytes of the hash input differ, the final hash differs entirely. The ML-DSA-65 signature verification will immediately mathematically reject the signature as invalid for the given payload.

Step-by-Step Flow: Encrypted Key Loading

Let’s walk through the exact execution trace when a user boots a node with an encrypted identity file.

  1. The node process starts and reads KINETIC_KEY_PATH.
  2. It calls fs::read and pulls the entire file into a heap-allocated Vec<u8>.
  3. It performs a boundary check: bytes.len() < 16 + 12 + 16. If true, it returns CorruptedIdentityFile.
  4. It slices &bytes[0..16] to extract the PBKDF2 salt.
  5. It slices &bytes[16..28] to extract the AES-GCM Nonce.
  6. It slices &bytes[28..] to extract the ciphertext and MAC.
  7. It allocates a [0u8; 32] buffer for the AES key on the stack.
  8. It invokes pbkdf2_hmac::<Sha512>. This blocks the thread, executing 5,000,000 SHA-512 hashes.
  9. It instantiates Aes256Gcm::new using the resulting stack buffer.
  10. It calls cipher.decrypt.
  11. AES-GCM calculates the authentication tag over the ciphertext and compares it to the MAC.
  12. If they match, the plaintext 32 bytes are returned.
  13. The plaintext bytes are passed to ml_dsa::SigningKey::from_seed.
  14. The fully hydrated key is returned to the node’s core state machine.

Detailed Error Analysis

The identity module utilizes a strict, custom error enum named IdentityError. Each variant maps to a highly specific failure mode during the bootstrap process. Understanding these errors is critical for debugging node initialization failures.

IdentityNotFound (KIN-IDN-003)

  • Trigger: The fs::read system call yields a NotFound standard library IO error.
  • Context: The node was started, but the identity.bin file simply does not exist at the resolved path.
  • Resolution: The user must explicitly run kinetic seed init to generate a new key.
  • Resolution (Recovery): Alternatively, the user must run kinetic seed restore and provide their 24-word backup phrase.

CorruptedIdentityFile (KIN-IDN-002)

  • Trigger: The filesystem read succeeds, but the resulting byte vector length is unexpected.
  • Context (Plaintext): The file length is not exactly 32 bytes.
  • Context (Encrypted): The file length is less than 44 bytes (16 salt + 12 nonce + 16 MAC).
  • Resolution: The file has been fundamentally tampered with or corrupted by the OS block layer. The user must delete the file and restore from their mnemonic backup.

DecryptionFailed

  • Trigger: The Aes256Gcm::decrypt method returns an error.
  • Context: This occurs for two primary reasons. First, the user provided the incorrect password, resulting in the wrong AES key, causing MAC validation to fail. Second, the ciphertext bytes on disk were modified, causing MAC validation to fail even with the correct password.
  • Resolution: The user must retry with the correct password. If they have forgotten the password, the encrypted file is mathematically useless, and they must restore from the mnemonic backup.

InvalidSeedPhrase (KIN-IDN-004)

  • Trigger: The bip39::Mnemonic::parse_in method returns an error.
  • Context: The user attempted to restore a node, but the phrase provided contains invalid words, incorrect spelling, or an invalid cryptographic checksum.
  • Resolution: The user must carefully verify their physical backup and ensure all words strictly match the BIP-39 English dictionary specification.

Error Design Philosophy

The overarching philosophy of this module is to fail closed. If there is any ambiguity whatsoever regarding the integrity of the key material, the node will panic and exit. It will never attempt to automatically guess a password. It will never attempt to truncate a corrupted file to 32 bytes. It will never bypass MAC validation. This rigid “fail closed” architecture prevents the node from ever booting into an undefined cryptographic state.

Types Dns


title: kinetic-core/src/types/dns.rs stage: 4 reading_time: 30 minutes depends_on: kinetic-types::dns, kinetic-core/src/error/dns.rs

What Is This?

This module serves as the boundary between the external internet and Kinetic’s internal DNS state machine.

It takes raw DNS zone data—submitted as JSON payloads—and enforces strict validation before allowing it to persist into the decentralized ledger.

In the Kinetic ecosystem, Domain Name System (DNS) resolution is not just about translating legacy domains like example.com into an IPv4 address. It is the fundamental routing layer for the decentralized web. Because of this dual mandate, this file handles both traditional internet DNS concepts (such as A, AAAA, CNAME, and TXT records) alongside Kinetic-native decentralized routing primitives (such as libp2p Peer IDs, Kinetic Key Identifiers, and IPFS content hashes).

Specifically, this file in kinetic-core implements the DnsZoneExt extension trait. This trait binds heavy, computationally expensive validation logic and memory-safe parsing directly to the bare, lightweight DnsZone data structures that are defined over in the kinetic-types crate.

Why Kinetic Needs This

When you build a decentralized peer-to-peer network, the data you receive from the outside world is fundamentally adversarial. You cannot trust the client submitting the data. If Kinetic validator nodes were to blindly accept whatever DNS JSON payloads were broadcasted to them, the network would collapse almost immediately due to a variety of attack vectors. Here is a detailed breakdown of why this specific validation layer is an absolute necessity:

  1. State Bloat Protection: In a decentralized network architecture, every full node has to store the global state of the network. Storage is a premium resource.

Warning

If a malicious actor could publish a single DNS zone containing 10,000 garbage records, they could rapidly fill up the hard drives of every node on the network, effectively executing a resource-exhaustion attack for pennies in transaction fees. This file steps in and enforces a strict, hard-coded 50-record cap per DNS zone to guarantee bounded state growth.

  1. Denial of Service (DoS) Prevention via JSON Bombs: Parsing JSON is computationally expensive and heavily utilizes the application’s call stack. Attackers often deploy “JSON bombs”—deeply nested recursive structures like [[[[{"a": "b"}]]]]. When standard JSON parsers attempt to evaluate these, they recurse so deeply that they blow past the operating system’s stack limit, instantly crashing the node. This module intentionally utilizes serde_json::from_slice which contains explicit, hard-coded recursion limits to protect the node’s memory architecture.
  2. Delayed Routing Integrity: If a user submits an invalid libp2p PeerId (e.g., a string missing a few characters) or a malformed IPFS CID, and the consensus network accepts it without checking, the network will not crash immediately. Instead, the error lies dormant in the database. Hours or days later, when an innocent client attempts to connect to that peer, the client’s routing engine will panic or fail. We must fail early and loudly. This file ensures that invalid routing parameters never make it into the state database.
  3. Standard DNS Specification Compatibility: Legacy DNS has strict, time- tested rules. For example, DNS RFCs mandate that a CNAME record acts as an exclusive alias—it cannot coexist on the same label as an IP address or a TXT record. Kinetic nodes must enforce these legacy rules flawlessly so that Kinetic domains can seamlessly bridge back to standard Web2 DNS resolvers (like Cloudflare or Google) without breaking them.
  4. Deterministic Cryptographic Verification: In a decentralized environment, nodes must prove ownership over their routing changes. The validation structures contained in this file ensure that the bytes being cryptographically signed by the user are highly deterministic and resistant to parsing ambiguities, preventing signature-spoofing attacks.

How It Works

This file orchestrates a highly robust, multi-stage processing pipeline. It takes an array of raw, untrusted bytes from the network and walks it through parsing, normalization, capacity checking, and deep cryptographic validation.

Stage 1: Safe and Bounded Payload Parsing

The entire process begins when the network receives a DNS update request. At this point, the request is nothing more than an array of raw bytes (&[u8]). -> See: kinetic-core/src/types/dns.rs — Lines 28-30

A naive implementation might convert these bytes into a Rust String (which requires the CPU to perform an expensive, full-pass UTF-8 validation) and then parse that string into JSON. Instead, the code directly invokes serde_json::from_slice. This function operates directly on the underlying byte array. Because it operates on bytes, it is extremely fast and can perform zero-copy deserialization where possible, meaning it borrows strings directly from the byte array instead of allocating new memory for them. More importantly, it utilizes Serde’s built-in recursion limits, protecting the node from the stack- overflow DoS attacks mentioned earlier. If the JSON is invalid, it throws a standard error, halting the transaction instantly.

Stage 2: Normalization and Case-Insensitivity

By standard definition, DNS labels are supposed to be entirely case-insensitive. A user visiting API.kinetic.host expects to be routed to the exact same destination as a user visiting api.kinetic.host. However, underneath the hood, the data is stored in a Rust HashMap. A hash map calculates hashes based on precise bytes, meaning it treats "API" and "api" as two completely distinct, non-colliding keys. -> See: kinetic-core/src/types/dns.rs — Lines 31-36

To solve this discrepancy without frustrating the user by rejecting their payload, the parser automatically normalizes the data. It achieves this by allocating a brand new HashMap. It then drains the contents of the original, raw parsed map. For every single key-value pair, it converts the key to lowercase. It utilizes the powerful Rust idiom: .entry(k.to_lowercase()).or_default().extend(v). This looks up the lowercase key in the new map. If it doesn’t exist, it gracefully inserts an empty vector. Then, it extends that vector with the incoming records. If a user poorly formatted their payload and submitted separate records for both API and api, this logic safely and efficiently merges all of those records into a single, unified api array.

Stage 3: Network Capacity Limits

Once the data structure is fully normalized, the validate() method takes command of the pipeline. -> See: kinetic-core/src/types/dns.rs — Lines 55-60

Before it even looks at the content of the records, it performs a purely quantitative network capacity check. It iterates over all the values (the vectors of records) contained within the hash map, maps them to their respective lengths, and sums them up into a single integer. If the total number of records across the entire DNS zone exceeds 50, the function immediately aborts, returning a TooManyRecords error.

Important

This is a critical security constraint. It definitively bounds the maximum size of a single DNS state entry in the blockchain, ensuring that validation times and state synchronization metrics remain predictable and lightweight.

Stage 4: Label Syntax Enforcement

Next, the validator enters a loop, evaluating every single label (the subdomain string, like www, api, or @) provided in the zone. -> See: kinetic-core/src/types/dns.rs — Lines 61-78

It applies a battery of string-manipulation checks to ensure compliance with DNS standards:

  • Length Limits: The label absolutely cannot be empty (length of 0), and it cannot exceed 63 characters in length. This is a strict limitation inherited from legacy DNS architecture.
  • Root and Wildcards: It explicitly bypasses further string-character checks if the label is precisely @ (which represents the root apex of the domain) or * (which represents a wildcard routing mechanism).
  • Hyphen Edge-Case Rules: The label cannot start with a hyphen, nor can it end with a hyphen (e.g., -www or api- are strictly invalid).
  • Character Allowlist: It iterates character by character over the string. If a character is not an ASCII alphanumeric, a hyphen, or an underscore, it throws an InvalidLabelCharacters error. No emojis or arbitrary Unicode are allowed in raw labels without punycode encoding.

Stage 5: CNAME Exclusivity and Collision Avoidance

-> See: kinetic-core/src/types/dns.rs — Lines 80-85

This stage enforces one of the most commonly misunderstood rules of DNS RFCs. If a label contains a CNAME (Canonical Name) record, that label is acting as a hard, redirecting alias to another domain entirely. Therefore, it is mathematically and logically impossible for that label to also contain an A record (an IP address) or a TXT record alongside it. The validator checks the array of records. If any single record is a CNAME variant, the code asserts that the total length of the array must be exactly 1. If the length is greater than 1, it realizes a collision has occurred and throws an InvalidCnameConfiguration error, saving the user from a broken domain setup.

Stage 6: Granular Record-Specific Validation

Finally, it matches on the specific enum variant of each individual record and applies bespoke, tight validation rules based on the intended destination. -> See: kinetic-core/src/types/dns.rs — Lines 87-122

  • TXT Records: Must be strictly bounded to 255 bytes or smaller, throwing TxtRecordTooLong if violated.
  • CNAME Targets: The destination domain string cannot be empty, and it cannot exceed 253 characters.
  • Peer IDs: The raw string is passed to libp2p_identity::PeerId::from_str. If the underlying libp2p cryptographic library cannot parse it into a valid, structurally sound peer identifier, the record is rejected. This guarantees routing integrity later in the node’s lifecycle.
  • KIDs (Kinetic Identifiers): Kinetic Identity Documents must strictly begin with the exact prefix did:kin:. If a user attempts to map a domain to a different decentralized identifier method (like did:eth: or did:pkh:), the system will emit a warning log and reject the payload, ensuring only native identities are bound.
  • IPFS CIDs: IPFS Content Identifiers must not be overly long (capped at 100 characters), and they must begin with either Qm (indicating an older CIDv0 Base58 encoded hash) or b (indicating a newer CIDv1 Base32 encoded hash).

Stage 7: Host Routing Serialization Protocol

While not part of the validate() function for the overarching zone, the HostRoutingRecord contains its own complex serialization pipeline for cryptographic validation. -> See: kinetic-types/src/dns.rs — Lines 61-79

When a routing record is signed, the signable_bytes() method constructs a deterministic byte array. It pre-allocates a vector with exact capacity to prevent memory reallocation lag. It then pushes the bytes in a highly structured format: first a network ID, then a hardcoded string -routing-v1, then the length of the host ID as a 4-byte big-endian integer, the host ID itself, the length of the peer ID as a 4-byte big-endian integer, the peer ID itself, and finally an 8-byte big-endian integer representing the Drand beacon timestamp. This rigidly structured byte payload guarantees that no two varying configurations can ever yield the same hash for signing.

Key Pieces

The Extension Trait Pattern (DnsZoneExt)

-> See: kinetic-core/src/types/dns.rs — Lines 9-16

-> See RUST_CONCEPTS.md for an explanation of the Extension Trait Pattern and the Orphan Rule. We use this to bolt validation logic onto DnsZone which is defined in the external kinetic-types crate.

DnsRecord Enum (from kinetic-types)

-> See: kinetic-types/src/dns.rs — Lines 19-40

This is a strongly typed enumeration defining every possible standard and decentralized DNS record type the Kinetic ecosystem understands. It utilizes the Serde attribute #[serde(tag = "type", content = "value")] to create an internally tagged representation for clean JSON parsing. Here is how the variants break down:

  • A(Ipv4Addr): Stores a standard IPv4 address object for legacy routing.
  • AAAA(Ipv6Addr): Stores a standard IPv6 address object.
  • CNAME(String): A canonical alias string pointing to another domain label entirely.
  • TXT(String): An arbitrary text field often utilized for domain ownership verification protocols.
  • PeerId(String): Contains a libp2p cryptographic peer identifier, bypassing IP routing entirely in favor of direct peer-to-peer transport.
  • KID(String): Contains a Kinetic Key Identifier (did:kin:...), mapping the domain to a globally authorized decentralized identity document.
  • IPFS(String): Contains a content identifier hash, allowing a domain to seamlessly serve decentralized static web pages stored on IPFS.
  • Other: The catch-all variant utilizing #[serde(other)]. If a newer network upgrade introduces a record type that an older node does not understand, this variant catches it, preventing a deserialization crash and ensuring forward- compatibility.

DnsZone Struct (from kinetic-types)

-> See: kinetic-types/src/dns.rs — Lines 11-17

What it does: A structural wrapper that encapsulates a HashMap<String, Vec<DnsRecord>>.

Why it matters: Notice the strategic #[serde(default)] attribute placed above the HashMap definition. If an end-user submits a JSON payload with an entirely empty body {} instead of the expected {"records": {}}, Serde will not panic and reject it. It will realize the records field is missing, look up the default implementation for a Rust HashMap (which is an empty map), and automatically substitute it. This makes the API endpoint extremely resilient and forgiving to slightly malformed but logically harmless inputs.

HostRoutingRecord (from kinetic-types)

-> See: kinetic-types/src/dns.rs — Lines 42-81

What it does: Maps a persistent, decentralized host identifier string to a fluid, currently active libp2p PeerId.

Why it matters: While standard DNS maps domains to static IP addresses, a peer-to-peer node’s active network connection (its peer ID and IP address) can fluctuate wildly as it disconnects, reboots, or changes networks. This record is highly dynamic. Crucially, look at the signable_bytes method provided in the implementation block. When a user applies a cryptographic signature to this record to prove they own the domain, the system does not just stringify the JSON and sign it. JSON keys can be arbitrarily rearranged by different libraries, which would cause the signature verification to fail unpredictably. Instead, the code aggressively packs the data into a strict, length-prefixed byte array format. By explicitly length-prefixing the individual strings before concatenating them together, it mathematically ensures that a clever attacker cannot dynamically shift bytes between different fields to trick the signature verification algorithm into validating a malicious payload. It also integrates a drand_kyn field. Instead of relying on a highly unreliable local UNIX timestamp to prove exactly when the routing record was generated, it utilizes the block number from the Drand distributed randomness beacon. This provides a globally verifiable, entirely decentralized clock mechanism, ensuring that attackers absolutely cannot replay older, intercepted routing records to maliciously hijack network traffic.

How This Connects to the Rest of Kinetic

CROSS-CRATE: The Divide Between kinetic-types and kinetic-core

A common question when viewing this file is: Why aren’t the DnsZone struct and the DnsZoneExt validation trait simply bundled together in the exact same file? The answer lies in strict dependency management. The kinetic-types crate is purposefully engineered to be universally importable across any platform architecture. A lightweight WebAssembly wallet running inside a mobile browser can import kinetic-types to properly serialize a DNS request. That browser environment does not need—and likely cannot support—the heavy cryptographic validation dependencies required to verify it, because the backend network will perform that validation anyway. By brutally splitting the raw data types from the complex validation logic, Kinetic maintains a very clean, modular, and fast- compiling dependency graph.

FORWARD DEPENDENCY: The State Transition Machine (Consensus)

When a validator node receives a transaction block containing a DNS state update payload, the core consensus engine will intercept the raw bytes and immediately pass them into the DnsZone::parse_payload(bytes) function defined in this file. If this function returns a validation error, the transaction is immediately marked as structurally invalid. The user loses their transaction gas fee, and the global state is never modified. The consensus layer trusts this specific file implicitly and exclusively to act as the ultimate gatekeeper of state purity.

FORWARD DEPENDENCY: The P2P Peer Routing Table

When a node resolves a domain like application.kin and receives a decentralized PeerId in response, it passes that raw string directly down into the libp2p networking stack to attempt a direct connection. If the string was malformed, the connection engine would panic or silently swallow the failure. Because this file strictly and definitively ran libp2p_identity::PeerId::from_str during the initial block validation phase, the routing layer can blindly trust that the string it pulled from the database is a cryptographically sound identifier, eliminating the need for redundant string checks during active routing.

Quick Reference

  • Maximum Total Records: Hard-capped at precisely 50 records per domain zone to prevent unmitigated state bloat.
  • Label Validation Parameters: Must be exactly 1 to 63 characters in length. Only alphanumeric characters, hyphens (but explicitly not at the leading or trailing edges), and underscores are permitted. The special @ (apex) and * (wildcard) labels bypass these specific string checks entirely.
  • Record Collision Mechanics: A label housing a CNAME redirect record cannot possess any other records whatsoever; it demands exclusivity.
  • Field Size Constraints: Standard TXT records are heavily constrained to 255 bytes. CNAME targets are constrained to 253 characters.
  • JSON Payload Structure: Enforces {"type": "...", "value": "..."} formatting via Serde internal tags, diverging from raw nested structs.
  • Data Normalization: All subdomain labels are automatically cast to lowercase before permanent storage. Duplicate labels submitted in varying cases (like Api and API) are seamlessly merged together utilizing hash map entry logic.
  • P2P Ecosystem Integrations: Strictly enforces the internal string structure of libp2p PeerIds and native Kinetic KIDs before they touch the database.
  • Dynamic Routing Safety: The HostRoutingRecord struct utilizes length- prefixed byte concatenation for deterministic cryptographic signing, anchored exclusively by a drand_kyn decentralized timestamp rather than local system clocks.

Open Questions / Things to Revisit

  1. TXT Record Constraints and Developer Experience: Should TXT records really be strictly limited to a single 255-byte string limitation? Legacy standard DNS actually allows for multiple 255-byte chunks that can be conceptually concatenated by the client application. If external application developers need to store large cryptographic verification keys (like a Matrix server verification token or a domain-ownership proof) directly inside a TXT record, this hard 255-byte limit could cause massive developer friction and require hacky protocol workarounds.
  2. The “Unknown Record” Silently Passing: The DnsRecord::Other fallback variant is an excellent architectural choice for forwards-compatibility, but during the validation loop, the code does exactly this: DnsRecord::Other => {}. It simply ignores it. This means a malicious actor could theoretically submit payloads filled entirely with Other records, bypassing all structural checks while still taking up valuable slots within the 50-record limit. Is this intentional behavior, or should Other records be explicitly rejected during active zone updates to prevent state-spamming?
  3. Rudimentary IPFS CID Validation Mechanics: The current CID validation logic simply checks if the provided string begins with Qm or b. This is extremely rudimentary and highly trusting of the user. It does not verify the Base58 or Base32 encoding, nor does it verify the cryptographic multihash structure nested inside the CID. This implies that a blatantly invalid CID (like the string b-this-is-garbage-data-that-will-fail) will easily pass the validation layer, which could cause downstream IPFS resolver clients to crash when they attempt to fetch the non-existent content. We should strongly consider importing the official cid crate to perform actual, rigorous parsing at the edge.

Crate: kinetic-core

Stage: 5 - Core Types & Network Primitives

Reading Time: ~20 minutes

Depends On: kinetic-types, kinetic-verify

What Is This?

In the Kinetic Network, the boundary between raw cryptographic consensus and user-facing features is mediated by core network primitives. This document explores the miscellaneous, yet vital, types located within the kinetic-core/src/types directory. We are specifically looking at the implementations and rules defined in clock.rs, infrastructure.rs, vdf.rs, name_record.rs, and the overarching mod.rs file.

These files do not represent a single monolithic subsystem like the DNS resolver or the identity layer. Instead, they provide the essential structural “glue” that binds the decentralized network together. They handle the translation of absolute machine consensus time into human-readable network time formats. They safeguard the critical routing infrastructure of the network from hostile takeovers and domain squatters. They enforce strict memory bounds on incoming cryptographic proofs to prevent denial-of-service attacks. And they define the mathematical rules for distributing data securely across the distributed hash table (DHT). Understanding these primitives is absolutely essential because they are injected into nearly every other component of the Kinetic architecture, from the mining pool loop to the frontend RPC endpoints.

Why Kinetic Needs This

Standard blockchains rely on relatively simple primitives: Unix timestamps for block times, basic ECDSA signatures for transactions, and flat state structures. Kinetic’s architecture is radically different. Because it is a decentralized, quantum-resistant naming system driven by Verifiable Delay Functions (VDFs) and synchronized by an external randomness beacon (Drand), standard primitives are entirely insufficient.

The Timekeeping Problem: In a distributed, permissionless system, relying on local machine clocks leads to clock drift and consensus failures. Malicious actors manipulating their local time causes nodes to disagree on when events occurred, leading to network forks. Kinetic solves this by tying all time strictly to the emissions of the Drand randomness beacon. Every time Drand emits a pulse (every 3 seconds), the network advances by one tick, known as a “Kyn”. However, a raw integer counter of Kyns is completely unreadable to end users. A governance proposal stating “Voting ends at Kyn 1,532,900” is incomprehensible. We need a deterministic system to translate raw machine time into a predictable, branded hierarchy (known as The Crystal Lexicon) so that frontends, block explorers, and users have a shared, intuitive understanding of network time.

The Infrastructure Security Problem: Kinetic is an open, permissionless system; anyone can compute a VDF to claim an available name. But what happens if a malicious actor claims the domain seed.kin or explorer.kin? The seed domain is hardcoded into new node software to bootstrap their initial connection to the network. If an attacker hijacked it, they could execute a massive eclipse attack, isolating new nodes into a fake, shadow network. We require absolute, structural protection for these category 2 reserved names baked directly into the protocol’s validation logic.

The Cryptographic Payload Size Problem: Kinetic utilizes ML-DSA-65, a lattice-based post-quantum cryptographic signature scheme. Lattice cryptography produces enormous keys and signatures. An ML-DSA-65 public key is 1,952 bytes, and the signature is 4,627 bytes. Contrast this with a traditional Ed25519 keypair which is 32 bytes and 64 bytes respectively. If the network blindly accepted and attempted to deserialize any payload submitted to it, an attacker could flood nodes with large invalid payloads. We need strict validation traits that enforce length limits before any cryptographic processing begins.

The Storage Redundancy Problem: When a user claims a name, that record is not stored on a central server; it is broadcast into the Kademlia DHT. Nodes in a peer-to-peer network constantly churn—they go offline, lose internet connection, or crash. If a name record is stored on only one node, the domain goes dark the moment that node restarts. We need a mathematically deterministic way to derive multiple storage locations (redundancy) across the network, ensuring that no matter which nodes drop off, the domain remains resolvable to the rest of the world.

How It Works

1. The Clock and Branded Time (clock.rs)

At the core of the network’s timekeeping is the concept of the Drand beacon. The network defines a Genesis Kyn, which represents the specific Drand round when the Kinetic network officially launched and generated its genesis state. All time calculations across the entire ecosystem are relative to this single genesis point.

-> See: kinetic-core/src/types/clock.rs — Lines 1 to 15

The time units are structured mathematically to map perfectly to standard Earth time while maintaining the precise 3-second heartbeat of the consensus engine:

  • 1 Kyn = The atomic unit of time. Exactly 3 seconds.
  • 1 Facet = 1,200 Kyns. Because 1,200 * 3 = 3,600 seconds, this equals exactly 1 Hour.
  • 1 Prism = 28,800 Kyns. Because 28,800 * 3 = 86,400 seconds, this equals exactly 1 Day.
  • 1 Matrix = 7 Prisms. This equals exactly 1 Week.
  • 1 Lattice = 30 Prisms. This equals exactly 1 Month (standardized to 30 days for simplicity in smart contracts).
  • 1 Apex = 365 Prisms. This equals exactly 1 Year.

-> See: kinetic-core/src/types/clock.rs — Lines 47 to 60

When translating raw machine time to user time, the function KineticTime::from_kyn(target_kyn, genesis) subtracts the genesis Kyn from the target_kyn to determine the total_kyns elapsed since the network launched. It then uses modulo arithmetic and integer division to break this vast number down into its constituent parts. For instance, if 31,245 total Kyns have elapsed since genesis:

  • 31,245 / 28,800 = 1 Prism.
  • The remainder is 2,445 Kyns.
  • 2,445 / 1,200 = 2 Facets.
  • The remainder is 45 Kyns. The result is precisely Prism 1, Facet 2, Kyn 45. This ensures that every explorer and frontend renders time exactly the same way without relying on external APIs.

2. Safeguarding the Core Network (infrastructure.rs)

The Kinetic network differentiates heavily between standard user-owned domains and critical infrastructure that the protocol relies on to function.

-> See: kinetic-core/src/types/infrastructure.rs — Lines 22 to 24

The INFRASTRUCTURE_NAMES list statically defines domains like seed, node, docs, dao, explorer, status, api, blog, and rpc. These are classified as Category 2 reservations. (Note that Category 1 reservations are standard ICANN testing domains like localhost or test, which are handled by a different validation rule).

-> See: kinetic-core/src/types/infrastructure.rs — Lines 34 to 43

The is_infrastructure_name function acts as the primary protocol gatekeeper. When a raw domain string is passed in, it first calls normalize_name to strip out erratic capitalization and whitespace (for example, normalizing “ SEED.kin “ to “seed.kin”). It then extracts the apex label (the root name without the TLD). If this label matches anything in the reserved array, the function returns true. This function is executed at the very edge of the RPC server and the mining pool. If a user attempts to mine an infrastructure name, the process halts immediately and returns a NamesError::InfrastructureName before any CPU cycles are wasted on VDF verification.

-> See: kinetic-core/src/types/infrastructure.rs — Lines 45 to 60

Furthermore, standard domains on the Kinetic network are subject to “thermodynamic pruning”. If a user abandons a domain, they stop broadcasting their cryptographic heartbeat signals. After a certain period (STEAL_TARGET_ROUNDS), the domain’s temperature drops, and it becomes eligible for takeover by another user. However, infrastructure names must never drop off the network. The requires_heartbeat function hardcodes a permanent exemption for these names. Because is_infrastructure_name evaluates to true, requires_heartbeat returns false, completely removing these vital names from the garbage collection and pruning cycles.

3. Validation of Verifiable Delay Function Payloads (vdf.rs)

In order to claim a name, a miner must submit a Reveal payload containing their VDF proof and the necessary cryptographic signatures proving ownership of the public key.

-> See: kinetic-core/src/types/vdf.rs — Lines 26 to 74

The RevealExt trait implements the validate method. This method serves as a rigid, unforgiving series of fail-fast checks designed to protect the node from malicious data. First, it explicitly checks that self.protocol_version == 1. This allows the network to seamlessly upgrade payload structures in the future without breaking older clients or creating consensus ambiguities. Second, it validates the requested name itself, ensuring it strictly adheres to valid Letter-Digit-Hyphen (LDH) rules. Third, it checks the payload size against MAX_PAYLOAD_SIZE. Finally, it validates the structural lengths of the cryptography arrays. ML-DSA-65 requires exactly 1952 bytes for a public key and 4627 bytes for a signature. The Drand signature requires exactly 192 bytes. The VDF proof must not exceed 2048 bytes. If an attacker submits a payload where the signature is off by one byte, validation fails.

4. Distributed Hash Table Storage Derivation (name_record.rs)

Once a domain is successfully claimed, its DNS and routing records must be stored in the Kademlia DHT.

-> See: kinetic-core/src/types/name_record.rs — Lines 14 to 16

The protocol mandates an M_REDUNDANCY constant set to 32. This means that every single domain record is duplicated across 32 distinct, geographically dispersed storage nodes. Given the typical churn rates of peer-to-peer networks, an M-value of 32 statistically guarantees that the domain remains highly available and resolvable even if 90% of the network temporarily partitions or goes offline.

-> See: kinetic-core/src/types/name_record.rs — Lines 24 to 39

The derive_storage_keys function is responsible for deterministically calculating these 32 locations. It loops from index 0 up to 31. For each iteration, it concatenates the raw bytes of the domain name, the current loop index integer, and the KINETIC_NETWORK_ID. It then hashes this combined, salted byte array using SHA-256. The resulting 32 hashes represent the exact topological locations in the DHT ring where the data must be placed. The inclusion of the network ID (for example, testnet-alpha versus mainnet-beta) is absolutely critical. Without this salt, a node running on a testnet might inadvertently overwrite or conflict with a mainnet record if they happen to connect to the same bootstrap peer, as both networks would otherwise derive the exact same DHT keys for the domain saif.kin.

5. Module Aggregation (mod.rs)

The mod.rs file acts as the facade for all these types.

-> See: kinetic-core/src/types/mod.rs — Lines 1 to 30

It exports clock, dns, name_record, identity, infrastructure, names, and vdf. -> See RUST_CONCEPTS.md for an explanation of pub use.

Key Pieces

KineticTime

  • What it does: Represents a specific, exact point in time on the Kinetic network, dynamically broken down into Prisms (Days), Facets (Hours), and Kyns (3-second ticks).
  • File & Line: Re-exported from kinetic-types in clock.rs:15. (Logic is defined deeply within kinetic-types).
  • Why it matters: It bridges the architectural gap between raw machine consensus (meaningless Drand rounds) and human-readable time, allowing user interfaces and developers to display branded Kinetic time reliably.

INFRASTRUCTURE_NAMES

  • What it does: A static, immutable array of string slices defining the core network infrastructure names (seed, node, explorer, api, etc.).
  • File & Line: infrastructure.rs:22
  • Why it matters: It prevents the core mechanics of the network from being disrupted by aggressive domain squatters. These specific names are structurally protected at the protocol level and cannot be mined by conventional means.

is_infrastructure_name

  • What it does: Takes a raw string, passes it through the domain normalizer, extracts the apex portion, and strictly checks if it exists in the INFRASTRUCTURE_NAMES array.
  • File & Line: infrastructure.rs:34
  • Why it matters: Used heavily by the mining pool layer and the inbound RPC server to instantly reject any attempts to mine protected names before the system wastes any valuable compute cycles evaluating VDF proofs.

requires_heartbeat

  • What it does: Determines if a domain name is subject to the network’s thermodynamic pruning and expiration rules.
  • File & Line: infrastructure.rs:57
  • Why it matters: Explicitly exempts infrastructure names from needing constant thermodynamic upkeep, ensuring core services and bootstrap nodes never accidentally drop off the network due to missed heartbeats.

RevealExt::validate

  • What it does: A trait method implementation that exhaustively validates the structure, version, and byte-sizes of a VDF Reveal payload.
  • File & Line: vdf.rs:26
  • Why it matters: Acts as a strict memory firewall against malformed or maliciously oversized cryptographic payloads. Given the massive sizes of quantum-resistant ML-DSA-65 keys, this is crucial for preventing out-of-memory crashes on validators.

derive_storage_keys

  • What it does: Generates 32 distinct, mathematically deterministic DHT storage keys for a single domain name, incorporating the specific network ID to prevent cross-network state collisions.
  • File & Line: Re-exported in name_record.rs:14, tested and demonstrated via name_record.rs:24.
  • Why it matters: Ensures domain name resolution is highly resilient to localized node failure and churn, distributing the query and storage load evenly across the Kademlia DHT ring.

How This Connects to the Rest of Kinetic

FORWARD DEPENDENCY: kinetic-rpc The RPC layer will heavily utilize requires_heartbeat and is_infrastructure_name. When a frontend client queries the status of a domain or attempts to register a new commitment, the RPC server will use these functions to validate the business logic of the request before forwarding it to the mining pool or the DHT network. If an RPC call attempts to touch seed.kin, it will be blocked here.

FORWARD DEPENDENCY: kinetic-dht The peer-to-peer DHT networking crate relies entirely on derive_storage_keys. When it receives a request to put a domain record into the network, it will call this specific function to determine the 32 exact PeerId locations on the Kademlia ring where the data should be replicated. Without this, routing fails completely.

CROSS-CRATE: kinetic-verify The vdf.rs module is tightly coupled to kinetic-verify. It re-exports types like Commitment, Reveal, and VdfProof. While kinetic-verify handles the pure mathematical verification of the underlying cryptographic proof, kinetic-core wraps these types in network-specific business logic via traits like RevealExt, providing the contextual validation required for the Kinetic protocol.

EXTERNAL DEPENDENCY: Explorers and Frontends Any visual representation of the network will pull the KineticTime struct to render timestamps. When a block is minted or a domain expires, a UI won’t display “Expires at Unix Timestamp 1754029100”; it will render natively as “Expires at Prism 342, Facet 12”.

Quick Reference

  • 1 Kyn = Exactly 3 seconds (One Drand beacon tick).
  • Genesis Kyn = The exact Drand round at which the Kinetic network launched. All KineticTime is relative to this zero-point.
  • Infrastructure Names = seed, node, docs, dao, explorer, status, api, blog, rpc.
  • Heartbeat Exemption = Infrastructure names are mathematically exempt; they NEVER expire.
  • VDF Payload Limits = Protocol Version 1 only. ML-DSA pubkeys must be exactly 1952 bytes. Signatures exactly 4627 bytes.
  • M_REDUNDANCY = 32. Every single domain is stored 32 separate times on the DHT.
  • Network Isolation = DHT keys are permanently salted with KINETIC_NETWORK_ID to prevent testnet/mainnet data bleed.

Open Questions / Things to Revisit

  • Hardcoded Cryptographic Lengths: In vdf.rs, the expected lengths for pubkey (1952) and signature (4627) are statically hardcoded for ML-DSA-65. If the network ever needs to upgrade to ML-DSA-87 (due to future cryptographic advancements breaking ML-DSA-65), this validate function will need a protocol version bump and a complex branching logic path. This is a potential pain point for seamless future upgrades.
  • Infrastructure List Extensibility: Currently, INFRASTRUCTURE_NAMES is a hardcoded static array baked into the node binary. If the Kinetic Council ever votes to add a new infrastructure name (e.g., bridge or relay), a full node software update across the entire network is required. Should this list be governed by an on-chain or in-network configuration state rather than hardcoded in the rust binary?
  • Genesis Kyn Dependency: The time logic tests heavily rely on KINETIC_GENESIS_DRAND_KYN being pulled from constants. If a node’s configuration is slightly out of sync regarding the genesis round, its entire timekeeping perspective will be misaligned with the rest of the network, potentially causing it to reject valid blocks. We need to ensure genesis state is aggressively validated and checksummed upon node boot.
  • Redundancy Cost: Storing 32 copies of every domain record provides excellent availability, but as the network grows to millions of domains, the bandwidth and storage overhead of M=32 could be significant. We may need to actively monitor DHT performance at scale and consider lowering this constant or introducing dynamic redundancy based on a domain’s semantic value.

Crate: kinetic-core

Stage: 3

Reading Time: 45 mins

Depends On: api_error.rs, vdf.rs, constants.rs

What Is This?

This documentation covers the file kinetic-core/src/error/dht.rs. This file is the definitive, comprehensive taxonomy of error states for the Kademlia Distributed Hash Table (DHT) within the Kinetic network. It is responsible for defining the exhaustive list of failure modes that can occur during the highly complex, multi-stage lifecycle of managing decentralized .kin names on the peer-to-peer network. The file maps low-level peer-to-peer networking failures, cryptographic proof rejections, and state transition errors into a standardized, RFC-7807-compliant API error taxonomy.

At its core, this file provides four primary enumerations that correspond to the four major phases of interaction with the DHT. The four primary enumerations are:

  1. RecordRejectReason: The internal, fine-grained reasons why the local node’s KineticRecordStore might refuse to store an incoming DHT record from a remote peer.
  2. ResolutionError: The errors that can occur when a user or client application attempts to look up a .kin name and read its associated data from the network.
  3. PublishError: The errors that can occur when the local node attempts to broadcast a new or updated record out to the wider DHT.
  4. RegistrationError: The high-level orchestrator errors that encapsulate the entire two-phase commit-reveal process of claiming a new .kin name.

By centralizing these error definitions, Kinetic ensures that front-end clients, mobile applications, and CLI tools receive consistent, actionable feedback whenever a decentralized operation fails. It eliminates the ambiguity of generic network errors.

Why Kinetic Needs This

In standard centralized web architecture (often referred to as Web2), errors are usually straightforward, binary, and absolute. A database row doesn’t exist (HTTP 404), a server is down (HTTP 503), or a payload is malformed (HTTP 400). In these models, you trust the server’s response implicitly because the server is the single source of truth.

In a peer-to-peer network like Kinetic, failure is not just common; it is the default operating environment. A DHT operation can fail for dozens of nuanced reasons that have no equivalent in traditional client-server models. Consider the act of registering a name on the Kinetic network. A peer routing your request might go offline halfway through the process. A cryptographic verifiable delay function (VDF) might be spoofed by a malicious actor attempting to squat on a name. A temporary network partition might cause the DHT to split, leading to inconsistent state across the swarm. Two completely honest users might try to register the exact same name at the exact same drand (distributed randomness) round, resulting in a cryptographic tie-break.

Without a highly granular, robust error taxonomy, the Kinetic node would be forced to emit generic, opaque errors like “connection closed”, “validation failed”, or “Kademlia timeout”. This leaves developers and end-users completely blind to what actually went wrong and, more importantly, how to fix it. If a user spends ten minutes computing a VDF only for it to fail, they need to know exactly why so they don’t waste another ten minutes.

Kinetic requires an error system that bridges the gap between Byzantine network failures and user experience. It must distinguish between a temporary network timeout (which should be retried automatically by the client) and a cryptographic forgery (which means the data is poisoned and must be immediately discarded). Furthermore, because Kinetic uses a dynamic difficulty adjustment system for its VDFs, an error might not just indicate a failure, but an instruction. For example, it might tell the client that their proof was mathematically valid, but the difficulty threshold has increased, and they need to compute the VDF for more iterations.

This specific file translates peer-to-peer networking and verifiable delay functions into predictable, actionable API errors. Every error has a stable RFC 7807 compatible code (e.g., KIN-RES-003), a user-friendly message that strips away technical jargon, a severity level for telemetry and monitoring, and a boolean flag indicating whether the operation is safe to retry. This transforms the decentralized backend into something predictable for frontend developers, completely hiding the complex peer-to-peer reality underneath.

How It Works

The architecture of this file is divided into four main functional areas, each represented by a distinct Rust enum. These enums use the thiserror crate to automatically implement the standard std::error::Error trait, while also providing custom methods that map the errors into the Kinetic standard taxonomy. We will break down how each phase operates, the specific variants it contains, and the theoretical concepts underpinning them.

1. Local Store Validation (RecordRejectReason)

-> See: kinetic-core/src/error/dht.rs — Lines 18 to 52

In a Kademlia DHT, nodes are constantly asking each other to store data. When a remote peer sends a PUT request to our local Kinetic node, our node cannot blindly accept and store the payload. If it did, malicious actors could easily fill our hard drive with garbage data or overwrite legitimate .kin name registrations. To prevent this, the KineticRecordStore acts as a strict gatekeeper. It evaluates every single incoming record against the consensus rules of the Kinetic naming protocol. If the record violates any rule, it is immediately dropped, and a RecordRejectReason is returned to the sender.

This enum does not map directly to a KIN-* error code because it is an internal p2p protocol mechanism, not something typically surfaced directly to an end-user client. However, it may be wrapped by a PublishError or RegistrationError later in the stack.

The validation checks happen in a specific order to minimize computational waste. They are detailed below:

InvalidSignature (Line 23)

  • This error is triggered if the payload’s Ed25519 signature does not verify against the claimed public key.
  • It indicates that the record was likely tampered with in transit by a malicious intermediary.
  • Alternatively, it means the sender is attempting to forge a record for a public key they do not own.
  • This is a fatal cryptographic failure and the record is immediately discarded.
  • It prevents unauthorized users from hijacking names that do not belong to them.

InvalidVdf (Line 26)

  • This error occurs when the mathematical proof provided in the record fails the verification algorithm.
  • Kinetic relies on Verifiable Delay Functions (VDFs) to prove that real-world time has elapsed.
  • If the Wesolowski proof is mathematically invalid, it means the sender tried to bypass the time delay.
  • This immediately flags the sender as a malicious actor attempting a Sybil or squatting attack.
  • The record is dropped to protect the integrity of the naming system.

Expired (Line 29)

  • Records in Kinetic are not permanent; they are tied to specific drand rounds and epochs.
  • If a record is submitted, but the epoch it belongs to has already passed into history, it is no longer valid.
  • The network considers it stale and purges it, returning this expiration error.
  • This mechanism keeps the DHT clean of ancient, irrelevant data.
  • It also ensures that users must periodically renew their names to maintain ownership.

AlreadyOwned (Line 32)

  • This error is returned when a node attempts to claim a name that is already fully registered.
  • The local store checks its existing records and sees that another public key holds the claim.
  • Because the epoch has closed, the existing claim is unassailable.
  • The incoming record is rejected, and the sender is informed that they are too late.
  • This enforces the fundamental rule that a name can only have one owner at a time.

InsufficientIterations (Line 36)

  • Kinetic enforces a dynamic difficulty for its VDFs based on network demand and name length.
  • If the proof is mathematically valid, but the number of iterations is too low, this error is thrown.
  • It means the sender did not compute the VDF for long enough to meet the current threshold.
  • This stops attackers from using trivial, instantaneous proofs to bypass the required delay.
  • The client must recalculate the VDF with a higher iteration count and try again.

TieBroken (Line 39)

  • In a decentralized network, two honest users might generate valid proofs for the same name at the same time.
  • The network must resolve this collision deterministically using the Kademlia XOR distance metric.
  • The node takes the hash of the data payload and XORs it with the hash of the name.
  • The record that results in the mathematically smaller XOR distance wins.
  • The loser is rejected with this error, guaranteeing consensus without centralized arbitration.

CommitmentMismatch (Line 42)

  • To prevent front-running, Kinetic uses a two-phase commit-reveal scheme.
  • A user first publishes a hashed commitment of their key, and later reveals the key itself.
  • When evaluating a reveal record, the store hashes the revealed key and compares it to the stored commitment.
  • If they do not match exactly, this error is thrown.
  • This prevents an attacker from hijacking a commitment that was secured by someone else.

InvalidDrandHex (Line 45)

  • Kinetic uses drand (distributed randomness) beacons to seed the VDF computation.
  • The beacon signature must be passed as a valid hexadecimal string.
  • If the string contains non-hex characters, this error is triggered before any expensive math is done.
  • This is a fast-fail structural check.
  • It protects the node from panicking while trying to parse garbage data.

InvalidPublicKey (Line 48)

  • The record must contain the owner’s Ed25519 public key.
  • If the provided bytes cannot be parsed into a valid point on the elliptic curve, this error occurs.
  • Like the hex check, this is a fast-fail structural validation.
  • It prevents the cryptographic library from crashing on malformed input.
  • Malformed keys are instantly dropped.

MalformedSignature (Line 51)

  • An Ed25519 signature must be exactly 64 bytes long.
  • If the byte array is the wrong length, this error is returned.
  • This is the cheapest and fastest check the node performs.
  • It acts as the first line of defense against badly formatted packets.
  • Records failing this check are discarded immediately.

2. DHT Resolution Phase (ResolutionError)

-> See: kinetic-core/src/error/dht.rs — Lines 54 to 188

When a client application needs to know the public key associated with alice.kin, it asks the local node to resolve the name. The node executes a Kademlia DHT GET operation, crawling the network to find the peers closest to the hash of the name. The ResolutionError enum captures all the ways this read operation can fail.

These errors are directly surfaced to the client via the API boundary. Therefore, the enum implements specific methods to conform to the Kinetic API taxonomy.

  • The code() method (Line 110) returns the stable identifier (e.g., KIN-RES-001).
  • This ensures frontend code can switch on a stable string rather than parsing error messages.
  • The error_type_uri() method (Line 122) appends the code to the base docs URL to create an RFC 7807 compliant type URI.
  • This allows developers to click a link in their API response and read the documentation for that specific error.
  • The is_retryable() method (Line 127) is a crucial boolean.
  • If true, the frontend client should automatically initiate a retry with exponential backoff.
  • For example, timeouts are retryable, but missing data is not.
  • The severity() method (Line 132) maps the error to a logging severity (Info, Warning, Error).
  • A missing record is an Info event, as it’s standard operating behavior.
  • An internal panic is an Error event requiring developer attention.
  • The user_message() method (Line 144) provides a clean, sanitized string meant to be displayed directly in a UI alert box.
  • It strips away all mentions of “Kademlia,” “DHT,” and “VDFs.”
  • Finally, the details() method (Line 171) packages developer-centric metadata into a serde_json::Value object.
  • This allows the frontend to access structured data without parsing strings.

The variants cover the reality of network reads in a potentially hostile environment:

Offline (Line 62 / KIN-RES-001)

  • The node’s libp2p swarm has exactly zero active connections to the outside world.
  • The DHT cannot be reached at all because there are no peers in the routing table.
  • The node is effectively isolated in a silo.
  • The operation aborts immediately without attempting to send packets.
  • The client should prompt the user to check their internet connection or firewall settings.

NotFound (Line 65 / KIN-RES-002)

  • The crawler successfully contacted the closest peers, but none of them had the record.
  • This means the name is currently available for registration.
  • The peers_queried field indicates how exhaustive the search was before the node gave up.
  • This is not a fatal error; it is an informational response.
  • The UI should simply display a message stating that the name is unregistered.

VdfVerificationFailed (Line 73 / KIN-RES-003)

  • This is a critical security feature built into the resolution process.
  • If the node successfully retrieves a record from the DHT, it does not inherently trust it.
  • It must verify the VDF proof locally using the chiavdf verifier.
  • If a malicious peer hands us a forged record, the resolution process aborts and throws this error.
  • The count field indicates how many fake records were discarded during the query.

Expired (Line 80 / KIN-RES-004)

  • The record was found on the network, and its mathematical proofs are entirely valid.
  • However, its associated drand epoch has passed into history.
  • The registration is no longer legally binding on the network, and the owner must renew it.
  • The age field shows how old the record is in rounds, giving the UI context on exactly how stale it is.
  • The UI should display the name as available for claim, allowing a new user to overwrite the old data.

Timeout (Line 89 / KIN-RES-005)

  • The asynchronous Kademlia query exceeded the maximum allowed wall-clock time.
  • This frequently happens in highly congested networks or during widespread packet loss.
  • It can also occur if the local routing table is heavily polluted with dead or unresponsive nodes.
  • This variant includes elapsed_ms and peers_queried to aid in debugging telemetry.
  • It is fully retryable, meaning a client should automatically try again after a brief exponential backoff.

Internal (Line 98 / KIN-RES-006)

  • An unexpected panic, filesystem error, or underlying library failure occurred on the local machine.
  • This contains a developer message explaining the nature of the fault.
  • It also optionally contains a boxed trait object for the source error to trace the root cause.
  • It indicates a bug or hardware issue on the local node itself, not a network protocol problem.
  • This should ideally trigger an automated crash report or alert the user to restart the node.

3. DHT Publishing Phase (PublishError)

-> See: kinetic-core/src/error/dht.rs — Lines 190 to 286

Writing to a Kademlia DHT is significantly more complex than reading. A read only requires finding one honest peer with the data. A write requires convincing a quorum of the k closest peers to accept and store the data simultaneously. The PublishError enum tracks the failure modes of the Kademlia PUT lifecycle. Like resolution errors, these are mapped to stable API codes, in this case prefixed with KIN-PUB.

Before a record is even broadcast over the wire, it undergoes a sanity check on the local node. This prevents the node from spamming the network with invalid data and wasting bandwidth.

Offline (Line 197 / KIN-PUB-001)

  • Similar to the resolution phase, the node checks if it has any connected peers.
  • If the local node is entirely isolated from the network, it immediately returns this error.
  • No network traffic is generated.
  • The client should inform the user that they must connect to the network before publishing.
  • This is a transient error and is safe to retry once connectivity is restored.

InvalidProof (Line 200 / KIN-PUB-002)

  • Before broadcasting, the local node verifies the VDF proof it just generated or received.
  • If the node realizes the VDF proof attached to the outbound record is mathematically broken, it aborts early.
  • It wraps the underlying VdfRejectReason to provide specific context.
  • This shouldn’t happen under normal operation but serves as a safety net against local logic bugs.
  • It guarantees the node never willingly pollutes the network with invalid data.

AlreadyOwned (Line 203 / KIN-PUB-003)

  • The local node checks its own local cache before initiating a costly network broadcast.
  • If it realizes the name is already firmly owned by a different key, it throws this error.
  • This saves significant bandwidth and prevents an inevitable rejection from the remote peers.
  • It serves as a fast-fail optimization for the publish pipeline.
  • The client should be instructed to pick a new name.

AllFailed (Line 209 / KIN-PUB-004)

  • In a Kademlia network, a PUT operation broadcasts the record to multiple peers simultaneously.
  • A publish is generally considered successful if at least a specific quorum of those peers accept it.
  • However, if every single peer contacted explicitly rejects the record, the operation fails completely.
  • This can happen if our local clock is wildly out of sync or if we are running an incompatible protocol version.
  • The count field tells the developer exactly how many peers slammed the door in our face.

Rejected (Line 215 / KIN-PUB-005)

  • When a specific peer rejects the message, it returns a RecordRejectReason (like TieBroken).
  • If this rejection causes the overall publish quorum to fail, it is mapped into this generic variant.
  • The inner string allows the local node to log the remote peer’s exact justification.
  • This is incredibly useful for debugging why the network at large is refusing to accept our valid data.
  • It provides a window into the consensus state of the remote nodes.

Internal (Line 217 / KIN-PUB-006)

  • This catches unforeseen local errors during the complex asynchronous broadcast phase.
  • Examples include serialization failures, memory allocation issues, or async executor thread panics.
  • It contains a developer message and an optional source error.
  • It indicates a severe local malfunction rather than a network protocol issue.
  • This error is generally not retryable, as the local state is likely corrupted.

4. High-Level Registration Flow (RegistrationError)

-> See: kinetic-core/src/error/dht.rs — Lines 288 to 393

While resolution and publishing deal with atomic, individual DHT RPCs, registration represents the overarching business logic of claiming a .kin name. This is a complex saga involving multiple coordinated operations over several minutes. It involves validating syntax, fetching drand beacons, computing VDFs, and orchestrating the commit-reveal flow.

The RegistrationError enum encapsulates the failures of this entire long-running saga. It utilizes stable KIN-REG-NNN codes. Because this process is highly user-facing, these errors are the most likely to be seen by an end-user.

InvalidName (Line 295 / KIN-REG-001)

  • This is the very first gate in the registration process.
  • If the user requests a name with uppercase letters, emojis, or spaces, the process halts immediately.
  • The Kinetic protocol enforces strict lowercase alphanumeric and hyphen rules to prevent homograph attacks.
  • The user message explicitly tells them to use valid characters (Line 374).
  • No network requests are made; this is a pure local validation failure.

VdfFailed (Line 301 / KIN-REG-002)

  • Computing a VDF is an intensely heavy operation handled by an external C++ library (chiavdf).
  • If that external process panics, runs out of memory, or returns a mathematical error, it is caught here.
  • This variant explicitly wraps the underlying VdfRejectReason.
  • Note that is_retryable() returns true only for this variant (Line 355).
  • A transient hardware glitch during computation is worth retrying automatically.

CommitmentMismatch (Line 304 / KIN-REG-003)

  • During the reveal phase, the orchestrator checks its data before broadcasting.
  • If it realizes that the reveal data does not hash to the commitment it previously broadcast, it halts.
  • This represents a severe inconsistency in local state.
  • It is perhaps caused by the client switching keys mid-registration or a database corruption.
  • This is a fatal logic error and the registration must be restarted from scratch.

AlreadyOwned (Line 307 / KIN-REG-004)

  • The name was available when the client started computing the 10-minute VDF.
  • However, by the time the VDF finished, someone else had successfully claimed the name and locked the epoch.
  • The client was literally beaten to the punch because a competitor started calculating their proof earlier.
  • The UI should instruct the user to choose a different name entirely.
  • The computation time was unfortunately wasted, which is a known risk in decentralized systems.

AlreadyInProgress (Line 312 / KIN-REG-005)

  • This is a vital concurrency guard for the node’s resources.
  • VDF computation consumes significant CPU cycles and can run for minutes.
  • The node must aggressively prevent users from accidentally clicking “Register” twice.
  • If a task for alice.kin is already in the orchestrator’s state map, a second request immediately returns this error.
  • This prevents the machine from starving itself of resources.

NetworkRejected (Line 319 / KIN-REG-006)

  • This is a powerful wrapper around the lower-level store errors.
  • If the final PUT step fails, the low-level RecordRejectReason (like TieBroken) is bubbled all the way up.
  • It is wrapped in this variant so the UI knows exactly why the registration failed at the literal finish line.
  • The details() method expertly extracts the underlying reason string into a JSON payload (Line 388).
  • This provides deep visibility into network consensus failures directly to the client.

Internal (Line 324 / KIN-REG-007)

  • The standard catch-all for orchestrator panics, channel communication failures, or local disk errors.
  • It handles issues that don’t fit anywhere else in the taxonomy.
  • It provides developer context and stack trace information if available.
  • It indicates a severe local malfunction requiring developer attention.
  • It is explicitly not retryable.

5. Deep Dive: serde_json::Value and RFC 7807

One of the most powerful aspects of this error file is how it extracts internal Rust struct data and formats it into standard JSON. If an error happens on a node, but the user is using a web dashboard, the web dashboard doesn’t understand Rust memory layouts. It understands JSON. The details() method on these enums explicitly maps internal fields to a serde_json::Value (a dynamic JSON tree). The details() method extracts internal struct data and formats it into standard JSON. Instead of forcing the client to parse strings, details() returns structured data. This conforms to RFC 7807, defining standard schemas for problem details. -> See RUST_CONCEPTS.md for explanations of how the thiserror crate operates and how PartialEq is manually implemented for external crate types (like std::error::Error).

7. Deep Dive: Decoding the XOR Tie-Break Mechanism

The RecordRejectReason::TieBroken variant is an artifact of decentralized consensus. When two people register a username simultaneously, there is no global clock to order them. Kinetic resolves this using the XOR distance metric between the name hash and the public key hash. Whichever key is closer to the name hash wins; the loser receives a TieBroken error.

8. Deep Dive: Logging Severities

The severity() method maps errors to standard levels:

  • Info: Expected protocol states (e.g., NotFound).
  • Warning: Transient network issues or peer behavior (e.g., AllFailed).
  • Error: Failures requiring intervention (e.g., VdfVerificationFailed, Internal).

9. Deep Dive: Exponential Backoff Strategies

The is_retryable() flag informs the client if a request should be retried using exponential backoff. This prevents network congestion from being exacerbated by constant automated retries.

Key Pieces

RecordRejectReason

  • What it is: Low-level reasons a KineticRecordStore refuses to save a DHT record.
  • File & Line: kinetic-core/src/error/dht.rs — Lines 18 to 52
  • Why it matters: It is the primary, frontline defense against spam, Sybil attacks, and invalid state transitions in the peer-to-peer network.
  • Impact: Without these stringent checks, the distributed hash table would instantly fill with unverified garbage data.

ResolutionError

  • What it is: The rich error enum specifically tailored for the GET (read) path of the DHT.
  • File & Line: kinetic-core/src/error/dht.rs — Lines 54 to 188
  • Why it matters: It maps asynchronous Kademlia lookup failures—like timeouts, missing records, or cryptographically forged data—into the stable KIN-RES API taxonomy.
  • Impact: This allows frontends to handle them gracefully without needing to understand the underlying libp2p implementation details.

PublishError

  • What it is: The rich error enum specifically tailored for the PUT (write) path of the DHT.
  • File & Line: kinetic-core/src/error/dht.rs — Lines 190 to 286
  • Why it matters: It captures complex multi-peer quorum failures (like AllFailed) and offline states when the local node attempts to broadcast data.
  • Impact: It utilizes the KIN-PUB code prefix and provides structured feedback for write operations.

RegistrationError

  • What it is: The highest-level error enum governing the complex, multi-minute commit-reveal flow.
  • File & Line: kinetic-core/src/error/dht.rs — Lines 288 to 393
  • Why it matters: This is the error type that the application orchestrator returns directly to the user client.
  • Impact: It handles UX-centric problems like concurrency (AlreadyInProgress), user typos (InvalidName), and hardware computation failures (VdfFailed). It uses the KIN-REG code prefix.

code(), severity(), and is_retryable() Methods

  • What it is: A suite of methods implemented on all three rich error enums.
  • File & Line: Scattered throughout the file, e.g., Line 110, 127, 132.
  • Why it matters: These methods mathematically enforce the Kinetic error taxonomy.
  • Impact: They guarantee that every error can be deterministically categorized, logged with the correct urgency level, and automatically retried by the frontend if appropriate, removing guesswork for client developers.

details() Method

  • What it is: A method implemented on the rich error enums returning a serde_json::Value.
  • File & Line: Scattered throughout the file, e.g., Line 171, Line 279, Line 385.
  • Why it matters: This allows the error taxonomy to pass strongly typed, structured metadata (like peers_queried, elapsed_ms, or age_rounds) across the FFI or HTTP boundary.
  • Impact: This prevents frontends from having to parse fragile string messages with Regex to figure out what happened.

How This Connects to the Rest of Kinetic

FORWARD DEPENDENCY: The API Layer Boundary These errors are explicitly designed not to live in isolation deep within the core logic. They are built to be seamlessly converted into the generic ApiError struct (defined elsewhere in the kinetic-core crate). The methods implemented on these enums (code(), user_message(), and details()) align perfectly with the fields required to construct a standard RFC-7807 JSON error response. This response is then sent over the local HTTP server to a web interface, or passed across the Foreign Function Interface (FFI) boundary to a mobile application or desktop GUI. This boundary is the primary consumer of this file.

CROSS-CRATE: chiavdf and drand Primitives The VdfFailed variants intimately connect this error system to the heavy C++ VDF bindings provided by the chiavdf integration. Furthermore, the InvalidDrandHex and Expired variants rely entirely on the network’s understanding of the global, decentralized drand beacon schedule. This error module assumes the absolute existence and correctness of these external cryptographic primitives and provides the necessary error-handling glue for when they inevitably fail or time out.

CROSS-CRATE: libp2p-kad Abstracted The AllFailed variant in PublishError and the NotFound variant in ResolutionError are direct abstractions over the success/failure thresholds of the libp2p crate’s Kademlia routing table. These errors hide the immense complexity of iterative routing queries, XOR distance metrics, and K-bucket evictions from the rest of the application, surfacing only the actionable outcome.

Quick Reference

Code PrefixGeneral DescriptionExample Triggers
KIN-RES-*Resolution (Read) ErrorsNode offline, name not found, VDF mathematically spoofed, Kademlia lookup timeout.
KIN-PUB-*Publish (Write) ErrorsNetwork partitioned, quorum rejected the PUT, invalid local proof before broadcast.
KIN-REG-*Registration (Flow) ErrorsInvalid characters in name, VDF thread crashed, registration already in progress.
N/ARecordRejectReasonInternal p2p rejection (XOR TieBroken, CommitmentMismatch). No API code associated.

Open Questions / Things to Revisit

  1. Drand Dependency Errors: There is currently no explicit error variant for “The drand network is unreachable.”
  • Retrieving the latest beacon is a strict prerequisite for generating a VDF and starting a registration.
  • If drand fails, it likely bubbles up as an opaque Internal error.
  • We should critically consider adding a specific DrandUnreachable variant to RegistrationError so the UI can tell the user that the global randomness beacon is down, rather than displaying an internal panic message.
  1. Box<dyn Error> Equality Checks: The manual PartialEq implementation for Internal variants ignores the boxed underlying source error and only compares the developer message string.
  • This is generally fine for unit testing the outer enum, but it is technically a lossy comparison.
  • We might want to explore using a crate like dyn-clone or switching to anyhow if we ever need strict, deep equality checks on internal errors for complex test scenarios.
  1. Hardcoded Timeout Visibility: ResolutionError::Timeout currently reports elapsed_ms.
  • The actual timeout duration is defined as a constant elsewhere in the codebase.
  • It might be highly useful to include the configured maximum timeout in the details() JSON payload, so clients know exactly what threshold was breached and can adjust their expectations or retry policies.
  1. Tie-Break Visibility: RecordRejectReason::TieBroken is currently swallowed by the generic RegistrationError::NetworkRejected.
  • A client whose registration fails because they lost a legitimate XOR tie-break might benefit from knowing exactly who beat them (e.g., returning the winning public key and their iteration count in the error details).
  • Currently, this valuable debugging data is dropped entirely by the orchestrator.

Core Errors: Network, VDF, DNS, and Drand

Crate: kinetic-core Stage: 7 Reading Time: ~45 minutes Depends On:

  • kinetic-core/src/error/network.rs
  • kinetic-core/src/error/vdf.rs
  • kinetic-core/src/error/dns.rs
  • kinetic-core/src/error/drand.rs

What Is This?

This document provides a highly detailed, comprehensive examination of four distinct error handling modules within the kinetic-core crate. These modules are:

  • network.rs
  • vdf.rs
  • dns.rs
  • drand.rs

In the Kinetic architecture, error handling is treated as a first-class citizen. It is a critical component of the state machine. Rather than relying on a single, monolithic, and vaguely defined error enumerator, the system fragments its failure states. It places them into highly specific, protocol-aware domains. This fragmentation allows the node to differentiate between completely unrelated classes of failure. For example, an operational timeout in the peer-to-peer network is fundamentally different from a mathematical cryptographic failure in a verifiable delay function (VDF). These four modules define the rigorous standards by which Kinetic handles interactions. They govern interactions with the underlying libp2p network stack. They dictate the failure states for Wesolowski VDF generation and validation. They provide the structural boundary for decentralized DNS zone validation within DHT reveal payloads. They also manage the Drand randomness beacon acquisitions. These acquisitions form the bedrock of the protocol’s time-locks. Each error type is equipped with strict RFC 7807 codes. They also have carefully defined severity levels. Finally, they provide actionable retry heuristics that the surrounding async event loops rely upon heavily.

Why Kinetic Needs This

In a peer-to-peer environment, simply stating “an error occurred” is insufficient. The system must be capable of autonomously deciding its next operational action without any human intervention. For instance, consider if a peer submits a VDF proof over the network. The local system needs to know exactly why the proof failed. Did it fail because the peer is intentionally malicious, resulting in a mathematical mismatch? Or did it fail because the local engine crashed due to an out-of-memory platform error? By categorizing these errors strictly, the system can apply tailored logic to every situation. It can penalize a malicious peer by dropping them from the Kademlia routing table. It can retry a transient network timeout with a randomized exponential backoff. It can gracefully degrade to local storage caches if an external randomness beacon is currently offline.

Furthermore, these specific error types prevent the daemon from panicking under edge-case loads. The error namespaces in Kinetic are rigidly structured and enforced. These namespaces include:

  • KIN-NET-NNN
  • KIN-VDF-NNN
  • KIN-DNS-NNN
  • KIN-DRA-NNN

This rigid structure ensures that frontend clients interacting with the node’s RPC or HTTP interfaces can parse failure states programmatically. Remote network debuggers and local logging aggregators can trace the exact lineage of a failure through the network stack. Developers and client applications do not need to parse brittle, continuously changing error string messages. Instead, they can rely on the stable protocol error codes provided by these modules. These codes act as an immutable contract between the node and the outside world. Without this extreme level of granularity, the Kinetic daemon would be completely unable to maintain stability. It must survive the harsh realities of async network churn. It must survive CPU-bound cryptographic operations. It must survive potentially malformed or explicitly malicious network payloads.

Scenario: Handling Network Churn

Imagine a peer connects to the node and requests a DHT record. Halfway through the transfer, the peer loses internet connectivity. Without NetworkClientError::StreamDropped, the node might treat this as a systemic disk failure and crash. By explicitly identifying the stream drop, the node simply logs a warning and closes the local socket, preserving system health.

Scenario: Defending Against JSON Bombs

An attacker attempts to upload a DNS zone payload containing 10,000 nested JSON arrays. Without DnsError::NestedTooDeeply, the serde_json parser might overflow the stack memory. The explicit bounds checking prevents this.

How It Works

This section unpacks the implementation details, variant structures, and trait implementations for each of the four error modules. We will examine how they map to specific protocol behaviors. We will see how they interface with external dependencies like libp2p, chiavdf, reqwest, and serde_json. We will also examine how they handle Rust-specific nuances like manual trait implementations.

Network Errors (network.rs)

The network error module serves as a critical isolation layer. It sits between the raw, underlying libp2p primitives and Kinetic’s internal command loop. It converts complex state transitions into standard, understandable Kinetic errors. These transitions originate from:

  • The Kademlia Distributed Hash Table (DHT).
  • The GossipSub pub/sub router.
  • Multiplexed network streams.

-> See: kinetic-core/src/error/network.rs — Lines 19-38

The NetworkClientError enum is decorated with standard Rust derives. Specifically: #[derive(Error, Debug, PartialEq, Eq)]. It isolates client-side operational failures encountered during network command dispatch. The variants are specifically designed to capture the lifecycle of asynchronous peer-to-peer operations:

  • Timeout:
    • Emitted when a requested DHT query or an open stream operation exceeds its hard deadline.
    • In P2P networks, peers vanish silently without sending TCP FIN packets.
    • Timeouts are standard operating procedure and must be handled gracefully.
  • Offline:
    • Emitted when the local node detects it has zero reachable peers.
    • When the node is offline, it cannot perform any outbound operations.
    • It must wait for bootstrap nodes to connect.
  • RoutingTableEmpty:
    • Emitted specifically when the Kademlia routing table contains no known peers.
    • A node might have generic connections via GossipSub.
    • However, if they aren’t structured in the DHT routing table, Kademlia operations (PUT / GET) fundamentally cannot proceed.
  • ChannelClosed:
    • A critical internal fault.
    • Kinetic uses internal asynchronous channels (like mpsc or oneshot) to pass messages.
    • These messages go between the HTTP API threads and the singleton network event loop.
    • If this channel closes unexpectedly, the node is internally broken and cannot dispatch commands.
  • StreamDropped:
    • Happens when the remote peer ungracefully severs the underlying TCP or QUIC stream.
    • This occurs before a response is fully delivered to the local node.
  • UnsupportedProtocol:
    • Fired during the libp2p identify protocol handshake.
    • If the remote peer speaks an incompatible version of the Kinetic protocol, communication halts immediately.
    • This prevents parsing errors down the line.
  • GossipSubError(String):
    • Wraps dynamic, stringified errors from the GossipSub protocol.
    • Examples include publish rejections, subscription limits, or mesh validation failures.
  • StoreError(String):
    • Wraps dynamic errors from the Kademlia record store.
    • Examples include out-of-space errors when trying to PUT a record into the local database.
  • Other(String):
    • A fallback catch-all for miscellaneous faults that do not fit into the standard taxonomy.

-> See: kinetic-core/src/error/network.rs — Lines 41-94

The impl NetworkClientError block provides the operational metadata required by the broader system state machine:

  • code():
    • Exhaustively matches every variant to a string in the KIN-NET-001 through KIN-NET-009 range.
  • error_type_uri():
    • Constructs a full URI for RFC 7807 compliance.
    • It does this by prepending the official documentation URL.
  • severity():
    • Splits the errors by operational impact.
    • Timeouts, empty routing tables, offline status, dropped streams, and GossipSub errors are treated as Severity::Warning.
    • This is because they are expected realities of decentralized networking.
    • Conversely, UnsupportedProtocol, StoreError, and Other indicate systemic misconfigurations or storage issues.
    • Thus, they are marked as Severity::Error.
  • is_retryable():
    • This is a crucial heuristic for the command dispatcher.
    • It explicitly returns true for Timeout, Offline, RoutingTableEmpty, and StreamDropped.
    • When this is true, the outer application can loop and attempt the network call again with an exponential backoff.
  • user_message():
    • Maps every variant to a clean, human-readable string.
    • This string is intended for CLI outputs or frontend UI display.

Crucial Namespace Note: As heavily documented in the module comments, the namespace KIN-NET-NNN is heavily overloaded across the codebase. This NetworkClientError occupies 001..009. However, the KineticStoreError defined upstream in the kinetic-network crate occupies 001..020. The KineticStoreError takes precedence for external API responses. This is because it carries richer rejection context regarding the DHT. NetworkClientError is used almost exclusively internally within the event loop to signal command dispatch failures.

VDF Errors (vdf.rs)

Kinetic utilizes Verifiable Delay Functions (VDFs) to enforce a strict cryptographic time-delay on name registrations. This prevents network spam, squatting, and front-running. Specifically, it embeds the Wesolowski VDF protocol through the chiavdf C++ library. The error surface in this module is deliberately split into two distinct enums. This separates remote validation logic from local computation execution.

-> See: kinetic-core/src/error/vdf.rs — Lines 23-37

The VdfRejectReason enum defines exactly why a VDF proof submitted by a remote peer was rejected. This verification happens at the local DHT store verifier:

  • MalformedProof:
    • The byte array provided over the network was the wrong size.
    • Or, it structurally failed to parse before any cryptographic math was attempted.
  • ChallengeMismatch:
    • A critical security violation.
    • The proof mathematically verifies as a valid VDF.
    • However, it was generated for the wrong challenge.
    • In Kinetic, the challenge is deterministic: SHA-256(network_id || name || salt || drand_signature_hex).
    • A mismatch means the peer is trying to reuse an old proof.
    • Or they are trying to apply a proof to a completely different domain name.
  • EngineError(String):
    • The underlying chiavdf verification bindings threw a C++ exception.
    • Or they encountered an internal math fault.
  • DiscriminantFailed:
    • The SHA-256 challenge payload could not be mathematically mapped into an RSA discriminant.
    • This discriminant is required to initialize the Wesolowski group.

-> See: kinetic-core/src/error/vdf.rs — Lines 40-60

The VdfError enum covers failures that occur when the local node itself attempts to run the VDF prover. It needs to do this to generate a new proof for its own outbound registrations:

  • LockFileError(String):
    • VDF generation is an intensive, single-threaded CPU task that can starve a machine.
    • Kinetic serializes prover execution across the entire operating system using a filesystem lock.
    • This error occurs if the OS denies permission to create the lock file.
  • LockAcquireError(String):
    • Occurs when the local node times out waiting for another local process to release the VDF lock.
  • DiscriminantError:
    • Local failure to create the VDF discriminant prior to computation.
  • ProofGenerationError:
    • The local chiavdf prover panicked.
    • Or it exhausted system memory resources.
    • Or it aborted computation midway.
  • UnsupportedPlatform:
    • Emitted immediately if the node is running on an unsupported architecture.
    • Examples include certain ARM variants or older Windows OS.
    • This happens because the embedded C++ library is not compiled or supported.
  • InvalidProof:
    • The locally generated proof exceeded acceptable byte bounds.
    • This is checked before it was even returned to the caller.

-> See: kinetic-core/src/error/vdf.rs — Lines 62-109

The behavior implementation for VdfError manages the node’s local prover state:

  • code():
    • Error codes map sequentially from KIN-VDF-001 to KIN-VDF-006.
  • severity():
    • The severity() method is notable here.
    • UnsupportedPlatform is escalated to Severity::Critical.
    • This is because it means the daemon is fundamentally crippled and cannot participate in the protocol’s write operations.
    • All other errors are standard Severity::Error.
  • is_retryable():
    • The is_retryable() method only returns true for LockAcquireError.
    • If the prover is locked by another process, it is perfectly safe (and expected) for the node to sleep and retry.
    • All other cryptographic errors are fatal for that specific proof attempt.

DNS Validation Errors (dns.rs)

Kinetic supports decentralized websites by embedding standard DNS zone data inside DHT reveal records. Because this data is serialized as JSON and propagated globally without central authority, it represents a massive attack surface. Strict, aggressive validation is applied to this payload. This happens before the node accepts it into memory or writes it to disk.

-> See: kinetic-core/src/error/dns.rs — Lines 12-57

The DnsError enum captures every conceivable structural and protocol violation associated with DNS zones:

  • NestedTooDeeply:
    • Guards against JSON-bomb attacks.
    • These attacks are designed to blow the call stack and crash the node during deserialization of heavily nested arrays or objects.
  • ParseError(serde_json::Error):
    • Standard JSON parsing failure if the payload is malformed.
  • TooManyRecords:
    • A critical protocol cap.
    • A zone is limited to a maximum of 50 records.
    • This limit is mathematically derived to ensure that the final JSON payload stays well below the 80 KB libp2p maximum DHT record size limit.
    • This leaves adequate room for VDF proofs and cryptographic signatures.
  • InvalidLabelLength(String):
    • Rejects DNS labels that are empty.
    • Or, it rejects them if they exceed the 62-character maximum defined by RFC 1035.
  • InvalidLabelCharacters(String):
    • Rejects labels containing characters outside the standard alphanumeric-and-hyphen specification.
  • InvalidCnameConfiguration(String):
    • Enforces the rigid DNS RFC rule regarding CNAMEs.
    • The rule states that if a CNAME record exists for a label, no other records (like A, TXT, or MX) can exist for that same label.
  • TxtRecordTooLong(String):
    • Enforces a strict 255-byte limit on TXT records.
    • This is designed to prevent network bloat and abuse.
  • InvalidCnameTarget(String):
    • Rejects empty or excessively long string targets for CNAMEs.
  • InvalidPeerId(String):
    • Specific to Kinetic’s decentralized routing.
    • Ensures that libp2p PeerId strings are correctly formatted base-58 or base-36 representations.
  • InvalidKid(String):
    • Ensures cryptographic DID identifiers are valid.
    • They must start with the mandatory did:kin: prefix scheme.
  • InvalidIpfsCid(String):
    • Ensures IPFS pointers are correctly formatted Content Identifiers (CIDs).

-> See: kinetic-core/src/error/dns.rs — Lines 59-78

A significant Rust implementation detail is found here: the manual PartialEq implementation. Because the ParseError variant contains a serde_json::Error, things get tricky. The serde_json::Error does not implement PartialEq natively in the standard library. Therefore, the DnsError enum cannot simply use #[derive(PartialEq)]. Instead, the code manually implements the trait. It unwraps the variants and uses string comparison (a.to_string() == b.to_string()) for the serde error. This ensures tests and state machines can still safely check for equality without compilation failures.

-> See: kinetic-core/src/error/dns.rs — Lines 80-142

The impl DnsError block specifies the error lifecycle:

  • code():
    • Codes map sequentially from KIN-DNS-001 to KIN-DNS-011.
  • is_retryable():
    • Every single error variant evaluates to is_retryable() == false.
    • These are deterministic payload validation failures.
    • Resubmitting the identical JSON payload will always yield the exact same error.
  • severity():
    • The severity() is uniformly designated as Severity::Warning.
    • From the perspective of the Kinetic daemon, receiving an invalid payload from the network is not a local system failure.
    • It is simply a bad request from a peer.
    • It is handled by dropping the payload and moving on.

Drand Quicknet Errors (drand.rs)

The Drand randomness beacon is the cryptographic heartbeat of the Kinetic protocol. Every single VDF commitment utilizes the current Drand “kyn”. A “kyn” is a discrete round of randomness. It is used as a mathematical salt. When the commitment is finally revealed to the network, it must include the exact Drand randomness signature. This signature must have been valid at the precise time of the commitment. If a node cannot fetch this randomness, the system halts. Or, if the randomness is proven invalid, the entire time-lock guarantee collapses.

-> See: kinetic-core/src/error/drand.rs — Lines 19-53

The DrandError enum encapsulates the myriad ways fetching from the external Quicknet network can fail:

  • AllEndpointsFailed:
    • The node maintains a list of multiple external Drand HTTP relays.
    • This error fires if every single relay in the fallback list is unreachable or returns an error.
  • Network(String):
    • Wraps lower-level connection faults.
    • Examples include DNS resolution failures or TCP connections being refused by the host.
  • HttpError(u16):
    • Fired when a relay successfully connects but returns a non-200 status code.
    • Examples include 502 Bad Gateway or 429 Too Many Requests.
  • NoCachedKyn:
    • Kinetic attempts to failover to a locally cached randomness payload if the external network is completely down.
    • This error occurs if that local cache is empty.
  • Serde(serde_json::Error):
    • The relay returned a 200 OK.
    • However, the JSON payload was malformed or missing required protocol fields.
  • Storage(StorageError):
    • An IO failure occurred when attempting to read or write the local fallback cache to the filesystem.
  • Reqwest(reqwest::Error):
    • Wraps internal failures from the HTTP client library itself.
  • InvalidSignature:
    • The most critical cryptographic check in the module.
    • Drand randomness is accompanied by a BLS threshold signature generated by the League of Entropy.
    • Kinetic mathematically verifies this signature locally.
    • If it fails, the relay is attempting to feed forged randomness to the node.
    • This would allow attackers to bypass the VDF time delay.
  • StaleKyn { expected: u64, got: u64 }:
    • Enforces strict temporal bounds on the protocol.
    • The node calculates what the current Drand round should be based on the local system clock.
    • If the relay returns a round (got) that is significantly older than the expected round, it is rejected.
    • This prevents replay attacks where old randomness is used to pre-compute VDF proofs.

-> See: kinetic-core/src/error/drand.rs — Lines 55-80

Similar to DnsError, the DrandError enum requires a complex manual implementation of the PartialEq trait. It must handle stringified comparisons for serde_json::Error. It must also handle stringified comparisons for reqwest::Error. Crucially, it must also safely unwrap and compare the internal fields of the StaleKyn struct variant. This is done using e1 == e2 && g1 == g2 to verify equality.

-> See: kinetic-core/src/error/drand.rs — Lines 82-144

The operational definitions for Drand:

  • code():
    • Codes map sequentially from KIN-DRA-001 to KIN-DRA-009.
  • severity():
    • The severity() method flags Serde, Storage, and InvalidSignature as Severity::Error.
    • This highlights severe data corruption or active malicious interference.
    • Network timeouts and stale kyns are considered Severity::Warning.
  • is_retryable():
    • The is_retryable() method dictates that AllEndpointsFailed, Network, HttpError, Reqwest, and StaleKyn are fully retryable.
    • Querying the relay pool again seconds later will likely succeed.
    • However, InvalidSignature or Storage faults immediately halt the sequence.

Rust Trait Implementation Deep Dive

-> See RUST_CONCEPTS.md for explanations of how thiserror automates Display generation, and how PartialEq is manually implemented for external crate errors like serde_json::Error and reqwest::Error to enable ergonomic unit testing.

Key Pieces

NetworkClientError::code

  • What it does: Assigns the KIN-NET-001 to KIN-NET-009 string identifiers to client-side network failures.
  • File & Line: kinetic-core/src/error/network.rs — Line 41
  • Why it matters: Provides a programmatic, stable identifier. This avoids parsing display strings when deciding how to handle P2P layer drops. The system uses these strings to index telemetry metrics.

VdfRejectReason::ChallengeMismatch

  • What it does: Identifies a scenario where a mathematical VDF proof is structurally valid, but explicitly generated for the wrong Drand challenge.
  • File & Line: kinetic-core/src/error/vdf.rs — Line 28
  • Why it matters: This is the absolute primary defense against proof-reuse attacks. If an attacker tries to reuse an old VDF proof for a new name registration, the challenge mismatch guarantees the record is discarded by the network.

The PartialEq implementation for DnsError

  • What it does: Manually defines equality comparisons for an enum containing serde_json::Error.
  • File & Line: kinetic-core/src/error/dns.rs — Line 59
  • Why it matters: Standard Rust #[derive(PartialEq)] fails on complex third-party errors. By unwrapping and comparing .to_string(), Kinetic ensures that its state machine and unit test assertions can reliably compare expected validation states without compilation boilerplate.

DrandError::StaleKyn

  • What it does: Compares the incoming Drand round number (got) against the mathematically expected round number (expected).
  • File & Line: kinetic-core/src/error/drand.rs — Line 47
  • Why it matters: Prevents temporal drift and replay attacks. If an HTTP relay goes out of sync and serves randomness from two hours ago, accepting it would allow attackers to pre-compute VDF proofs for that exact salt. Stale kyns are strictly rejected to preserve network fairness.

How This Connects to the Rest of Kinetic

CROSS-CRATE DEPENDENCY: As noted, the KIN-NET-NNN namespace is dangerously overloaded. kinetic-core uses it for local operational NetworkClientError (001..009). However, the upstream kinetic-network crate hijacks the identical namespace for KineticStoreError (001..020). In practice, NetworkClientError is swallowed internally by the multiplexing loops. So end-users only ever see the KineticStoreError variant over HTTP or RPC. Developers working across both crates must be acutely aware of this overlap.

FORWARD DEPENDENCY: The VdfRejectReason logic heavily influences the DHT validation layer found in kinetic-network. When a remote peer pushes a Reveal record, the local DHT store extracts the embedded VDF proof. It validates it using the chiavdf engine. If any variant of VdfRejectReason is returned, the libp2p Kademlia store immediately drops the record. It then penalizes the peer’s routing table reputation.

FORWARD DEPENDENCY: The DnsError represents the absolute gatekeeper for the DnsZone structs. These structs are defined in kinetic-core/src/types/dns.rs. Any time a name registration attempts to attach domain data, it must survive this exact list of enum constraints. This must happen before it can ever be serialized to the network or stored in local persistence.

FORWARD DEPENDENCY: The DrandError dictates the lifecycle of the DrandClient fetching loop. The system’s heartbeat entirely relies on this error module. If is_retryable() returns true, the heartbeat loop will sleep and poll again. If it returns false, the entire node synchronization process might stall. It will then await manual intervention or local cache repair.

Quick Reference

  • network.rs
    • Codes: KIN-NET-001 through KIN-NET-009.
    • Retryable: Timeouts, Offline, RoutingTableEmpty, StreamDropped.
    • Focus: Libp2p stream drops, empty routing tables, and internal channel failures.
  • vdf.rs
    • Codes: KIN-VDF-001 through KIN-VDF-006.
    • Retryable: LockAcquireError is the only retryable engine state.
    • Focus: Chiavdf C++ bindings, platform support, and mathematical proof constraints.
  • dns.rs
    • Codes: KIN-DNS-001 through KIN-DNS-011.
    • Retryable: None. Validation failures are strictly deterministic.
    • Focus: Enforcing the 50-record limit, JSON parsing depth, and strict RFC character lengths.
  • drand.rs
    • Codes: KIN-DRA-001 through KIN-DRA-009.
    • Retryable: Network connection faults, timeouts, and stale kyns.
    • Focus: BLS signature verification, stale randomness bounds, and JSON structure.

Open Questions / Things to Revisit

  • Namespace Overlap Danger:
    • The overlapping KIN-NET-NNN namespace between NetworkClientError (client-side) and KineticStoreError (DHT store-side) is a major architectural quirk.
    • It could lead to severe debugging confusion if a developer searches for KIN-NET-005 in an internal telemetry log versus an external API response, assuming they map to the same failure.
    • Should these namespaces be hard-segregated in the future (e.g., migrating internal errors to KIN-NET-INT-NNN)?
  • VDF Lock Granularity:
    • VdfError::LockAcquireError uses a global, system-wide filesystem lock to prevent CPU starvation.
    • However, does this single lock severely bottleneck high-performance nodes running on massive 64-core enterprise servers that could technically parallelize multiple Wesolowski VDF computations simultaneously?
  • Drand Fallback Expiry Priorities:
    • The DrandError::NoCachedKyn module implies a graceful fallback to a local cache when HTTP relays fail.
    • However, how long is a cached kyn considered functionally valid before it triggers a StaleKyn error?
    • The documentation does not strictly define the exact temporal window allowed for cache drift before the node must forcibly halt.
  • Pure Rust VDF Platform Support:
    • The VdfError::UnsupportedPlatform variant immediately signals Severity::Critical.
    • This cripples the node because the chiavdf C++ bindings are not portable to every environment.
    • Is there a roadmap for a pure-Rust, unoptimized fallback VDF prover so that low-end hardware can still participate in the protocol, albeit slower?

Debugging Workflow for Developers

When a Kinetic node encounters one of these categorized errors, developers must follow a systematic approach. First, identify the exact error namespace emitted in the logs. If the error falls under KIN-NET-NNN, immediately check the libp2p connection manager state. Verify if the peer is actively dropping connections or if the local network interface is saturated. If the error is KIN-VDF-NNN, the first check must be the local filesystem permissions. Ensure the daemon has write access to the directory where the VDF lock file is created. For KIN-DNS-NNN errors, developers should not debug the node itself. Instead, they should capture the raw JSON payload submitted by the client application. Run that JSON payload through a standard linter to find the exact character or nesting violation. Finally, for KIN-DRA-NNN, the primary debugging step is to curl the Drand HTTP relay directly. Compare the local system clock of the server against an NTP time source. If the local clock is skewed by even a few seconds, the node will aggressively emit StaleKyn errors. This clock skew is the most common operational failure mode for Drand integrations.

Core: Other Errors (Governance, Identity, Names, Storage)

Crate: kinetic-core Stage: 8 Reading Time: 35 minutes Depends On: kinetic-core/src/error/mod.rs

What Is This?

This file provides comprehensive documentation for four highly specialized and critical error domains within the Kinetic network. These domains are:

  1. Governance execution.
  2. Identity validation.
  3. Name registration.
  4. Persistent storage. These error modules are located in kinetic-core/src/error/governance.rs, kinetic-core/src/error/identity.rs, kinetic-core/src/error/names.rs, and kinetic-core/src/error/storage.rs respectively. Rather than relying on generic, network-wide error enumerations, these modules carve out highly specific, domain-isolated taxonomies. This deep isolation ensures that when a failure occurs in a very specific subsystem, the error is perfectly tailored. For example, if the embedded Sled database fails to acquire a file lock, or a governance proposal lacks the required cryptographic signatures, the resulting error code is precise. The resulting error code, severity level, and user-facing message are perfectly tailored to that specific context. By separating these, Kinetic avoids a massive, monolithic Error enum that would require recompiling the world every time a single naming rule changes. It also allows us to implement very specific is_retryable() logic for each distinct domain.

Why Kinetic Needs This

Kinetic is designed as a fully autonomous network. This means it requires built-in sub-protocols for self-management, persistence, and human interaction. Each of these sub-protocols has unique constraints and failure conditions. These constraints simply cannot be mapped to generic “bad request” or “internal server error” paradigms.

  • Governance (KIN-GOV-NNN): The Kinetic network rules are updated dynamically via on-chain governance. This involves cryptographic thresholds, timelocks, and state machines. If a node attempts to process a proposal that doesn’t meet the required threshold of signatures, it must be rejected. If a node hasn’t waited the required timelock period, the governance engine must reject it explicitly. The network operators need to know exactly why a proposal was rejected. Was it too old? Was it improperly signed? Was it structurally invalid? These precise codes ensure operators can debug their governance CLI tools effectively. Without these specific errors, governance would be a black box of failed state transitions.

  • Identity (KIN-IDN-NNN): Every Kinetic node relies on a local, quantum-resistant ML-DSA-65 identity key. This identity is used to sign its messages. This identity is the foundational anchor of network trust. If a node boots up and cannot read, parse, or decrypt its identity file, it cannot participate in the network. It must halt immediately and alert the operator. Silently generating a new identity would result in the loss of all reputation. It would also result in the loss of routing history associated with the old key. Therefore, the daemon needs granular errors to differentiate between a bad password and a corrupted file.

  • Names (KIN-NAM-NNN): Kinetic provides a decentralized naming system. This allows human-readable apex names (like saif.kin). To maintain order and compatibility, we must strictly enforce global standards. Specifically, we enforce RFC 1035 and RFC 5891 limits. Furthermore, the network must protect reserved infrastructure names (like seed or explorer). This protects them from being squatted by early adopters. When a registration transaction violates these rules, the client needs a clear, actionable error. This allows the wallet software to present a helpful message to the end user.

  • Storage (KIN-STO-NNN): Kinetic relies on the Sled embedded B-tree database for local data persistence. Embedded databases run within the daemon process itself. This means they are highly sensitive to file locks and abrupt process termination. If a user tries to run two Kinetic daemons targeting the same storage directory, Sled will hit a file lock constraint. This needs to be trapped and bubbled up as a critical error (KIN-STO-001). This prevents catastrophic data corruption. A generic IO error would not adequately explain the severity of this lock contention.

How It Works

These four error modules implement the standard Kinetic error taxonomy traits. This means they each define specific functions to standardise network-wide errors. The required functions are code(), severity(), user_message(), error_type_uri(), and is_retryable(). However, the logic inside these implementations is deeply tied to the business rules of their respective domains.

1. Governance Validation Logic

-> See: kinetic-core/src/error/governance.rs — Lines 23 to 57

When a Kinetic node receives a SignedGovernanceMessage, the active GovernanceEngine intercepts the message. This engine can be configured in network.json as sovereign, council, or permissionless. The engine begins a rigorous verification sequence. This sequence must pass completely before any state mutation is allowed.

  • The Missing Root Key Crisis: The network must have an anchor of trust. During initialization, the engine checks for the ROOT_PUBLIC_KEY_HEX environment variable. If this is completely missing, the node yields GovernanceError::MissingRootKey. Because a node cannot evaluate founder-level overrides or verify network genesis state without this key, this is a fatal condition. The daemon will likely log this and invoke an immediate exit.

  • Cryptographic Key Validation: Governance actions often carry public keys for rotation or delegation. Since Kinetic uses ML-DSA-65, these public keys must be exactly 1,952 bytes long. If a provided byte slice does not match this strict length requirement, KeyLengthMismatch is returned. This prevents malformed payloads from wasting CPU cycles in the signature verification engine.

  • Time Windows and Replay Protection: The engine evaluates the timestamp embedded in the governance proposal. If the timestamp is older than the globally allowed replay window, it returns StaleProposal. The network sees old proposals drifting through gossip protocols all the time. Rejecting them is routine maintenance, not a critical fault.

  • Quorum and Thresholds: For networks operating under the council engine, a proposal must accumulate a specific threshold of signatures. These signatures must come from valid, active council members. If a proposal attempts execution before hitting this threshold, the engine returns InsufficientSignatures.

  • Mandatory Timelocks: To prevent governance attacks where a malicious quorum pushes a sudden change, proposals have mandatory delay periods. If the TimelockNotExpired error occurs, the network is simply enforcing patience. The operator must wait for the timelock window to naturally expire.

  • State Machine Compliance: Governance actions flow through a strict state machine. You can only execute or veto a proposal that is currently in a pending state. Attempting to act on a hash that is already finalized or unknown yields NotPendingOrVetoed.

  • Mode Restrictions: If a node operator explicitly configures their network.json to be permissionless, they are changing the node’s behaviour. They are declaring that their node will not accept any global governance updates. Any incoming governance action is immediately rejected with GovernanceDisabled.

  • Special Naming Actions: Governance has the power to forcefully grant or revoke names. However, rules still apply even to root authorities. Premium names must be exactly 1 character long. Violating this yields InvalidPremiumNameLength. Infrastructure name grants must target a valid Category 2 list. Violating this yields InvalidInfrastructureName.

2. Node Identity Loading Sequence

-> See: kinetic-core/src/error/identity.rs — Lines 13 to 33

The node identity subsystem is the very first component that executes when the Kinetic daemon starts. It dictates whether the node has the cryptographic authorization to join the network.

  • Disk I/O and File Access: The daemon attempts to locate and open {base_dir}/identity.key. If there is a filesystem permissions issue, a missing directory, or a hardware failure, an error is generated. This is a standard library std::io::Error. It is wrapped in the IdentityError::Io variant.

  • File Integrity Checks: If the file is successfully read, the daemon evaluates its size and structure. If the byte length does not match the strict layout of an encrypted ML-DSA-65 key payload, the engine aborts. It yields CorruptedIdentityFile. This is a critical safety check to prevent parsing garbage data.

  • Decryption Protocol: Identity files are protected. If the node operator provides an incorrect password via the environment or prompt, decryption fails. Additionally, if the cryptographic Message Authentication Code (MAC) on the payload fails validation, it also fails. In either case, DecryptionFailed is triggered.

  • Missing File Scenarios: If the file is simply absent (IdentityNotFound), the daemon’s bootloader must decide how to handle it. In a fresh install, it might silently generate a new key. But if the file is expected (e.g., restarting an existing node), this error forces the daemon to halt.

  • Seed Phrase Parsing: Kinetic allows node operators to restore their identity from a human-readable BIP-39 mnemonic seed phrase. If the user provides a phrase that has an invalid word count, it fails. If it fails the dictionary lookup, it fails. If it fails the checksum validation, it fails. Any of these failures results in InvalidSeedPhrase.

3. Naming System Enforcement Engine

-> See: kinetic-core/src/error/names.rs — Lines 15 to 44

The Kinetic naming system allows users to claim short, human-readable identifiers. When a user submits a registration transaction to the network, it is intercepted. It is intercepted by the is_valid_apex_name function, which enforces global standards.

  • RFC 5891 LDH Compliance: The naming engine iterates through every character in the requested name. If it finds emojis, uppercase letters, spaces, or special symbols, it instantly fails. The error yielded is InvalidCharacter. The LDH rule (Letters, Digits, Hyphens) is heavily enforced to ensure cross-platform compatibility.

  • RFC 1035 Length Constraints: To prevent network spam and ensure memory bounds, the entire name string is capped. It is capped at a maximum of 253 characters, yielding NameTooLong if exceeded. Furthermore, any single label (the word between dots, if applicable) is strictly capped. It is capped at 63 characters, yielding LabelTooLong if exceeded.

  • Hierarchy and Apex Enforcement: The global Kinetic DHT is designed to handle top-level domains and apex names, not infinite sub-trees. If a user attempts to register a deeply nested subname directly on the global network, it fails. The NotAnApexName error enforces the rule that subnames must be managed locally by the apex owner.

  • Category 1 Reserved Names: To prevent confusion with local DNS and networking standards, certain words are permanently blacklisted. Names like localhost, test, invalid, or example trigger the ReservedName error.

  • Category 2 Infrastructure Names: Kinetic reserves specific names for protocol infrastructure and internal tooling. Trying to register seed, explorer, docs, or api will fail. This triggers the InfrastructureName error. These are locked to prevent squatters from holding network infrastructure hostage.

  • TLD Enforcement: If the network enforces a specific top-level domain for all registrations (such as .kin), it checks this first. Submitting a name without it yields InvalidTLD.

4. Sled Storage Error Mapping

-> See: kinetic-core/src/error/storage.rs — Lines 13 to 24

Kinetic uses Sled, an embedded B-tree database, to persist state to the local disk. The storage error module acts as a translation layer. It maps Sled’s internal panics and errors into clean, structured Kinetic errors.

  • Lock Contention Management: Sled is strictly a single-writer database. It requires exclusive lock access to its database files. If a user accidentally runs kinetic start twice in the same terminal, it fails. The second instance will be blocked by the operating system file lock. This triggers DatabaseLocked (KIN-STO-001). This is a fatal condition that immediately kills the second process. It is the most critical error in the storage module.

  • Structural Corruption Detection: If the machine loses power abruptly and the Sled write-ahead log becomes unreadable, it corrupts. Or, if the B-tree pointers become mangled, the database detects this during the startup integrity check. It returns Corruption. This forces the node operator to intervene manually, possibly wiping the data folder.

  • Routine Operational Failures: If a standard GET, PUT, or DELETE operation fails due to transient disk issues, it yields an error. The engine yields OperationFailed. These are generally transient and safe to retry.

Key Pieces

The GovernanceError Enum Architecture

-> See: kinetic-core/src/error/governance.rs — Lines 23 to 57

This enumeration defines the entirety of the KIN-GOV-NNN taxonomy. Each variant has specific mappings for standard trait methods.

  • MissingRootKey (KIN-GOV-001): This is mapped to Severity::Critical. The user_message() clearly instructs the operator that the ROOT_PUBLIC_KEY_HEX variable is missing. It emphasizes that this is a fatal configuration error.
  • GovernanceDisabled (KIN-GOV-002): This is mapped to Severity::Warning. It informs the client that their request was rejected due to local node configuration. The node is operating in a permissionless state.
  • KeyLengthMismatch (KIN-GOV-003): This is mapped to Severity::Error. This indicates a cryptographic malformation.
  • StaleProposal (KIN-GOV-004): This is mapped to Severity::Info. The is_retryable() method returns false because an old proposal will never become valid.
  • TimelockNotExpired (KIN-GOV-005): This is mapped to Severity::Info. Crucially, is_retryable() returns true for this variant. A client simply needs to wait for the timelock window to pass and try broadcasting the execution again.
  • InsufficientSignatures (KIN-GOV-016): This is mapped to Severity::Warning. This is also is_retryable() == true, because the proposal might gather more signatures in the future.
  • Note on Skipped Codes: KIN-GOV-010, 011, and 012 are intentionally skipped. This is to allow for future expansion in the stable registry.

The IdentityError Enum Architecture

-> See: kinetic-core/src/error/identity.rs — Lines 13 to 33

This enumeration defines the KIN-IDN-NNN taxonomy. It has some unique trait implementations compared to the others.

  • Manual PartialEq Implementation: -> See RUST_CONCEPTS.md for why IdentityError implements PartialEq manually to handle std::io::Error.
  • Io (KIN-IDN-001): This is mapped to Severity::Error. This is an unavoidable reality of disk-based identity files.
  • CorruptedIdentityFile (KIN-IDN-002): This is mapped to Severity::Error. The user_message() warns that the file cannot be used.
  • DecryptionFailed (KIN-IDN-005): This is mapped to Severity::Error. The user_message() clearly states that either the password is incorrect or the payload is mangled.
  • InvalidSeedPhrase (KIN-IDN-004): Interestingly, this is mapped to Severity::Warning. This is because providing a bad seed phrase in a CLI recovery command is usually user error. It is not a systemic failure of the node itself.
  • Retry Logic: Every single variant in IdentityError returns false for is_retryable(). Identity failures require manual intervention. Automated polling will not fix a corrupted file.

The NamesError Enum Architecture

-> See: kinetic-core/src/error/names.rs — Lines 15 to 44

This enumeration defines the KIN-NAM-NNN taxonomy. It is purely focused on input validation.

  • Trait Derivations: This enum cleanly derives Error, Debug, PartialEq, Eq, and Clone.
  • Severity Mapping: The severity() method returns Severity::Warning for all variants. This is a specific design decision. A user submitting a bad name is a validation failure, not a node health crisis. It warrants a warning in the logs, not an alert.
  • Retry Logic: The is_retryable() method unconditionally returns false. If a name violates the RFC LDH rule once, it will violate it forever.
  • User Messages: The user_message() implementations are highly descriptive. For example, InvalidCharacter explicitly lists the allowed subset. “Only lowercase letters, digits, and internal hyphens are allowed.”

The StorageError Enum Architecture

-> See: kinetic-core/src/error/storage.rs — Lines 13 to 24

This enumeration defines the KIN-STO-NNN taxonomy. It is a bridge between third-party crates and Kinetic internal rules.

  • DatabaseLocked (KIN-STO-001): This is the only storage error mapped to Severity::Critical. The user_message() clearly explains that another instance of the Kinetic daemon is already running.
  • Corruption (KIN-STO-002): This is mapped to Severity::Error. The user_message() hints that the local database may need to be reset.
  • Retry Logic: The is_retryable() method uses a matches! macro. It returns true only if the error is OperationFailed. Lock contention and corruption are permanent dead ends. These require human intervention to fix.

How This Connects to the Rest of Kinetic

FORWARD DEPENDENCY: kinetic-storage The StorageError enum is defined in the kinetic-core crate. However, it is actually instantiated and returned by the database wrapper implementations. These implementations live inside the kinetic-storage crate. The core crate defines the abstract interface, and the storage crate fulfills it. This inversion of dependency keeps the core clean.

CROSS-CRATE: kinetic-daemon The kinetic-daemon crate’s boot sequence heavily relies on IdentityError and StorageError. If the daemon encounters DatabaseLocked during boot, it halts. If the daemon encounters MissingRootKey during governance initialization, it halts. The daemon will invoke a hard std::process::exit(1). These errors dictate the survival of the node process.

CROSS-CRATE: kinetic-rpc When a wallet client submits a name registration via the JSON-RPC interface, it is verified. The RPC server will invoke is_valid_apex_name. If that function returns a NamesError, the RPC server intercepts it. It extracts the user_message(), and serializes it into a JSON-RPC error response. This ensures the wallet UI displays a clean, human-readable reason for the rejection.

FORWARD DEPENDENCY: Governance Execution Engines The actual business logic that yields GovernanceError variants lives in the specialized execution engines. These are the sovereign, council, and permissionless modules. They implement the traits defined in core.

Quick Reference

  • KIN-GOV-001: MissingRootKey.
    • Severity: Critical.
    • Action: The node cannot initialize governance without the founder root.
  • KIN-GOV-003: KeyLengthMismatch.
    • Severity: Error.
    • Action: Ensure the key byte slice is exactly 1,952 bytes (ML-DSA-65).
  • KIN-GOV-004: StaleProposal.
    • Severity: Info.
    • Action: The proposal is too old. Non-retryable.
  • KIN-GOV-005: TimelockNotExpired.
    • Severity: Info.
    • Action: The waiting period is active. Retryable later.
  • KIN-GOV-016: InsufficientSignatures.
    • Severity: Warning.
    • Action: Proposal lacks quorum. Retryable later.
  • KIN-IDN-001: Io.
    • Severity: Error.
    • Action: Check filesystem permissions on the identity key directory.
  • KIN-IDN-002: CorruptedIdentityFile.
    • Severity: Error.
    • Action: The ML-DSA-65 key payload is structurally invalid.
  • KIN-IDN-004: InvalidSeedPhrase.
    • Severity: Warning.
    • Action: The BIP-39 mnemonic failed dictionary or checksum validation.
  • KIN-IDN-005: DecryptionFailed.
    • Severity: Error.
    • Action: Incorrect password or failed MAC check.
  • KIN-NAM-003: InvalidCharacter.
    • Severity: Warning.
    • Action: Ensure strict compliance with the RFC 5891 LDH rule.
  • KIN-NAM-007: NotAnApexName.
    • Severity: Warning.
    • Action: Subnames cannot be registered directly on the global network.
  • KIN-STO-001: DatabaseLocked.
    • Severity: Critical.
    • Action: Kill any ghost daemon processes competing for the Sled file lock.
  • KIN-STO-002: Corruption.
    • Severity: Error.
    • Action: The Sled write-ahead log is mangled. Requires manual database wipe.

Open Questions / Things to Revisit

  • Identity Key Format Flexibility: Currently, IdentityError::CorruptedIdentityFile expects a very specific and rigid ML-DSA-65 format length. If Kinetic ever expands to support hardware security modules (HSMs), this must change. If we support different quantum-resistant signature schemes in the future, it must also change. The corruption logic will need to parse version headers rather than relying on raw byte length checks.
  • Name TLD Hardcoding: The InvalidTLD error implies a hardcoded list of allowed TLDs (most likely just .kin). Should this list be adjustable via on-chain governance in the future? If so, the validator needs dynamic state access, not just static regex checks.
  • Sled Recovery Tooling: StorageError::Corruption is a dead end right now. Could we implement a standalone recovery tool? Or an auto-compaction mechanism that attempts to salvage key-value pairs from a corrupted Sled log? This would be better than forcing the user to wipe their node state.
  • Manual PartialEq Overhead: We should ensure manual equality checking for std::io::Error doesn’t lead to false positives if two distinct IO errors happen to share the same .kind().
  • Skipped Error Codes: The governance module intentionally skips KIN-GOV-010, KIN-GOV-011, and KIN-GOV-012. We should ensure that when new features are added, developers know these specific codes are reserved. They should be documented elsewhere to prevent collisions. These skipped blocks might suggest that features were removed or planned and never finished.

Kinetic Error Taxonomy and HTTP Serialization

Crate: kinetic-core Stage: 9 Reading Time: 20 minutes Depends On: thiserror, serde, RFC 7807

What Is This?

In the Kinetic network, errors are not just arbitrary text strings. They are not raw operating system numbers or generic panics. Instead, they are highly structured, rigidly defined data payloads. The files kinetic-core/src/error.rs and kinetic-core/src/api_error.rs define this system. Together, they form a unified error taxonomy for the entire node architecture. They map deep internal domain failures into stable, recognizable protocol codes. For example, a mathematically invalid VDF proof is not just a “bad math” string. It becomes a structured entity with a specific HTTP status and retry logic. A rejected DHT record is translated into an RFC 7807-compliant HTTP JSON response. This system acts as a translation layer spanning the entire codebase. It guarantees that whether a failure happens in the P2P network layer, it is caught. If it happens in the Sled local storage engine, it is properly categorized. If it occurs inside the cryptographic verifier, it is safely wrapped. All errors are annotated with retry logic. They are presented to end-users over REST APIs safely. Crucially, they never leak sensitive internal execution details. They never leak raw Rust stack panics to external HTTP clients. This protects both the security and the usability of the daemon.

Why Kinetic Needs This

When building a decentralized P2P network node like Kinetic, error handling is critical. An error is rarely meant just for the local developer’s terminal console. Errors in Kinetic cross multiple complex boundaries constantly.

First, consider the User Boundary. When a user registers a name using the Kinetic command line interface (CLI). They absolutely do not want to see a Rust stack trace on their screen. They need to know immediately if they should expect a “Name Already Owned” error. Or, conversely, they need a clear “Wait 5 minutes and retry” warning. The error system must provide strings that are clean, non-technical, and actionable.

Second, consider the Network Boundary. When an HTTP RPC client queries the daemon, standard JSON payloads are mandatory. We cannot emit raw Rust panics over an HTTP socket. We cannot emit generic std::io::Error messages either. RFC 7807 standardizes exactly how problem details should look on the web. Kinetic must adhere to this standard so that web dashboards and frontend apps work.

Third, consider the Operational Boundary. The daemon constantly writes logs to disk or stdout. In a P2P network, a failure to parse a DHT record from a peer is common. It might just be an Info level event representing a noisy, misconfigured peer. However, a corrupted identity key file on disk is a vastly different scenario. That is a Critical event requiring immediate alerts and a forced node shutdown. The error system itself must dictate this operational severity. It cannot be left up to the developer to guess the log level at the call site.

Finally, consider the Support Boundary. We need stable, easily greppable error codes. Codes like KIN-RES-001 allow developers and end users to look up documentation. They identify specific failures independent of changes to the English error messages. If we change the text in a future release, the code remains the same. This prevents breaking automated tooling that regex-matches errors.

Without a strict error taxonomy, inconsistencies arise. Ad-hoc wrapping with libraries like anyhow can lead to inconsistent HTTP status codes, often defaulting to a generic HTTP 500 Internal Server Error.

How It Works

The error handling strategy in Kinetic is intentionally split into two distinct halves. The internal Rust taxonomy is defined in error.rs. The external HTTP API serialization is defined in api_error.rs.

The Domain Error Abstraction

Inside kinetic-core and its sibling crates, each operational subsystem defines errors. They use the thiserror crate to generate standard std::error::Error traits. -> See: kinetic-core/src/error.rs — Lines 37 to 55.

You will see submodules for dht, dns, drand, governance, and identity. You will also see modules for names, network, storage, and vdf. Each of these specialized domain errors is required to implement a rich metadata interface. Instead of just returning a single string message, an error type in Kinetic must provide six things:

  1. Stable Protocol Code: A static string like KIN-RES-001. This uniquely identifies the exact failure mode across all versions of the software. It is never dynamically generated.

  2. RFC 7807 Type URI: A web URI, usually pointing to kinetic.network/errors/. This points to external, hosted documentation explaining the error in deep detail.

  3. Retryability Flag: A simple boolean value. This indicates if the client can safely retry the exact same operation. A network timeout is retryable. An invalid cryptographic signature is definitely not.

  4. Severity Classifier: An enum dictating the tracing logging level. This ensures the log writer does not have to guess the severity.

  5. User Message: A clean, non-technical string intended for UI display. This is what the CLI or the web frontend will actually render to the human.

  6. Developer Details: A serde_json::Value payload. This contains exact diagnostic variables, state transitions, or invalid input dumps. It is meant for the developer reading the network payload payload.

The Unified Catch-All: KineticError

For operations that cross subsystem boundaries, we need a wrapper. For example, the core daemon kernel executing a complex flow might hit network or disk errors. Kinetic provides a unified enum called KineticError for this. -> See: kinetic-core/src/error.rs — Lines 79 to 158.

This massive enum wraps lower-level errors. It also defines generic failures like InvalidVdfProof or CommitmentMismatch. -> See RUST_CONCEPTS.md for an explanation of how thiserror’s #[from] attribute allows the Rust compiler to automatically convert inner errors, keeping business logic clean.

Severity and Operational Routing

The Severity enum is crucial for the internal logging and tracing pipeline. -> See: kinetic-core/src/error.rs — Lines 178 to 197.

It defines four distinct levels of operational impact.

  • Info: Represents normal protocol outcomes. If a DHT query finds no results, it is not a node failure. It is just reality; the name does not exist. The node functioned correctly.
  • Warning: Represents transient issues. This includes things like rate limits, temporary peer disconnects, or slow query responses.
  • Error: Represents unexpected failures requiring investigation. An example is a mathematically invalid VDF proof being sent by a peer. The peer is misbehaving, or our math is wrong.
  • Critical: Represents node-breaking states. This includes a missing identity key or local database corruption that prevents boot. The node must crash or page an operator immediately.

Crossing the HTTP Boundary: api_error.rs

When a Kinetic internal error needs to be returned to an RPC client, it must be serialized. It must cross the network boundary as JSON over HTTP. -> See: kinetic-core/src/api_error.rs — Lines 13 to 37.

The ApiError struct defines this standard payload format. It uses serde macros to serialize itself cleanly into JSON. RFC 7807 dictates that the error type field must be named exactly type. However, type is a reserved language keyword in Rust. To bypass this, the struct uses the attribute #[serde(rename = "type")] on the error_type field.

Kinetic extends the base RFC 7807 spec with custom JSON fields. These extensions are: code, retryable, details, and request_id.

To prevent massive, noisy payloads full of empty JSON objects, we use skipping. The #[serde(skip_serializing_if = ...)] macro is used heavily on fields like instance and details. If there is no specific instance URI, the key is omitted. If there are no extra developer details attached to the error, the details key is omitted entirely. This keeps the HTTP payload lightweight and readable.

The Conversion Boilerplate (From<T> for ApiError)

The bulk of api_error.rs consists of manual trait implementation blocks. These blocks convert internal domain errors into the universal ApiError struct. -> See: kinetic-core/src/api_error.rs — Lines 50 to 323.

For every domain error in the system, there is an impl From<DomainError> for ApiError. This includes ResolutionError, PublishError, DrandError, and many others. Inside these implementations, a robust match statement evaluates the specific enum variant. It then assigns a correct HTTP status code and a standard, human-readable title.

This architecture ensures that the HTTP API routing layer is ignorant. The Axum handlers never have to think about how to translate a specific system failure. The mapping logic is encapsulated centrally and safely in this exact file. If we want to change a status code, we do it here, and it applies globally.

Task-Local Request Tracing

Notice that every single ApiError conversion calls the current_request_id() function. -> See: kinetic-core/src/api_error.rs — Lines 46 to 48.

This function fetches a unique correlation ID from a task-local variable. This is likely managed by the tokio runtime and the tracing instrumentation crate. This unique ID is injected directly into the HTTP JSON response payload. When an API client receives an error with request_id: "req-12345". They can provide that exact string to the node operator. The operator can then grep their internal server logs for req-12345. They will instantly find the exact stack trace, debug variables, and timing context. This drastically reduces debugging time in a distributed system environment.

Key Pieces

KineticError

What it does: The top-level, catch-all error enum. It wraps core operations spanning multiple internal subsystems. Where it lives: kinetic-core/src/error.rs — Lines 79-158. Why it matters: It acts as the canonical return type for high-level daemon kernel functions. A core flow returns Result<(), KineticError>. Instead of forcing generic boundaries to explicitly depend on 15 different subsystem enums. They depend on this one wrapper, significantly simplifying function signatures. It drastically reduces module coupling across the crate.

Severity

What it does: Classifies the operational impact and logging necessity of an error. Where it lives: kinetic-core/src/error.rs — Lines 178-197. Why it matters: It prevents log spam in production. Decentralized, chaotic P2P networks produce a massive amount of expected failures. Peers go offline mid-stream constantly. If every offline peer logged a red Error to stdout, the logs would be useless. The node operator would quickly ignore them. Severity allows the node to downgrade structural network noise to Warning or Info. It reserves the Error and Critical levels for actual internal bugs or security issues.

ApiError

What it does: The RFC 7807 compliant struct. It is safely serialized into JSON for HTTP responses to external API clients. Where it lives: kinetic-core/src/api_error.rs — Lines 14-37. Why it matters: It ensures absolute consistency across the daemon. Every REST or JSON-RPC endpoint responds with exactly the same shape of JSON on failure. API consumers, like web dashboards or mobile apps, can write a single global parser. They can rely on it entirely without writing endpoint-specific error handling.

From<PublishError> for ApiError

What it does: Maps distributed hash table publishing failures into standard HTTP concepts. Where it lives: kinetic-core/src/api_error.rs — Lines 76-98. Why it matters: It perfectly demonstrates internal-to-external domain translation. If a user tries to publish a name that they do not cryptographically own. The subsystem raises PublishError::AlreadyOwned. This trait implementation correctly translates that variant into HTTP 409 Conflict. It communicates to the HTTP client exactly why their network request was rejected. It does this without needing to explain DHT semantics to the web client.

From<NetworkClientError> for ApiError

What it does: Translates low-level libp2p and socket networking errors into HTTP concepts. Where it lives: kinetic-core/src/api_error.rs — Lines 152-179. Why it matters: P2P networks have highly specific failure modes. These include empty routing tables (RoutingTableEmpty) or dropped multiplexed streams. This block safely maps these internal networking realities to standard HTTP codes. It uses 503 Service Unavailable and 504 Gateway Timeout responses. This allows traditional web tooling, like reverse proxies and load balancers, to understand the failure state natively.

From<RegistrationError> for ApiError

What it does: Handles the complex two-phase name registration lifecycle errors. Where it lives: kinetic-core/src/api_error.rs — Lines 100-123. Why it matters: Registration is stateful and complex. If a user tries to register a name they are already in the middle of registering, it throws AlreadyInProgress. This maps to HTTP 409 Conflict. If the VDF computation fails mid-registration, it throws VdfFailed, mapping to HTTP 500. This safely translates asynchronous state machine failures into synchronous HTTP responses.

From<GovernanceError> for ApiError

What it does: Translates council governance, voting, and timelock parameter failures. Where it lives: kinetic-core/src/api_error.rs — Lines 125-150. Why it matters: Governance rules are strict. A StaleProposal or TimelockNotExpired correctly maps to 409 Conflict. An InsufficientSignatures variant correctly maps to 401 Unauthorized. This enforces strict security semantics over standard REST APIs, making authorization boundaries clear.

From<StorageError> for ApiError

What it does: Maps sled local database corruption or lock failures. Where it lives: kinetic-core/src/api_error.rs — Lines 181-200. Why it matters: Storage errors are almost always fatal or critical. DatabaseLocked yields a 423 Locked HTTP status. Corruption yields a 500 Internal Server Error. This signals to the API consumer that the node itself is unhealthy, not just the specific request.

From<VdfError> for ApiError

What it does: Translates verifiable delay function computation and proof errors. Where it lives: kinetic-core/src/api_error.rs — Lines 202-226. Why it matters: VDFs are computationally heavy and require specific hardware. UnsupportedPlatform maps to 501 Not Implemented. InvalidProof maps to 400 Bad Request. This clearly delineates between “my node cannot run this” and “your math is wrong.”

From<DrandError> for ApiError

What it does: Translates Drand distributed randomness beacon failures. Where it lives: kinetic-core/src/api_error.rs — Lines 228-252. Why it matters: Kinetic relies on Drand for unbiased entropy. If the daemon cannot reach Drand HTTP endpoints, it throws AllEndpointsFailed. This maps cleanly to a 502 Bad Gateway, accurately representing an upstream dependency failure. NoCachedKyn maps to 404 Not Found.

From<DnsError> for ApiError

What it does: Translates traditional DNS zone file parsing and structural failures. Where it lives: kinetic-core/src/api_error.rs — Lines 254-281. Why it matters: Parsing text records is prone to user error. Every single variant in this enum, from NestedTooDeeply to TxtRecordTooLong, maps to 400 Bad Request. They represent deterministic validation failures based on bad user input.

From<IdentityError> for ApiError

What it does: Translates node cryptographic identity and seed phrase issues. Where it lives: kinetic-core/src/api_error.rs — Lines 283-305. Why it matters: If the node’s private key file is corrupted on disk, CorruptedIdentityFile maps to a 500. If the user provides an invalid BIP-39 seed phrase, it maps to a 400. This securely handles sensitive material operations without leaking the key material itself.

From<NamesError> for ApiError

What it does: Maps pure name validation logic failures. Where it lives: kinetic-core/src/api_error.rs — Lines 307-323. Why it matters: Names must follow strict regex and length limits. Any failure here is immediately mapped to a 400 Bad Request. It is entirely deterministic and requires the client to fix their spelling or formatting.

Detailed Variant Mappings

The following section explicitly defines the internal Rust enum variant to HTTP Status Code mappings logic embedded within api_error.rs. Understanding these mappings is critical for writing robust HTTP client integrations.

ResolutionError Variants

  • Offline: The local node is disconnected from the P2P swarm. Maps strictly to HTTP 503.
  • NotFound: The requested name does not exist in the DHT routing tables. Maps strictly to HTTP 404.
  • VdfVerificationFailed: The cryptography provided by a remote peer is invalid. Maps strictly to HTTP 422.
  • Expired: The name exists in the store, but its registration timelock is stale. Maps strictly to HTTP 410 (Gone).
  • Timeout: The DHT routing query took too long to complete. Maps strictly to HTTP 504.
  • Internal: An unexpected internal memory panic occurred during resolution. Maps strictly to HTTP 500.

PublishError Variants

  • Offline: The local node is disconnected and cannot broadcast. Maps strictly to HTTP 503.
  • InvalidProof: The user-provided VDF proof was explicitly malformed before sending. Maps strictly to HTTP 400.
  • AlreadyOwned: Another cryptographic key already owns this specific name. Maps strictly to HTTP 409.
  • AllFailed: The DHT propagation algorithm failed to reach any responsive peers. Maps strictly to HTTP 503.
  • Rejected: The network peers actively rejected the payload due to validation rules. Maps strictly to HTTP 422.
  • Internal: A local memory or state failure occurred before broadcast. Maps strictly to HTTP 500.

RegistrationError Variants

  • InvalidName: The requested name string failed regex or length validation. Maps strictly to HTTP 400.
  • VdfFailed: The local background VDF generation engine crashed. Maps strictly to HTTP 500.
  • CommitmentMismatch: The phase-two reveal payload does not match the phase-one commit hash. Maps strictly to HTTP 422.
  • AlreadyOwned: The user lost the registration race condition to another peer. Maps strictly to HTTP 409.
  • AlreadyInProgress: A state machine conflict occurred; the user is already registering this name. Maps strictly to HTTP 409.
  • NetworkRejected: The P2P peers refused the initial phase-one commit payload. Maps strictly to HTTP 422.
  • Internal: A local state machine memory failure occurred. Maps strictly to HTTP 500.

GovernanceError Variants

  • MissingRootKey: The node configuration is missing the mandatory governance public key. Maps strictly to HTTP 500.
  • StaleProposal: The provided governance proposal ID is too old or already executed. Maps strictly to HTTP 409.
  • TimelockNotExpired: The voting action was attempted too early in the lifecycle. Maps strictly to HTTP 409.
  • NotPendingOrVetoed: A state conflict occurred regarding the proposal status. Maps strictly to HTTP 409.
  • InsufficientSignatures: The council quorum math was not met for this execution. Maps strictly to HTTP 401.
  • GovernanceDisabled: The local node explicitly opted out of governance participation via config. Maps strictly to HTTP 403.
  • KeyLengthMismatch: The provided key schema is structurally invalid. Maps strictly to HTTP 400.
  • InvalidPremiumNameLength: The network parameter update violates core rules. Maps strictly to HTTP 400.
  • InvalidInfrastructureName: The requested parameter violates internal rules. Maps strictly to HTTP 400.

NetworkClientError Variants

  • Timeout: A remote peer did not respond within the deadline. Maps strictly to HTTP 504.
  • StreamDropped: The multiplexed libp2p stream unexpectedly disconnected mid-flight. Maps strictly to HTTP 504.
  • Offline: The local node has zero active peer connections. Maps strictly to HTTP 503.
  • RoutingTableEmpty: The Kademlia routing table is currently blank. Maps strictly to HTTP 503.
  • ChannelClosed: The internal tokio mpsc async channel was dropped. Maps strictly to HTTP 500.
  • StoreError: A wrapped local DHT store exception occurred. Maps strictly to HTTP 500.
  • UnsupportedProtocol: The remote peer does not speak the correct version of Kinetic. Maps strictly to HTTP 501.
  • GossipSubError: The mesh routing broadcast experienced a failure. Maps strictly to HTTP 502.
  • Other: A catch-all for deeply generic networking exceptions. Maps strictly to HTTP 500.

StorageError Variants

  • DatabaseLocked: The Sled lock file is currently held by another background process. Maps strictly to HTTP 423.
  • Corruption: The Sled disk checksum failed during a read operation. Maps strictly to HTTP 500.
  • OperationFailed: A generic disk IO or flushing operation failed. Maps strictly to HTTP 500.

VdfError Variants

  • LockFileError: The chiavdf classgroup engine lock file logic failed. Maps strictly to HTTP 503.
  • LockAcquireError: A local threading lock acquisition timed out. Maps strictly to HTTP 503.
  • DiscriminantError: The mathematical initialization for the VDF failed. Maps strictly to HTTP 500.
  • ProofGenerationError: The chiavdf underlying C++ engine crashed. Maps strictly to HTTP 500.
  • UnsupportedPlatform: The operating system or CPU architecture cannot run ChiaVDF. Maps strictly to HTTP 501.
  • InvalidProof: An externally provided proof string is structurally bad. Maps strictly to HTTP 400.

DrandError Variants

  • AllEndpointsFailed: The node cannot reach any League of Entropy HTTP endpoints. Maps strictly to HTTP 502.
  • Network: A low-level TCP socket failure occurred during fetch. Maps strictly to HTTP 502.
  • Reqwest: The internal HTTP client library experienced a failure. Maps strictly to HTTP 502.
  • HttpError: The upstream Drand server returned a specific error code. Maps exactly to that returned code.
  • NoCachedKyn: The background synchronizer task hasn’t fetched the first kyn yet. Maps strictly to HTTP 404.
  • Serde: The upstream Drand JSON payload schema unexpectedly changed. Maps strictly to HTTP 500.
  • Storage: Writing the downloaded kyn to the local cache failed. Maps strictly to HTTP 500.
  • InvalidSignature: The upstream BLS signature is fake or tampered with. Maps strictly to HTTP 422.
  • StaleKyn: The downloaded network kyn is older than our required target sequence. Maps strictly to HTTP 400.

DnsError Variants

  • NestedTooDeeply: The CNAME recursion depth exceeded safety limits. Maps strictly to HTTP 400.
  • ParseError: The provided text zone file syntax is badly formed. Maps strictly to HTTP 400.
  • TooManyRecords: The zone file exceeds maximum allowed resource records. Maps strictly to HTTP 400.
  • InvalidLabelLength: A specific domain label exceeds the standard 63 character limit. Maps strictly to HTTP 400.
  • InvalidLabelCharacters: A specific domain label contains non-LDH characters. Maps strictly to HTTP 400.
  • InvalidCnameConfiguration: The zone violates APEX CNAME exclusitivity rules. Maps strictly to HTTP 400.
  • TxtRecordTooLong: A specific TXT string exceeds the 255 character limit. Maps strictly to HTTP 400.
  • InvalidCnameTarget: The target of the CNAME alias is malformed. Maps strictly to HTTP 400.
  • InvalidPeerId: A _peer TXT record does not parse as a valid libp2p multiaddr. Maps strictly to HTTP 400.
  • InvalidKid: A _kid TXT record does not parse as a valid cryptographic key. Maps strictly to HTTP 400.
  • InvalidIpfsCid: An _ipfs TXT record does not parse as a valid base58 CID. Maps strictly to HTTP 400.

IdentityError Variants

  • Io: Reading the identity file from disk failed due to permissions or IO. Maps strictly to HTTP 500.
  • CorruptedIdentityFile: The file exists but the protobuf parsing failed. Maps strictly to HTTP 500.
  • IdentityNotFound: The node has not been initialized with kinetic init yet. Maps strictly to HTTP 404.
  • InvalidSeedPhrase: The user provided an invalid BIP-39 mnemonic string. Maps strictly to HTTP 400.
  • DecryptionFailed: The user provided the wrong password for their encrypted identity. Maps strictly to HTTP 401.

NamesError Variants

  • All validation failures map strictly to HTTP 400.
  • This includes regex failures, illegal unicode characters, length boundary violations, and reserved namespace conflicts.

How This Connects to the Rest of Kinetic

This error taxonomy is the central nervous system connecting disjointed parts of the daemon.

FORWARD DEPENDENCY: kinetic-rpc The HTTP server implementation, likely utilizing Axum or a similar framework, will depend heavily on ApiError. When an RPC handler executes business logic and returns an internal Result<T, DomainError>. The framework will leverage the ? operator or the IntoResponse traits. These will automatically convert the DomainError into an ApiError. It will serialize it to JSON, and correctly inject the HTTP response status code headers.

CROSS-CRATE: kinetic-network & kinetic-daemon These crates contain the actual runtime execution logic that produces the errors. For instance, the background worker module that mathematically verifies a VDF proof will return VdfError::InvalidProof. The kinetic-core crate is strictly responsible for defining the error interface and the translation mechanics. However, the actual instantiation and raising of the errors happen at the edge of the network or storage bounds in these other crates.

CROSS-CRATE: kinetic-cli The standalone command-line utility will ingest and parse the ApiError JSON returned by the daemon. Because the retryable boolean and protocol code fields are always guaranteed to be present. The CLI can automatically implement robust, silent retry loops with exponential backoff if retryable == true. This creates a seamless, resilient user experience without forcing the user to manually retype commands.

Quick Reference

Standard Error Code Prefixes

When debugging Kinetic stdout logs or analyzing HTTP API responses, use this prefix guide. It will instantly locate the subsystem that triggered the failure:

  • KIN-RES-*: Name Resolution issues. This includes DHT lookups, parsing, or encountering expired names in the store.
  • KIN-PUB-*: Name Publishing issues. This includes putting data to the DHT, or peer proof rejection.
  • KIN-REG-*: Name Registration lifecycle issues. This includes the two-phase commit and reveal process.
  • KIN-VDF-*: Verifiable Delay Function issues. This covers both proof generation engines and verifier math failures.
  • KIN-GOV-*: Council governance issues. This covers parameter updates, stale proposals, and timelocks.
  • KIN-DNS-*: Traditional DNS issues. This covers zone rendering, TXT parsing, or detecting CNAME loops.
  • KIN-DRA-*: Drand network issues. This covers HTTP acquisition of the Quicknet beacon kyns.
  • KIN-IDN-*: Cryptographic identity issues. This covers node key management or seed phrase hydration.
  • KIN-NAM-*: Name validation issues. This covers regex failures, bad characters, and length bound violations.
  • KIN-STO-*: Sled on-disk storage issues. This covers local database corruption, file locking errors, or disk I/O.
  • KIN-NET-*: P2P networking issues. This covers libp2p routing tables and gossipsub mesh broadcasting.

HTTP Status Code Mappings (Common)

The translation layer strictly adheres to the following mapping philosophy:

  • 400 Bad Request: Deterministic validation failures. Examples include NamesError, InvalidVdfProof. The client sent structurally malformed data.
  • 401 Unauthorized: Security failures. Examples include bad cryptographic signatures on governance proposals, or identity decryption failures.
  • 404 Not Found: Missing resources. Examples include a name not existing in the DHT, or a Drand kyn missing from the network cache.
  • 409 Conflict: State collisions. Examples include a name registration collision in progress, or attempting to vote on stale/expired governance proposals.
  • 422 Unprocessable Entity: Complex cryptographic checks failed. Examples include VDF math, Ed25519 signatures, or reveal commitments. The JSON structure was valid, but the cryptography was wrong.
  • 500 Internal Server Error: Fatal local failures. Examples include Sled local database corruption on disk, or internal engine panics that should never occur.
  • 503 Service Unavailable: Network isolation. Examples include the node being completely offline, or the P2P Kademlia routing table currently being empty.
  • 504 Gateway Timeout: Upstream delays. Examples include a DHT remote peer query taking too long to return verifiable data over libp2p streams.

Open Questions / Things to Revisit

  • Task-Local IDs: The current_request_id() function depends heavily on crate::request_id::current(). What exactly happens if an internal error is converted to an ApiError completely outside the context of an active async trace span? For example, what happens during the synchronous boot process before tokio starts? Does the function panic, or does it gracefully return a generic fallback string like “system-boot”? This needs to be verified to prevent startup crashes.

  • Developer Details Omission: Currently, several manual conversions hardcode details: serde_json::Value::Null. This happens in DnsError and IdentityError, for instance. This practice leaves extremely valuable, context-rich debugging data out of the payload. We should systematically revisit these specific conversions. We should serialize inner state parameters instead. For example, we should include which specific DNS CID string failed validation. We should also include the exact absolute path of the corrupted identity file on disk.

  • Sled Storage Coupling: The KineticError::StorageError variant wraps a generic String. This is done because we want to actively avoid exposing the specific sled crate error types across the API boundary. However, if we switch storage engines in the very near future, perhaps to RocksDB or SQLite. This generic string format might not provide enough deeply structured data for automated recovery tooling to fix the database automatically.

  • HTTP 422 vs HTTP 400: Cryptographic verification failures are currently mapped strictly to 422 Unprocessable Entity. While this is academically and semantically accurate according to RFCs. Many external web client frontend libraries and frameworks handle the standard 400 Bad Request much better for validation issues. We should carefully monitor how external API consumers react to these 422 responses in the wild. We may need to adjust this mapping back to 400 if it consistently breaks frontend error handling logic.

  • Tracing Overhead: The heavy use of request_id::current() on every single error conversion implies a span lookup. If a high-throughput endpoint, like a DHT gossip flood, generates thousands of Info level errors per second. Does the task-local span lookup introduce measurable CPU overhead into the serialization path? We may need to benchmark the From<T> for ApiError trait implementations under heavy load to ensure they do not become a bottleneck.

Governance Engine Drivers

Crate: kinetic-core Stage: 10 Reading Time: 25 minutes Depends On: governance/types.rs, traits::GovernanceEngine

What Is This?

The governance/engine directory contains the core implementations of Kinetic’s governance models. These models dictate exactly how protocol-level decisions are authorized, verified, and executed. It acts as the strict cryptographic gatekeeper for all highly privileged network operations. These operations include triggering an emergency halt during a crisis. They also include registering exclusive 1-character premium names. Furthermore, they include rotating the supreme root key used for administrative overrides. Depending on the specific network configuration provided in network.json at compile time, the node will dynamically instantiate a specific engine driver to enforce these rules. Currently, the Kinetic codebase ships with two operational engines. The first is the SovereignEngine. This model grants administrative control to a single offline Founder Root key. It bypasses decentralized voting thresholds. The second is the PermissionlessEngine. This model acts as a null-operation sink for governance commands. It intentionally rejects all proposals to enforce immutability on the network. By abstracting these operational modes behind a unified GovernanceEngine trait, the rest of the node’s architecture remains decoupled. The consensus mechanisms, mempool validators, and block executors remain unaware of the authorization rules. They simply pass a requested action to the active engine to receive a verification response.

Why Kinetic Needs This

Blockchain networks are not static entities. They undergo massive paradigm shifts throughout their lifecycles. They move from highly centralized inception to fully decentralized maturity. A governance model that is appropriate for a globally distributed, ossified network is highly dangerous for a nascent network that is just bootstrapping. Kinetic acknowledges this reality by providing hot-swappable governance engines that perfectly fit the network’s current maturity stage. During the earliest stages of the network’s launch, the core development team requires the ability to intervene rapidly and decisively. This is also true when deploying private enterprise testnets. If a critical zero-day vulnerability in the smart contract virtual machine is discovered, the network must be halted immediately. If a consensus logic bug is found, a halt prevents a catastrophic loss of user funds. Furthermore, the initial distributions of vital infrastructure designations need to be processed directly by the founders. The manual curation of premium vanity handles also requires administrative access. This is exactly why the Sovereign model was built. It provides administrators with an override mechanism. It bypasses decentralized voting processes. Conversely, as the network matures, this level of centralized control transforms from a necessary safety net into a massive liability. It contradicts the core ethos of a trustless web3 protocol. When the protocol reaches a state of true ossification and stable equilibrium, the founders intend to permanently revoke their own access. For local development environments, intensive stress testing, or the eventual “final stage” of the production mainnet, the network must guarantee absolute safety. It must guarantee to its users that nobody can alter the protocol, mint names, or pause block production. The Permissionless model fulfills this requirement by making governance impossible at the protocol level. By structuring the codebase to dynamically dispatch to one of these predefined engines based on a simple configuration flag, Kinetic ensures a clean transition. Transitioning a network from “Founder Controlled” to “Completely Immutable” does not require rewriting the core consensus algorithm.

How It Works

The governance engine architecture relies heavily on Rust’s dynamic dispatch capabilities to route verification and execution requests to the correct operational model. The process begins at the node’s initial startup phase. It continues indefinitely, executing every time a governance transaction is pulled from the network mempool.

1. Engine Instantiation and Dynamic Dispatch

When a Kinetic node boots up, its very first task regarding governance is to determine which specific ruleset it is supposed to enforce. -> See: kinetic-core/src/governance/engine/mod.rs — Lines 20 to 29 The get_active_engine() function acts as the singleton factory for the entire governance system. It inspects the crate::constants::GOVERNANCE_MODEL static string. This string is statically injected into the binary at compile time by reading the network.json configuration file. The function returns a Box<dyn GovernanceEngine>. This use of a heap-allocated trait object is a deliberate architectural choice. It trades micro-optimization for architectural cleanliness. In Rust, utilizing dyn Trait requires dynamic dispatch. This means the program must perform a vtable lookup at runtime to resolve the correct function pointer for verify_action and execute_action. In highly performance-critical paths, like the core transaction execution loop, this overhead is strictly avoided. However, governance actions are exceedingly rare. They might occur only a few times a year for network halts, or a few times a week for name registrations. Therefore, the nanosecond penalty of a vtable lookup is entirely negligible. The benefit is a highly modular codebase where new governance models can be seamlessly integrated without refactoring the core node logic. Future integrations might include a DAO or Multisig engine. Crucially, if the configuration string is unrecognized, misspelled, or entirely missing, the factory immediately invokes the panic!() macro. This is a deliberate and aggressive fail-fast mechanism. A blockchain node must never be allowed to participate in consensus if its foundational governance rules are undefined. Doing so would inevitably lead to an immediate and unrecoverable chain split.

2. The Permissionless Blackhole

When the node is compiled and configured for pure decentralization, it utilizes the Permissionless engine to enforce its immutability. -> See: kinetic-core/src/governance/engine/permissionless.rs — Lines 15 to 40 The PermissionlessEngine struct implements the required GovernanceEngine trait. However, its implementation is intentionally hollow and adversarial to the caller. It acts as an absolute cryptographic firewall against all privileged actions. It functions as a black hole for governance commands. When the network processes a SignedGovernanceMessage, it attempts to authorize it by calling verify_action(). The engine completely ignores the message contents. It does not inspect the payload. It does not validate the attached signatures. It does not check the current network timestamp. It immediately and unconditionally returns an Err(GovernanceError::GovernanceDisabled). Because this verification step universally fails under all conditions, the corresponding execute_action() method can never be legitimately invoked during standard block production. However, to satisfy the strict requirements of the Rust trait system, it must still be implemented. The implementation simply returns None. This ensures that no state mutations are ever applied and no events are ever emitted.

3. Sovereign Verification: Defending Against Replay Attacks

In Sovereign mode, the network places its absolute, uncompromising trust in a single offline Root key. However, the protocol itself still enforces strict cryptographic and temporal checks. These checks prevent malicious actors from exploiting that trusted key. -> See: kinetic-core/src/governance/engine/sovereign.rs — Lines 31 to 43 When verify_action() is called on the SovereignEngine, its very first defensive maneuver is to protect against delayed broadcast attacks and replay exploits. It calculates the absolute difference using .abs_diff() between two times. These are the network’s agreed-upon current_time_sec and the proposal’s internal timestamp_sec. If this calculated difference is strictly greater than crate::constants::MAX_AGE_SECONDS, the engine immediately rejects the payload. The rejection returns a GovernanceError::StaleProposal. This vital check ensures that an old, intercepted emergency halt command cannot be hoarded by a malicious actor. It prevents the actor from rebroadcasting it years later to indefinitely disrupt the network. Once the timestamp is deemed fresh, the engine proceeds to authenticate the sender’s identity. It attempts to retrieve the currently active root_key from the mutable GovernanceState. If the state is corrupt and has not initialized the root key, this will naturally throw an error. It then serializes the requested action payload into a deterministic array of canonical bytes. This canonical serialization is a vital cryptographic safety measure. Because Rust structs do not guarantee a stable in-memory layout, the payload must be deterministically serialized before it is signed or verified. Using the .iter().any() iterator pattern, the engine sweeps through all the cryptographic signatures attached to the incoming message. It explicitly verifies them against the retrieved root key using the verify_signature helper. If the root signature is completely absent, mathematically invalid, or corrupted in transit, the verification process immediately fails. It drops to the bottom of the function, returning GovernanceError::InsufficientSignatures.

4. Sovereign Verification: Strict Payload Sanity Checks

Even when an action is successfully authenticated by the almighty Root key, the Sovereign engine does not blindly execute it. It still enforces strict protocol-level data integrity. This prevents the founders from accidentally bricking the network due to a typo or a malformed transaction builder. -> See: kinetic-core/src/governance/engine/sovereign.rs — Lines 44 to 80 The engine utilizes pattern matching on the GovernanceAction enum to perform highly specific payload validations based on the requested operation:

  • Premium Name Grants and Revocations: In the Kinetic ecosystem, premium vanity names are incredibly rare, highly prestigious, and fiercely restricted. When evaluating a request, the engine deliberately strips the top-level domain suffix (e.g., .knt) from the requested name string. It then measures the length of the remaining label string. It strictly enforces that this resulting label length is exactly 1. This means only single-character handles (like @.knt, x.knt, or 1.knt) are legally allowed to be registered via governance mechanisms. Any name longer than one character is immediately rejected with an InvalidPremiumNameLength error. This forces users to use the standard, decentralized registration process for normal names.
  • Infrastructure Name Management: For granting or revoking infrastructure designations, the engine defers string validation directly to a utility function. This function is crate::types::infrastructure::is_infrastructure_name(). This ensures the string conforms to the required formatting for critical network nodes.
  • Root Key Rotation: If the founders attempt to permanently rotate the supreme root key, the engine strictly verifies the raw byte length of the provided new_key array. It demands exactly 1952 bytes. If the length mismatches by even a single byte, it rejects the rotation request with a KeyLengthMismatch error. This specific check prevents the catastrophic installation of a truncated, misformatted, or incompatible cryptographic key. Such a bad key could permanently lock the network and render future governance impossible.
  • Emergency Toggles: Highly destructive actions like EmergencyHalt and EmergencyResume require no additional payload data within the enum variant. Therefore, the root signature itself is deemed sufficient authorization to proceed.

5. Execution, State Bloat, and Timekeeping

Once a governance action has successfully passed all temporal, cryptographic, and strict sanity checks, the engine finally proceeds to apply its permanent effects to the blockchain’s state. -> See: kinetic-core/src/governance/engine/sovereign.rs — Lines 89 to 144 The execute_action() method takes a mutable reference to the active GovernanceState. Its very first act is to cryptographically hash the incoming message. It inserts that hash, along with the timestamp, into the state.executed_hashes map. This provides the final layer of replay protection. It ensures a signed message can only ever be executed exactly once. However, this implementation detail introduces a potential vector for state bloat. The executed_hashes map will grow monotonically over the entire lifespan of the network. Because the SovereignEngine does not currently implement any garbage collection or pruning logic for this map, every single premium name grant and key rotation will permanently consume RAM and disk space on every node in the network. The engine then pattern-matches on the action payload once again. This time, it actually mutates the internal state:

  • Key Rotations: It overwrites the state.active_root_key variable with the newly provided, deeply validated key array.
  • Network Halts: It sets the state.is_halted boolean flag to true. This flag acts as the primary kill switch. Downstream consensus modules will actively read this flag and refuse to produce new blocks if it is set.
  • Resumptions and Advanced Timekeeping: This is the single most complex execution path in the governance module. Kinetic does not rely on wall-clock time for consensus because nodes exist across the globe with skewed clocks. Instead, it measures time in kyns. A kyn is a protocol-defined epoch or tick. It is hardcoded to represent exactly 3 seconds of elapsed time. When the network is halted by the Sovereign engine, block production stops. This means the kyn counter freezes. However, wall-clock time continues to pass. When the network is eventually resumed, it experiences a “time warp.” To prevent this from breaking time-locked contracts, staking unbonding periods, and scheduled inflation rewards, the network must mathematically account for the exact amount of time it spent frozen.
    • The engine uses the safe .saturating_add() method to carefully update the state.total_paused_kyns counter based on the arbitrary paused_kyns value declared by the admin in the resume message.
    • It then calculates the end_kyn.
    • It does this by taking the message’s unix timestamp, safely subtracting the global KINETIC_GENESIS_TIME, and dividing the remainder by 3.
    • Next, it calculates the start_kyn by subtracting the administrator-provided paused_kyns duration from the newly calculated end_kyn.
    • Finally, it records this historical time gap by pushing the resulting (start_kyn, end_kyn) tuple into the state.pause_history vector.
    • Downstream modules parsing the chain state will read this history to calculate the “effective kyn”.
    • They subtract the paused durations from the current counter to ensure rewards are distributed fairly. At the very end of execution, the engine constructs and returns a Some(GovernanceEffect::...) enum variant. This effect acts as a verifiable receipt. It allows the overarching node software to emit JSON-RPC logs, trigger internal hooks, or notify connected indexers about the precise governance change that just occurred.

Key Pieces

get_active_engine

  • What it does: The singleton factory function that reads the compile-time network configuration and dynamically returns the appropriate governance driver as a boxed dynamic trait.
  • File & Line: kinetic-core/src/governance/engine/mod.rs:20
  • Why it matters: This function is the primary entry point for the entire governance subsystem. By aggressively leveraging dynamic dispatch, the core node binary does not need to be littered with complex conditional branches checking the governance model. The active ruleset is seamlessly abstracted away from the caller.

PermissionlessEngine

  • What it does: A null-operation governance driver that intentionally and unconditionally rejects all proposals, preventing any and all state mutations.
  • File & Line: kinetic-core/src/governance/engine/permissionless.rs:13
  • Why it matters: This specific struct serves as the mathematical guarantee of total immutability. When deployed on the production mainnet in its final stage, this engine ensures that absolutely no developer, founder, or hacker can alter the protocol rules or arbitrarily halt the chain. It fulfills the ultimate promise of web3 decentralization.

SovereignEngine

  • What it does: The autocratic governance driver that grants a single, offline Root key the unchecked power to bypass standard voting thresholds and violently force state changes.
  • File & Line: kinetic-core/src/governance/engine/sovereign.rs:14
  • Why it matters: This engine is the crucial lifeblood of the network’s early bootstrapping lifecycle. It provides the absolute necessary administrative override required to fix critical consensus bugs, deploy vital infrastructure upgrades, and manually curate the premium name registry long before the network is stable enough to be fully autonomous. The root key itself is expected to be held in an ultra-secure hardware security module (HSM) or distributed via Shamir’s Secret Sharing.

SovereignEngine::verify_action

  • What it does: Performs exhaustive, multi-layered validation on incoming governance proposals, specifically checking timestamp freshness for replay protection, cryptographic signatures for authentication, and payload data constraints for structural integrity.
  • File & Line: kinetic-core/src/governance/engine/sovereign.rs:25
  • Why it matters: This function acts as the primary firewall protecting the network’s internal state. It guarantees that even a fully authorized administrator cannot submit malformed data that could corrupt the chain, such as registering a premium name that violates the strict 1-character limit or deploying a mathematically incompatible cryptographic key.

SovereignEngine::execute_action

  • What it does: Mutates the core GovernanceState in direct response to a validated action, applying secondary replay protection and calculating incredibly complex timekeeping adjustments for network pauses.
  • File & Line: kinetic-core/src/governance/engine/sovereign.rs:89
  • Why it matters: This specific method applies the actual, permanent effects of a governance decision. Its detailed tracking of pause_history and calculation of missed kyns is absolutely vital for ensuring that the rest of the consensus mechanism correctly adjusts block rewards and time-based unlocks after recovering from an emergency freeze.

How This Connects to the Rest of Kinetic

The governance engine acts as a centralized, highly privileged authority that interfaces directly with several other critical subsystems within the Kinetic protocol architecture:

  • FORWARD DEPENDENCY: The block production engine, consensus state machine, or mempool validator will act as the primary consumer of this module. Before definitively including any transaction flagged as a governance proposal into a block, those upstream systems must call get_active_engine().verify_action(...) and gracefully handle the resulting Result before proceeding.
  • CROSS-CRATE: The engine is deeply and fundamentally coupled to the data structures defined in crate::governance::types. It specifically mutates the GovernanceState struct, consumes the SignedGovernanceMessage, and emits GovernanceEffect variants.
  • CROSS-CRATE: The module actively relies on global network parameters rigidly defined in crate::constants. These include the pivotal GOVERNANCE_MODEL, MAX_AGE_SECONDS, TLD_SUFFIX, and KINETIC_GENESIS_TIME.
  • CROSS-CRATE: For all infrastructure namespace validation, the engine completely outsources the string checking logic to the crate::types::infrastructure::is_infrastructure_name() utility function.

Quick Reference

  • Dynamic Instantiation: Use get_active_engine() to reliably obtain the currently active governance driver based on the network.json configuration file.
  • Sovereign Authorization Mode: Requires exactly 1 valid cryptographic signature from the pre-defined, offline Root key.
  • Permissionless Authorization Mode: Requires exactly 0 signatures because it intentionally and unconditionally rejects all proposals without exception.
  • Time-to-Live (TTL) Enforcement: Governance proposals with a timestamp older than the defined MAX_AGE_SECONDS are permanently rejected as stale to aggressively prevent replay attacks.
  • Premium Name Constraints: Premium vanity names are strictly and rigidly limited to exactly 1 character in length (excluding the .knt TLD suffix).
  • Cryptographic Rotations: Root key rotation payloads must provide a new key that is exactly 1952 bytes in length, matching the underlying signature scheme.
  • Network Timekeeping Mechanics (kyns): Time in the Kinetic protocol is measured in discrete, atomic ticks called kyns. These are calculated as exactly 3-second intervals elapsed since the hardcoded KINETIC_GENESIS_TIME. Emergency pauses actively track the start and end kyn to allow the network to mathematically reconcile the lost time immediately upon resumption.

Open Questions / Things to Revisit

  • Missing Council/Threshold Engine: The top-level documentation residing in mod.rs explicitly mentions “signature thresholds” and “council member signatures”. However, only the Sovereign (1-of-1) and Permissionless (0-of-0) engines currently exist in the codebase. This strongly implies that a CouncilEngine or ThresholdEngine (for example, a 5-of-9 multisig model) is either completely missing, currently incomplete, or planned for future development and integration.
  • Fragile Hardcoded Magic Numbers: The RotateRootKey validation explicitly and dangerously hardcodes the expected key length to the magic number 1952 bytes. This is an extremely fragile anti-pattern. If the underlying cryptographic library updates, or if the network attempts to switch to a different post-quantum signature scheme, this hardcoded integer will cause catastrophic, silent failures. This value absolutely should be extracted to a shared constant in crate::constants or dynamically derived from the cryptographic key type’s .LEN property.
  • Startup Panics vs. Graceful Results: The get_active_engine() factory function recklessly utilizes panic!() if the configuration string is invalid. While failing fast on startup is a somewhat standard practice for critical misconfigurations, returning a Result<Box<dyn GovernanceEngine>, CoreError> might be a significantly cleaner approach. This would allow downstream node bootstrapping logic to handle the error gracefully. It would also allow emitting a structured JSON log, or facilitate much better integration testing without crashing the test runner.
  • Saturation Math Vulnerability on Kyns Timekeeping: In the EmergencyResume execution block, the start_kyn and end_kyn calculations heavily rely on the .saturating_sub() method. If the paused_kyns payload value is maliciously or accidentally misreported by the administrator, or if the genesis time calculation is slightly off, these saturating subtractions will silently cap at 0 instead of explicitly throwing an underflow error. This silent, hidden failure could potentially corrupt the network’s critical timekeeping history without raising any alarms to the monitoring systems.
  • Unbounded State Bloat: The executed_hashes map used for replay protection grows monotonically. Because there is currently no garbage collection or pruning logic, every single governance action will permanently consume RAM and disk space on all nodes. This technical debt will need to be addressed, potentially by pruning hashes older than the MAX_AGE_SECONDS threshold.

Governance Types


crate: kinetic-core stage: 11 reading_time: 15 mins depends_on: [01_kyns.md, 10_governance_engine.md]

What Is This?

Kinetic’s governance isn’t just a configuration file or a loose set of admin scripts. It is a structured cryptographic state machine. This machine is resistant to quantum threats.

The types.rs and mod.rs files inside the kinetic-core governance module are critical. Coupled with their foundational counterparts in the kinetic-types crate, they construct the system. Specifically, they define the exact data structures required for executing privileged, network-wide actions.

These files lay out an exhaustive dictionary of what administrative actions are technically possible. Examples include granting 1-character premium domains, triggering emergency network halts, or permanently delegating root authority.

Furthermore, they establish precisely how these high-level Rust enums are encoded into flat canonical byte arrays. They define how they are cryptographically signed using ML-DSA-65 post-quantum signatures. Finally, they dictate how historical execution effects are persistently tracked on-disk across all active nodes.

Why Kinetic Needs This

For a distributed, decentralized, and sovereign network to remain secure, there must be absolute consensus. There must be zero ambiguity regarding how administrative commands are structured and authorized.

Without strongly typed governance actions (like the GovernanceAction enum), nodes might disagree on how to parse an emergency network halt packet. This would lead to chain splits.

Furthermore, Kinetic deliberately separates the strict serialization and deserialization of these actions. They are pushed into a completely isolated core crate (kinetic-types). Why? To allow offline, air-gapped signing tools to operate independently. Tools like hardware wallets, cold storage companion apps, or CLI utilities need to construct proposals. They need to mathematically verify governance proposals without compiling the entire network daemon’s dependency tree.

By strictly delineating intent, authorization, and state, Kinetic ensures security. Intent is what the action is. Authorization is who signed it and with what quantum-resistant keys. State is how the node remembers the execution history. This separation ensures that network governance remains deterministic, perfectly auditable, and secure.

How It Works

1. The Engine Variants (mod.rs)

-> See: kinetic-core/src/governance/mod.rs — Lines 6 to 16

Kinetic supports multiple, distinct governance models. These are fundamentally chosen at compile-time via the GOVERNANCE_MODEL flag. This flag is typically located in the deployment network.json.

  • sovereign: This represents the bootstrap or “dictator” phase. The designated Root key acts as a single-signer authority. If the Root signs the message, it bypasses all thresholds and executes instantly.
  • council: This is a threshold multi-signature voting system. It is designed for the decentralized phase of the network. In this model, at least 50% of an elected council’s members must cryptographically sign a proposal for it to pass.
  • permissionless: This mode strips away all cryptographic signing requirements. It exists purely for local testing and developer environments. Signature verification would just slow down iteration locally.

This abstraction means the underlying type system doesn’t have to change. Even as the live network migrates from a single founder to a decentralized committee, the structs remain identical.

2. The Two-Phase Commit Protocol

-> See: kinetic-core/src/governance/types.rs — Lines 7 to 12

Governance state changes in Kinetic do not happen magically or instantaneously. They follow a strict two-phase commit protocol.

First Phase (Proposal): A SignedGovernanceMessage is constructed and broadcast to the network. This packet acts as a pending “proposal.”

Second Phase (Execution): The active governance engine intercepts this message. It performs threshold verification on the signatures. If the signatures pass the cryptographic checks and meet the engine’s required threshold, it proceeds. The action is executed, producing a GovernanceEffect. This effect is then used to notify the rest of the node’s architecture.

3. Enumerating the Governance Actions

-> See: kinetic-types/src/governance.rs — Lines 30 to 69

Every single privileged action maps to a variant inside the GovernanceAction enum.

  • GrantPremiumName: Allows the root authority to mint a 1-character name. This name is granted directly to a specific user’s public key.
  • RevokePremiumName: Allows the root authority to revoke a premium name.
  • GrantInfrastructureName and RevokeInfrastructureName: Manages Category 2 infrastructure labels. Examples include “api”, “seed”, or “metrics”. This ensures only official nodes can bind to these protected namespaces.
  • RotateRootKey: Permanently delegates the root authority to a new ML-DSA-65 public key. This is a highly sensitive action used for disaster recovery or operational hand-offs.
  • EmergencyHalt: Used to forcefully pause all domain name registrations and renewals. This is invoked during catastrophic network bugs.
  • EmergencyResume: Lifts the emergency halt. It takes the number of kyns the network was paused for and appends it to the global state.

4. Canonical Byte Serialization

-> See: kinetic-types/src/governance.rs — Lines 82 to 154

To securely sign a proposal using a post-quantum algorithm, the types must be reduced to bytes. Kinetic uses a custom, highly deterministic “canonical” byte format. It avoids formats like JSON or MessagePack. Those libraries might reorder keys or pad bytes differently, invalidating the signature.

Inside the SignedGovernanceMessage::to_canonical_bytes() function, the payload is packed manually:

  1. The function pushes exactly 1-byte opcode representing the specific action. For example, 0x0A corresponds directly to granting a premium name.
  2. It packs variable-length data (like UTF-8 strings). It does this by first prefixing them with a 4-byte big-endian length header (u32).
  3. It appends fixed-length data directly to the buffer. For example, the 1952-byte ML-DSA-65 public keys.
  4. Finally, it always appends the proposal’s creation Unix timestamp. This is packed as an 8-byte big-endian u64 at the very end of the array.

This guarantees that an action will always resolve to the exact same bytes. This makes it safe to hash with SHA-256 and sign.

5. Post-Quantum Signature Verification

-> See: kinetic-core/src/governance/types.rs — Lines 24 to 37

Kinetic relies exclusively on post-quantum ML-DSA-65 signatures. The verify_signature utility function is the cryptographic gatekeeper.

It takes three arguments:

  • The raw bytes of the signer’s public key.
  • The canonical bytes of the message payload.
  • The raw bytes of the signature itself.

It uses the official ml_dsa Rust crate to deserialize the verifying key and the signature object. If parsing succeeds, it calls .verify(). Proposals cannot be forged because of these strict requirements.

6. Managing Time and Network Pauses

-> See: kinetic-core/src/governance/types.rs — Lines 77 to 117

When the network is forcibly halted via an EmergencyHalt action, “time” effectively stops. If a user paid for a 365-day domain name, and the network goes down for 7 days, they should not lose 7 days.

The GovernanceState struct tracks this phenomenon using pause_history. This is a vector containing tuples that mark the exact start and end “drand kyns”. These kyns represent the window during which the network was frozen.

The paused_kyns_since function calculates exactly how much “paused time” a registration gets. It iterates through the entire pause_history vector. It meticulously calculates the duration of pauses that occurred strictly after the target kyn.

7. Preventing Double-Granting Flaws

-> See: kinetic-core/src/governance/types.rs — Lines 120 to 191

The tests in types.rs highlight why the math in paused_kyns_since is so complex.

  • test_pause_history_double_granting_flaw: If a pause happens between kyn 1000 and 1100, but a user registers their name at kyn 2000… They should receive 0 paused kyns. They didn’t experience the outage.
  • test_pause_history_overlapping_pause: If a pause happens from kyn 1000 to 1100, and a user registers at kyn 1050… They should only be credited for the 50 kyns that occurred after their registration.

This meticulous edge-case handling ensures users are treated fairly. It prevents malicious actors from exploiting the system for free registration time.

8. Handling Execution Side Effects

-> See: kinetic-core/src/governance/types.rs — Lines 39 to 75

When a governance action successfully executes, it produces a GovernanceEffect. This is a lightweight enum used purely for internal node communication.

For example, if the root authority is rotated, the engine emits GovernanceEffect::RootKeyRotated. Other critical subsystems in the node listen for these effects via internal channels.

  • The p2p networking layer
  • The RPC API layer
  • The block production engine

When they receive the effect, they instantly update their local in-memory caches. This ensures the entire node pivots synchronously to respect the new state.

9. Governance Error Taxonomy

-> See: kinetic-types/src/governance.rs — Lines 157 to 219

Because governance payloads travel over the public internet, they can be manipulated. Parsing them must be incredibly defensive. GovernanceTypeError implements Kinetic’s standard, network-wide error taxonomy.

  • It maps BufferTooSmall to the deterministic code KIN-GOV-030.
  • It maps UnknownOpcode to KIN-GOV-031.
  • It maps InvalidUtf8 to KIN-GOV-032.

It explicitly sets is_retryable() to false. An invalid, corrupted payload will never miraculously become valid upon a retry. It also provides highly readable user_message strings. Block explorers and frontend dashboards can display exactly why a proposal was rejected.

Key Pieces

GovernanceAction

-> See: kinetic-types/src/governance.rs — Line 31 What it is: The central enum defining all permissible, privileged administrative actions. Why it matters: It strictly bounds the state space of what an administrator can actually do. If a desired command is not represented as a variant in this enum, it cannot be executed.

SignedGovernanceMessage

-> See: kinetic-types/src/governance.rs — Line 73 What it is: The outer envelope containing the internal action payload, timestamp, and signatures. Why it matters: This is the actual struct that travels over the wire via gossip protocols. It seamlessly bundles the administrative intent with the irrefutable cryptographic proof.

GovernanceState

-> See: kinetic-core/src/governance/types.rs — Line 79 What it is: The persistent on-disk database representation for the entire governance subsystem. Why it matters: It acts as the ultimate source of truth across restarts. It tracks the active root key, network halts, historical pauses, and executed hashes.

paused_kyns_since

-> See: kinetic-core/src/governance/types.rs — Line 100 What it is: A mathematical function calculating “forgiveness time” for domain names. Why it matters: Without this precise logic, a network outage would unfairly expire domains. This function protects innocent users from paying the price for infrastructure emergencies.

verify_signature

-> See: kinetic-core/src/governance/types.rs — Line 30 What it is: A robust wrapper around the ml_dsa crate for validating post-quantum signatures. Why it matters: It acts as the ultimate gatekeeper for governance operations. If this function evaluates to false, the proposal is instantly discarded as invalid.

to_canonical_bytes

-> See: kinetic-types/src/governance.rs — Line 103 What it is: A custom serialization implementation that reduces an enum to a flat byte array. Why it matters: Standard serializers (like serde_json) are not deterministic enough. By manually packing bytes, Kinetic ensures every node computes the exact same SHA-256 hash.

How This Connects to the Rest of Kinetic

CROSS-CRATE: The kinetic-core/src/governance/types.rs file heavily re-exports types. These types are natively defined in kinetic_types::governance. This intentional design pattern allows lightweight CLI signing tools to depend exclusively on kinetic-types. They can build and sign canonical byte slices without compiling the massive node software.

FORWARD DEPENDENCY: The actual, concrete execution of these types happens later down the line. It takes place in kinetic-core/src/governance/engine/. The engine takes the SignedGovernanceMessage, validates all the signatures against the thresholds, and mutates state.

CROSS-CRATE: The paused_kyns_since mathematical logic directly influences the Name Registry layer (kinetic-core/src/registry/). The registry must constantly consult the governance state. It uses this to calculate the true, adjusted expiration kyn of user domains.

Quick Reference

Governance Action Opcodes:

  • 0x0A - GrantPremiumName: Mint 1-char name.
  • 0x0B - RotateRootKey: Change network authority.
  • 0x0C - EmergencyHalt: Freeze registrations.
  • 0x0D - EmergencyResume: Unfreeze registrations.
  • 0x0E - RevokePremiumName: Remove 1-char name.
  • 0x0F - GrantInfrastructureName: Mint category 2 name.
  • 0x10 - RevokeInfrastructureName: Remove category 2 name.

Deterministic Serialization Rules:

  • Big-endian byte order is strictly enforced for all integers (u32, u64).
  • The specific action Opcode is always exactly 1 byte at the very start of the payload.
  • All variable-length fields (like strings) are preceded by a 4-byte u32 length header.
  • The proposal’s timestamp is universally appended to the extreme end of the payload buffer as an 8-byte u64.

Governance Effect Targets:

  • Networking Layer: Needs to know when the root key rotates to accept new commands.
  • RPC Layer: Needs to know when the network is halted to reject new HTTP requests.
  • Block Producer: Needs to know when premium names are granted to inject them into state.

Open Questions / Things to Revisit

  • Unbounded State Growth: The pause_history vector located inside the GovernanceState struct grows infinitely. If the network experiences thousands of micro-pauses over decades of operation, it could be bad. Iterating through this massive array for every single domain registration lookup could become a CPU bottleneck. We may need to investigate mechanisms to snapshot or compress historical pause records.

  • Council Implementation Details: While mod.rs clearly defines a council engine variant, the details are sparse here. The exact, low-level details of how a 50% threshold is configured requires more work. We need deeper exploration in the actual engine files to see how keys are added/removed.

  • Replay Protection Garbage Collection: The executed_hashes map fundamentally prevents old proposals from being maliciously replayed. However, it never currently drops old hashes. We need to implement a pruning strategy based on the proposal timestamp. This would avoid unbounded memory usage on disk, potentially rejecting any proposal older than a specific window.

  • Key Revocation Mechanics: The current implementation allows the root key to be rotated via action. But there isn’t a direct mechanism to handle compromised keys if the rotation itself is contested. The bounds of Sovereign mode might need emergency fallback protocols.

Governance Logic and State I/O

Crate: kinetic-core Stage: 12 Reading Time: 30-45 minutes Depends On: types.rs, traits/, constants/

What Is This?

These files define the core operational heartbeat of Kinetic’s governance subsystem. While types.rs defines the structural schema of what governance messages look like on the wire (the data models), logic.rs determines what actually happens when those messages arrive at a validator node (the business logic). Together with state_io.rs, these modules manage the complete end-to-end lifecycle of governance execution: how the network accepts messages, verifies cryptographic signatures, executes state mutations, and ultimately stores network-wide consensus changes on the local disk. They operate as the critical bridge between raw cryptographic network messages and actual mutating state transitions within the Kinetic daemon. Every time a network halt is triggered, a premium is minted, or the governance council is updated, the instruction flow passes directly through the verification gates and state mutations defined in this exact codebase. In essence, logic.rs is the brain of governance processing, and state_io.rs is its persistent memory.

Why Kinetic Needs This

In a decentralized infrastructure project like Kinetic, governance is not just a simple database flag that can be casually updated via an administrative web dashboard. Governance is a critical, highly security-sensitive cryptographic process that must be universally agreed upon by all participating nodes in the peer-to-peer network. We need a robust, deterministic, and highly defensive set of mechanisms to ensure that:

  1. Deterministic Execution and State Cohesion: Every single node processing the exact same sequence of governance messages must arrive at the exact same internal state representation. If Node A and Node B process the same proposal but disagree on the outcome (even slightly), the network will immediately experience a hard fork and split into two incompatible chains. Therefore, the logic enclosed here must be entirely devoid of non-determinism—no random number generation, no reliance on local system time zones, and no undefined behavior.

  2. Strict Replay Protection: An action signed by the network founders or council members must be executed exactly one time. We cannot allow a situation where an old, previously executed command (for example, an emergency command to pause all network transactions) is quietly captured by a malicious actor over the wire, and then rebroadcast months later to artificially disrupt the network. This requires us to keep an active, historical ledger of what has been executed.

  3. Bounded Memory Footprint: Governance state lives in the active application memory (RAM) to allow for extremely fast verification of incoming blocks and peer messages. Without an aggressive and automated memory pruning mechanism, a daemon running continuously for months or years would slowly leak memory as historical proposal hashes indefinitely pile up in the tracking maps. Eventually, the node would be killed by the operating system’s Out-Of-Memory (OOM) killer.

  4. Failure Protection: If a node’s persistent disk state becomes corrupted, it must shut down entirely. If a node silently initialized a blank state upon failure, it would mistakenly believe the network has no governance rules. This would open the door for exploitation. Atomic disk writing and strict loading protocols that intentionally crash the application are requirements for Kinetic’s architecture.

How It Works

The lifecycle of a governance update is handled systematically across logic.rs and state_io.rs. Let’s break down the exact flow from initial key validation, through memory management, down to the final atomic disk persistence layer.

1. Initialization and Cryptographic Key Validation

Before a Kinetic node even begins to bind to a network port or accept incoming peer connections, it must rigorously verify that its cryptographic environment is sane and securely configured. This prevents catastrophic deployment errors.

-> See: kinetic-core/src/governance/logic.rs — Lines 21 to 39

The validate_keys_initialized function acts as a mandatory safety gate during the node boot sequence. Its primary objective is to evaluate the ROOT_PUBLIC_KEY_HEX constant. First, it explicitly verifies that this key is not merely the default "REPLACE_ME" placeholder string that developers use during initial codebase scaffolding. If the placeholder is found, it immediately aborts the boot process. Furthermore, it instantiates a dummy GovernanceState struct entirely in memory for the sole purpose of parsing the root key via the get_root_key() internal method. This parsing step validates two critical cryptographic properties: First, that the configured hexadecimal string can be correctly decoded into raw binary bytes without throwing an invalid character error. Second, that the resulting byte array precisely matches the required 1,952-byte length expected for our specific post-quantum signature scheme. In systems programming with Rust, checking the exact length of a slice or vector is crucial for memory safety before passing those raw bytes into a C-bindings cryptography library (which might otherwise cause a segmentation fault). If a developer is actively running the node locally on their machine, and the crate::config::is_dev_mode() function evaluates to true, this stringent check is intentionally bypassed. This is an ergonomic affordance designed to allow for rapid, localized testing of the networking layers without requiring engineers to undergo complex, manual key generation ceremonies every single time they compile the code.

2. Message Intake and Deterministic Hash Generation

When a SignedGovernanceMessage successfully traverses the peer-to-peer gossip network and arrives at a local node, the system must first uniquely and deterministically identify it before it can be processed.

-> See: kinetic-core/src/governance/logic.rs — Lines 69 to 76

The hash_action function is responsible for this task. It takes the underlying SignedGovernanceMessage and extracts its canonical bytes. It is absolutely critical that we only hash these canonical bytes—meaning the raw, predictable instruction payload—completely devoid of transient network wrapper data or the variable-length cryptographic signatures themselves. If we were to just hash the entire Rust struct directly using a derived hashing trait, any padding bytes introduced by the compiler for memory alignment, or any slight mathematical variations in the signature data, could result in a completely different hash for the exact same logical instruction. This would open the network to “signature malleability” attacks, where an attacker modifies the signature slightly (keeping it mathematically valid) to bypass replay protection mechanisms. To prevent this, the canonical bytes are fed strictly into a standard SHA-256 hashing algorithm utilizing the sha2 crate. Notice the specific API usage pattern: we create a Sha256::new() hasher instance, stream the canonical data in using .update(), and extract the final cryptographic hash using .finalize(). The output slice is then manually copied into a fixed-size 32-byte array ([u8; 32]). This resulting deterministic hash becomes the primary, immutable key used for tracking the proposal’s entire lifecycle across the network. It ensures that duplicate network transmissions, regardless of who sends them, map perfectly to the exact same logical event in the state tracking maps.

3. Core Processing Pipeline and Replay Prevention

Once a unique hash is reliably derived, the incoming message is funneled into the core processing and verification pipeline, which serves as the traffic cop for all state mutations.

-> See: kinetic-core/src/governance/logic.rs — Lines 135 to 152

The process_governance_message function serves as this central orchestration point. The operational sequence executed here is incredibly strict, unforgiving, and explicitly ordered to prevent race conditions. First, it retrieves the current UNIX timestamp. It explicitly uses the web_time crate rather than the standard library std::time::SystemTime. This is a strategic architectural choice: standard library time operations will instantly panic if the codebase is compiled to WebAssembly (WASM), since web browser sandboxes do not expose direct OS-level time APIs. By utilizing web_time, the core governance logic remains fully cross-compilable, allowing us to eventually build WASM light-clients that run directly in the browser without rewriting the verification logic. Second, the function proactively calls state.prune(current_time_sec). This clears out any stale, obsolete hashes from the node’s memory, ensuring the tracking map remains clean before any new business logic is evaluated. Third, it consults the executed_hashes map. If the action_hash we generated in the previous step is already present as a key in this HashMap, it mathematically proves that the network has already processed this exact proposal. In this scenario, the function immediately short-circuits execution and rejects the message, returning a GovernanceError::StaleProposal. This check constitutes our foundational replay protection layer, preventing malicious actors from resubmitting old commands. Finally, if the proposal is genuinely novel, the function defers the heavy cryptographic lifting to the active GovernanceEngine (by invoking the verify_action method) to confirm the mathematical signatures and assess dynamic quorum requirements. Notice the specific return type of this function: Result<Option<GovernanceEffect>, GovernanceError>. This nested type means the function can explicitly fail (returning an Error), it can succeed but have no immediate side-effects (returning Ok(None)), or it can succeed and trigger a network-wide action (returning Ok(Some(GovernanceEffect))).

4. Bounded In-Memory State Management

To guarantee that a node does not incrementally consume all available system RAM by infinitely caching old proposal data, a proactive and efficient pruning routine is executed continuously.

-> See: kinetic-core/src/governance/logic.rs — Lines 82 to 87

The prune function relies heavily on Rust’s highly optimized HashMap::retain method to manage memory bounds. Instead of creating a brand new HashMap and copying over the valid entries (which would require expensive memory heap allocations and degrade node performance), retain iterates over the map strictly in-place. It evaluates a closure for every single key-value pair currently stored in memory. If the closure returns true, the item is kept; if it returns false, the item is cleanly removed and its memory is instantly freed back to the operating system. For every single entry, the closure compares the originally recorded execution timestamp of the hash against the currently active UNIX timestamp provided by the caller. If the proposal was successfully executed further in the past than the network’s globally defined MAX_AGE_SECONDS parameter, the closure returns false, and the entry is ruthlessly discarded from memory. This approach is entirely cryptographically safe because any new proposal arriving over the wire that is older than MAX_AGE_SECONDS would inherently be rejected by the base signature expiration rules evaluated during the engine verification phase anyway. Because of this deliberate mathematical interplay, we do not need to track historical execution hashes forever. A rolling sliding window of recent history is entirely sufficient to prevent all practical replay vectors while keeping memory utilization perfectly flat.

5. Atomic File System Persistence

Because the in-memory state is entirely ephemeral and lost when the application closes, the node must possess a robust, enterprise-grade mechanism for writing changes to disk so that they survive application restarts, unexpected panics, or hard server reboots.

-> See: kinetic-core/src/governance/state_io.rs — Lines 32 to 49

The save_to_disk function implements an industry-standard “Write-Then-Rename” atomic operation pattern. When saving state, the system absolutely does not simply open the existing file and begin overwriting bytes sequentially from the start of the file. Doing so is incredibly dangerous: if the daemon experiences an Out-Of-Memory (OOM) kill, a kernel panic, or the physical server loses power midway through the write operation, the file would be left in a partially written, corrupted state. This would permanently destroy the node’s local governance record. Instead, the function utilizes the tempfile crate to intelligently allocate a temporary file in the exact same parent directory as the target persistence file. The complete GovernanceState struct is then serialized directly into this temporary file using bincode::serialize_into. This is a highly optimized Rust pattern: rather than allocating a massive Vec<u8> in RAM to hold all the serialized data at once and then writing it in one block, serialize_into streams the bytes directly into the file buffer as they are generated. This keeps RAM usage perfectly flat regardless of how large the state file grows. Only after the operating system confirms that the serialization and disk flush have completed successfully does the application execute the temp_file.persist() command. Under the hood, on POSIX-compliant systems like Linux, this directly translates to a rename() system call. Because the temporary file and the target file exist on the exact same filesystem mount point, replacing one with the other involves merely swapping inode pointers in the underlying filesystem table. This operation is guaranteed by the OS to be perfectly atomic—either the entire new file replaces the old one instantly, or the swap fails and the old file remains entirely intact.

6. Crash-Safe and Strict Boot Protocols

When a Kinetic node initially boots up, it must load its persistent state from disk in a manner that prioritizes absolute security and determinism over seamless operational continuity.

-> See: kinetic-core/src/governance/state_io.rs — Lines 65 to 94

The load_from_disk function is explicitly designed to be intentionally inflexible and highly defensive against corruption. It utilizes a powerful Rust match statement against the std::fs::File::open operation to handle distinct failure modes with precise, context-aware granularity. If the expected state file does not exist on disk, the Err(e) branch checks if e.kind() == std::io::ErrorKind::NotFound. If this specific condition is met, the system assumes this is a freshly provisioned node spinning up for the very first time, and it gracefully initializes a default genesis state to begin syncing the chain. However, if the file does exist but the Bincode deserialization process fails (perhaps due to a random bit-flip, underlying disk rot, or malicious external tampering), the node takes drastic action. It immediately calls panic!() and deliberately crashes the entire application process, refusing to bind to any ports or connect to peers. Before initiating this fatal crash sequence, it dynamically renames the corrupted file by appending a .corrupt.{unix_timestamp} extension to the filename. This ensures that the damaged data is not accidentally deleted or overwritten upon subsequent restart attempts, allowing DevOps engineers to retrieve and forensically inspect the file to determine the root cause. This extreme strictness is an absolute security necessity. If the state was allowed to silently ignore read errors and blindly reset itself to genesis, a malicious actor who manages to slightly corrupt the local disk could force the node into thinking it has reverted to GovernanceMode::Founder. The node would then erroneously accept unauthorized root commands, leading to catastrophic security compromises for the entire network. By crashing unequivocally, we ensure that human intervention is required to remediate the issue safely.

Key Pieces

validate_keys_initialized

  • What it does: Explicitly ensures the static, compiled-in root governance key is actively configured, hex-decodable, and mathematically valid in length.
  • File & Line: logic.rs : 21-39
  • Rust Concepts Used: Uses Result<(), GovernanceError> for error handling instead of exceptions. It uses String::contains to check for placeholder strings, and hex::decode which returns a Result that is mapped to a custom error.
  • Why it matters: It serves as a vital safeguard. Imagine a scenario where a node operator compiles the code straight from the repository without running the setup scripts. If this function did not exist, the node would deploy into a live production environment with the string "REPLACE_ME" acting as its master cryptographic key. This would render the node completely unsecured. By checking this actively at boot, the application refuses to start, forcing the operator to configure their environment variables correctly.
  • Why a Constant String?: You might wonder why the root key is compiled into the binary as a constant string rather than loaded from an external config file at runtime. This is an intentional security design for decentralized networks. By compiling the key directly into the binary, every node that runs this specific software version explicitly agrees to exactly the same root authority. If an attacker modified the config file on a node to point to their own key, they would fork themselves off the network entirely, as other nodes would reject their signatures.
  • Bypass condition: If crate::config::is_dev_mode() is true, the node skips this check. This is an ergonomic affordance for developers running the test suite or local devnets, so they don’t have to constantly generate massive 1952-byte keys just to test unrelated peer code.

GovernanceState::hash_action

  • What it does: Generates a stable, deterministic SHA-256 hash strictly from the immutable canonical payload of a signed governance message.
  • File & Line: logic.rs : 69-76
  • Rust Concepts Used: Utilizes the sha2 crate’s Digest trait. It creates a mutable hasher instance, feeds data via update(), and consumes the hasher via finalize(). It then manually copies the slice output into a fixed size array [0u8; 32].
  • Why it matters: It provides the universally agreed-upon unique identifier required for tracking proposal lifecycles. When a proposal is submitted to halt the network, every node needs a way to refer to that specific proposal globally.
  • Hardware Acceleration: The sha2 crate in Rust is highly optimized. When compiled for modern server architectures (x86_64 or ARM64), it automatically utilizes hardware-accelerated instructions (like AES-NI or ARM Cryptography Extensions) to compute these hashes with almost zero CPU overhead, ensuring the network can process thousands of proposals per second if necessary.
  • Security Implication: By only hashing the canonical bytes (the true instruction) and deliberately excluding the signatures, it prevents “signature malleability.” If we hashed the entire struct, an attacker could slightly alter the signature bytes—keeping the math valid but changing the hash—and trick the node into thinking it’s processing a brand new proposal, effectively bypassing replay protection.

GovernanceState::prune

  • What it does: Iteratively removes expired proposal hashes from the local executed_hashes map based on strict timestamp expiration thresholds.
  • File & Line: logic.rs : 82-87
  • Rust Concepts Used: Leverages HashMap::retain, taking a closure |_, exec_time|. The underscore means we intentionally discard the key (the hash itself) in the closure scope, as we only care about evaluating the value (the execution timestamp).
  • Why it matters: It is the primary mechanism preventing unbounded memory consumption. A server running autonomously for years would otherwise inevitably crash from RAM exhaustion as it continuously tracks millions of historical, irrelevant execution hashes.
  • Performance consideration: Because retain operates “in-place”, it is incredibly fast and avoids allocating a new memory heap for a new map. It simply drops the elements that evaluate to false.

process_governance_message

  • What it does: The primary intake and validation controller function for all new governance events arriving from the network layer.
  • File & Line: logic.rs : 135-152
  • Rust Concepts Used: Uses unwrap_or_default() when interacting with the system time, providing a safe fallback if the system clock is somehow configured to before the UNIX epoch (1970). Uses early returns (return Err(...)) to cleanly abort if replay protection fails.
  • Why it matters: It explicitly orchestrates the exact sequence of governance evaluation: it handles timestamping, triggers aggressive pruning, enforces replay-checking, and ultimately delegates verification to the policy engine.
  • Role in the system: It acts as the unbypassable traffic cop for all governance mutations. No proposal can affect the state without first passing through this exact function’s logic.

GLOBAL_GOVERNANCE_STATE

  • What it does: A thread-safe, globally accessible singleton containing the entirety of the network’s current governance state.
  • File & Line: state_io.rs : 23-29
  • Rust Concepts Used: Uses the lazy_static! macro to allow complex initialization logic (like reading the genesis time constant) at runtime. Wraps the struct in a Mutex to guarantee memory safety across threads.
  • Why it matters: In an asynchronous web server or P2P daemon, multiple threads are constantly spinning up to handle incoming requests. If a REST API request wants to check if the network is halted, and a P2P thread wants to update the council simultaneously, they need a safe way to share this data without causing data races.
  • Design philosophy: While pure dependency injection (passing a reference down through every function) is often preferred in pure functional programming, for something as globally relevant and frequently accessed as governance rules, a managed singleton prevents creating a “spaghetti” architecture of lifetime parameters and reference counting throughout the entire codebase.

save_to_disk & load_from_disk

  • What it does: Completely manages the atomic binary serialization to, and strict deserialization from, the local server filesystem using Bincode.
  • File & Line: state_io.rs : 32-94
  • Rust Concepts Used: match statements for exhaustive error handling. std::io::ErrorKind to differentiate between a missing file and a locked file. bincode::serialize_into for zero-allocation streaming directly to a file buffer.
  • Why it matters: It guarantees profound crash resilience. The write operation is inherently atomic (using tempfile renaming) to structurally prevent mid-write file corruption if the power fails.
  • Failure philosophy: The read operation is severely strict. By purposefully panicking (panic!()) on a corrupted read, it preemptively eliminates the risk of catastrophic security downgrades, where a node might accidentally reset to a genesis state and allow unauthorized founder commands.

How This Connects to the Rest of Kinetic

CROSS-CRATE DEPENDENCIES:

  • kinetic-api: The user-facing REST and WebSocket API layers will frequently request read-locks on the GLOBAL_GOVERNANCE_STATE in order to accurately report network health metrics, current halt status, or pending council proposals to external web dashboards and node operators. Because it uses a Mutex, these reads must be kept extremely brief to avoid blocking network threads.
  • kinetic-network: The underlying P2P peer gossip layer receives raw byte streams over TCP connections, deserializes them into strongly typed SignedGovernanceMessage objects, and pipes them directly into process_governance_message for immediate evaluation.
  • kinetic-core::governance::engine: While logic.rs manages the state structural wrapper and handles basic topological checks (like replay protection and pruning), it heavily delegates to the dynamically active GovernanceEngine trait implementations (such as the FounderEngine or the CouncilEngine) for executing the actual cryptographic math, signature verification, and quorum fraction tallying.

FORWARD DEPENDENCY:

  • Consensus Layer and Block Production: The successful execution of a valid governance action will frequently produce a tangible GovernanceEffect enum variant (such as emitting a command to halt network transactions or minting a governance premium to a developer). The broader Kinetic consensus logic must carefully intercept, interpret, and apply these effects, fundamentally altering block production and transaction validation accordingly.

Deep Dive: Panic-Driven Security

In load_from_disk, panic!() is explicitly invoked when a corrupted file is encountered.

If Kinetic attempted to degrade gracefully by wiping a corrupted governance state and initializing a blank genesis state, the network would reset its rules. An attacker who corrupts the bincode file could force the node into a vulnerable state.

By invoking panic!(), Kinetic explicitly declares that a corrupted disk state is an unrecoverable fault requiring human intervention. The node shuts down, refusing to broadcast invalid blocks or participate in consensus.

Deep Dive: Replay Attacks

The executed_hashes map and the prune function defeat Temporal Replay Attacks.

Without replay protection, an attacker could broadcast an old, previously executed message (e.g., an emergency halt command) and trigger its effects again because the signatures remain valid.

This is why hash_action and executed_hashes are critical. When a message arrives, process_governance_message checks the hash against executed_hashes. If found, it drops the message.

If the original hash was pruned from memory due to MAX_AGE_SECONDS, the message’s internal timestamp will also be older than MAX_AGE_SECONDS. The verify_action function rejects it based on age, preventing replays even after pruning.

Deep Dive: The HashMap::retain Optimization

Rust’s memory model forces us to be extremely deliberate about how we handle collections like HashMaps. When the prune function needs to remove stale entries, a naive approach in other languages might look like this:

  1. Create a new, empty HashMap.
  2. Iterate over the old map.
  3. If an entry is fresh, copy it to the new map.
  4. Replace the old map with the new map.

In Rust, this naive approach would cause a massive heap allocation every time prune runs. As the map grows to thousands of entries, this allocation would cause noticeable latency spikes (stop-the-world garbage collection equivalents) during block processing.

Instead, Kinetic uses HashMap::retain. This function operates entirely “in-place”. It iterates over the underlying memory buffer of the map. When the closure evaluates to false, it marks that specific memory slot as empty, effectively deleting the entry without ever allocating a new heap buffer or copying data. This results in zero-allocation memory management, ensuring that governance processing remains blazingly fast regardless of how long the node has been online.

Deep Dive: WebAssembly (WASM) and Time

You might wonder why process_governance_message imports web_time::SystemTime instead of just using the standard library’s std::time::SystemTime.

Rust’s standard library is deeply tied to the operating system it compiles for. When you call std::time::SystemTime::now() on Linux, the Rust standard library makes a direct clock_gettime syscall to the Linux kernel.

However, Kinetic is designed to be highly portable. In the future, we may want to run “light nodes” directly inside a user’s web browser to allow them to vote on governance proposals without downloading a desktop daemon. Code compiled to run in a browser uses the WebAssembly (wasm32-unknown-unknown) target.

Web browsers are heavily sandboxed environments. They do not have kernels, and they do not allow raw syscalls to read the system clock (to prevent timing attacks and fingerprinting). If you compile std::time::SystemTime to WASM and execute it, the browser will instantly panic and crash the application because the syscall is missing.

The web_time crate acts as a transparent polyfill wrapper. When compiled for a native target (like Linux or macOS), it simply passes the call through to std::time, resulting in zero performance overhead. But when compiled for WASM, it automatically intercepts the call and routes it to the browser’s JavaScript engine (specifically Date.now() or performance.now()). This allows the governance logic to seamlessly execute in both native daemon and browser environments without any code duplication.

Deep Dive: The Anatomy of bincode Serialization

When saving the state to disk, state_io.rs uses the bincode crate rather than standard JSON (like serde_json).

In a decentralized network, the persistence layer needs to prioritize two things: speed and exact structural fidelity. JSON is a self-describing, text-based format. If you save a governance state with JSON, it writes out the names of every field ({"total_paused_kyns": 0}). This means parsing the file requires scanning for string tokens, matching them to field names, and converting ASCII strings back into integers. This is computationally expensive.

bincode, on the other hand, is a purely binary format. It strips away all field names and metadata. It simply writes the raw bytes of the struct directly to disk exactly as they appear in memory. When load_from_disk executes, bincode doesn’t have to parse tokens. It reads a continuous stream of bytes and structurally maps them straight back into the Rust struct.

Furthermore, we specifically use bincode::serialize_into(&mut temp_file, self). If we used bincode::serialize(self), the library would first allocate a massive contiguous Vec<u8> in RAM, write the entire state into that vector, and then write the vector to the file. For a node that has been running for a decade and has a large governance state, this could trigger an Out-Of-Memory error. By using serialize_into, we provide a direct reference to the file handle. bincode acts as a stream processor, taking small chunks of the struct and flushing them directly to the disk buffer. The RAM usage remains perfectly flat, bounded to a few kilobytes, regardless of how massive the overall governance state becomes.

Deep Dive: How lazy_static Creates a Global Safe Singleton

In state_io.rs, the entire governance state is wrapped in a macro called lazy_static!.

In Rust, global variables are generally discouraged, and creating complex global variables (like a struct that requires runtime initialization or reading constants) is natively disallowed by the compiler. This is because Rust cannot guarantee when or how that global memory is initialized before the main function starts.

lazy_static! circumvents this by delaying the initialization until the exact moment the variable is accessed for the very first time. When the node boots, GLOBAL_GOVERNANCE_STATE is conceptually empty. The first time the HTTP API or the P2P network tries to read it, the macro automatically executes GovernanceState::new(crate::constants::KINETIC_GENESIS_TIME), allocates it on the heap, and caches the reference. All subsequent calls use this cached reference.

Why is this necessary? In a highly concurrent daemon, different subsystems (API, Consensus, Network) all need to know if the network is halted. If we didn’t use a global singleton, we would have to use “Dependency Injection” — passing a reference to GovernanceState down through hundreds of nested function calls across multiple crates. This would create a nightmare of lifetime annotations ('a) and reference counting (Arc). The lazy_static pattern, combined with an OS-level Mutex, provides a clean, globally accessible registry.

Deep Dive: Understanding the Result<Option<...>, ...> Return Type

The process_governance_message function has a fascinating return type: Result<Option<GovernanceEffect>, GovernanceError>. For developers coming from languages with exceptions (like Python or Java), this nested type can look confusing. However, it explicitly forces the caller to handle three distinct outcomes:

  1. The Error Case (Err(GovernanceError)): The message was invalid. Maybe the signature failed math checks, maybe it was a temporal replay, or maybe the keys weren’t initialized. The calling function must explicitly pattern-match this Err and likely log it or drop the peer connection.
  2. The “Nothing Happened” Case (Ok(None)): The message was cryptographically valid, but it didn’t trigger a global state change. For example, a council member voted on a proposal, but quorum (the required majority) hasn’t been reached yet. The system state updated internally (recording the vote), but there is no overarching network effect to broadcast yet.
  3. The “Action Triggered” Case (Ok(Some(GovernanceEffect))): The message was valid AND it was the final vote needed to hit quorum. The proposal passed. The function returns a GovernanceEffect (such as HaltNetwork or MintPremium). The calling consensus layer must immediately intercept this effect and mutate the active blockchain rules.

By encoding these three distinct states directly into the type system, the Rust compiler guarantees that the network layer cannot accidentally ignore a successful governance effect, completely eliminating an entire class of consensus bugs.

Deep Dive: The Role of Mutex and System Locks

When you look at GLOBAL_GOVERNANCE_STATE, you will notice it is wrapped in std::sync::Mutex. A Mutex (Mutual Exclusion lock) is an operating system-level construct that guarantees that only one thread can access the underlying data at any given time.

If an API thread is trying to read the state to return a JSON response to a web dashboard, and simultaneously the P2P networking thread receives a new signed governance message that mutates the state, a race condition occurs. Without a Mutex, both threads would try to read/write the exact same memory address at the same time. The resulting data would be a corrupted combination of the old and new states.

In Rust, the Mutex forces the developer to explicitly call .lock().unwrap() before accessing the data. If the network thread currently holds the lock to update the council, the API thread is completely paused (blocked) by the operating system until the network thread finishes and releases the lock.

This design completely eliminates data races at compile time. However, it introduces a performance tradeoff: if one thread holds the lock for too long (for example, by performing a slow disk write while holding the lock), all other threads queue up and wait, causing the node’s API to stall. This is why state_io.rs must serialize to tempfile as quickly as possible.

Quick Reference

  • Where is the memory state initially constructed? via the GovernanceState::new method located inside logic.rs.
  • How exactly are unique proposal hashes derived? By feeding the canonical payload bytes through standard SHA-256 cryptography within the hash_action function using the sha2 crate.
  • How is replay execution technically prevented? The executed_hashes HashMap acts as a boolean filter, checking if a freshly derived action_hash has been witnessed previously.
  • How are obsolete hashes cleared from memory? The prune function proactively drops any hash older than the globally configured MAX_AGE_SECONDS threshold using HashMap::retain.
  • How is state saved safely to disk? It is written fully to a tempfile, which is subsequently atomically renamed over the active file using OS-level file operations (inode swapping).
  • What occurs if a corrupt state load is detected? The damaged file is permanently renamed with a timestamp, and the application instantly panics, effectively mandating manual operator intervention to prevent insecure operation.

Open Questions / Things to Revisit

  1. State File Storage Density and Growth Vectors: While the in-memory execution map is regularly pruned, the underlying bincode serialized state file might grow significantly large if we eventually need to track extensive historical archives of council member rotations, large multi-signature aggregations, or highly complex state variables over multiple years. We must closely monitor disk deserialization latency times on node startup to prevent boot bottlenecks on low-resource hardware.
  2. web_time Chronological Precision Limitations: We are currently utilizing whole seconds (.as_secs()) for all internal timestamping operations. If future Kinetic governance transitions ever require granular sub-second precision for high-frequency state updates, we may be forced to migrate to milliseconds or microseconds. Although whole seconds are generally the standard for most major layer-one blockchains, it’s worth keeping in mind.
  3. Global Mutex Contention Under Heavy RPC Load: GLOBAL_GOVERNANCE_STATE currently relies on an OS-level exclusive Mutex. Under extremely high external RPC load—where potentially thousands of clients or block explorers are concurrently querying the governance status—this single mutual exclusion lock could easily become a massive performance bottleneck. Upgrading this to a robust RwLock (allowing multiple simultaneous readers while restricting writers) represents a highly logical and relatively straightforward future optimization.
  4. Harsh Panic Mechanics on Corrupt Load: While deliberately panicking on a corrupt state file is fundamentally the most secure posture from a theoretical standpoint, it strictly requires manual human operator intervention to recover (for example, by stopping the daemon, SSHing into the server, and restoring the state from a known-good backup snapshot). We must ensure that infrastructure operators have reliable, automated DevOps tooling available to orchestrate and execute these file restorations seamlessly to minimize network downtime during an incident.
  5. Cross-Platform Tempfile Limitations: The tempfile crate relies heavily on POSIX-compliant file semantics (specifically inode renaming). If we ever intend to officially support running full Kinetic validator nodes natively on Windows (which has very different filesystem locking semantics), the atomic write strategy in save_to_disk will need to be thoroughly tested and potentially abstracted behind OS-specific conditional compilation flags (#[cfg(windows)]).
  6. Zeroize for Cryptographic Key Memory Wiping: Currently, get_root_key() parses the hex string into a byte array in memory. For advanced node security against physical memory dumping attacks, we should consider implementing the zeroize crate to guarantee that these temporary key buffers are explicitly overwritten with zeroes the moment they go out of scope.
  7. Pruning Granularity Customization: The MAX_AGE_SECONDS threshold is currently a globally hardcoded network constant. Node operators with extensive storage resources might want the ability to run “Archive Nodes” that never prune the executed_hashes map, keeping a permanent, locally verifiable history of every governance proposal ever processed. Providing a CLI flag to override the pruning behavior would be a worthwhile addition for the block explorer ecosystem.
  8. Asynchronous I/O Upgrades: The save_to_disk function currently relies entirely on blocking, synchronous file system operations via the standard library (std::fs). In a high-throughput, massively parallel node using tokio, blocking the async thread pool with synchronous disk I/O can cause widespread latency. Migrating state_io.rs to use tokio::fs asynchronous operations would significantly improve throughput during heavy governance voting periods.

Crate: kinetic-core

Stage: 13

Reading Time: 35 mins

Depends on: governance/logic.rs, governance/types.rs, governance/engine/sovereign.rs

What Is This?

This file contains the comprehensive test suite for the Kinetic Governance module, specifically covering the invariants and boundary conditions of critical governance actions. It rigorously validates that the deterministic state transitions, the complex cryptographic signatures, and the business logic rules (such as premium name lengths and emergency halting) behave exactly as expected under both normal operation and extreme edge-case conditions. By exercising the core governance engine against artificially constructed and simulated network proposals, these tests guarantee that root keys cannot be bypassed under any circumstances, that unauthorized actors are locked out, and that malformed inputs do not crash the node. It serves as the definitive proof that the theoretical rules defined in the SovereignEngine and GovernanceState hold up in practice when executed by the compiled Rust binary. Ultimately, this test suite ensures that the decentralized network’s ultimate authority mechanisms are robust, secure, and impervious to external manipulation. It provides developers with the absolute confidence required to modify the core consensus engine without fearing invisible regressions.

Why Kinetic Needs This

Kinetic, by its very design as a decentralized network, relies intensely on its governance systems to manage the most critical and economically sensitive parameters of the entire blockchain ecosystem. This encompasses updating the overarching Root Key, issuing premium and infrastructure top-level domains that inherently hold high economic value, and manipulating the emergency network state (such as halting and resuming global consensus). A logic flaw or regression in governance code could be catastrophic for the entire network. A malicious actor exploiting a minor loophole could theoretically hijack the network by bypassing signature validation checks. They could mint unauthorized valuable domain names for themselves, effectively stealing digital assets directly from the community. They could intentionally halt consensus indefinitely without the true founder’s consent, essentially destroying the network’s liveness guarantees. This dedicated test suite acts as the primary, unyielding line of defense against such regressions creeping into the repository during future updates, refactors, or optimizations. It rigorously verifies that signature aggregations are airtight and impossible to cryptographically forge. It ensures that execution order is enforced by the state machine, and that actions always process sequentially without race conditions. This prevents dangerous replay attacks or unauthorized parallel execution of state transitions. Furthermore, it ensures that the serialization of actions into canonical bytes is completely, deterministic across all architectures (x86, ARM, WebAssembly). Because these specific governance actions are executed directly by the Active Engine during the block processing phase by every single node in the p2p network, any non-determinism in serialization would immediately cause the network to splinter into a hard fork. The tests are also uniquely designed to ensure that the system gracefully handles invalid or garbage inputs without triggering a fatal Rust thread panic. This is vital for preventing widespread denial-of-service (DoS) vectors where a malformed proposal broadcasted over gossip could systematically crash target nodes and bring down the network entirely.

Core Testing Structures

Before diving into the logic flow of the individual test cases, it is critical to understand the foundational data structures that the test suite manipulates to achieve its goals. These structs act as the fundamental bedrock for simulating network state and peer-to-peer proposals. -> See: kinetic-core/src/governance/types.rs (Implicitly utilized across all tests)

  • GovernanceState: This struct acts as the localized, in-memory representation of the network’s current governance reality.
    • It precisely tracks exactly who the current root key is via the active_root_key optional field.
    • It tracks whether the network is currently frozen using the is_halted boolean flag.
    • It meticulously maintains the comprehensive historical log of network pauses through total_paused_kyns and the pause_history vector.
    • The test suite constantly initializes and mutates this state object to artificially simulate the temporal progression of the blockchain.
  • GovernanceAction: This is a comprehensive enum containing every single permissible administrative action the root authority can execute.
    • Variants include GrantPremiumName, which requires a string name and a target public key.
    • Variants include RotateRootKey, which exclusively takes the raw bytes of the new authoritative keypair.
    • Variants include EmergencyHalt and EmergencyResume, which govern the liveness of the entire chain.
    • The tests manually construct specific instances of these variants to intentionally probe different logical pathways deeply embedded in the state machine logic.
  • SignedGovernanceMessage: This struct serves as the ultimate wrapper payload that encapsulates the action.
    • It contains the raw GovernanceAction enum variant itself.
    • It holds the exact timestamp_sec when the action was requested (to enforce strict TTL and age limits).
    • It possesses a vector of cryptographic signatures proving authorization.
    • The test suite utilizes this struct to faithfully simulate the exact byte-for-byte payload structure that is transmitted over the peer-to-peer gossip network.
  • GovernanceEffect: When a SignedGovernanceMessage is successfully processed, it emits a specific effect.
    • It doesn’t just quietly mutate local variables; it actively emits an observable state change event.
    • The tests rely on extracting these GovernanceEffect enums to verify correctness.
    • This guarantees that the internal state engine correctly and publicly signals its changes to the rest of the node’s architecture.

Deep Dive: The assert! and matches! Macro Pattern

When auditing this test suite, a recurring Rust macro pattern is utilized to verify state mutations accurately.

  • The assert! macro is the fundamental building block of Rust testing architecture.
  • It takes a boolean expression. If the expression successfully evaluates to true, the test continues seamlessly.
  • If the expression evaluate to false, the macro immediately triggers a thread panic, instantly failing the test run.
  • The matches! macro is frequently and elegantly combined with assert!.
  • It takes two primary arguments: a variable (usually an enum instance) and a specific pattern to match against.
  • It returns true if the variable matches the pattern, ignoring any internal fields that are not bound.
  • This combination (assert!(matches!(...))) is exceptionally powerful for complex governance testing.
  • It allows the test suite to rapidly verify that a function returned a specific GovernanceError variant, like InvalidPremiumNameLength.
  • It sidesteps the tedious need for the error enum to implement the PartialEq trait, which would otherwise be required for a standard assert_eq!.
  • By actively avoiding PartialEq, the codebase remains significantly cleaner and the structs can contain non-equatable types inside them without breaking the tests.
  • This pattern is ubiquitous across the kinetic-core module.

Understanding the unwrap() and unwrap_err() Strategy

Another visible pattern in this test suite is the aggressive use of unwrap() and its counterpart unwrap_err().

  • The process_governance_message function returns a standard Rust Result<Option<GovernanceEffect>, GovernanceError>.
  • In standard production code, developers are strongly encouraged to gracefully handle both the Ok and Err variants using match statements or the ? operator.
  • However, within the controlled environment of a test suite, the paradigm shifts entirely.
  • Tests are inherently designed to expect either a definitive success or a definitive failure.
  • When a test expects a governance action to be processed (the “happy path”), it calls .unwrap() directly on the returned Result.
  • If the engine unexpectedly returns an Err, the unwrap() call will panic, immediately failing the test and printing the unexpected error payload to standard output for debugging.
  • Conversely, when a test is specifically designed to trigger a rejection (the “sad path”), such as submitting a malformed name length, it calls .unwrap_err() on the returned Result.
  • If the engine mistakenly processes the malformed action and returns an Ok, the .unwrap_err() call will immediately panic, catching the severe security vulnerability before it merges.
  • This aggressive unwrapping strategy ensures that tests fail loudly, immediately, and as close to the actual source of the logical bug as humanly possible.
  • It eliminates the need for deeply nested match statements, keeping the test code linear, readable, and focused on the specific state transitions being verified.

How It Works: Test Environment Setup

The tests begin by establishing a controlled, sound cryptographic environment. Because Kinetic’s governance relies on ML-DSA-65 signatures, the tests cannot simply pass around fake string identifiers. They must perform real, heavy cryptographic operations to satisfy the engine’s rigorous verification checks. -> See: kinetic-core/src/governance/tests.rs — Lines 13 to 30

  • The Root Key Generator: The get_root_sk() function is crucial for reliably simulating the genesis state of the network.
    • It takes a hardcoded 32-byte hexadecimal string, which represents a known, deterministic cryptographic seed.
    • It decodes this string utilizing the hex::decode utility, transforming it into a raw byte array.
    • It then directly feeds this decoded byte array into the SigningKey::<MlDsa65>::from_seed method.
    • By doing this, the test suite guarantees that it will always generate the exact same root signing key on every single execution.
    • This ensures hermetic and reproducible test runs regardless of the host operating system or execution environment.
  • The Ephemeral Key Generator: The generate_key(seed: u8) function serves as a rapid mock-generation utility for the tests.
    • It takes a simple 1-byte seed and elegantly expands it into a full 32-byte array by repeating the byte across the array.
    • It derives a brand new SigningKey from this expanded seed array.
    • It simultaneously extracts the corresponding public verifying key by calling verifying_key().to_bytes().
    • This elegant setup allows individual tests to easily spin up mock target users or simulate compromised keys on the fly without heavy setup code.
  • The Signature Bridge: The sign_action(msg, signer) function acts as the critical bridge between the governance structs and the ml_dsa cryptography crate.
    • When a test wants to submit a mock proposal, it constructs a SignedGovernanceMessage with an intentionally empty signatures vector.
    • It passes this raw message to sign_action, which immediately calls the msg.to_canonical_bytes() implementation.
    • This precise step ensures the resulting signature is calculated against the exact byte representation that the node’s consensus engine will ultimately see.
    • The function then calls signer.sign(&serialized) to produce a true, verifiable post-quantum Dilithium signature over those exact bytes.
    • Finally, it encodes the signature utilizing MlDsaSignatureEncoding::to_bytes and returns the vector.
    • The test can then confidently push this valid vector into the message’s internal signatures array, successfully completing the mocking process.

How It Works: Validating Premium Name Invariants

The test_premium_grants function methodically verifies that the engine correctly rejects malformed premium name requests while flawlessly accepting valid ones. -> See: kinetic-core/src/governance/tests.rs — Lines 32 to 86

  • The test intelligently begins by setting up a fresh, clean GovernanceState struct from scratch.
  • It populates this state with the current UNIX timestamp, dynamically fetched via web_time::SystemTime::now() to ensure temporal relevance.
  • It dynamically generates a mock target_pubkey utilizing the generate_key(99) utility to act as the ultimate recipient of the granted premium names.
  • Testing the Negative Path: The primary phase of the test expertly constructs an intentionally malformed SignedGovernanceMessage.
    • The action is specifically set to the GovernanceAction::GrantPremiumName enum variant.
    • The requested name is set to the string "ab", which actively violates the length parameter for premium assets.
    • The test deliberately cryptographically signs this invalid message using the true, authoritative root key.
    • This is done to conclusively prove that even if an action is cryptographically sound and signed by the absolute authority of the network, the underlying business logic will still intercept and outright reject it.
    • It confidently calls process_governance_message(&mut state, &msg_invalid_len).
    • The test utilizes the unwrap_err() method to safely extract the resulting error, intentionally panicking immediately if the engine mistakenly attempts to accept the proposal.
    • It then leverages the assert! macro combined with matches! to verify the exact error type.
    • It confirms that the extracted error is precisely the crate::error::GovernanceError::InvalidPremiumNameLength enum variant.
  • Testing the Positive Path: In the secondary phase, the test successfully enters a localized, controlled for i in 0..5 loop.
    • Inside this loop, it dynamically generates valid 1-character strings ("a", "b", "c", "d", "e").
    • It elegantly achieves this by performing simple byte arithmetic on characters: (b'a' + i) as char.
    • For every single generated character, it builds a brand new SignedGovernanceMessage from scratch.
    • It cryptographically signs the message utilizing the helper utility and executes it deeply through the engine.
    • It utilizes unwrap() to ensure execution is successful, extracting the resulting GovernanceEffect.
    • Finally, it uniquely utilizes an if let binding to safely unpack the GovernanceEffect::PremiumNameGranted enum variant.
    • It asserts that the granted_name inside the effect matches the character it just requested, preventing silent failures.

How It Works: The Key Rotation Lifecycle

The test_rotate_root_key function rigorously tests the single most dangerous action in the protocol: replacing the ultimate authority of the network. It verifies that rotation is both complete and immediate, leaving no lingering security vulnerabilities whatsoever. -> See: kinetic-core/src/governance/tests.rs — Lines 88 to 149

  • The test initializes the base state and fetches the original genesis root key via get_root_sk().
  • It simultaneously generates an new root keypair utilizing generate_key(123) to safely serve as the successor.
  • Executing the Rotation: The very first step involves creating a GovernanceAction::RotateRootKey proposal.
    • It specifies the exact new public key bytes as the internal payload for the state mutation.
    • Crucially, this specific proposal is forcefully cryptographically signed by the old genesis root key, proving initial authorization to rotate.
    • The test processes the message and strongly verifies that it correctly returns a GovernanceEffect::RootKeyRotated effect to publicly signal the change.
    • It then directly inspects the underlying GovernanceState struct by actively calling state.get_root_key().unwrap().
    • It asserts that the returned bytes match the new root public key, proving the internal state was actually mutated and the key was successfully written to memory.
  • Validating Immediate Invalidation: The subsequent step is a critical negative test designed to verify immediate invalidation of the predecessor key.
    • The test intentionally attempts to grant a premium name ("b") to an arbitrary user.
    • However, it maliciously signs the new request utilizing the old, now-deposed genesis root key.
    • Because the simulated node time has actively advanced (current_time + 1), the action hash is different, ensuring it’s not rejected merely as a stale or duplicate proposal in the cache.
    • When process_governance_message is finally called, it correctly and predictably fails.
    • The test asserts that the resulting error is InsufficientSignatures.
    • This is the exact necessary behavior: because the old key is definitively no longer in the active state memory, the engine treats its signature as irrelevant garbage, rejecting the proposal.
  • Proving Successor Control: In the deeply concluding step, the test clears the invalid signatures vector from the struct memory.
    • It meticulously re-signs the exact same proposal using the new authoritative root key, and processes it again.
    • This time, process_governance_message flawlessly and elegantly succeeds.
    • The test safely and assertively checks that a PremiumNameGranted effect is successfully emitted.
    • This comprehensive testing sequence and undeniably validates the forward security of the rotation mechanism, firmly proving ownership has transferred permanently.

How It Works: Canonical Serialization Fuzzing via Proptest

Because blockchain consensus relies on identical byte representations of actions across distributed nodes, this test utilizes property-based fuzzing. It ensures the complex serialization logic is deterministic and panic-free regardless of malicious input. -> See: kinetic-core/src/governance/tests.rs — Lines 151 to 184

  • This specific test intelligently utilizes the proptest! macro, and fundamentally bypassing standard static unit testing constraints.
    • This macro actively instructs the core testing framework to dynamically run this isolated function hundreds or thousands of times consecutively.
    • It relentlessly feeds the testing function randomly generated inputs on every single iteration to rapidly hunt for obscure edge cases.
  • It safely utilizes proptest::string::string_regex to constantly generate random strings.
    • These strings and rigidly conform to the precise regex "[a-z0-9_-]{1,63}", accurately mimicking real-world domain name constraints.
    • It simultaneously generates random u64 integers to act as the unpredictable timestamp.
  • For every single randomly generated combination of name and timestamp, the test rapidly constructs a GovernanceAction::GrantPremiumName.
    • It wraps this specific action inside a brand new SignedGovernanceMessage struct.
    • The signatures vector is intentionally left empty because signatures are excluded from the canonical hash computation by strict protocol rules.
  • Enforcing the No-Panic Invariant: The primary assertion, prop_assert!(!bytes.is_empty()), is critical for long-term network stability.
    • It guarantees that the complex serialization process (msg.to_canonical_bytes()) successfully executed.
    • It definitively proves that the core function did not panic or return an empty vector, regardless of how weird or extreme the input string was.
  • Enforcing the Determinism Invariant: The secondary assertion is by far the most critical for guaranteeing ongoing network consensus.
    • The test deeply and safely clones the entire original message struct to create a perfect memory replica.
    • It deeply serializes the newly cloned struct independently from the original.
    • It uses prop_assert_eq! to ensure that both resulting byte vectors are exactly 100% identical.
    • This thoroughly proves absolute determinism—given the exact same internal Rust struct, the output bytes will always, without fail, be identical everywhere.
  • Enforcing Hashing Integrity: Finally, it and directly calls GovernanceState::hash_action(&msg).
    • It strongly asserts that the resulting hash vector has a strict length of exactly 32 bytes (a standard SHA-256 output).
    • This actively proves the hashing layer doesn’t encounter a fatal panic on extreme fuzz data, locking down the pipeline.

How It Works: Emergency State Transitions

This function deeply validates the state machine logic that allows the root authority to freeze and unfreeze the network in the event of an existential protocol crisis. -> See: kinetic-core/src/governance/tests.rs — Lines 186 to 226

  • The test constructs a brand new GovernanceState from the ground up.
  • It forcefully overrides the active_root_key memory field with a custom generated key, rather than relying on the genesis defaults, successfully proving architectural flexibility.
  • Verifying Initial State: It properly begins by and successfully asserting the default state expectations.
    • It checks !state.is_halted, meaning the network is actively running normally and safely accepting transactions.
    • It checks state.total_paused_kyns == 0, meaning no network time has ever been administratively paused in history.
  • Executing the Halt: It crafts a critical GovernanceAction::EmergencyHalt message.
    • It cryptographically signs this critical message with the authoritative root key.
    • It processes it directly through the core state machine architecture.
    • After processing conclusively ends, it thoroughly verifies that the engine successfully returned a NetworkHalted effect.
    • Crucially, it then manually and deeply inspects the GovernanceState struct fields directly in memory.
    • It verifies that the state.is_halted boolean has correctly flipped to true.
    • This boolean forcefully acts as the primary, unyielding gatekeeper for the rest of the node’s block processing pipeline.
  • Executing the Resume: Next, it precisely constructs a GovernanceAction::EmergencyResume message to safely unfreeze the chain.
    • This specific administrative action requires a defined payload parameter: paused_kyns: 1000.
    • This integer represents the total exact number of consensus cycles (kyns) that supposedly occurred while the network was frozen.
    • It reliably signs and processes the resume action, forcefully verifying that a NetworkResumed effect is successfully emitted.
  • Verifying the Aftermath: Finally, the test meticulously inspects the state variables one last time.
    • It actively and assertively checks that !state.is_halted (meaning the network is successfully un-frozen and accepting blocks again).
    • It and asserts that state.total_paused_kyns == 1000.
    • This critical state tracker is imperative because the network utilizes it to precisely offset internal time calculations.
    • It safely ensures that staking rewards, unlocking periods, and punitive slashing windows are not unfairly triggered by the administrative downtime.

How It Works: Invalid Revocation Testing

This function creatively acts as a critical mirror to the grant test, ensuring that name revocations undergo the exact same rigorous validation checks as creation events. -> See: kinetic-core/src/governance/tests.rs — Lines 228 to 272

  • The test initializes a fresh state and safely designates a fully active root key.
  • Testing Malicious Revocation: It first actively constructs a specific GovernanceAction::RevokePremiumName action.
    • It maliciously targets the 2-character string "ab", which is structurally and invalid for premium names.
    • It signs the specific action and executes it, actively expecting a total and predictable failure in the pipeline.
    • Utilizing unwrap_err(), it strongly asserts that the engine correctly returns the specific InvalidPremiumNameLength error type.
    • This extensively and successfully proves that an attacker simply cannot trick the system into accidentally revoking a standard, valuable domain name by illicitly packaging it inside a premium name revocation proposal.
  • Testing Valid Revocation: It then creates a and valid revocation targeting the exact 1-character string "a".
    • It cryptographically signs this legitimate request and seamlessly executes it normally.
    • Utilizing unwrap(), it confidently and happily asserts absolute execution success.
    • It directly verifies that the required GovernanceEffect::PremiumNameRevoked effect is emitted safely back to the node, successfully completing the lifecycle test.

Key Pieces

  • get_root_sk()
    • What it does: Generates a deterministic ML-DSA-65 signing key utilizing a hardcoded 32-byte hexadecimal seed array.
    • File:Line: kinetic-core/src/governance/tests.rs:13
    • Why it matters: Testing complex, multi-layered governance mechanics requires a reliable, known root key to continuously sign authoritative proposals. This function strongly guarantees consistent test runs.
  • generate_key(seed: u8)
    • What it does: Instantly and mints an arbitrary ML-DSA-65 cryptographic keypair based on a simple 1-byte seed, returning both the sensitive signing key and safe verifying bytes.
    • File:Line: kinetic-core/src/governance/tests.rs:19
    • Why it matters: It continuously provides a lightning-fast, reproducible way to rapidly spin up ephemeral testing keys for thoroughly mocking users or structural replacements.
  • sign_action(msg: &SignedGovernanceMessage, signer: &SigningKey)
    • What it does: efficiently extracts the exact canonical bytes of a message, cryptographically signs them using robust Dilithium mathematics, and immediately returns the raw signature byte vector ready for structural insertion.
    • File:Line: kinetic-core/src/governance/tests.rs:26
    • Why it matters: It elegantly centralizes the complex signature logic, preventing verbose and unmaintainable test code clutter across the utilized suite.
  • test_premium_grants
    • What it does: methodically asserts that valuable premium names must adhere to a restrictive length of exactly 1 character.
    • File:Line: kinetic-core/src/governance/tests.rs:32
    • Why it matters: protects the network against severe logical bugs where standard infrastructure names might be mistakenly granted, fundamentally breaking network registration economics.
  • test_rotate_root_key
    • What it does: Extensively and thoroughly verifies that a sensitive root key rotation seamlessly updates the active state variables and invalidates any future signatures originating from the dangerous old root key.
    • File:Line: kinetic-core/src/governance/tests.rs:88
    • Why it matters: This is undeniably the single most critical test for long-term system security. It prevents deposed founder keys from ever regaining illegal control of the secure blockchain.
  • test_fuzz_to_canonical_bytes
    • What it does: Intelligently leverages the robust proptest macro to relentlessly and randomly throw thousands of randomized strings and varied integers at the sensitive serialization pipeline.
    • File:Line: kinetic-core/src/governance/tests.rs:154
    • Why it matters: Fuzzing is an strict critical requirement for robust network consensus. It prevents malicious payloads from accidentally or intentionally inducing severe node panics or hard forks.
  • test_emergency_halt_resume
    • What it does: Methodically and logically validates the internal state machine transitions bouncing between the active and halted network states.
    • File:Line: kinetic-core/src/governance/tests.rs:186
    • Why it matters: The internal pause history vector is crucial and indispensable for accurately adjusting time-based network economics long after a severe network freeze naturally resolves.

How This Connects to the Rest of Kinetic

  • CROSS-CRATE (ml-dsa): The core testing framework utilizes the secure external ml_dsa post-quantum cryptography library. It actively ensures that the compiled node is actively utilizing actual, sound Dilithium signatures rather than insecure mock validation loops during its testing procedures.
  • CROSS-CRATE (proptest): Integrates tightly and natively with the proptest property-based testing crate to virtually guarantee complete serialization correctness under a vast, theoretically unlimited mathematical domain of randomized string inputs.
  • FORWARD DEPENDENCY: The rigid state invariants and uncompromising validation rules tested within this file (like exact name lengths and totally flawless key transitions) are the exact structural rules and continuously relied upon by the GovernanceEngine trait implementations deeply throughout the active consensus layer.

Quick Reference

  • Testing Setup: Deterministic testing keys are rapidly and minted from simple, short byte seeds (get_root_sk, generate_key) to ensure stable and robust test runs.
  • Signature Helper: The utilized sign_action function seamlessly abstracts away complex canonicalization and heavy ML-DSA signing into a single, clean, easily digestible function call.
  • Premium Names: uncompromisingly enforced to be exactly mathematical length 1. Any deviation of any kind instantly results in a severe, immediate InvalidPremiumNameLength error being forcefully thrown by the rigid engine checks.
  • Key Rotation Security: Old cryptographic keys instantly and permanently, lose their coveted signing authority the exact mathematical moment a valid rotation payload is actively processed and firmly accepted into state.
  • Fuzzing Determinism: proptest rigorously ensures that the sensitive, critical to_canonical_bytes function is fully deterministic and panic-free regardless of the weird string input it actively receives.
  • Emergency States: The critical total_paused_kyns metric permanently and increments when successfully processing an EmergencyResume action to account for missed execution time across nodes.

Open Questions / Things to Revisit

  • Missing Infrastructure Tests: Are there eventually going to be dedicated tests for the GovernanceAction::GrantInfrastructureName variant? It currently logically seems premium names are tested and intensely fuzz-tested, but critical infrastructure names are currently unverified and ignored within this specific test suite.
  • Unexpected Resume Behavior: How exactly does the system logically handle an EmergencyResume action if the network was technically never actually halted in the very first place? The suite doesn’t check if an unexpected resume dynamically throws an error, dangerously panics, or is simply and quietly ignored by the state machine logic.
  • Limited Fuzzing Scope: The effective, randomized proptest coverage currently only actively targets the specific GrantPremiumName action. Actively fuzzing all possible GovernanceAction variants would ensure holistic serialization safety across the entire API surface.
  • Clock Dependency: The automated tests currently use the platform-dependent web_time::SystemTime::now() to dynamically generate test timestamps. This could theoretically and problematically lead to flaky tests if execution happens precisely across a second boundary. Should these tests intentionally migrate to use a purely deterministic mock clock?

Crate: kinetic-core

Stage: 2

Reading Time: 45 mins

Depends On: 13_constants.md

What Is This?

This file (kinetic-core/src/config.rs) is the absolute heart of the Kinetic network’s configuration system.

It defines the comprehensive global configuration models, default values, and port definitions for the entire Kinetic network stack.

It provides the exact data structures that are serialized and deserialized into the config.toml file.

This is the exact file that all Kinetic node operators interact with on a daily basis when tuning their machines.

Beyond merely parsing a static file, this module is the authoritative, deterministic source for how a Kinetic node behaves when it first boots up from a cold start.

It dictates where the node looks for its initial peers.

It dictates what local network ports it binds to for peer-to-peer communication.

It dictates how it connects to the Drand randomness beacon for consensus operations.

It dictates where it physically stores its local database files on the host operating system.

Furthermore, it implements a critical “fail-closed” security posture across the entire application.

This ensures that misconfigured nodes refuse to start, rather than silently falling back to potentially unsafe default settings.

The code relies on Rust’s serde framework to seamlessly translate between typed Rust structs in memory and human-readable, easily editable TOML files on disk.

By encapsulating all default states within this single, cohesive file, Kinetic ensures that changes to network topology, port mappings, or core parameters are traceable.

This makes debugging node startup issues drastically easier for the core development team.

Why Kinetic Needs This

In a decentralized network environment like Kinetic, the underlying node software must be resilient.

It must be user-friendly for non-technical node operators.

And it must be deterministic in how it initializes its state.

Kinetic fundamentally needs this file for several critical architectural reasons:

1. Avoiding Port Collisions on Shared Hardware

The Kinetic ecosystem consists of multiple distinct binaries that often run concurrently.

These binaries are kinetic-daemon, kinetic-node, and kinetic-host.

Node operators, especially those running on cost-effective VPS environments (like DigitalOcean, Linode, or AWS EC2), frequently run all three of these background processes on the exact same server instance.

Without a coordinated, centralized registry of default ports, these discrete binaries would constantly fight over TCP and UDP socket bindings.

This would lead to unpredictable, frustrating crashes on startup.

This file physically separates the port ranges for each daemon type to guarantee smooth coexistence without manual operator intervention.

2. Bootstrapping the Decentralized Network from Zero

When a brand new node turns on for the very first time, it knows nothing about the outside world.

It has no peer connections.

It has no routing table.

It needs a predefined list of hardcoded, trusted entry points to successfully join the DHT (Distributed Hash Table).

Once connected to these entry points, it can start finding other peers dynamically.

This configuration file manages those critical bootstrap nodes and seed domains.

Without this hardcoded starting point, a node would be isolated forever on startup, unable to synchronize blocks or participate in consensus.

3. Cross-Platform Consistency and Path Resolution

Kinetic is uniquely designed to run literally anywhere.

It runs on headless Linux servers.

It runs on macOS desktop machines.

It runs on Windows workstations.

It even runs sandboxed inside the web browser via WebAssembly (Wasm).

The node software requires a unified, abstraction-layered way to figure out where its logical “home” directory is across all these drastically different operating systems.

This file abstracts away the OS-level file system differences.

It deterministically maps configuration paths to ~/.local/share on Linux.

It maps to AppData on Windows.

And it mocks virtual directories in the WebAssembly context to prevent immediate crashes.

4. Ironclad Security Against Fail-Open Scenarios

If a node operator accidentally mangles their config.toml file, bad things can happen.

For example, by adding a typo to a critical IP address field or forgetting a quotation mark.

A naive parser might fail to read the file and silently fall back to default, factory settings.

Falling back to a default 0.0.0.0 IP binding could accidentally expose a private, authenticated API port directly to the public internet.

This opens the node to immediate remote attacks.

Kinetic requires a strict, unforgiving parser that immediately crashes the application if the configuration is even slightly invalid.

5. Strict Separation of Global State vs Local State

Kinetic and deliberately distinguishes between two concepts: network.json and config.toml.

The network.json represents the global, immutable definition of the network.

This includes things like the genesis block hash and the canonical bootstrap nodes.

The config.toml represents the local, mutable configuration for a specific physical machine.

This includes things like which local ports to use for API traffic.

This file handles the local config.toml logic while simultaneously running compile-time tests to ensure the local code stays in perfect sync with the global network defaults.

6. Environment Variable Overrides for Modern Containerization

Modern backend infrastructure relies on containerization technologies.

Tools like Docker, Kubernetes, and HashiCorp Nomad are common for node deployment.

This configuration file provides crucial environmental hooks, specifically KINETIC_CONFIG_PATH and KINETIC_DATA_DIR.

These hooks allow orchestration tools to inject configuration variables dynamically at runtime.

This avoids the need to write physical configuration files to the container’s ephemeral disk beforehand.

7. Zero-Friction Extensibility for Future Upgrades

By using the serde serialization library, adding a brand new feature to the network is trivial.

Whether it is a new proxy protocol, a new consensus parameter, or a new background sub-service.

It only requires adding a single field to the Rust structs in this file.

The serialization logic automatically handles reading, writing, and parsing the new field.

No complex, custom, hand-written parser modifications are ever required.

How It Works

The configuration system in Kinetic relies on the serde crate (Serializer/Deserializer) to map deeply nested Rust structs directly to TOML text files on disk.

When a Kinetic binary starts up, the execution flow proceeds through several deterministic phases.

1. Configuration Resolution Order and Path Discovery

-> See: kinetic-core/src/config.rs — Lines 377 to 405

When the static method KineticConfig::load() is invoked, the system attempts to pinpoint the exact configuration file by traversing a very specific sequence of checks.

First, the code inspects the host operating system for the KINETIC_CONFIG_PATH environment variable.

If the node operator has defined this variable, the node will stubbornly use that exact file path, overriding all other logic.

Second, if the environment variable is missing, the code falls back to a standard, platform-specific default directory.

It does this by invoking the get_base_dir() helper function.

For a standard Linux deployment, this resolves to ~/.local/share/kinetic/config.toml.

Third, the code attempts to open the file at the resolved path.

If it discovers that the file does not exist at all, the software pivots into initialization mode.

This commonly occurs when a node is booting up for the very first time.

It will automatically generate a pristine, default configuration object in memory.

It achieves this by instantiating KineticConfig::default().

It then passes the struct through the TOML serializer to generate a formatted string.

It recursively creates any deeply nested parent directories on the physical disk using fs::create_dir_all.

Finally, it writes the raw string to the file using fs::write.

2. The “Fail-Closed” Security Posture and Panic Logic

-> See: kinetic-core/src/config.rs — Lines 382 to 389

This is one of the most important architectural decisions contained within the entire file.

You will notice that if toml::from_str(&config_str) returns a standard Rust Err, the node and abruptly aborts execution.

It does this via std::process::exit(1).

In many older, legacy software systems, if a configuration file is corrupt or unreadable, the system simply logs a polite warning to the console.

It then continues to boot up using factory default values.

In cybersecurity, this anti-pattern is known as “failing open”.

Failing open is a massive, systemic security risk for decentralized nodes.

Consider a realistic scenario where a cautious Kinetic operator configures their privileged internal API port to bind to 127.0.0.1 (localhost).

This keeps the API private and inaccessible from the outside world.

If the operator makes a simple typographical error in the configuration file, and the node’s software chooses to “fail open”, disaster strikes.

The node might automatically revert to a default behavior that accidentally binds the API to 0.0.0.0 (all available network interfaces).

This tiny, silent failure would inadvertently expose the privileged, private API to the hostile public internet.

This could potentially allow remote attackers to hijack the node or steal funds.

Kinetic’s strict usage of std::process::exit(1) guarantees that a broken configuration file results in a broken node that refuses to run.

It eliminates the risk of a node running insecurely in the background due to a typo.

3. Strict Port Allocation and Logical Namespacing Strategy

-> See: kinetic-core/src/config.rs — Lines 26 to 49

The ports submodule acts as the global, indisputable registry for all network socket bindings within the Kinetic framework.

By manually grouping these hardcoded constants together in a single module, the core developers ensure system stability.

They ensure that adding a new HTTP service to the kinetic-daemon process doesn’t accidentally trample on a port already in use by the kinetic-host process.

Notice specifically how the P2P network ports are tiered in a logical, sequential manner.

The Daemon’s P2P swarm is allocated port 6070.

The Node’s P2P swarm is allocated port 6071.

The Host’s P2P swarm is allocated port 6072.

This dead-simple, sequential tiering design means that system administrators and node operators can easily efficiently firewall a small block of ports.

They can open ports 6070 through 6072 and rest assured they have fully covered all P2P traffic for the entire ecosystem.

The exact same sequential pattern applies directly to the internal API ports.

The Daemon’s internal API is allocated port 16002.

The Node’s internal API is allocated port 16003.

The Host’s internal API is allocated port 16004.

By keeping these numbers centralized, Kinetic avoids spaghetti code where magic port numbers are scattered randomly across dozens of different files.

4. Custom Serialization Defaults to Maintain Clean Files

-> See: kinetic-core/src/config.rs — Lines 112 to 160

Rust’s macro-based serde crate is powerful.

Kinetic leverages its advanced features to drastically improve the operator experience.

If you closely examine the DaemonConfig struct, you will notice custom procedural macro attributes attached to fields.

For example: #[serde(default = "local_bind_ip", skip_serializing_if = "is_default_bind_ip")].

This dual-attribute optimization approach has two major, visible effects on the resulting software.

First, when reading the configuration from the physical disk, the field might be missing from the operator’s TOML file.

If it is missing, the Rust deserializer will automatically invoke the local_bind_ip helper function.

This function dynamically fills in the missing default value (127.0.0.1) in memory.

Second, and more importantly, this affects when writing the configuration back to disk.

Perhaps an operator dynamically updates an API via a REST call, triggering a save().

If the node’s current bind_ip is equal to the default value, the serde serializer will omit that specific field from the generated TOML file.

This specific optimization keeps the resulting config.toml file small.

It keeps it clean.

And it keeps it readable for human eyes.

Node operators will only ever see the specific fields they have actively chosen to override or change.

If an operator hasn’t touched the default API port, that port will not clutter up their configuration file with unnecessary, redundant lines of text.

5. Handling WebAssembly (Wasm) Compilation Constraints

-> See: kinetic-core/src/config.rs — Lines 410 to 414

The Kinetic network stack is ambitiously designed to run literally anywhere.

This includes running inside secure web browsers via WebAssembly (Wasm) compilation.

However, web browsers operate inside a strict security sandbox.

They do not have raw access to a user’s local, physical filesystem.

Executing standard std::fs calls to read files will fatally panic the Wasm module instantly.

Because reading and writing TOML files from disk is physically impossible in that sandboxed environment, the file loading architecture must adapt.

To solve this, the load and save methods within KineticConfig are guarded by Rust’s conditional compilation flags.

Specifically, the #[cfg(not(target_arch = "wasm32"))] attribute is used.

When the source code is compiled down for a WebAssembly target, the Rust compiler physically strips out the file-system logic entirely.

The load() function is replaced with a simple stub that returns a fresh, in-memory KineticConfig::default() object.

Similarly, the save() function is reduced to an empty, zero-cost stub.

This stub does nothing, modifies no files, and immediately returns a successful Ok(()).

This elegant, macro-driven workaround allows the core configuration logic to remain unified across all supported platforms.

It prevents fatal compilation errors when targeting the web browser environment.

6. The network.json Compile-Time Synchronization Guarantee

-> See: kinetic-core/src/config.rs — Lines 485 to 514

Right at the bottom of the source file, there is a seemingly simple but vital unit test.

This test directly compares the contents of ../network.json with a locally bundled default_network.json file.

In the Kinetic paradigm, the network.json file represents the global constitution of the entire network.

It rigidly defines the fundamental parameters that cannot be changed by individual, rogue operators.

These parameters include the canonical bootstrap nodes, the mathematical genesis block hash, and the core protocol version.

However, because Rust binaries are designed to compile down to a single, monolithic, self-contained executable file, they cannot rely on reading external JSON files at runtime.

They desperately need a hardcoded, baked-in version of this network data embedded directly into the binary file itself.

This specific unit test asserts that the statically hardcoded default_network.json file matches the global network.json file sitting at the root of the GitHub repository.

If a core developer pushes an update to the global network definition (perhaps adding a new bootstrap peer) but forgets to update the embedded copy inside kinetic-core, the build fails.

This unit test will fail the CI/CD pipeline immediately.

This prevents catastrophic split-brain network scenarios from ever reaching production environments.

Key Pieces

The ports Module

  • What it does: This is the centralized, definitive registry for all default TCP/UDP network ports used across the entire Kinetic software stack.
  • Location: kinetic-core/src/config.rs — Lines 26 to 49.
  • Why it matters: It serves as the absolute single source of truth for port allocation across all binaries.
  • Specific Allocations: It defines P2P_DAEMON (6070), P2P_NODE (6071), P2P_HOST (6072).
  • Additional Allocations: It also defines API ports, Proxy routing ports, internal DNS ports, and the PAC server port.
  • Operator Impact: By physically centralizing this data in one module, it actively prevents copy-paste developer errors and process collisions during local testing.

KineticConfig (Top-Level Configuration Struct)

  • What it does: This is the primary, overarching container structure that holds the entire configuration state of the application.
  • Location: kinetic-core/src/config.rs — Lines 54 to 63.
  • Why it matters: This struct is the ultimate, undisputed source of truth for a running Kinetic node.
  • Structure: It acts as the root object that gets dynamically serialized to and from the config.toml file.
  • Sub-components: It is divided logically into three distinct, modular sub-structs: daemon settings for the local binary, network settings for P2P routing, and drand settings for cryptography.
  • Operator Impact: It encapsulates the complete totality of a node’s configurable footprint in a single memory address.

DaemonConfig (Struct Fields and Operations Breakdown)

  • What it does: This deeply nested struct specifically configures the operational behavior of the kinetic-daemon background process itself.

  • Location: kinetic-core/src/config.rs — Lines 108 to 160.

  • Why it matters: It controls critical operational parameters that dictate how the node functions on the host machine. Let’s look at the specific, actionable fields in incredible detail:

    • bind_ip:

      • Type: String
      • Purpose: The specific local IP address to bind internal services to.
      • Operator Impact: This is usually set to 127.0.0.1 for maximum security, ensuring privileged APIs are not exposed publicly to the internet.
    • pac_bind_ip:

      • Type: String
      • Purpose: The designated IP address used specifically and exclusively by the Proxy Auto-Config script server.
    • api_port:

      • Type: u16
      • Purpose: The precise port number for the authenticated, privileged daemon REST API.
      • Operator Impact: Operators use this port to monitor node health and execute administrative commands via the CLI.
    • dns_port:

      • Type: u16
      • Purpose: The port for the daemon’s built-in, local UDP DNS resolver.
      • Operator Impact: This almost always defaults to port 53. If the operator lacks root privileges on Linux, binding to port 53 will fail, requiring them to change this port or use capabilities mapping.
    • proxy_port:

      • Type: u16
      • Purpose: The network port for the built-in HTTP reverse proxy.
      • Operator Impact: This actively intercepts .kin domain traffic on the local machine and routes it into the decentralized network.
    • backend_port:

      • Type: u16
      • Purpose: The default local backend web server port mapping for routing internal requests.
    • enable_dns:

      • Type: bool
      • Purpose: A simple boolean flag that toggles the internal DNS resolver on or off.
    • storage_dir:

      • Type: PathBuf
      • Purpose: The exact, absolute filesystem path where the node’s local database engine (usually RocksDB or Sled) will physically write its state files.
      • Operator Impact: critical for operators who want to mount a secondary, high-speed NVMe drive specifically for blockchain state.
    • network_mode:

      • Type: String
      • Purpose: A string that defines if the node operates as a heavy “FullNode” or a lightweight “LightNode”.
      • Operator Impact: FullNodes actively participate in DHT storage and routing. LightNodes only query the network, refusing to store records for others, saving massive amounts of disk space.
    • auto_update:

      • Type: bool
      • Purpose: A vital boolean flag that controls OTA (Over-The-Air) binary updates.
      • Operator Impact: If true, the background daemon will actively poll external servers for binary updates and automatically restart itself when a new version is detected.
    • ipfs_gateway:

      • Type: String
      • Purpose: The complete HTTP URL of the public IPFS gateway used to resolve external content links.
      • Operator Impact: Used extensively when users navigate decentralized applications via the Kinetic proxy.
    • atlas_port:

      • Type: u16
      • Purpose: The specific UDP port used to communicate locally with the Kinetic Atlas Bridge daemon.
      • Operator Impact: Necessary for advanced blockchain state migrations and synchronization.

P2pConfig (Struct Fields and Operations Breakdown)

  • What it does: This struct specifically configures the low-level libp2p networking stack and the various peer discovery subsystems.

  • Location: kinetic-core/src/config.rs — Lines 224 to 274.

  • Why it matters: It controls how the isolated node attempts to talk to the hostile outside world. Specific configuration fields include:

    • daemon_port & daemon_quic_port:

      • Type: u16
      • Purpose: The primary TCP and experimental QUIC listen ports for the daemon’s libp2p network swarm.
    • node_port & node_quic_port:

      • Type: u16
      • Purpose: The designated routing ports for the lightweight node’s network swarm.
    • host_port & host_quic_port:

      • Type: u16
      • Purpose: The designated routing ports for the host’s isolated network swarm.
    • bootstrap_nodes:

      • Type: Vec<String>
      • Purpose: An array of formatted libp2p multiaddrs.
      • Operator Impact: These represent the initial, trusted peers the node contacts to break its isolation and join the global DHT routing network.
    • seed_domain:

      • Type: Vec<String>
      • Purpose: An array of DNS domains that the node queries for specialized TXT records.
      • Operator Impact: Used to discover dynamic bootstrap peers when hardcoded peers are offline.
    • enable_mdns:

      • Type: bool
      • Purpose: A boolean flag that toggles local area network peer discovery.
      • Operator Impact: When set to true, multiple Kinetic nodes on the exact same Wi-Fi network will automatically find and connect to each other without ever needing internet access.
    • external_address:

      • Type: Option<String>
      • Purpose: An optional field specifically built for nodes operating behind complex NATs, firewalls, or Docker containers.
      • Operator Impact: It allows a frustrated operator to broadcast a public IP and port to the network to bypass automatic discovery failures.

DrandConfig (Struct Fields and Operations Breakdown)

  • What it does: This struct configures exactly how the node fetches verifiable cryptographic randomness from the Drand Quicknet beacon network.

  • Location: kinetic-core/src/config.rs — Lines 65 to 81.

  • Why it matters: The Kinetic consensus algorithm requires unbiased, cryptographically verifiable randomness to function correctly. Specific fields include:

    • endpoints:

      • Type: Vec<String>
      • Purpose: A ordered list of HTTP API endpoints (run by the League of Entropy) to query for randomness payloads.
    • drand_domain:

      • Type: Vec<String>
      • Purpose: A specific domain name used to query DNS TXT records to dynamically resolve and update Drand endpoints.
      • Operator Impact: Ensures the node can find randomness even if the hardcoded endpoints go offline.
    • p2p_only:

      • Type: bool
      • Purpose: A critical boolean security flag.
      • Operator Impact: If set to true, the node disables its HTTP clients entirely. It will only accept Drand randomness packets that arrive via the internal P2P Gossipsub network. This flag is designed for high-security, air-gapped node environments that lack outbound HTTP access.

KineticConfig::load() (Core Initialization Function)

  • What it does: This robust function attempts to read the TOML file from the physical disk, parse it, validate it, and instantiate the Rust structs in memory.
  • Location: kinetic-core/src/config.rs — Lines 377 to 405.
  • Why it matters: This single function physically implements the core file-system fallback logic and the crucial fail-closed security posture.
  • Operator Impact: It rigorously manages environment variable overrides, automatically generates missing configs for new users, and intentionally panics the entire application on corrupted files to prevent hidden exploitation.

KineticConfig::save() (Core Serialization Function)

  • What it does: This function executes the exact inverse of load().
  • Location: kinetic-core/src/config.rs — Lines 416 to 441.
  • Why it matters: It serializes the current in-memory configuration state back into a nicely formatted TOML string and safely writes it to disk.
  • Operator Impact: This function is utilized by the Kinetic CLI or the REST API whenever an operator attempts to change a setting dynamically while the node is currently running. It includes robust safeguards to ensure the parent directory physically exists on the disk before making any attempt to write the file.

get_base_dir() (Path Resolution Function)

  • What it does: This useful helper function calculates the correct, platform-specific directory where the Kinetic application should store all of its sensitive data and configuration files.
  • Location: kinetic-core/src/config.rs — Lines 455 to 478.
  • Why it matters: This function ensures that Kinetic always behaves exactly like a properly written, native application on every single operating system it targets.
  • Linux Behavior: On Linux, it correctly maps paths to ~/.local/share/{NETWORK_ID}.
  • Override Capability: Crucially, it deeply respects the KINETIC_DATA_DIR environment variable. This is required for power users who want to move their blockchain data to a custom external hard drive or a mounted NAS system.
  • Dependencies: It utilizes the external dirs crate to map standard OS paths seamlessly and flawlessly.

How This Connects to the Rest of Kinetic

This foundational configuration module directly touches almost every other critical system within the Kinetic ecosystem. It acts as the central nervous system for node initialization, network bindings, and routing protocols:

CROSS-CRATE DEPENDENCY (kinetic-daemon initialization)

When the main daemon process starts up in kinetic-daemon/src/main.rs, the very first operational instruction it executes is invoking KineticConfig::load(). The massive configuration object that is returned from this function call is then passed around by reference to almost every single subsystem. It is given to the API servers to know where to bind. It is given to the DNS resolvers to know which UDP ports to capture. It is given to the Proxy routing algorithms to understand .kin resolution. Furthermore, if the auto_update flag is enabled within this struct, the daemon will subsequently spawn active background threads to poll for new binary releases from GitHub.

CROSS-CRATE DEPENDENCY (kinetic-node networking)

The complex P2P swarm relies and exclusively on the parameters defined within the P2pConfig struct. When the libp2p framework starts initializing its transports, it looks specifically at the daemon_port and daemon_quic_port variables. It uses these variables to figure out exactly how to bind its listeners to the host OS’s networking sockets. It then immediately iterates through the bootstrap_nodes string array. For each multiaddr in that array, it attempts to forcefully establish its first outbound network connections. This process is the exact mechanism by which a cold node officially joins the global DHT routing table.

FORWARD DEPENDENCY (Atlas Bridge Integration and State Sync)

Take special notice of the atlas_port parameter located deep within the DaemonConfig. The Atlas Bridge is a separate, specialized background utility in the broader Kinetic ecosystem. It is utilized specifically for migrating historical chain state or syncing massive network datasets efficiently across large distances. The core network daemon uses this specific UDP port to orchestrate commands with the local Atlas instance securely.

FORWARD DEPENDENCY (Drand Cryptographic Networking)

The deeply integrated randomness module utilizes the endpoints and drand_domain string arrays from the DrandConfig to actively query the trusted League of Entropy servers. Crucially, if the operator sets p2p_only to true, the node modifies its internal network stack to disable its outbound HTTP client entirely. From that point on, it relies exclusively on the libp2p Gossipsub protocol to receive cryptographically signed Drand updates from other Kinetic peers. This complex architectural flow is critical for validating nodes running behind strict, unforgiving corporate firewalls where outbound HTTP traffic is blocked.

FORWARD DEPENDENCY (Proxy Subsystem Resolution)

The node’s built-in HTTP reverse proxy relies on the configured ipfs_gateway field located in DaemonConfig. When a local web browser requests a specialized .kin decentralized domain name that resolves to an IPFS CID on the backend, the local proxy intercepts this request. It uses this configured gateway URL to fetch the decentralized content via standard HTTP. It then rapidly streams it back to the user’s browser. This essentially translates complex decentralized web data into standard, legacy HTTP traffic that older browsers can instantly understand without native IPFS integration.

Quick Reference for Node Operators

Important Environment Variable Overrides

If an advanced system operator needs to quickly override the default software paths without actually editing the TOML file on disk (which is a very common requirement in automated CI/CD pipelines or immutable container deployments), the Kinetic binary deeply respects these global environment variables:

  • KINETIC_CONFIG_PATH: This variable forcefully commands the daemon to load a specific, custom config.toml file from anywhere on the disk. Example usage: KINETIC_CONFIG_PATH=/etc/kinetic/my_custom_production_config.toml kinetic-daemon

  • KINETIC_DATA_DIR: This variable fundamentally changes the primary root directory used for all database storage, all configuration files, and all sensitive API authentication tokens. Example usage: KINETIC_DATA_DIR=/mnt/nvme_storage_array/kinetic_data kinetic-daemon

  • KINETIC_ENV: This variable is used in strict combination with hardcoded constants to determine the network ID. This is critical for isolating testnet data from mainnet data on the exact same physical server instance.

Default Port Mapping Overview for System Administrators

For network administrators configuring strict firewall constraints (using tools like UFW, iptables, pfSense, or AWS Security Groups), here is exactly how the Kinetic software logically maps its default local network ports:

  • Ports 6070 - 6072: Dedicated to libp2p Networking. This supports standard TCP, UDP, and experimental QUIC protocols for the Daemon, Node, and Host background processes.
  • Ports 16002 - 16004: Dedicated exclusively to Internal authenticated REST APIs and basic HTTP Health Check endpoints. These should never be exposed to the public internet.
  • Port 17001: Dedicated to the Built-in HTTP Reverse Proxy used specifically for internal .kin domain name resolution routing.
  • Port 16001: Dedicated to the Proxy Auto-Config (PAC) HTTP server, which is queried automatically by local web browsers on the host machine.
  • Port 53: Dedicated to the Local UDP DNS Resolver. This specific port usually requires root capabilities on Linux and is used for actively intercepting native OS-level domain queries before they hit the upstream ISP.
  • Port 80: The default local backend port mapping for routing general web traffic to local applications.
  • Port 34291: Dedicated specifically to the external Atlas Bridge utility for efficient UDP state communication.

Open Questions / Things to Revisit During Next Refactor

  1. WebAssembly Persistent Configuration Limitations: Currently, the WebAssembly build targets have a empty, non-functional stub for configuration loading (config::load() simply returns an empty default() object). If the engineering team ever wants Wasm-based web nodes to actively support persistent custom configurations (for example, saving custom user preferences directly into the browser’s localStorage or IndexedDB APIs across sessions), we will need to and rewrite the Wasm conditional implementation. Both load() and save() would need to deeply integrate with those specific browser storage APIs natively.

  2. Missing Semantic Input Validation for Edge Cases: While the strict TOML parser effectively protects against basic structural syntax errors (like missing quotes), there is surprisingly very little semantic validation of the parsed operational data itself. For an extreme example, if a rogue user configures api_port = 99999 (which is technically an invalid port number as it is vastly larger than a standard u16 maximum of 65535, though serde macro serialization will eventually catch strict type mismatches at runtime). But consider a far more dangerous edge case: what if an operator sets the proxy port to port 1, which requires high-level root privileges on Linux? We desperately might want to add a robust, explicit .validate() method directly to the KineticConfig struct. This method should actively check for things like privileged ports or overlapping internal port assignments before ever allowing the boot process to blindly continue forward and panic cryptically later in the startup sequence.

  3. Handling Configuration Schema Migrations Over Time: If a future iteration of the Kinetic core software decides to rename a configuration field (for a simple example, changing the variable name storage_dir to database_path), a catastrophic failure will occur for existing nodes. All older, existing config.toml files will instantly fail to parse when the daemon boots up. Because of our strict security model, this will trigger the fail-closed panic exit, crashing all legacy nodes globally. We currently have no systemic mechanism in place for gracefully migrating outdated TOML configurations to newer schema formats without manual operator intervention. We likely need to introduce an explicit version field directly inside the config file to track these breaking schema changes safely.

  4. IPFS Gateway Hardcoding Reliability Risks: The ipfs_gateway field currently defaults to a singular, specific public gateway defined statically inside constants.rs. Public IPFS gateways are notoriously unreliable over long periods. They frequently implement aggressive rate-limiting protocols or experience massive, unpredictable downtime spikes. We desperately might want to update the schema to support a full array of fallback gateway URLs rather than a single string. Alternatively, we could perhaps implement a smart round-robin load balancing strategy for fetching external decentralized content reliably.

  5. Lack of Dynamic Port Allocation Fallbacks: If the default requested port is already actively in use by another random application on the host machine, the daemon will currently crash and burn instantly. Should the engineering team implement a smart, automatic port fallback mechanism? For example, attempting to natively bind to port 6073 if port 6070 is already taken by another process on the box? If successful, it would dynamically update the configuration file on disk. While this would drastically improve the non-technical user onboarding experience by avoiding cryptic “address already in use” panics, it would significantly complicate manual firewall rules for system administrators who expect 100% deterministic, predictable behavior.

Kinetic Core: Drand Quicknet Client

Crate: kinetic-core Stage: 15 Reading Time: 80 minutes Depends On: config, constants, traits::StorageEngine, error::DrandError

What Is This?

This file implements the core client for interacting with the League of Entropy’s Drand network. Specifically, it targets the “Quicknet” instance of the Drand network. Drand is a decentralized randomness beacon that operates continuously on the open internet. It produces un-gameable, publicly verifiable random numbers at regular intervals. In the Quicknet configuration, these intervals occur exactly every 3 seconds. This rapid cadence is a significant upgrade over older Drand networks which operated on 30-second cycles. Within the Kinetic codebase, the outputs from Drand are uniquely referred to as “kyns”. This is a Kinetic-specific naming convention that purposefully overrides the standard drand term, which is “round”. The drand.rs file provides the critical infrastructure to discover Drand HTTP endpoints dynamically. It queries DNS TXT records to find active endpoints without requiring hardcoded IP addresses. It fetches the latest kyns with resilient, exponential retry logic. Most importantly, it cryptographically verifies every single response it receives from these endpoints. Because these randomness beacons consist of untrusted data originating from the open internet, the client cannot trust the payload blindly. It performs intense cryptographic validation on every byte of the response. It validates the advanced BLS12-381 signatures attached to every beacon using hardcoded public keys. It verifies that the 256-bit randomness string and binds to that signature via a SHA-256 hash. Finally, it caches successfully validated kyns to local storage via an abstracted storage engine. This caching mechanism provides a robust fallback during internet outages or severe network partitions.

Why Kinetic Needs This

Kinetic fundamentally requires an external, impartial source of truth for both time and randomness. Relying solely on internal network consensus for randomness generation is dangerous and fundamentally flawed. It allows malicious miners or powerful validators to potentially manipulate or predict outcomes in their favor. This is known as a bias attack, where block producers withhold blocks that contain unfavorable random seeds. By delegating randomness generation to the League of Entropy, Kinetic sidesteps this attack vector entirely. It achieves several structural guarantees that are impossible to build internally without massive overhead.

Firstly, it provides an un-gameable random seed that is utilized throughout the network for critical cryptographic operations. Most notably, this external randomness serves as the absolute foundation for Verifiable Delay Function (VDF) name registrations. If a user registers a name on the Kinetic network, the VDF generator requires a starting seed to begin its sequential computation. It uses the drand randomness as this starting seed. Because the League of Entropy uses threshold cryptography distributed across dozens of independent organizations, no single member can predict the randomness before it is published. They certainly cannot bias the output to favor a specific name registration. This ensures that name registrations are fair, transparent, and immune to front-running by network participants.

Secondly, the Quicknet network produces a beacon exactly every 3 seconds. This regular, predictable, and unstoppable cadence acts as a decentralized, global clock for the entire Kinetic network. When a kyn is retrieved from the network, it functions as a cryptographically signed, unforgeable timestamp. Kinetic utilizes this exact property as the core foundation of its node heartbeat validation system. Nodes broadcast heartbeats across the peer-to-peer network attached to a specific kyn number. By validating that the attached kyn is not excessively old, the network ensures nodes are online and actively synchronized. It proves they are observing the current global state of the network. Without drand.rs, the Kinetic network would lack a secure timekeeping mechanism entirely. It would also lack a manipulation-resistant source of randomness for its cryptographic primitives. This absence would break both the global naming system and the peer-to-peer node presence tracking.

How It Works

The lifecycle of acquiring a randomness beacon in Kinetic is deliberately complex and multi-staged. It is engineered from the ground up for maximum resilience, security, and fault tolerance across hostile networks. Here is the granular, step-by-step breakdown of how the DrandClient operates under the hood.

1. Endpoint Discovery via DNS TXT Records

When the fetch_latest function is invoked, the client first attempts to discover the most up-to-date HTTP endpoints available. -> See: kinetic-core/src/drand.rs — Lines 185 to 206 The system is configured with a base, hardcoded set of endpoints defined in the user’s local config.toml. However, relying purely on hardcoded endpoints is brittle and prone to failure over a multi-year timeframe. Therefore, the client leverages the hickory_resolver crate to actively query DNS TXT records. It queries these records against a configured drand_domain. If it successfully resolves these TXT records over the network, it parses the text payload to extract secure HTTPS URLs. It strips quotation marks and validates the schema. It then injects up to 5 of these dynamically discovered endpoints into the active connection pool. This dynamic discovery architecture is crucial for long-term network health and autonomy. The League of Entropy periodically cycles their edge node URLs, deprecates old servers, and spins up new infrastructure. By relying on DNS as a dynamic registry, Kinetic ensures nodes never get stranded on dead, legacy URLs. It is important to note that this entire DNS block is wrapped in conditional compilation flags. It is stripped out when the compiler targets WebAssembly (wasm32). This is a strict requirement because standard UDP/TCP DNS resolution is fundamentally unavailable inside a sandboxed browser environment. WASM clients must rely solely on the hardcoded endpoint array.

2. The Resilient Fetch Loop and Memory Defenses

Once the comprehensive list of endpoints is assembled, the client enters its primary retry loop. -> See: kinetic-core/src/drand.rs — Lines 276 to 341 It iterates sequentially through the list of URLs. For any given endpoint, the client calls the internal helper function fetch_with_backoff. This inner function is designed to make up to 3 separate HTTP GET requests before giving up on the URL entirely. If a request fails due to a TCP network timeout, or if it returns a non-200 HTTP status code, the client deliberately pauses. The delay is implemented as a strict exponential backoff algorithm to prevent spamming struggling servers. It starts at a conservative 500 millisecond delay. Upon a second failure, the delay doubles to 1 full second. For the third and final attempt, the delay doubles once again to 2 seconds. If all 3 attempts fail entirely, it abandons the endpoint and smoothly moves to the next URL in the array. During the HTTP download phase on desktop environments, the system utilizes a specialized bytes::BytesMut buffer. It manually reads the incoming network stream chunk by chunk from the socket. It deliberately avoids calling convenience methods that load the whole payload into memory at once. This is a critical security defense mechanism against malicious, compromised, or misconfigured servers. The system constantly and checks the buffer length against LIMITS_DRAND_MAX_RESPONSE_BYTES (64 KB) on every single iteration of the read loop. If a malicious endpoint attempts a memory exhaustion attack by streaming infinite garbage data, the threshold is tripped. The client defensively severs the TCP connection immediately, returning a DrandError::Network.

3. Advanced Cryptographic Verification Mechanics

When an endpoint successfully returns a JSON payload under the 64KB limit, it is deserialized into a RawKyn structure. However, at this stage, the data is untrusted and potentially hostile. -> See: kinetic-core/src/drand.rs — Lines 87 to 128 The client must rigorously verify the cryptographic integrity of the kyn before passing it to the rest of the Kinetic node. Drand Quicknet operates using BLS12-381, a advanced, pairing-friendly elliptic curve widely used in modern cryptography. The system loads the hardcoded League of Entropy public key from the constants::DRAND_PUBLIC_KEY variable. It decodes this hex string and parses this raw byte array into a strongly-typed G2PubkeyRfc object. Quicknet is deployed in what is known as “unchained” mode. This means that every single round’s signature is independent of the previous round’s data. Older drand networks required validating a chain of signatures all the way back to genesis, which was computationally heavy. The client executes the BLS verification algorithm over the current kyn number directly. It passes an empty byte slice &[] for the previous signature chain, signaling unchained mode. If the signature cryptographically matches the League of Entropy public key, it proceeds to the second critical validation check. It must bind the signature to the actual randomness output provided in the JSON. In the Quicknet architecture, the final 256-bit randomness string must equal the exact SHA-256 hash of the binary signature itself. The client computes this SHA-256 hash locally on the host CPU. It then compares the resulting byte array to the randomness string provided in the parsed JSON payload. Only if both the BLS curve check and the SHA-256 hash check pass with absolute certainty is the kyn officially considered verified and safe for consumption.

4. Staleness Calculation and Time bounds

A cryptographically valid kyn is essentially useless to the network if it was generated three days ago. Replay attacks, where malicious nodes broadcast old, valid data as if it were new, are a major concern for decentralized networks. -> See: kinetic-core/src/drand.rs — Lines 223 to 242 To combat replay vulnerabilities, the client dynamically calculates an “expected” current kyn number based on the local system time. It takes the current local UNIX timestamp in seconds. It subtracts the known DRAND_GENESIS_TIME, which represents the exact second the Quicknet network launched. It then divides this difference by the 3-second DRAND_PERIOD. This simple division provides a accurate, mathematical estimation of exactly what the current kyn number should be at this very second. It then takes this estimation and subtracts the actual fetched kyn number from it. Notice the deliberate use of the saturating_sub Rust method during this subtraction. If the node’s local clock is accidentally behind the Drand genesis time, saturating_sub ensures the subtraction safely bounds to zero. A standard subtraction would underflow and trigger a catastrophic panic, crashing the entire node process. If the final calculated age of the fetched kyn is greater than MAX_STALE_ROUNDS_FOR_HEARTBEAT (which translates to 200 kyns, or exactly 10 minutes), it rejects the kyn entirely. It surfaces a StaleKyn warning to the terminal, indicating the network clock is drifting or the endpoint is serving outdated data. This strict time-bound enforcement ensures the Kinetic network only ever operates on fresh, recent synchronization data.

5. Disk Caching and Offline Fallback

If the kyn successfully passes all BLS verification math, SHA-256 checks, and staleness boundaries, it is persisted to disk. -> See: kinetic-core/src/drand.rs — Lines 349 to 355 The client immediately serializes the JSON and writes it to the local storage engine using the cache_kyn method. This local cache acts as a vital, resilient safety net for the node. If the entire global internet experiences a catastrophic outage, the live fetch loop will fail. If all League of Entropy edge nodes are simultaneously offline or unreachable due to routing issues, the HTTP requests will time out. When the endpoint array is fully exhausted after all backoff retries, the client gracefully falls back to the local database. It invokes load_cached_kyn to read the last successfully verified kyn from the DB_PREFIX_LAST_DRAND database key. While this cached kyn will obviously grow progressively stale as time passes without network access, it provides a crucial operational buffer. It allows the Kinetic node to maintain local heartbeat operations and remain technically functional. This graceful degradation is essential for surviving transient network blips without tearing down the entire local peer-to-peer state.

6. Dev Mode Bypasses and Hallucinations

For local development and continuous integration testing, waiting on live drand network fetches is frustrating. Furthermore, executing heavy BLS elliptic curve signature verification on mock data instantly breaks offline test suites. -> See: kinetic-core/src/drand.rs — Lines 92 to 95 and Lines 375 to 384 When the global is_dev_mode() flag is active, the system and deliberately shortcuts these heavy security checks. The verify() method instantly returns true without actually executing any elliptic curve mathematics whatsoever. This allows developers to feed garbage data into the system during testing without failing the cryptographic bounds. Furthermore, if the node is booted offline in dev mode and the local database cache is empty, it refuses to fail. Instead of returning a NoCachedKyn error, it intentionally hallucinates a synthetic kyn structure. It statically sets the round number to 5,000,000. It fills the randomness payload with a literal mock_randomness string. This architectural bypass allows the rest of the complex Kinetic software stack to boot up seamlessly. It operates predictably in a disconnected, local sandbox environment without demanding active internet access.

Deep Dive: The verify() Method Line by Line

The verify() method on RawKyn is the most cryptographically dense portion of this file. Let us examine exactly what happens during validation. First, it checks if the kyn is marked as unavailable. If so, it returns true instantly, as unavailable kyns are sentinels that carry no payload. Next, it checks is_dev_mode(). If active, it returns true instantly to bypass expensive elliptic curve math during local testing. Then, it proceeds to decode the hardcoded DRAND_PUBLIC_KEY. This key is provided as a hex string in the constants file. It attempts to decode the hex string into a raw 96-byte array. If the hex decoding fails, or if the array is not exactly 96 bytes long, verification fails immediately and returns false. This 96-byte array represents a public key on the G2 elliptic curve of BLS12-381. The drand_verify crate is then invoked via G2PubkeyRfc::from_fixed(pubkey_bytes). This parses the raw bytes into a mathematical curve point. If the bytes do not represent a valid point on the G2 curve, it returns false. Next, it decodes the signature field of the RawKyn from hex into a raw byte array. Now comes the actual threshold BLS verification. It calls pk.verify(self.kyn, &[], &sig_bytes). The first argument is the round number (the kyn). The second argument is the previous signature chain. Because Quicknet is “unchained”, this is passed as an empty byte slice &[]. The third argument is the signature bytes to verify. If this BLS curve verification returns false, the kyn is fundamentally invalid and is rejected. Finally, if the signature is valid, it proceeds to bind the randomness. It instantiates a SHA-256 digest engine from the sha2 crate. It passes the raw signature bytes into the SHA-256 hasher. It extracts the 32-byte hash output. It then hex-decodes the randomness string provided by the drand API. It compares the 32-byte hash output against the decoded randomness bytes. Only if these two byte arrays are identical does the verify() method finally return true.

Deep Dive: The fetch_with_backoff() Method Line by Line

The fetch_with_backoff() method implements the resilient networking layer for the client. It takes a single URL string as an argument. It initializes a delay variable to Duration::from_millis(500). It initializes a max_attempts counter to 3. It enters a for loop that will run exactly 3 times. Inside the loop, it uses the shared reqwest::Client to construct an HTTP GET request against the provided URL. Crucially, it attaches a strict .timeout(Duration::from_secs(5)) to the request builder. This prevents the async thread from hanging indefinitely if the endpoint stops responding mid-flight. It then .send().awaits the request. If the response is Ok and the status code is a 2xx success, it proceeds to read the body. This is where the WASM and Desktop implementations diverge via #[cfg] macros. On WebAssembly, it simply calls resp.bytes().await. It then checks if the resulting byte array exceeds the 64KB limit. If it exceeds the limit, it immediately returns a DrandError::Network complaining about the size. On desktop, it initializes an empty bytes::BytesMut buffer. It enters a while let Some(chunk) = resp.chunk().await loop. This reads the TCP stream iteratively. For every single chunk received, it appends the bytes to the BytesMut buffer. Immediately after appending, it checks if the buffer size exceeds the 64KB limit. If it does, it returns an error, forcefully tearing down the underlying TCP connection to prevent memory exhaustion. Once the body is fully read safely, it uses serde_json::from_slice::<RawKyn> to parse the bytes into the struct. If parsing succeeds, it returns the RawKyn. If the HTTP request returned a non-200 status (e.g., a 502 Bad Gateway), it matches on Ok(_resp) if attempt < max_attempts - 1. It then sleeps the async task for the duration of the current delay. It uses tokio::time::sleep on desktop, and gloo_timers::future::sleep on WebAssembly. After sleeping, it multiplies the delay by 2 (exponential backoff) and the loop continues. If the request threw a hard network error (e.g., DNS failure, connection refused), it matches on Err(_) if attempt < max_attempts - 1. It sleeps and doubles the delay exactly the same way. If all 3 loop iterations execute and fail, the loop exits. The function then returns Err(DrandError::AllEndpointsFailed) to the caller.

Deep Dive: The WebAssembly Compilation Matrix

You will notice extensive use of #[cfg(target_arch = "wasm32")] and #[cfg(not(target_arch = "wasm32"))] throughout drand.rs. This is a critical architectural requirement for Kinetic, which is designed to run natively inside web browsers. WebAssembly environments operate inside a strict JavaScript sandbox. They do not have access to raw operating system features like POSIX sockets, raw UDP, or raw TCP. Because standard DNS resolution requires firing raw UDP packets at port 53, the hickory_resolver crate fundamentally cannot compile to WebAssembly. Therefore, lines 26-27 wrap the hickory_resolver import in a not(wasm32) macro. The resolver field in the DrandClient struct is similarly conditionally compiled out on WASM. The entire dynamic DNS TXT discovery block inside fetch_latest is removed on WASM builds. Furthermore, the reqwest crate utilizes different internal backends depending on the target. On desktop, it uses hyper and tokio. On WebAssembly, it compiles down to utilizing the browser’s native fetch() API via js-sys and wasm-bindgen. Because of this, manual chunked stream reading via resp.chunk() is handled differently on WASM. The desktop build manually streams chunks into a BytesMut buffer to prevent memory attacks. The WASM build simply calls resp.bytes() because the browser’s underlying fetch implementation handles the memory safety boundaries internally. Finally, asynchronous sleeping is fundamentally different. Desktop Rust uses tokio::time::sleep to yield the thread to the Tokio runtime. WASM has no operating system threads, so it must use gloo_timers::future::sleep to interface with the JavaScript setTimeout API under the hood.

Deep Dive: The Cryptography of BLS12-381 in Drand

The League of Entropy utilizes the BLS12-381 elliptic curve for the Quicknet network. This specific curve is uniquely suited for decentralized randomness beacons because it is “pairing-friendly”. Pairing-friendly curves allow for advanced threshold signatures. In a threshold signature scheme, no single entity holds the private key. Instead, organizations like Cloudflare, Protocol Labs, and others each hold a “share” of the private key. To generate a kyn, each organization independently signs the round number using their share. They broadcast these partial signatures over the network. Once a specific threshold of partial signatures is gathered (e.g., 51% of participants), they are aggregated. This aggregation produces a single, unified signature that looks exactly as if a single master private key had signed the data. This master signature is what is delivered in the signature field of RawKyn. This is why Kinetic must perform heavy BLS verification. It must prove that the threshold was successfully met by the League. Furthermore, BLS12-381 operates on two distinct groups: G1 and G2. G1 signatures are short and fast to verify, but the public keys are large. G2 signatures are large and slow to verify, but the public keys are short. Quicknet is optimized for bandwidth, so it places public keys on G2 (96 bytes) and signatures on G1 (48 bytes). This is why the DRAND_PUBLIC_KEY constant is exactly 96 bytes long, and why it is parsed using G2PubkeyRfc. The combination of threshold generation and strict BLS verification ensures that the randomness Kinetic consumes is impossible to predict or bias.

Deep Dive: Storage Integration and Caching Mechanics

The caching layer of drand.rs interfaces dynamically with the core storage engine. -> See: kinetic-core/src/drand.rs — Lines 349 to 387 The cache_kyn function is straightforward but vital. It first unwraps the Option<Arc<dyn StorageEngine>>. If the node was booted without a database, it skips caching entirely. If storage is present, it serializes the strongly-typed RawKyn back into raw JSON bytes using serde_json::to_vec. It then calls storage.put() using the globally defined DB_PREFIX_LAST_DRAND key. Conversely, the load_cached_kyn function handles the retrieval side. It asks the storage engine for the bytes associated with DB_PREFIX_LAST_DRAND. If the bytes exist, it deserializes them back into a RawKyn. Crucially, it intercepts the struct and forces kyn.is_from_cache = true before returning it. This strict override ensures that even if the JSON on disk had is_from_cache set to false, the runtime engine corrects it. This prevents a stale, cached kyn from masquerading as a freshly fetched kyn after a node reboot. This flag is then relied upon by is_usable_for_registration to deny registrations based on rebooted state.

Deep Dive: Handling DrandError Variants

The error handling within this file relies extensively on the strongly-typed DrandError enum. When verifying a fetched payload, if the BLS signature is corrupted, it returns DrandError::InvalidSignature. If the time-bound staleness check identifies an old beacon, it returns DrandError::StaleKyn, embedding the expected versus actual round numbers inside the error payload. If an endpoint responds with a 404 Not Found or a 500 Internal Server Error, it traps the code and returns DrandError::HttpError. If the network socket drops unexpectedly, or if the malicious 64KB buffer limit is deliberately breached, it surfaces a generic DrandError::Network. During offline operation, if the local storage layer is empty, it correctly yields DrandError::NoCachedKyn. Finally, if every single endpoint in the dynamically discovered array fails sequentially during the backoff loop, it emits the terminal DrandError::AllEndpointsFailed. By surfacing these explicit error variants, the upstream calling modules can make precise decisions about whether to retry or fail gracefully.

Deep Dive: The Role of serde and Hex Encoding

A significant portion of this file acts as a translation layer between the network wire format and internal Rust representations. The League of Entropy endpoints serve JSON payloads where all cryptographic materials are encoded as raw hexadecimal strings. To process this, the file utilizes the hex::decode function. It decodes the 96-byte signature string into raw memory buffers. It also decodes the randomness string into a 32-byte hash buffer for strict comparison. Simultaneously, the file leverages the serde::Deserialize trait implementation on the RawKyn struct. This macro-driven approach allows the serde_json engine to rapidly stream the incoming TCP bytes directly into memory. The #[serde(alias = "round")] directive proves especially critical here. It bridges the gap between the external API’s standard nomenclature (“round”) and Kinetic’s internal, domain-specific terminology (“kyn”). Without these powerful deserialization directives, the file would require hundreds of lines of fragile, manual string parsing.

Deep Dive: The Sentinel State

Sometimes, the node must initialize variables before any kyn has actually been fetched. -> See: kinetic-core/src/drand.rs — Lines 51 to 61 This is handled via the RawKyn::unavailable() constructor. It creates a deliberate sentinel object representing a unavailable beacon state. It sets the round number to 0. It fills the randomness and signature buffers with empty strings. Crucially, it sets the is_unavailable flag to true. This sentinel struct is safe to pass around the system. The verify() method specifically checks for this flag, returning true instantly without doing math. The is_usable_for_registration() method checks this flag and firmly returns false. This prevents uninitialized state from leaking into cryptographic generation logic before the true network state is synchronized.

Deep Dive: The Testing Suite

The file concludes with a rigorous #[cfg(test)] module containing unit tests. -> See: kinetic-core/src/drand.rs — Lines 390 to 433 The test_valid_quicknet_kyn_verification function hardcodes a known, verified payload from Quicknet round 30290678. It passes this payload directly into the verify() method. This acts as an integration test against the drand_verify crate and the hardcoded BLS public key. If this test fails, it means the public key in constants is corrupted or the cryptographic dependency is broken. The test_invalid_quicknet_kyn_verification function performs the inverse. It takes the exact same valid payload but deliberately corrupts the first character of the signature string. It then asserts that the verify() method rejects the payload. Interestingly, it contains conditional logic for is_dev_mode(). If the test runner executes with dev mode active, it asserts that the corrupted payload passes verification. This proves that the dev mode shortcut (returning true instantly) is fully functioning as designed.

Key Pieces

The RawKyn Struct

-> See: kinetic-core/src/drand.rs — Lines 33 to 49 This is the core, fundamental data model representing a single randomness beacon payload. The kyn field holds the monotonically increasing round number, which increments exactly every 3 seconds globally. Notice the use of the #[serde(alias = "round")] attribute above the field definition. This is a powerful directive that instructs the Serde deserializer to map the incoming JSON key "round" from external APIs directly into the Kinetic-specific struct field kyn. This avoids manual mapping logic and keeps the struct aligned with internal terminology. The randomness field is the hex-encoded string of the final randomness output, bound to the signature. The signature field contains the hex-encoded BLS signature, generated cooperatively by the League of Entropy threshold network. The struct also contains two crucial, privately-managed boolean flags: is_from_cache and is_unavailable. These flags track the provenance and network status of the data. They inform downstream consumers whether the data is fresh from the internet, stale from disk, or missing.

RawKyn::is_usable_for_registration

-> See: kinetic-core/src/drand.rs — Lines 63 to 66 This method dynamically determines if a specific kyn is fresh enough to be used specifically for VDF name registrations. Because global name registrations are sensitive to cryptographic manipulation, they require live, freshly generated randomness from the internet. This function returns false if the is_from_cache flag is set to true. Registrations simply cannot, and must not, proceed during a network partition where the node is operating on stale, cached randomness.

RawKyn::is_usable_for_heartbeat

-> See: kinetic-core/src/drand.rs — Lines 68 to 81 In stark contrast to name registrations, P2P network heartbeats are slightly more forgiving of network delays and brief partitions. This method actively accepts cached kyns, provided they are not excessively, dangerously stale relative to the global clock. It compares the cached kyn against a provided current_live_kyn parameter passed down from the network layer. If the raw mathematical difference is less than or equal to 200 kyns (which equals exactly 10 minutes), it permits the heartbeat to fire. This specific, measured allowance is exactly what keeps the network topology stable during brief League of Entropy outages or local ISP drops.

The DrandClient Struct

-> See: kinetic-core/src/drand.rs — Lines 131 to 138 This is the stateful, long-lived orchestrator for the entire fetching and verification process. It holds a persistent, internal reqwest::Client instance to pool HTTP connections. This allows the client to reuse TLS handshakes across multiple fetches, maximizing efficiency and minimizing latency. It holds the storage field, typed densely as an Option<Arc<dyn StorageEngine>>. This specific type signature utilizes dynamic dispatch (dyn) to remain agnostic to the underlying database technology. It does not care whether the node is running RocksDB on a Linux server or IndexedDB inside a WebAssembly browser context. The Arc pointer allows this single database connection to be safely cloned and shared across multiple asynchronous networking threads. Finally, it holds the TokioAsyncResolver for performing non-blocking DNS TXT queries, ensuring DNS lookups do not stall the async runtime.

How This Connects to the Rest of Kinetic

FORWARD DEPENDENCY: The RawKyn struct is and critically utilized by the VDF (Verifiable Delay Function) subsystem. When a user initiates the process to register a domain name on the Kinetic network, the VDF generator requires a secure starting seed. It pulls the randomness field from the absolute latest RawKyn and uses it as the foundational cryptographic input for the sequential delay function. FORWARD DEPENDENCY: The P2P Node Heartbeat manager relies on the RawKyn::kyn field. It uses this monotonically increasing round number to digitally stamp all outgoing heartbeats. This acts as definitive, un-gameable proof that the node was online and active at that specific, global 3-second interval, ensuring node presence maps remain accurate. CROSS-CRATE: The broader peer-to-peer networking layer utilizes this file extensively. It ensures that all incoming packets, heartbeats, and blocks from foreign peers are actively evaluated against a synchronized, global clock derived from drand. CROSS-CRATE: The core StorageEngine trait from kinetic-core is implemented differently across various deployment platforms. The DrandClient remains blissfully unaware of the underlying storage mechanism because it interfaces with the generic trait boundary, showcasing excellent modular design.

Quick Reference

  • Beacon Frequency Cadence: Exactly every 3 seconds (Quicknet specification).
  • Absolute Staleness Threshold: 200 kyns (which equals 10 minutes of real time).
  • Network Retry Logic: 3 attempts per endpoint, doubling delay (500ms, 1000ms, 2000ms).
  • Maximum Response Buffer Size: 64 Kilobytes (Strict memory exhaustion protection).
  • Core Cryptography Setup: BLS12-381 G2 Public Keys, Unchained Signatures.
  • Randomness Derivation Math: SHA-256 Randomness Binding directly to the raw BLS signature bytes.
  • Database Cache Key Prefix: DB_PREFIX_LAST_DRAND (Used for offline fallback).

Open Questions / Things to Revisit

  • WebAssembly DNS Limitations: The system currently strips out all DNS TXT record discovery when compiling for WASM targets. If the hardcoded fallback endpoints defined in the configuration file eventually die, WASM clients will fail and silently. We desperately need to investigate implementing DNS-over-HTTPS (DoH) utilizing standard fetch APIs inside the browser. This would restore dynamic URL discovery specifically for WASM clients, significantly improving long-term reliability.
  • Hardcoded Endpoint Injection Limits: The DNS discovery loop breaks after injecting exactly 5 endpoints into the active routing array. Is this 5-endpoint limit an arbitrary, magic number based on an old assumption? We should strongly consider migrating this threshold to make it a customizable integer inside config.toml, allowing node operators to tweak their discovery radius.
  • System Clock Drift Vulnerabilities: The staleness check relies intensely and exclusively on the local host’s system time via SystemTime::now(). If the host operating system’s clock is severely desynchronized (for example, if the local NTP daemon is broken or blocked by a firewall), the node will exhibit strange behavior. It will either incorrectly reject valid kyns as “stale” and drop off the network, or worse, it will accept dangerously old, maliciously replayed kyns as valid data.
  • Memory Allocation in Response Parsing: The 64KB limit check inside the manual BytesMut loop is an excellent, necessary defense. However, a sophisticated malicious server could theoretically stream exactly 64KB infinitely slowly, sending one byte per hour (a classic “slowloris” style network attack). While we have a rigid 5-second timeout defined on the request builder, we need to ensure the reqwest framework enforces that timeout across the entire read operation, and not just during the initial header response handshake.
  • Dev Mode Cache Mutation Inconsistencies: The mock kyn 5,000,000 is returned dynamically by the fallback logic when dev mode is active and the database cache is empty. However, if you trace the execution path, this synthetic mock kyn is never actually written back to the disk cache. It acts purely as a temporary, ephemeral, in-memory hallucination to satisfy the caller. This architectural quirk could potentially lead to inconsistent system state if other, unrelated modules expect to read from the drand cache directly on disk during end-to-end integration testing.

16. Core Traits & Dependency Inversion (kinetic-core)

Crate: kinetic-core Stage: 16 Reading Time: ~30 minutes Depends On: kinetic-core::error (Stage 15), kinetic-core::types (Stage 14)

What Is This?

This file defines the foundational abstractions that power Kinetic’s dependency injection architecture. Instead of hardcoding concrete implementations like Sled for storage or ChiaVDF for delay functions directly into the core logic, Kinetic relies on a set of tightly defined traits: VdfEngine, StorageEngine, and GovernanceEngine. This abstraction layer makes the kinetic-core crate portable, network-agnostic, and decoupled from heavyweight backend libraries. By relying on these interfaces, the core protocol logic can operate purely on abstract contracts without knowing or caring about the underlying implementation details. This means that when the core node processes a new block, it just calls engine.evaluate() or storage.put(), blind to whether it is writing to disk, a mock memory store, or evaluating a C++ VDF prover. The traits defined in this file serve as the boundary line between Kinetic’s pure, platform-independent state machine and the messy reality of file systems, multi-threading, and hardware acceleration. By enforcing these boundaries early on, Kinetic ensures that the central consensus logic remains and focused solely on validating the rules of the network. Without these traits, the core logic would quickly become tangled with platform-specific code, making cross-compilation or testing nearly impossible. In Rust, a trait is a collection of methods defined for an unknown type: Self. They can access other methods declared in the same trait and serve as shared behavior interfaces.

Why Kinetic Needs This

Kinetic is designed to be a flexible, modular network protocol that can adapt to different deployment environments. If the core logic were tightly coupled to specific databases or VDF provers, it would be difficult to swap them out for testing, alternative platforms, or future upgrades. By defining abstract contracts in traits.rs, Kinetic allows developers to plug in mock engines for lightning-fast unit tests. For example, without VdfEngine, every unit test that touches timekeeping would have to wait minutes for a real cryptographic proof to generate. Instead, developers can inject a dummy engine that returns immediately, speeding up the CI pipeline tremendously. Similarly, if Sled becomes unmaintained or a new database engine shows superior performance, Kinetic can swap it out for RocksDB, Redb, or even a cloud-hosted Postgres instance without rewriting a single line of the core state machine. Furthermore, this design isolates the complex Foreign Function Interface (FFI) logic of chiavdf and the specific filesystem operations of local storage away from the pure protocol logic. If chiavdf needs a C++ compiler and specific environment variables to build, putting it behind a trait ensures that developers working on the core governance state machine don’t necessarily have to compile the heavy VDF prover just to run their tests. The trait boundary acts as a firewall, protecting the core business logic from the messy details of hardware interaction, cryptographic proofs, and file I/O operations. It is a direct application of the Dependency Inversion Principle: high-level modules (consensus) should not depend on low-level modules (database writes); both should depend on abstractions (traits). This ensures that the codebase remains maintainable, scalable, and easy to reason about even as the network grows in complexity. It also enables better division of labor, as a developer working on the database wrapper doesn’t need to understand the governance rules, and vice versa.

How It Works

Kinetic’s dependency inversion is achieved by defining Rust traits that require implementors to be thread-safe. This is enforced by requiring the traits to inherit from Send + Sync. This ensures that the engine implementations can be safely shared across multiple async workers, task executors, or operating system threads in the network node without causing data races or requiring heavy mutex locks everywhere.

The Role of Send + Sync Bounds

In Rust, Send means a type can be safely transferred between threads, and Sync means it can be safely referenced from multiple threads simultaneously. Because Kinetic runs in an async environment (likely Tokio), a single engine instance might be accessed by thousands of lightweight tasks at the same time. By enforcing Send + Sync on the trait itself (pub trait StorageEngine: Send + Sync), the compiler guarantees that any concrete implementation passed into the system is fundamentally thread-safe. If a developer tries to implement StorageEngine on an unsafe type, the compiler will refuse to compile it, stopping a potential data race before the program even runs.

1. The VDF Engine Abstraction

-> See: kinetic-core/src/traits.rs — Lines 18 to 58

The Verifiable Delay Function (VDF) engine is crucial for Kinetic’s cryptographic timekeeping and prevention of block spam. The trait exposes two main operations: evaluate and verify. The evaluate function is a heavy, CPU-bound operation. It calculates a mathematical proof that a certain amount of sequential, unparallelizable work has been performed. Because it takes seconds to minutes depending on hardware, it is expected to block the calling thread for a long time. It returns a Result<VdfProof, VdfError>. The documentation warns that this must not be called on an async executor thread (like the main Tokio runtime), as doing so would starve the executor and halt the node’s network progress. Instead, it must be offloaded to a dedicated blocking thread pool (e.g., using tokio::task::spawn_blocking). Conversely, the verify method is an fast, non-blocking operation. It operates in O(log n) time complexity. It takes a challenge, a proof, and the iterations count, returning a boolean indicating if the proof is valid. This asymmetry is the core property of a VDF: slow to generate, fast to verify. This trait captures that dichotomy, allowing fast verification in the async networking loop and pushing slow generation to background threads.

2. The Storage Engine Interface

-> See: kinetic-core/src/traits.rs — Lines 60 to 111

Because Kinetic needs to store local state across node restarts and crashes, it defines a generic key-value store abstraction. The available methods are put, get, delete, and scan_prefix. Unlike a simple HashMap, this trait implies a persistent, on-disk backend. By making scan_prefix available, Kinetic expects the underlying storage to be ordered (like a B-tree or an LSM tree). This is used to find keys that fall under a specific namespace. In Kinetic, all keys are namespaced with a {NETWORK_ID}_ prefix. This clever design choice allows multiple independent Kinetic networks (like a testnet, a mainnet, and isolated app-chains) to coexist within the exact same physical database file on disk without their state colliding. The use of Option<bytes::Bytes> in get avoids unnecessary allocations compared to standard Vec<u8>, allowing zero-copy sharing of the underlying data buffer where the database backend supports it. When data is written using put, it overwrites any existing value, ensuring atomic updates for a given key. The delete method provides a clean way to drop data, and is designed as a no-op if the key is already missing.

Why Option<bytes::Bytes> in get?

Returning bytes::Bytes instead of a plain Vec<u8> is a very intentional performance optimization. The Bytes type from the bytes crate allows for cheap cloning via atomic reference counting. If the underlying database can return a memory-mapped buffer, Bytes allows the caller to hold onto that slice without copying the entire payload into a new memory location. This is crucial when retrieving multi-megabyte blocks or large governance proposals from disk.

3. The Governance Engine Contract

-> See: kinetic-core/src/traits.rs — Lines 113 to 162

Governance in Kinetic can operate under different models, such as sovereign, council, or permissionless. To support this flexibility, the governance engine is abstracted behind the GovernanceEngine trait. The operations are split into a strict two-step sequence. First, verify_action checks if a signed message meets the threshold and timelock requirements of the active governance model. Importantly, it does not mutate the node state itself, passing the GovernanceState as a mutable reference but leaving it unchanged structurally. Second, execute_action takes a verified action and actually applies the changes to the protocol state. This split ensures that validation and execution remain separated, which is a critical pattern in secure state machine design. If an action fails midway through verification (e.g., due to an invalid signature), the system hasn’t partially updated any internal tracking parameters. The returned GovernanceEffect dictates side effects that the broader node architecture needs to apply, such as rotating keys or ejecting a peer.

4. Testing with Mock Engines

One of the most powerful benefits of the traits.rs design is how it simplifies unit testing. When writing tests for the core consensus logic, interacting with a real database or a real VDF prover introduces massive overhead. A real StorageEngine requires writing files to a temporary directory on disk, dealing with OS-level file locking, and cleaning up afterward. A real VdfEngine requires linking against C++ libraries and wasting CPU cycles to generate actual cryptographic proofs. Because Kinetic uses these traits, developers can easily create a MockStorageEngine using a standard std::collections::HashMap protected by a Mutex. This mock engine implements put, get, delete, and scan_prefix purely in memory. Since it lives in RAM, tests run in microseconds instead of milliseconds. Similarly, a MockVdfEngine can be implemented to return Ok(VdfProof::default()) immediately when evaluate is called, bypassing the heavy mathematical operations entirely. This allows the test suite to execute thousands of edge cases in the consensus state machine almost instantly. Furthermore, mock engines can be instrumented to intentionally fail. If a developer wants to test how the node handles a sudden disk failure, they can create a FailingStorageEngine where put always returns Err(StorageError::OperationFailed). Without this trait-based dependency injection, simulating such edge cases reliably in an automated test environment would be nearly impossible.

Error Propagation and Result Types

The traits extensively use Rust’s Result type to enforce robust error handling. For example, VdfEngine::evaluate returns Result<VdfProof, VdfError>. By returning a typed error, the trait communicates to the caller that the operation might fail (e.g., due to an OS lock error or a hardware panic). This forces the caller in kinetic-core to handle the Err case, preventing silent failures. Similarly, StorageEngine::get returns Result<Option<bytes::Bytes>, StorageError>. Notice the nested Option inside the Result. This is a critical distinction in database operations. The Ok(None) variant means the database was successfully queried, but the requested key simply does not exist. The Err(StorageError) variant means the query itself failed, perhaps due to database corruption, an IO error, or a disconnected storage medium. If the trait simply returned Option<bytes::Bytes>, the node would not be able to distinguish between missing data and a catastrophic disk failure. By forcing this distinction, Kinetic ensures that nodes will gracefully crash or retry on hardware failures, rather than silently assuming data is missing.

Key Pieces

VdfEngine Trait

  • What it does: Defines the contract for evaluating and verifying Verifiable Delay Functions.
  • File & Line: kinetic-core/src/traits.rs:23
  • Why it matters: It isolates the heavy chiavdf FFI logic from the core state machine, allowing the core to remain pure Rust and easily testable with mock VDF implementations that return instantly. This architectural boundary means that developers working on consensus rules do not need to install complex C++ build chains or wait minutes for a proof to generate during a simple unit test. It significantly accelerates development velocity and ensures the core remains modular.

VdfEngine::evaluate

  • What it does: Computes a VDF proof given a 32-byte challenge and an iterations count.
  • File & Line: kinetic-core/src/traits.rs:37
  • Why it matters: This is the heart of Kinetic’s cryptographic time mechanism. Because it is computationally heavy and blocks, it must be spawned on a dedicated blocking thread, never on the main async executor, to prevent locking up the node. It forces the proposer to expend real-world time before they are allowed to broadcast a new block, ensuring the network paces itself securely.

VdfEngine::verify

  • What it does: Instantly verifies a provided VDF proof against a challenge.
  • File & Line: kinetic-core/src/traits.rs:52
  • Why it matters: Allows peer nodes to validate incoming network blocks quickly without needing to re-run the computationally intensive evaluate function. It enables fast-syncing of the chain, where a node can catch up on days of history in seconds, relying on the O(log n) verification property of the underlying Wesolowski VDF.

StorageEngine Trait

  • What it does: Represents the persistence layer interface for the Kinetic node.
  • File & Line: kinetic-core/src/traits.rs:65
  • Why it matters: It mandates thread safety via Send + Sync and provides the baseline CRUD operations, making the database backend swappable (e.g., swapping Sled for RocksDB) without touching the consensus code. This protects the protocol from bit-rot if a specific database engine is abandoned by its maintainers in the future.

StorageEngine::put

  • What it does: Overwrites the value at a specific key with the given byte slice.
  • File & Line: kinetic-core/src/traits.rs:71
  • Why it matters: It handles the raw byte injection into the database. All complex data structures must be serialized (using Serde or similar) before being passed here. This ensures the storage layer remains agnostic to the actual data schema used by the application, focusing on reliable writes.

StorageEngine::get

  • What it does: Retrieves the value associated with a key as a bytes::Bytes object.
  • File & Line: kinetic-core/src/traits.rs:83
  • Why it matters: Retrieves state data efficiently without unnecessary memory allocations, using reference-counted byte arrays. By wrapping the return value in an Option, it differentiates between a missing key and a failed database connection, returning Ok(None) for the former and Err for the latter.

StorageEngine::delete

  • What it does: Removes a key-value pair from the store entirely.
  • File & Line: kinetic-core/src/traits.rs:93
  • Why it matters: Provides state cleanup capabilities, functioning as a silent no-op if the key never existed in the first place. This simplifies logic for callers who might want to ensure a key is gone without first checking if it exists, saving an unnecessary database read operation.

StorageEngine::scan_prefix

  • What it does: Returns an iterator-like structure of all keys starting with a specific byte prefix, optionally bounded by a limit.
  • File & Line: kinetic-core/src/traits.rs:106
  • Why it matters: Essential for retrieving grouped data sequentially, such as all transactions within a certain network or all metadata for a specific block height. It relies on the lexicographical ordering of the underlying database engine, which is why a B-tree or LSM tree is required. By accepting a limit parameter, it provides rudimentary pagination, critical for preventing memory exhaustion when reading historical data that could span gigabytes.

GovernanceEngine Trait

  • What it does: Handles the validation and execution of protocol governance proposals and state transitions.
  • File & Line: kinetic-core/src/traits.rs:121
  • Why it matters: Enables the node to swap between different governance paradigms (e.g., sovereign vs permissionless) via a compile-time flag (GOVERNANCE_MODEL). This allows Kinetic to serve as the base layer for diverse networks with conflicting political philosophies, all while reusing the exact same networking and storage infrastructure.

GovernanceEngine::verify_action

  • What it does: Purely checks if a governance action is valid, verifying signatures, checking timelocks, and evaluating voting thresholds.
  • File & Line: kinetic-core/src/traits.rs:140
  • Why it matters: By separating validation from execution, Kinetic avoids partial state updates if validation fails midway. It also returns an option indicating whether the action is immediately executable or stuck in a timelock queue. This deterministic validation is crucial for preventing malicious actors from spamming invalid proposals.

GovernanceEngine::execute_action

  • What it does: Applies the side effects of a valid governance action directly to the state.
  • File & Line: kinetic-core/src/traits.rs:156
  • Why it matters: This function executes the actual mutation of the protocol’s state, returning any resulting GovernanceEffect that the rest of the network needs to process, such as kicking a bad actor out of the pool or rotating the genesis keys. It is only ever called after a successful verification.

How This Connects to the Rest of Kinetic

CROSS-CRATE: The concrete implementations of these traits are scattered across the workspace to maintain modularity and strict isolation of concerns.

  • The VdfEngine trait is concretely implemented in the kinetic-vdf crate. This implementation wraps the heavy chiavdf C++ FFI code and handles its complicated compilation via a custom build.rs script. By placing this in a separate crate and hiding it behind a trait, we prevent the kinetic-core crate from directly depending on C++ build toolchains, keeping the core pure, portable, and fast to compile.
  • The StorageEngine trait is implemented in the kinetic-storage crate. This crate acts as a dedicated wrapper around the Sled embedded database, managing its initialization, configuration caching, and graceful shutdown procedures.
  • The GovernanceEngine trait is implemented within kinetic-core/src/governance/engine/, where different sub-modules represent different governance models. The specific model is selected at compile time via the network.json configuration file, ensuring only the necessary code is bundled.

FORWARD DEPENDENCY: The actual node instantiation in crates like kinetic-node (which acts as the binary entry point) is responsible for taking these concrete implementations, boxing them up (for example, wrapping them in a Box<dyn StorageEngine> or Arc<dyn StorageEngine>), and passing them into the core networking and consensus services via dependency injection. Whenever you are tasked with adding a new storage backend (like RocksDB), creating a mock implementation for automated testing, or integrating a new VDF mechanism (like a GPU-accelerated prover), you will be implementing these exact traits. The core consensus loop inside Kinetic only ever speaks to dyn VdfEngine or dyn StorageEngine, ensuring absolute separation of concerns and preventing spaghetti dependencies.

Quick Reference

  • Send + Sync Required: All engine traits require these marker traits. This means their implementations must be inherently safe to share and move across thread boundaries in an asynchronous Tokio environment without relying on locking mechanisms like Mutexes from the caller.
  • Blocking vs Non-blocking: VdfEngine::evaluate blocks the thread and must run on a dedicated OS thread (via spawn_blocking); however, VdfEngine::verify is fast and can run anywhere, even inside an async networking loop without starving other tasks.
  • Prefix Isolation Design: The StorageEngine implicitly expects all keys to be prefixed with {NETWORK_ID}_. This is a deliberate architectural design choice to allow multiple networks to share a single unified database instance without leaking or corrupting data across boundaries.
  • Two-Step Governance Process: You must always call verify_action before calling execute_action. Protocol state must never be mutated during the verification step. State changes are reserved for the execution phase to guarantee safety and prevent state corruption upon failure.
  • Byte Array Abstraction: All storage interactions deal in raw &[u8] slices and bytes::Bytes objects. The serialization logic (whether it is JSON, Bincode, or MessagePack) happens above this layer in the core protocol, meaning the storage engine itself is oblivious to data schemas.
  • Error Propagation: Operations consistently return standard Result aliases like Result<_, StorageError> or Result<_, VdfError>, encapsulating internal state failures to the caller so they can decide how to handle them gracefully (e.g., retrying or crashing).

Open Questions / Things to Revisit

  • scan_prefix Memory Usage Warning: The scan_prefix method in the StorageEngine currently returns a fully realized memory-allocated Vec<(Vec<u8>, Vec<u8>)>. If the database grows massive, retrieving millions of keys into memory at once could cause an Out-Of-Memory (OOM) panic on smaller nodes. We should strongly consider refactoring this to return a boxed Iterator or a streaming async stream (like BoxStream) in the future to handle large queries efficiently and bound memory consumption to safe limits.
  • Governance Compile-Time Selection Limits: The governance engine is currently selected at compile-time via the GOVERNANCE_MODEL constant. This limits a single compiled node binary from simultaneously participating in networks with different governance models. Would using runtime dynamic dispatch (Box<dyn GovernanceEngine>) be a better approach long-term for node operators managing multiple distinct networks on the same server instance?
  • VDF Executor Isolation Enforcement: The documentation warns not to run evaluate on an async executor thread due to its blocking nature. Is there a way we can statically enforce this at the type level, or perhaps wrap the engine in a specialized executor handle, to prevent developers from accidentally misusing it and hanging the network node?
  • Missing delete_prefix: The StorageEngine currently lacks a bulk delete_prefix method. When purging a network namespace, looping over scan_prefix and calling delete on each key individually is inefficient and creates significant write-amplification. We should consider adding an optimized bulk deletion method to the interface to handle large-scale rollbacks.
  • Error Handling Granularity: The traits return generic crate-level errors (StorageError, VdfError). While they contain internal error codes (like KIN-VDF-001), returning Result types tied to a specific enum variant could make error matching cleaner for the caller.
  • Asynchronous Storage API: The StorageEngine trait methods are currently synchronous. While Sled is fast, if we ever switch to an external or cloud database like PostgreSQL, network latency will block the thread. Should we convert these trait methods to async fn using the async_trait macro or Rust 1.75 native async traits to prevent stalling the runtime?

Crate: kinetic-core

Stage: 17

Reading Time: 20 minutes

Depends On: network_constants (network.json), types::names, constants

What Is This?

This file contains the core consensus mathematical formulas used by the Kinetic network. It primarily calculates the required physical computational time (Verifiable Delay Function iterations) needed to claim a name. Because Kinetic does not use a built-in cryptocurrency token for purchasing domains, it requires an alternative economic barrier. This module calculates two primary anti-abuse metrics:

  1. The VDF “Squatter Cliff” curve, which penalizes short names with massive iteration requirements.
  2. The “Steal Decay Math,” which uses an inverse-square formula to gracefully decay protective difficulty on abandoned names.

Why Kinetic Needs This

Kinetic aims to build a truly decentralized naming system where users own their identities without paying perpetual rent. However, in any system where domains are ostensibly “free,” the immediate threat is domain squatting. A malicious actor with automated bots could instantly claim every single short, recognizable name (like ai.kin, pay.kin, or x.kin). Traditional systems like ENS solve this by charging monetary fees or running auctions for short names. Kinetic solves this by requiring time. The Squatter Cliff uses exponential scaling rather than linear scaling. A linear scale would fail because it wouldn’t create a strong enough deterrent for a motivated attacker with a botnet. If we did not have the Squatter Cliff, a botnet could sweep up millions of short names in a matter of seconds. By enforcing the Squatter Cliff, we force the attacker to dedicate a massive amount of sequential processing power to just one single name. A 1-character name requires effectively 100 years of sequential VDF generation. This effectively makes 1-character names impossible to register via casual brute force. A 2-character name takes 30 days of 100% CPU time. A 3-character name takes 24 days of constant VDF generation. This creates a powerful economic disincentive for squatters and hoarders. Simultaneously, it leaves normal-length user names (like saifmukhtar.kin) at a manageable 30-minute baseline.

Furthermore, because there are no renewal fees in Kinetic, we face a critical “graveyard problem.” If a user loses their private key, dies, or a bot registers a name and abandons it, the name would be permanently locked forever. To solve this, Kinetic requires active name owners to periodically broadcast cryptographic “heartbeats.” When these heartbeats stop, the name officially enters an “idle” state on the network. We need a decentralized mechanism to recycle these idle names back into the public pool. The Steal Decay math is crucial because identity systems need to be fluid over decades. If someone registers a name but never uses it, the network considers it wasted space. However, we cannot just drop the shield instantly. A brief network outage or a power failure could cause a legitimate user to lose their valuable name. The Steal Decay Math provides a graceful degradation curve. Using an inverse-square curve, the difficulty to steal the name stays high for a very long time. The inverse square ensures it’s virtually impossible to steal a name that just went offline yesterday, protecting against temporary internet outages. Then, it dramatically drops down to the baseline difficulty once the target idle duration is reached. This strikes the perfect balance between protecting legitimate users during temporary outages and eventually recycling permanently abandoned names.

How It Works

The math in this module relies on the parameters injected into the constants.rs file. These constants are sourced directly from the network.json configuration file during the Rust build process. This strict injection ensures that every node on the Kinetic network agrees exactly on the math parameters.

1. The VDF Squatter Cliff Calculation

When a user attempts to register a new name, the node must determine exactly how many VDF iterations they must perform. -> See: kinetic-core/src/consensus_math.rs — Lines 53 to 60 (required_iterations)

  1. Normalization: The function takes the raw string and normalizes it using IDNA conversions to handle unicode characters.
  2. TLD Stripping: It extracts the apex label by stripping the .kin Top Level Domain (TLD).
  3. Cheat Prevention: This stripping ensures that users cannot cheat the character count length by appending the TLD multiple times (e.g. foo.kin.kin).
  4. Dev Mode Bypass: If the node is currently running in development mode via is_dev_mode(), the math is bypassed. -> See: kinetic-core/src/consensus_math.rs — Lines 66 to 68
  5. Fast Testing: It returns a very small, flat number of iterations (typically 1,000) purely so developers can test network logic without waiting 30 minutes.
  6. Hardware Baseline Calculation: The network possesses a global BASE_ITERATIONS constant.
  7. Benchmarking: This represents a hardware anchor benchmark (the number of iterations a standard modern CPU can perform in 30 minutes).
  8. Time Normalization: It pulls the TARGET_MINUTES constant to normalize the calculation.
  9. Length Matching: The core economic logic is executed via a match statement on the exact string length of the extracted label (label.len()). -> See: kinetic-core/src/consensus_math.rs — Lines 78 to 91
  10. Character Count Independence: The squatter cliff curve is purely a function of the length of the string in characters.
  11. Unicode Support: It does not look at the bytes, meaning that emojis or other multibyte unicode characters are counted appropriately once normalized.
  12. Emoji Parity: This ensures a 1-emoji name is treated exactly the same as a 1-letter name.
  13. Multiplier Application: A closure named calc is defined to multiply the baseline iterations by a specific factor.
  14. Overflow Protection: The closure performs the math using u128 integers to prevent overflow, before safely casting back down to u64.
  15. Length 0-1: Maps to a multiplier that equals 100 years of computation.
  16. Length 2: Maps to a multiplier corresponding to 30 days.
  17. Length 3: Maps to 24 days.
  18. Length 4: Maps to 15 days.
  19. Length 5-7: Rapidly drops from 1 day down to a few hours.
  20. Length 21+: Plummets to exactly BASE_ITERATIONS.
  21. Baseline Return: Any name 21 characters or longer takes the flat baseline of 30 minutes.

This engineered cliff penalizes ultra-short names while ensuring that regular user identities remain accessible.

2. The Inverse-Square Steal Decay Math

When a name already exists on the network, its owner must continuously send Drand-synchronized heartbeats. When they stop, the name enters an “idle” state, and the required VDF effort to take over this name begins to decay. -> See: kinetic-core/src/consensus_math.rs — Lines 110 to 130 (steal_difficulty)

  1. Measuring Idleness: The network tracks idleness in terms of Drand “kyns” (the network’s fundamental unit of epoch time).
  2. Kyns Idle: The parameter kyns_idle indicates exactly how many epochs the name has missed its heartbeat.
  3. The Target Threshold: steal_target_kyns defines the exact point in time at which the difficulty multiplier should drop precisely to $1\times$.
  4. Vulnerability Point: For example, if this is set to one year’s worth of kyns, the name is fully vulnerable after one year.
  5. The Inverse-Square Multiplier Formula: The formula compares the target kyns squared against the idle kyns squared: (target_kyns / (kyns_idle + 1))^2.
  6. Decay Shape: Because the relationship is squared, the difficulty drops very slowly at first, and then accelerates as it gets closer to the target.
  7. Halfway Attempt: If someone tries to steal a name when it is only halfway to the target idle time, the required VDF iterations are exactly $2^2 = 4$ times higher than the baseline effort.
  8. Early Attempt: If someone tries to steal it early, say at 1/10th of the target time, the effort is an $10^2 = 100$ times higher.
  9. Applying the Multiplier: The calculated multiplier is applied directly to the base_iterations argument.
  10. Relative Baseline: Note that this base_iterations is NOT the network baseline; it is the specific baseline for that name calculated by the Squatter Cliff.
  11. Persistent Cliff: This means an abandoned 2-character name still requires 30 days of effort even after the steal decay has fully run its course.
  12. Capping and Safety: The final mathematical result is capped at u64::MAX.
  13. Saturation Point: If an attacker tries to steal a name that just went offline 1 epoch ago, the math might output a number larger than a 64-bit integer can hold.
  14. Panic Prevention: Rather than panicking and crashing the node, it saturates at u64::MAX, making the steal attempt impossible to complete before the universe ends.

Key Pieces

ConsensusParams (Struct)

-> See: kinetic-core/src/consensus_math.rs — Lines 21 to 24 What it does: This structure encapsulates the stateful configuration values required for all consensus math operations. Current State: Currently, it holds the steal_target_kyns value. Why it matters: It allows the mathematical functions to operate on stateful parameters rather than hardcoded global static variables. Testing Benefit: This dependency injection makes unit testing much easier. Mocking: Developers can instantiate a mock ConsensusParams struct with custom values to verify edge cases without needing to modify the global network configuration.

calculate_hardware_anchor()

-> See: kinetic-core/src/consensus_math.rs — Lines 36 to 38 What it does: This simple function returns the BASE_ITERATIONS constant, which is injected from the network constants file generated during the build. Why it matters: This serves as the fundamental anchor for all time-based proofs in the Kinetic network. Scaling Basis: It ensures that the squatter cliff multipliers are always scaling relative to the physical hardware benchmark. Security: If a rogue node operator manually lowers this constant in their local build, all of their generated VDF proofs will be rejected by the rest of the network.

required_iterations(name: &str)

-> See: kinetic-core/src/consensus_math.rs — Lines 53 to 60 What it does: This is the highest-level entry point used by the daemon. Normalization: It takes a raw string input, normalizes it into lowercase IDNA format, and strips out the Top Level Domain suffix. Why it matters: This ensures that users cannot manipulate the system. Case Safety: If they submit ExAmPlE.KiN, it is correctly normalized to example.kin. Length Safety: It also prevents the .kin suffix itself from counting towards the character length of the name, ensuring that a 3-character label is truly evaluated as 3 characters.

required_iterations_by_label(label: &str)

-> See: kinetic-core/src/consensus_math.rs — Lines 65 to 92 What it does: This function contains the actual Squatter Cliff math. Measurement: It measures the string length of the provided label and applies the corresponding massive multiplier to the network’s baseline iterations. Why it matters: This is the core economic engine of the network. Defense Mechanism: The multipliers used here (such as CONSENSUS_SQUATTER_LEN_2) are the primary defense mechanism preventing malicious botnets from draining the namespace.

steal_difficulty(base_iterations, kyns_idle)

-> See: kinetic-core/src/consensus_math.rs — Lines 110 to 130 What it does: This function calculates the inverse-square multiplier based on exactly how long a specific name has been dead (idle). Application: It applies this multiplier to the provided base_iterations argument. Why it matters: This provides a decentralized, enforced garbage collection system for the global namespace. Elegance: It solves the abandoned domain problem without relying on centralized registrars, auctions, or recurring monetary fees.

How This Connects to the Rest of Kinetic

FORWARD DEPENDENCY: Validation Engines The kinetic-core/src/validation module relies on these mathematical functions. When a node receives a Gossipsub message over the peer-to-peer network claiming a new name registration, it triggers validation. The validation engine will immediately call ConsensusParams::required_iterations for that name. It then checks if the VDF proof attached to the message actually performed that many iterations. If the proof is too short, the validation fails. The message is instantly dropped and the peer may be penalized for sending invalid data.

FORWARD DEPENDENCY: The VDF Prover & Client Daemon The kinetic-vdf crate and the client daemon use these math functions to figure out exactly how much work they need to do. They perform this check before they start solving the cryptographic puzzle. When a user runs a command to register a name, the client queries the daemon for the required iterations. The daemon calculates it using required_iterations. It warns the user if the registration will take 30 days to complete. Then it spins up the local VDF solver instructing it to execute exactly that many iterations.

CROSS-CRATE: Network Config Generation (build.rs) The multipliers and base iterations used throughout this file are not hardcoded in the Rust source code. They are dynamically generated by the build.rs script reading from the network.json configuration file at compile time. This architectural decision means that changing the Squatter Cliff curve is very easy. Adjusting the baseline difficulty for a new network fork does not require modifying consensus_math.rs directly. It only requires modifying the JSON config and recompiling the node software.

Quick Reference

  • Base Iterations (BASE_ITERATIONS): The absolute number of VDF iterations corresponding to the network’s target time benchmark.
  • Hardware Calibration: Usually calibrated to 30 minutes on modern consumer hardware.
  • Squatter Cliff: The exponential curve penalizing short names.
  • 1 Char Target: Impossible (Targeting 100 years of computation).
  • 2 Chars Target: Targeting 30 days of computation.
  • 21+ Chars Target: Targeting 30 minutes (Baseline).
  • Steal Target Kyns (STEAL_TARGET_KYNS): The precise duration (measured in Drand network epochs) that a name must be dead.
  • Decay Point: This is the point before the steal difficulty multiplier drops back down to $1\times$.
  • Inverse-Square Decay: The specific mathematical formula that keeps steal difficulty high initially.
  • Decay Landing: It decays rapidly as time goes on, eventually landing exactly at $1\times$ effort.
  • Dev Mode Override: An override switch that flattens all mathematical requirements to a trivially small number of iterations (e.g., 1000) for rapid local development and testing.

Open Questions / Things to Revisit

  • Integer Overflows in calc Closure: While steal_difficulty safely caps its output at u64::MAX, is there a risk in calc?
  • Absurd Multipliers: Is it possible that the u128 multiplication occurring during the calc closure in the Squatter Cliff could panic if someone configures network.json with absurdly large custom multiplier numbers?
  • Safer Math: Using a saturating_mul might be a safer architectural choice than standard multiplication here.
  • Label Normalization Overhead: The required_iterations function actively allocates new strings during the normalize_name process.
  • DoS Vector Potential: Since this function is called inside the hot path during Gossipsub peer validation, could this become a Denial of Service (DoS) vector?
  • Malformed Strings: A malicious peer could flood the node with massive, malformed unicode strings, causing constant heap allocations.
  • Refactor Candidate: We might want to require that validation engines pass pre-normalized labels into this function to avoid redundant heap allocations.
  • Hardware Drift Resilience: The documentation notes that hardware drift (computers getting faster over time) is handled manually via network updates.
  • ASIC Threats: If specialized ASICs are developed for Kinetic’s specific VDF construction, the BASE_ITERATIONS anchor will become obsolete.
  • Farming Short Names: This means 2-character short names will become farmable much faster than the intended 30 days.
  • Dynamic Difficulty: Should Kinetic implement an automatic, dynamic difficulty adjustment algorithm (similar to Bitcoin’s retargeting) rather than relying on manual hard forks?
  • Fractional Time Targets: TARGET_MINUTES is defined as an f64 float in the configuration file but is cast to a u64 inside the calc closure.
  • Loss of Precision: This cast loses all sub-minute precision.
  • Divide by Zero Risk: If a developer sets TARGET_MINUTES to 0.5 in a test network configuration, this will truncate to 0, causing a divide-by-zero panic during execution. This should be patched to use floating point math for the division or enforce a minimum integer bound of 1.

Crate: kinetic-core

Stage: 18 (Miscellaneous Core)

Reading Time: 15 minutes

Depends On: network.json, build.rs, Axum, Tokio, Sled

What Is This?

This document covers the miscellaneous but essential foundational elements of the kinetic-core crate. We are looking at a collection of files that act as the structural glue holding the protocol together. First, we have lib.rs, which serves as the crate root. It maps the entire crate architecture and re-exports critical error types for ease of use. Next, we have constants.rs, the immutable rulebook of the Kinetic network. It hardcodes everything from cryptographic constraints to database prefixes. We also deeply cover net.rs, which contains specialized network security primitives. These primitives are specifically designed to prevent Server-Side Request Forgery (SSRF) attacks. Furthermore, this module includes request_id.rs. This is a robust mechanism for task-local correlation ID generation. It is used in asynchronous tracing to track API requests across threads. Finally, we examine shutdown.rs. This provides cross-platform handlers for graceful node termination. Together, these seemingly disparate files define the environment. They define the limits. They define the operational safety boundaries of a Kinetic node.

Why Kinetic Needs This

A decentralized naming network like Kinetic operates in an inherently hostile environment. It is asynchronous. It is heterogeneous. These files address specific operational realities of that environment.

First, consider the necessity of constants.rs. A decentralized network only achieves consensus if every single participant agrees on the exact same rules. If one node thinks a DHT record lives for 2 hours, state will diverge. If another thinks it lives for 4 hours, the network will fracture. By hardcoding timing limits, we guarantee consistency. By hardcoding Proof-of-Work difficulty bits, we lock the security parameters. By hardcoding cryptographic key iterations, we standardize computation limits. By hardcoding database namespaces into a single constants file, we avoid collisions. We ensure that the compiled binary is locked to a specific protocol version. Furthermore, dynamically namespacing these constants using NETWORK_ID prevents disastrous mistakes. For example, it stops a user from accidentally pointing a Mainnet binary at a Testnet database directory.

Second, consider the security implications addressed by net.rs. Kinetic nodes often act as proxies or HTTP gateways. They fetch resources on behalf of decentralized applications. If a user can command a Kinetic node to fetch resources, they might act maliciously. A malicious user might instruct the node to fetch 127.0.0.1:1337. Or they might target 10.0.0.1:80. This is known as Server-Side Request Forgery (SSRF). The attacker could bypass external firewalls. They could access internal administrative panels. They could probe the node’s local network for vulnerabilities. Kinetic requires a watertight, zero-tolerance IP filtering mechanism. It must ensure it only ever proxies to legitimate, external, public-facing IP addresses.

Third, asynchronous tracing in Rust requires special handling. This is exactly why request_id.rs exists. Kinetic uses the Tokio asynchronous runtime. This means a single HTTP API request might jump across multiple operating system threads. Futures yield and resume based on I/O readiness. If an error occurs deep within a storage layer, the logs will be useless. They are useless if we cannot tie them back to the specific HTTP request that initiated the action. Passing a request_id down through every single function parameter signature is terrible. It would cause massive ergonomic damage to the codebase. Kinetic needs a way to transparently attach correlation IDs to execution contexts.

Finally, graceful shutdown via shutdown.rs is mandatory. It is mandatory for data integrity. If a node operator presses Ctrl+C, instantly killing the process is dangerous. The Sled database might be in the middle of writing a page to disk. Killing it could lead to corruption. The libp2p network swarm might drop connections suddenly. This happens without sending proper termination messages to peers. This causes routing disruptions across the DHT. Kinetic must intercept these termination signals. It must block the immediate exit. It must allow the asynchronous executor to gracefully spin down its tasks. It must flush its data to disk. It must bid farewell to its peers.

How It Works

The Protocol Rulebook: constants.rs

The constants.rs file acts as the immutable anchor for the protocol. It begins by dynamically injecting configuration generated by build.rs. This configuration comes from network.json. The macro include!(concat!(env!("OUT_DIR"), "/network_constants.rs")); does the heavy lifting. It pulls in values like the Top-Level Domain (TLD) and the NETWORK_ID at compile time. This ensures that the binary is permanently branded. It belongs to a specific network.

The file defines critical timing constraints for the Kademlia DHT. The constant KADEMLIA_PROVIDER_RECORD_TTL_SECS is set to 4 hours. That is 14,400 seconds. This dictates how long the decentralized network will remember a peer. It dictates how long it remembers that a specific peer hosts a specific name record. To complement this, KADEMLIA_PUBLICATION_INTERVAL_SECS is set to 3 hours. This creates a one-hour overlap window. During this window, a node republishes its records before they expire. This ensures high availability across the DHT.

Cryptographic limits are firmly established here as well. POW_DIFFICULTY_BITS is set to 15. This defines the amount of work required for certain network operations. Key derivation constraints are also hardcoded. WALLET_PBKDF2_ITERATIONS is set to 600,000. KEYGEN_PBKDF2_ITERATIONS is set to 2,048. These are hardcoded to balance security against brute-force attacks. They must also meet the practical performance needs of the application. The ARGON2_MEMORY_COST_KB is locked at 16,384. This equals 16MB. It fixes the memory hardness for specific hashing routines.

A crucial feature of constants.rs is strict namespacing. It namespaces storage and network channels. Variables like DB_PREFIX_OWNED_NAMES are constructed dynamically. DB_PREFIX_REVEAL is also constructed dynamically. GOSSIP_TOPIC_GOVERNANCE uses macros like concat!("n:", env!("KINETIC_NETWORK_ID"), "_owned_names"). This means that if NETWORK_ID is mainnet, the database prefix is n:mainnet_owned_names. If a node operator accidentally runs a Testnet binary against a Mainnet Sled database, it is safe. The binary will look for n:testnet_owned_names. It will find nothing. It will avoid corrupting the Mainnet data. It provides a robust, silent safeguard against cross-environment contamination.

Finally, the file houses the absolute root of trust for the network. These are the post-quantum ML-DSA-65 keys. The prod_keys module contains ROOT_PUBLIC_KEY_HEX. This is an air-gapped key used for signing governance proposals. To prevent accidental modification of this critical asset, we have a test. A dedicated unit test verifies the exact SHA-256 fingerprint of this hex string. If a developer accidentally deletes a character, the test fails. The test suite will instantly fail with a critical security alert.

SSRF Prevention: net.rs

The net.rs file implements a singular, paranoid security function. The function is is_ssrf_safe(ip: IpAddr). When a Kinetic proxy needs to forward traffic, it resolves the domain to an IP. It then passes the IP through this function.

For IPv4 addresses, the function inspects the properties of the address. It uses Rust’s standard library methods. It immediately rejects anything that evaluates to true for is_loopback(). This includes 127.0.0.1. It rejects is_unspecified(), like 0.0.0.0. It rejects is_private(), which covers the RFC 1918 spaces. These are 10.x.x.x, 172.16.x.x, and 192.168.x.x. It rejects is_link_local(). It rejects is_broadcast(). It rejects is_multicast(). It rejects is_documentation().

However, standard library checks are sometimes not enough. The function manually extracts the octets to check for edge cases. It blocks 0.0.0.0/8 by checking if the first octet is 0. It also checks for Carrier-Grade NAT (CGNAT) ranges. It ensures the IP does not fall into the 100.64.0.0/10 block. While CGNAT is technically public-facing for ISPs, it is dangerous. Routing to it internally can sometimes expose router administration interfaces. This makes it a significant SSRF risk.

For IPv6 addresses, the function is even more complex. This is due to the massive variety of IPv6 addressing modes. It blocks loopback. It blocks unspecified addresses. It blocks multicast addresses. It manually checks segments to block IPv6 Link-Local. Link-Local is fe80::/10. It blocks Unique Local addresses. Unique Local is fc00::/7.

Crucially, it checks for IPv4-mapped IPv6 addresses. An attacker might try to send an address like ::ffff:127.0.0.1. They do this to bypass the IPv4 loopback filter. The function calls v6.to_ipv4_mapped(). If it succeeds, it recursively calls is_ssrf_safe on the underlying IPv4 address. It also blocks the 64:ff9b::/96 NAT64 prefix. This prefix is used to translate IPv6 packets to IPv4. It presents another avenue to smuggle internal IPs through external proxies.

Asynchronous Tracing: request_id.rs

The request_id.rs file leverages a Tokio-specific macro. The macro is tokio::task_local!. It defines CURRENT_REQUEST_ID. Unlike std::thread_local!, which ties data to an operating system thread, this is different. task_local! ties data to a Tokio asynchronous task. This means that even if a future is suspended, the data is safe. It can be subsequently resumed on an different OS thread in the executor pool. The request_id remains intact.

The system relies on an AtomicU64 counter. It is initialized to 1. When a new API request enters the system, the scope function is called. It uses COUNTER.fetch_add(1, Ordering::Relaxed). This atomically increments the counter. It generates a string like "req-42".

The core magic happens in the line CURRENT_REQUEST_ID.scope(id, f).await. This takes the generated ID and a future f. It binds the ID to the execution context of that specific future. While that future runs, any nested function can access it. No matter how deep the function is, it can call current(). The current function attempts to read the task-local variable. It uses the try_with method. If successful, it clones the string and returns it. If it fails, it means the code is running outside of a wrapped scope. It safely degrades to returning "no-request-id". It does this instead of panicking.

There is also a scope_with_id function. This is designed for environments where an external correlation ID already exists. For example, if a client sends an X-Request-ID HTTP header. The Axum middleware can parse that header. It can pass it to scope_with_id. This ensures that the client’s original tracing ID is propagated. It propagates throughout the Kinetic node’s internal logs.

Graceful Termination: shutdown.rs

The shutdown.rs file provides the async function shutdown_signal(). This function abstracts away the underlying operating system differences. It provides a unified termination future.

On non-Wasm platforms, it sets up two distinct asynchronous listeners. This includes Linux, macOS, and Windows. The first is tokio::signal::ctrl_c(). This listens for the SIGINT signal. It is sent when a user presses Ctrl+C in the terminal. The second listener is specific to Unix platforms. It uses tokio::signal::unix::signal. It listens for SIGTERM. This is the standard signal sent by process managers. Systemd, Docker, or Kubernetes use this when they ask a container to shut down.

These two futures are placed inside a tokio::select! block. The select! macro races the futures against each other. Whichever signal resolves first will execute its corresponding block. It prints an informational log message. The message is "SIGTERM received, starting graceful shutdown". Then the overall shutdown_signal future will complete.

The main daemon loop typically tokio::select!s on its primary application logic. It races it against this shutdown_signal(). When the signal resolves, the main loop breaks. The application proceeds to execute its teardown code.

On WebAssembly, the environment is different. This is when target_arch = "wasm32". A browser tab does not have standard OS signals like SIGINT or SIGTERM. To keep the codebase uniform without littering it with cfg attributes, we adapt. The Wasm implementation of shutdown_signal() simply returns std::future::pending::<()>().await. This creates a future that never resolves. It ensures that the Wasm application loop continues indefinitely. It runs until the browser environment forcibly terminates the WebAssembly instance.

The Crate Root: lib.rs

The lib.rs file orchestrates the module structure. It uses #![deny(missing_docs)]. This enforces strict documentation standards across the entire crate.

It handles conditional compilation gracefully. kinetic-core is meant to be shared across multiple binaries. This includes the daemon, the RPC proxy, and lightweight WebAssembly clients. It must compile safely everywhere. Modules like api_error and request_id are gated. They use #[cfg(not(target_arch = "wasm32"))]. This is because a Wasm client running in a browser does not need them. It does not handle Axum HTTP requests. It does not generate server-side request IDs.

The crate root also serves as a facade for error handling. It prevents forcing developers to import kinetic_core::error::KineticError manually. Instead, it re-exports the primary error taxonomy directly at the crate root. This provides a clean, ergonomic API surface. It benefits the rest of the Kinetic ecosystem.

Key Pieces

constants::POW_DIFFICULTY_BITS

-> See: kinetic-core/src/constants.rs — Line 27 This constant defines the required number of leading zero bits. It is used for Proof-of-Work operations in the Kinetic network. It ensures that operations requiring spam resistance demand a consistent amount of computational effort. It is verifiable from all participants.

constants::KADEMLIA_PROVIDER_RECORD_TTL_SECS

-> See: kinetic-core/src/constants.rs — Lines 62 to 63 Sets the Time-To-Live for DHT provider records. It is set to 4 hours. This is a critical consensus parameter. If nodes do not agree on how long a record should live, they will prematurely drop data. Or they might hold onto stale data, breaking name resolution routing.

constants::KADEMLIA_PUBLICATION_INTERVAL_SECS

-> See: kinetic-core/src/constants.rs — Lines 65 to 66 Sets the republish interval for DHT records. It is set to 3 hours. This ensures a one-hour overlap where nodes can refresh their records.

constants::DB_PREFIX_OWNED_NAMES

-> See: kinetic-core/src/constants.rs — Lines 74 to 75 A dynamic database prefix. It is constructed at compile time using the NETWORK_ID. By injecting the network identifier into the byte array, it prevents collisions. It guarantees that the Sled database engine creates separate namespaces. It prevents state corruption if a single directory is accidentally shared between networks.

constants::ENV_DATA_DIR

-> See: kinetic-core/src/constants.rs — Lines 102 to 103 Constructs the environment variable string. It is used to override the data directory. For example, it generates KINETIC_MAINNET_DATA_DIR. This dynamic naming allows users to run multiple environments on the same machine.

constants::prod_keys::ROOT_PUBLIC_KEY_HEX

-> See: kinetic-core/src/constants.rs — Lines 41 to 42 The offline, air-gapped ML-DSA-65 post-quantum root of trust key. This hex string is the ultimate authority for governance proposals. It is protected by a dedicated SHA-256 fingerprint test. The test is in the same file to prevent accidental tampering.

net::is_ssrf_safe

-> See: kinetic-core/src/net.rs — Lines 23 to 90 The impenetrable firewall function for outgoing IP connections. It comprehensively blocks loopback. It blocks private RFC 1918 addresses. It blocks CGNAT, link-local, and multicast. It handles complex IPv6 edge cases like IPv4-mapped addresses. It ensures the node only connects to legitimate public infrastructure.

request_id::scope

-> See: kinetic-core/src/request_id.rs — Lines 21 to 25 The entry point for generating task-local correlation IDs. It atomically increments a counter. It generates a string like "req-N". It uses CURRENT_REQUEST_ID.scope(id, f) to bind that string. It binds it to the asynchronous execution context of the provided future.

request_id::current

-> See: kinetic-core/src/request_id.rs — Lines 14 to 19 Retrieves the correlation ID for the current asynchronous task. If the code is running outside of a wrapped scope, it handles it safely. It safely returns "no-request-id" instead of panicking. This ensures that logging macros never crash the application.

shutdown::shutdown_signal

-> See: kinetic-core/src/shutdown.rs — Lines 8 to 35 A cross-platform future. It resolves when the operating system issues a termination request. This could be SIGINT or SIGTERM. It allows the main executor loop to unblock. It begins the graceful teardown of network sockets and database handles.

lib.rs Root Re-exports

-> See: kinetic-core/src/lib.rs — Lines 67 to 70 The crate root re-exports KineticError and PublishError. It also exports RecordRejectReason, and ResolutionError. This provides a flattened, ergonomic API surface. It allows dependent crates to simply use kinetic_core::KineticError;. They do not need to traverse deep module paths.

How This Connects to the Rest of Kinetic

  • FORWARD DEPENDENCY: The HTTP daemons in kinetic-node or kinetic-daemon will rely on request_id::scope_with_id. Their Axum middleware will parse incoming headers. They establish the scope. They pass the future to the handler, ensuring all subsequent logging is traceable.

  • CROSS-CRATE: The net::is_ssrf_safe function is a critical dependency for kinetic-rpc. It is used for any proxy subsystems. Whenever the network resolves a .kin domain to a backend IP and attempts to open a socket, it is checked. It must pass the resolved IP through this gatekeeper first. This prevents malicious routing into the node’s internal network.

  • CROSS-CRATE: The storage engines using the Sled database depend on constants.rs. They use it to structure their disk state. They use the DB_PREFIX_* constants to open specific Sled trees. The dynamic injection of NETWORK_ID into these prefixes is what prevents catastrophic database collisions. It protects Mainnet from Testnet nodes.

  • CROSS-CRATE: The governance verification logic located in kinetic-core/src/governance/logic.rs imports the keys. It imports constants::prod_keys::ROOT_PUBLIC_KEY_HEX directly. It parses this hex string into a cryptographic public key. It uses it to verify the signatures attached to incoming network parameter updates.

  • FORWARD DEPENDENCY: The main entry point (main.rs) of any Kinetic binary running on a traditional OS will utilize this. It will utilize tokio::select! to race its primary application logic against shutdown::shutdown_signal(). This is what triggers the orderly shutdown of the P2P swarm. It triggers the flushing of the Sled database.

Quick Reference

  • Need to change how long records live in the DHT? Modify KADEMLIA_PROVIDER_RECORD_TTL_SECS in constants.rs. Ensure you understand the consensus implications before doing so.

  • Writing a new feature that connects to an external IP? You must validate the target by running it through net::is_ssrf_safe(ip). Do this before establishing the TCP connection.

  • Adding deep nested logs in an async function? Ensure that the log message includes a call to request_id::current(). This ensures that the log can be correlated back to the originating HTTP request.

  • Why isn’t my Wasm client responding to Ctrl+C? WebAssembly does not have OS signals. The shutdown_signal() function in Wasm simply returns a pending future. This future never resolves. Wasm clients are terminated by the browser environment.

  • Why is the root key test failing? If the unit test in constants.rs fails, it means the ROOT_PUBLIC_KEY_HEX was altered. Its SHA-256 fingerprint no longer matches. If this was an authorized key rotation, update the fingerprint in the test. If not, revert the change immediately.

Open Questions / Things to Revisit

  • IPv6 SSRF Coverage in net.rs: While is_ssrf_safe handles IPv4-mapped addresses and NAT64, IPv6 has numerous obscure transition mechanisms. This includes tunneling mechanisms like Teredo, 6to4, or ISATAP. Could an attacker use one of these obscure formats to trick the kernel? Could they route a seemingly safe IPv6 address into a private IPv4 space? We may need to expand the blacklist to cover more legacy IPv6 tunneling prefixes.

  • Key Fingerprint Tests in constants.rs: We currently test the SHA-256 fingerprint of the production root key. This is to prevent accidental changes. We should strongly consider adding a similar fingerprint test for test_keys::ROOT_PUBLIC_KEY_HEX. While less critical, accidentally swapping the test key could cause confusing failures. It would break tests in the CI pipeline unexpectedly.

  • Correlation ID Generation in request_id.rs: The system currently uses an AtomicU64 to generate IDs like "req-42". While this is fast and will effectively never overflow, it provides limited value. It is limited in a distributed tracing scenario where a request spans multiple nodes. We should evaluate whether migrating to UUID v4 generation would be worth it. It might have a minor performance penalty to gain true distributed observability.

  • Signal Handling Robustness in shutdown.rs: Currently, shutdown_signal listens for the first SIGINT or SIGTERM. It then resolves. If the shutdown process hangs due to a deadlocked future, the node operator might press Ctrl+C a second time. They would expect it to force quit. We might need to implement a mechanism where a second signal bypasses the graceful shutdown. It should trigger an immediate std::process::exit(1).

kinetic-core/build.rs & High-Level Integration Tests

Crate: kinetic-core Stage: 19 Reading Time: ~45 minutes Depends On: kinetic-core/src/types.rs, kinetic-core/src/constants.rs, network.json

What Is This?

This documentation serves as a comprehensive guide to the build-time configuration engine. It covers the high-level security integration tests for the kinetic-core crate. The kinetic-core/build.rs file is a Cargo build script. In Rust, a build.rs file is compiled and executed before the main crate is built. It has the power to generate source code dynamically. It interacts with the host environment to detect variables and capabilities. It dictates exactly how the main crate will be compiled. You mentioned protobuf/capnp compilation logic in your prompt. It is important to note that as the codebase has evolved, this is no longer accurate. build.rs no longer handles .proto or .capnp schemas. Instead, its primary and exclusive responsibility is parsing the network.json configuration file. It translates this JSON file into static, optimized Rust constants. If Kinetic were to reintroduce Cap’n Proto or Protobuf for RPC or on-wire serialization, their build directives would be restored here. They would sit alongside the JSON parsing logic using prost-build or capnpc. In addition to the build script, this document analyzes the security-focused integration tests. These tests are located in the kinetic-core/tests/ directory. While unit tests are typically co-located with their source code inside src/, the tests/ directory is used for black-box integration testing. These tests interact with the crate exactly as an external consumer would. They verify critical security invariants. Invariant 1: Preventing Out-Of-Memory (OOM) attacks from bloated network payloads. Invariant 2: Preventing adversaries from hijacking subdomains to spoof identities. Invariant 3: Stopping protocol downgrade attacks, where an attacker tricks a node into using an older, vulnerable parsing standard.

Why Kinetic Needs This

1. The Necessity of Build-Time Configuration

Kinetic is designed to be a flexible, decentralized protocol. Different deployments require different sets of physical rules. The global mainnet needs strict sybil resistance. The global mainnet needs high redundancy. A local developer testnet needs fast consensus. A local developer testnet needs low VDF (Verifiable Delay Function) iterations. A local developer testnet needs reduced redundancy for rapid iteration. These rules are defined in a human-readable network.json file. However, parsing this JSON file at runtime presents several massive disadvantages. Performance Overhead: Parsing JSON at runtime requires memory allocation. It requires string matching and error handling every time a node boots. This slows down startup times, especially on embedded devices. Lost Optimization Opportunities: If configuration values are runtime variables, the Rust compiler cannot optimize them. It cannot inline them into the assembly. It cannot unroll loops based on them. It cannot optimize away dead branches. State Inconsistency: If the JSON file is modified while the node is running, it could cause issues. If a user accidentally deploys a node with a missing JSON file, the node crashes at runtime. This potentially leads to consensus failures across the network. Dependency Bloat: If we parsed JSON at runtime, the final compiled binary would need to bundle a full JSON parsing library. This would pull serde_json into the final runtime binary, increasing the binary size. By using build.rs to parse the JSON file at compile time, we solve all these problems simultaneously. Zero-Cost Abstractions: The configuration values become hardcoded, static const primitives. They are baked into the final binary. The compiler optimizes them into raw assembly values. Fail-Fast Compilation: If network.json is malformed, missing fields, or contains invalid types, the compiler panics. The build fails before a single line of runtime code is compiled. You can never accidentally deploy a misconfigured node. Security Floor Guarantees: The build script actively inspects the values. For example, if a developer sets the DHT redundancy level too low, the build script aborts the compilation. This enforces security policies at the compiler level. Network Isolation: The build script exposes the NETWORK_ID to the compiler environment. This ensures that different forks of the network cannot accidentally gossip with one another. They cannot share P2P topic strings.

2. The Necessity of Security Integration Tests

While the build script ensures the node is configured safely, the integration tests ensure that the core data structures behave securely. They must be tested against adversarial data. P2P networks operate in a zero-trust environment. A Kinetic node will receive bytes from unknown IP addresses all over the world. If the node blindly trusts those bytes, it will be destroyed. The integration tests simulate specific, known attack vectors. They test against the protocol’s core data structures, like Reveal. They ensure that our validate() and signable_bytes() methods are robust. They ensure malicious data is dropped before it reaches the consensus or storage engines. We do this in the tests/ directory rather than inside src/ because integration tests compile the crate exactly as an external user would. This guarantees that we aren’t relying on private internal functions to achieve security. The public API itself is secure by default.

How It Works: The Build Script

The build script operates in several distinct phases. It starts from locating the configuration file. It ends by finally emitting valid Rust source code.

Phase 1: Defining the Deserialization Schema

-> See: kinetic-core/build.rs — Lines 6 to 97

The script uses the serde framework to map the unstructured JSON data. It maps it into typed Rust structures. Every field in network.json has a corresponding struct definition here. Here is a breakdown of the specific structs: 1. SquatterMultipliers This struct defines the multiplier curves for the Verifiable Delay Function (VDF). Short domain names (like kin) are valuable. To prevent domain squatters from snapping them up instantly, Kinetic requires a multiplied VDF proof. This proof is required to register short names. This struct maps domain lengths to their specific VDF multipliers. It parses len_0_to_1 for 0-1 character domains. It parses len_2 for 2 character domains. It parses len_3 for 3 character domains. It parses len_4 for 4 character domains. It parses len_5 for 5 character domains. It parses len_6 for 6 character domains. It parses len_7 for 7 character domains. It parses len_8_to_10 for 8 to 10 character domains. It parses len_11_to_17 for 11 to 17 character domains. It parses len_18_to_20 for 18 to 20 character domains. 2. ConsensusConfig This defines the core consensus rules. It includes the minimum_commit_age_kyns. This is how many Drand epochs must pass between a Commit and a Reveal. This prevents front-running on the network. It includes the vdf_squatter_multipliers which wraps the previous struct. It includes vdf_discount_min_iterations. It includes vdf_discount_percentage. It includes the absolute iteration caps for the VDF (vdf_max_iterations). It includes the size limits for proofs (vdf_max_proof_bytes). 3. LimitsConfig This is crucial for node stability. It defines strict memory and sizing limits. It parses p2p_max_packet_size. It parses p2p_max_circuit_bytes. It parses proxy_max_body_bytes. It parses storage_max_value_bytes. It parses kid_max_public_key_bytes. It parses kid_max_location_bytes. It parses kid_max_endpoint_bytes. It parses drand_max_response_bytes. It parses lru_cache_size. By baking these limits into the binary at compile time, the node can pre-allocate memory safely. 4. TimeoutsConfig Defines network resilience parameters. It parses idle_timeout_seconds. This is how long a node can be idle before being dropped. It parses heartbeat_age_warning_seconds. It parses heartbeat_age_critical_seconds. It parses dns_cache_ttl_seconds. This is how long a DNS cache entry remains valid. It parses network_prune_interval_seconds. It parses host_route_max_age_seconds. 5. NetworkSection It parses tld (Top Level Domain). It parses base_domain. It parses network_id. It parses docs_url. It parses ipfs_gateway. It parses local_bind_ip. It parses bootstrap_nodes (a list of initial P2P contacts). 6. DrandSection It parses drand_genesis_time. It parses drand_period. It parses kinetic_genesis_drand_kyn. It parses drand_public_key. It parses drand_http_endpoints. 7. GovernanceSection It parses governance_model. It parses max_age_seconds. 8. AdvancedSection It parses benchmark_base_iterations. It parses benchmark_target_minutes. It parses steal_target_kyns. It parses m_redundancy. It parses dev_mode_iterations. It includes the LimitsConfig and TimeoutsConfig. 9. NetworkConfig Finally, this struct ties them all together as the root object. It implements Deserialize. If the network.json file contains a string "100" where the LimitsConfig expects an integer 100, the serde deserializer will immediately fail. It will halt the build. This provides immense peace of mind.

Phase 2: Dependency Tracking and File Resolution

-> See: kinetic-core/build.rs — Lines 100 to 116

Cargo is designed to be lazy. It caches build artifacts aggressively. It only recompiles when necessary. If build.rs simply read a file using standard I/O, Cargo wouldn’t know that the file was a dependency. Changing the JSON file wouldn’t trigger a rebuild of the Rust code. This would lead to stale configurations in the final binary. To fix this, the script issues special directives to standard output. println!("cargo:rerun-if-changed=../network.json"); println!("cargo:rerun-if-env-changed=KINETIC_NETWORK_JSON"); These directives tell Cargo to invalidate the cache if these targets change. Next, the script must locate the JSON file. It uses a strict fallback hierarchy. First, it checks Environment Override. It checks if KINETIC_NETWORK_JSON is set via std::env::var. This allows CI/CD pipelines to inject custom testing configurations dynamically. Second, it checks Workspace Root. It looks for ../network.json. This is the standard location for developers working within the repository. Third, it checks Bundled Fallback. It looks for default_network.json in the current directory as a last resort. If all three fail, it panics. It throws the error: "Failed to find network.json in any location."

Phase 3: Parsing and Code Generation Setup

-> See: kinetic-core/build.rs — Lines 118 to 127

The script reads the file contents into a string. It passes it to serde_json::from_str. Assuming this succeeds, it now holds a fully populated NetworkConfig object in memory. It then queries the environment for OUT_DIR. When Cargo runs a build script, it creates an isolated, temporary directory for generated files. It passes its path via this environment variable. The script constructs a path to OUT_DIR/network_constants.rs. It initializes a mutable String named out. The rest of the script is responsible for appending valid Rust source code to this string.

Phase 4: Emitting Constants

-> See: kinetic-core/build.rs — Lines 128 to 362

The script uses the format! macro to generate pub const definitions. It meticulously injects doc-comments (///). This ensures that the generated code is self-documenting. When you use constants::TLD elsewhere in the codebase, your IDE will display the documentation generated here. Here are the key constants generated: TLD and TLD_SUFFIX: Defines the top-level domain (e.g., kin). DID_PREFIX: Defines the Decentralized Identifier prefix (e.g., did:kin:). BASE_DOMAIN: Base infrastructure domain. NETWORK_ID: The unique identifier isolating P2P protocols. BASE_ITERATIONS: The hardware anchor for the VDF. The script documents that lowering this on mainnet is dangerous. TARGET_MINUTES: Time target for iterations. STEAL_TARGET_KYNS: The decay rate for name stealing. It determines how long a name must be inactive before it can be reclaimed. M_REDUNDANCY: The DHT replication factor. GOVERNANCE_MODEL: The swappable governance engine. LOCAL_BIND_IP: The local IP address where services bind. MAX_AGE_SECONDS: Governance proposal expiry. DEV_MODE_ITERATIONS: Iterations for dev/sim mode. CONSENSUS_MINIMUM_COMMIT_AGE_KYNS: Drand commit age rules. DRAND_GENESIS_TIME: Drand chain genesis timestamp. DRAND_PERIOD: Duration of Drand epochs. KINETIC_GENESIS_DRAND_KYN: Kinetic genesis epoch. KINETIC_GENESIS_TIME: Absolute kinetic genesis timestamp. DRAND_PUBLIC_KEY: LoE public key. DOCS_URL: Documentation URL. IPFS_GATEWAY: IPFS gateway URL. It also loops through arrays to generate slices. It generates DRAND_HTTP_ENDPOINTS as a static array slice &[&str]. It generates BOOTSTRAP_NODES as a static array slice &[&str]. It unwraps all the limits from the LimitsConfig. It writes them out as LIMITS_... constants. It unwraps all the timeouts from TimeoutsConfig. It writes them out as TIMEOUTS_... constants.

Phase 5: The Eclipse Attack Safety Floor

-> See: kinetic-core/build.rs — Lines 168 to 184

This is a masterclass in using build scripts for security engineering. The script reads config.advanced.m_redundancy. This variable controls the Kademlia DHT replication factor. It dictates how many distinct, independent nodes must store a piece of data. If an attacker surrounds a node with malicious peers, they can censor data. This is known as an Eclipse attack. High redundancy makes this statistically improbable. The canonical mainnet uses a redundancy of 32. The build script enforces a hard floor. If m_redundancy < 5, it deliberately panics. This is a compile-time security guarantee. It is physically impossible to produce a Kinetic binary that is catastrophically vulnerable to basic Eclipse attacks. The compiler itself refuses to build it. If a developer tries to compile with m_redundancy = 4, the build instantly fails with this panic message.

Phase 6: Compiler Environment Injection

-> See: kinetic-core/build.rs — Lines 236 to 245

While generating a file is useful, sometimes we need values directly injected into the compiler’s environment variables. The script uses the cargo:rustc-env directive. It exposes KINETIC_NETWORK_ID directly to the rustc compiler. It exposes KINETIC_NETWORK_ID_UPPER directly to the rustc compiler. Why do this? Because in cryptographic functions, we often use the concat! macro to build domain separators. For example: concat!(env!("KINETIC_NETWORK_ID"), "-reveal"). The concat! macro is processed at compile time. It only works with string literals and environment variables. It cannot read constants from a generated file. By exposing the network ID as an environment variable, we enable zero-cost string concatenation for cryptographic prefixes.

Phase 7: Writing to Disk

-> See: kinetic-core/build.rs — Lines 363 to 364

Finally, the script calls fs::write. It dumps the massive generated string into network_constants.rs. The kinetic-core crate will later use include!(concat!(env!("OUT_DIR"), "/network_constants.rs")). This pulls this code into its module tree, completing the build-time generation cycle.


How It Works: The Integration Tests

The tests in kinetic-core/tests/ are designed to attack the data structures generated and validated by the core protocol. They verify that they hold up under duress.

1. The OOM Payload Bomb Test

-> See: kinetic-core/tests/test_003_oom_payload_bomb.rs

The Vulnerability: In Rust, Vec<u8> is dynamically sized. When deserializing network data, a node reads a header. If that header claims the payload is 500MB, the default behavior of many serializers is to allocate 500MB of RAM immediately. If an attacker spams 100 of these headers, the node attempts to allocate 50GB of RAM. The operating system’s OOM killer will terminate the node process. This is a trivial remote Denial-of-Service (DoS) attack. The Test Execution: The test constructs an attack vector. let oversized_payload = vec![0u8; MAX_PAYLOAD_SIZE + 1]; It creates a payload exactly one byte larger than the legally permitted MAX_PAYLOAD_SIZE. It then embeds this payload inside a dummy Reveal struct. This mocks a network message a node might receive. When the test calls reveal.validate(), it asserts that the result is_err(). The Guarantee: This test proves that the validate() method correctly enforces the upper bounds on memory allocation. It does this before the payload can be forwarded to the storage layer. It does this before it reaches the consensus engine. It does this before it reaches the signature verifier. The malicious data is dropped instantly. This preserves node stability and prevents memory exhaustion.

2. The Subdomain Hijack Test

-> See: kinetic-core/tests/test_021_subdomain_hijack.rs

The Vulnerability: The Kinetic namespace is flat. Unlike the traditional DNS system where com owns google.com which owns mail.google.com, Kinetic only recognizes apex domains. If a user registers satoshi.kin, they have absolute control over it. If the protocol validation logic was sloppy and permitted internal periods (e.g., blog.satoshi.kin), an attacker could register that subdomain independently. Users seeing blog.satoshi.kin would naturally assume it belongs to the owner of satoshi.kin. This would destroy the trust model of the decentralized namespace. It would lead to widespread phishing and identity spoofing. The Test Execution: The test creates two Reveal scenarios to verify the string validation logic. The Attack: It constructs an invalid_reveal targeting blog.saifmukhtar.kin. Because this name contains a subdomain (indicated by the internal period before the TLD suffix), it is malformed according to network rules. It calls .validate(). It asserts that the function returns an error. The protocol must reject any attempt to register a subdomain. The Baseline: It constructs a valid_reveal targeting saifmukhtar.kin (the apex domain). It calls .validate(). It asserts that the function succeeds, returning Ok(). The Guarantee: This verifies that the string parsing logic embedded in Reveal::validate() is impenetrable to hierarchical domain spoofing. It ensures the mathematical flatness of the namespace is enforced at the earliest possible stage of packet processing.

3. The Protocol Downgrade Test

-> See: kinetic-core/tests/test_029_protocol_downgrade.rs

The Vulnerability: Cryptographic signatures guarantee that data was authorized by a specific private key. However, signatures are “dumb”. They only prove authorization for the exact bytes provided to the signing algorithm. They have no concept of context or intent. Imagine Kinetic launches Protocol Version 1. Later, a severe flaw is found in how V1 parses payloads. The network upgrades to Protocol Version 2. Users begin signing Version 2 transactions. If the signature process does not include the version number in the signed bytes, an attacker could intercept a valid V2 transaction on the wire. They could modify the unencrypted version byte to 0 (representing V1). They could broadcast it to nodes that haven’t updated yet. Because the signature remains valid for the rest of the payload, the downgrade attack succeeds. The attacker exploits the V1 flaw using a signature intended for V2. The Test Execution: This test ensures cryptographic agility and domain separation. It proves that signatures are irrevocably bound to their specific protocol version. First, it creates a legitimate V1 Reveal. It computes the exact byte array the user must sign using reveal_v1.signable_bytes(env!("KINETIC_NETWORK_ID")). Second, it simulates the attacker. It clones the Reveal. It manually mutates protocol_version to 0. It computes the new byte array bytes_v0. Third, it asserts assert_ne!(bytes_v1, bytes_v0). Because changing the version number fundamentally alters the bytes output by signable_bytes, the attacker’s forged V0 message will fail signature verification. The attacker does not possess the private key required to generate a valid signature for the newly computed bytes_v0 array. Furthermore, the test inspects the exact byte layout. let prefix = concat!(env!("KINETIC_NETWORK_ID"), "-vdf-reveal-v1").as_bytes(); assert_eq!(bytes_v1[prefix.len()], 1); assert_eq!(bytes_v0[prefix.len()], 0); It verifies that the network ID is acting as a Domain Separator. This prevents replay attacks between mainnet and testnet. It verifies that the version byte is injected into the prefix. Finally, it confirms that reveal_v0.validate() fails outright. The Guarantee: The network is immune to downgrade attacks. Every signature commits to the specific protocol version it was intended for. Replay attacks across different networks are impossible.

Key Pieces

NetworkConfig (and Sub-Structs)

What it is: The root deserialization schema mapping to network.json. Location: kinetic-core/build.rs — Lines 90 to 97 Why it matters: It acts as the canonical bridge between human-readable JSON configurations and machine-optimized Rust compilation. It provides strict type safety to the network parameters.

Eclipse Resistance Enforcement

What it is: The compile-time panic trigger for low DHT redundancy. Location: kinetic-core/build.rs — Lines 168 to 184 Why it matters: It demonstrates how build scripts can be used as active security policy enforcers. It overrides developer misconfigurations before they can compile.

cargo:rustc-env Injection

What it is: The mechanism exposing NETWORK_ID to the compiler. Location: kinetic-core/build.rs — Lines 236 to 245 Why it matters: Enables zero-cost, compile-time string concatenation for cryptographic domain separators using the concat! and env! macros. It avoids the need for runtime string allocations.

test_003_oom_payload_bomb()

What it is: The defensive integration test against unbounded memory allocation. Location: kinetic-core/tests/test_003_oom_payload_bomb.rs Why it matters: Proves that the Reveal struct’s validate() method successfully blocks the most prevalent class of P2P Denial-of-Service attacks. It stops memory exhaustion via malicious Vec sizing.

test_subdomain_hijack_validation()

What it is: The integration test verifying namespace flatness. Location: kinetic-core/tests/test_021_subdomain_hijack.rs Why it matters: Ensures the decentralized identity model cannot be undermined by hierarchical domain spoofing. It maintains the integrity of the naming system.

test_protocol_downgrade_prevention()

What it is: The cryptographic agility test ensuring signatures commit to protocol versions. Location: kinetic-core/tests/test_029_protocol_downgrade.rs Why it matters: Prevents replay attacks across different protocol versions and different network forks (mainnet vs testnet). It ensures forward security.

How This Connects to the Rest of Kinetic

CROSS-CRATE / FORWARD DEPENDENCIES: To kinetic-core/src/constants.rs: The build script writes network_constants.rs into the OUT_DIR. The constants.rs file within the main crate source tree uses the include! macro. It physically pulls this generated text into the module structure. This is the handoff point between build-time generation and runtime availability. To kinetic-core/src/types.rs: The integration tests act as the primary consumer and verifier of the logic defined in types.rs. The tests directly import RevealExt::validate() and RevealExt::signable_bytes(). This ensures that modifications to the core types do not break fundamental security invariants. To P2P Gossip (Future): The LIMITS_P2P_MAX_PACKET_SIZE generated by build.rs will be consumed by the networking crate. It will configure the Kademlia swarm and libp2p behavior. This ensures the physical transport layer drops oversized packets before they even reach the application layer.

Quick Reference

Build Script Goal: Parse network.json at compile-time to generate statically typed const variables. This guarantees zero-cost abstraction. It prevents invalid or insecure configurations from ever compiling. OUT_DIR: The temporary directory provided by Cargo where generated files are stored. This is where network_constants.rs is stored before being included in the main build. OOM Protection: Validation logic drops any payload > MAX_PAYLOAD_SIZE. Namespace Rules: The protocol is apex-only. Validation drops any name containing internal periods. Downgrade Protection: Altering a struct’s protocol version alters the output of signable_bytes(). This immediately invalidates any associated cryptographic signatures.

Open Questions / Things to Revisit

Missing Protobuf/Capnp Directives: As noted, your prompt mentioned protobuf compilation, but the current build.rs relies on Serde and JSON. If you are migrating back to Protobuf for wire efficiency, you need to be aware that kinetic-core currently does not compile them. If you maintain .proto schemas in a separate crate (like kinetic-rpc), they need to be compiled there. Re-adding them here would require importing prost-build. Scaling Integration Tests: The current integration tests focus on the Reveal struct. As the protocol expands to include Transfer, Update, and Revoke messages, we must ensure coverage scales. These same OOM and downgrade tests must be systematically applied to all new data structures to maintain uniform security. JSON Fallback Safety: The build.rs script defaults to default_network.json if a specific environment or workspace file is not found. While convenient for local development, this introduces a risk. A production builder might accidentally compile the fallback network instead of the intended mainnet parameters. We may want to add a strict mode flag to disable the fallback in production builds to enforce explicit configuration. KINETIC_NETWORK_ID in Tests: The downgrade test relies on env!("KINETIC_NETWORK_ID"). This works during standard cargo test runs because Cargo executes build.rs first and populates the environment variables. However, if a developer tries to run a single test manually through certain IDE debuggers without triggering the build script, it may fail. It may fail to find the environment variable. It is a minor friction point worth documenting for new contributors.

01 — Overview and Native Storage

Crate: kinetic-storage Stage: 4 of 10 Reading time: ~8 minutes Depends on: kinetic-core Source files: kinetic-storage/src/lib.rs (Lines 1 to 172)


What Is This?

kinetic-storage is the persistent Key-Value database engine for the Kinetic network. It provides a unified, cross-platform storage interface that full nodes use to save the blockchain state, DIDs, and network routing tables to disk.


Why Kinetic Needs This

A decentralized network generates massive amounts of data that must be persisted across reboots. However, Kinetic is designed to run anywhere — from high-powered Linux servers to lightweight browser extensions.

Standard databases like PostgreSQL or SQLite are too heavy or require complex setups. Kinetic needs an embedded database. Furthermore, browser extensions cannot write to a real filesystem.

This crate solves both problems by providing a single storage API that automatically swaps its underlying engine based on where it is compiled.


How It Works

The crate consists of a single file (lib.rs) with 308 lines. It implements a trait called StorageEngine (defined in kinetic-core). This trait guarantees four fundamental operations:

  1. put(key, value)
  2. get(key)
  3. delete(key)
  4. scan_prefix(prefix)

The Native Engine (sled)

#![allow(unused)]
fn main() {
-> See: `kinetic-storage/src/lib.rs` — Lines 18 to 172
}

When you compile Kinetic for Linux, Mac, or Windows, the compiler activates the native module. This module wraps Sled, which is a pure-Rust, high-performance embedded database.

Opening the Database:

Warning

When SledStorage::new(path) is called, it attempts to acquire a lock on the directory. If two Kinetic nodes try to open the same folder, Sled will throw an error. The code catches WouldBlock or PermissionDenied errors and gracefully translates them to StorageError::DatabaseLocked.

Corruption Handling (The Safety Net):

#![allow(unused)]
fn main() {
-> See: `kinetic-storage/src/lib.rs` — Lines 55 to 91
}

Important

If a node loses power while writing, the database can corrupt. Sled detects this upon startup. Instead of just crashing and destroying the user’s data forever, the code intercepts the Corruption error, automatically renames the broken database folder to a backup file (e.g., .corrupt.1623910.bak), and alerts the operator.

Scanning Prefixes:

#![allow(unused)]
fn main() {
-> See: `kinetic-storage/src/lib.rs` — Lines 110 to 133
}

In a Key-Value store, you don’t have SQL SELECT * WHERE. Instead, you design your keys cleverly (e.g., did:kin:123, did:kin:456). The scan_prefix method takes a prefix (did:kin:) and an optional limit. It asks Sled to return an iterator starting exactly at that prefix, pulling records out extremely efficiently.


Key Pieces

SledStorage

  • What it does: The struct holding the raw sled::Db connection.
  • Location: lib.rs:24
  • Why it matters: It manages the lifetime of the database connection. As long as this struct exists in memory, the database is open and locked to that process.

CORRUPT_COUNTER

  • Location: lib.rs:56

Note

-> See RUST_CONCEPTS.md for an explanation of AtomicU64. It safely ensures multiple concurrent threads don’t overwrite the same corrupted backup file.


How This Connects to the Rest of Kinetic

  • FORWARD DEPENDENCY: The kinetic-daemon crate will instantiate this storage engine when the node boots up.
  • CROSS-CRATE: It uses StorageError and StorageEngine which are defined in kinetic-core. (Note: kinetic-core is technically Stage 7, so for now, treat those trait definitions as a black box.)

Quick Reference

PropertyValue
Engine (Desktop/Server):sled
Fallback Engine:In-memory BTreeMap (See file 02)
Core Operations:put, get, delete, scan_prefix
Corruption strategy:Auto-rename and preserve data.

Open Questions / Things to Revisit

  • Async Trait: The current StorageEngine trait uses synchronous (blocking) calls. In a highly concurrent P2P network using tokio, blocking file I/O operations can stall the async executor. We may need to wrap these Sled calls in tokio::task::spawn_blocking at the daemon layer, or upgrade the trait to be async fn.

02 — Wasm Storage (Browser Extension Fallback)

Crate: kinetic-storage Stage: 4 of 10 Reading time: ~6 minutes Depends on: kinetic-core Source files: kinetic-storage/src/lib.rs (Lines 174 to 266)


What Is This?

This is the second half of the kinetic-storage crate. It provides an entirely in-memory, zero-disk Key-Value database that automatically replaces the sled database whenever the Kinetic codebase is compiled for a Web browser (WASM).


Why Kinetic Needs This

Important

The sled database requires direct access to a computer’s file system (hard drive). When Kinetic is compiled as a Wasm browser extension, it runs inside a strict security sandbox that physically cannot touch the host computer’s hard drive. If you try to compile sled to Wasm, the compiler will violently reject it. By using conditional compilation, Kinetic can ship the exact same node architecture to the browser without crashing the compiler.


How It Works

#![allow(unused)]
fn main() {
-> See: `kinetic-storage/src/lib.rs` — Lines 174 to 266
}

When the target_arch = "wasm32" compiler flag is active, the native module is ignored and this wasm module is activated. It creates a mocked SledStorage struct that looks exactly like the real one from the outside, but operates completely differently on the inside.

The Internal Engine (BTreeMap):

Instead of a database file, the data is stored in a RwLock<BTreeMap<Vec<u8>, Vec<u8>>>.

Note

-> See RUST_CONCEPTS.md for an explanation of BTreeMap and why it is used for fast prefix scanning.

Memory Constraints (DoS Protection):

#![allow(unused)]
fn main() {
-> See: `kinetic-storage/src/lib.rs` — Lines 240 to 242
}

Warning

Because this database lives entirely in RAM (memory), there is a severe risk of a browser tab crashing if the node downloads too much data. To prevent this, the put() function includes a hardcoded limit:

#![allow(unused)]
fn main() {
if db.len() >= 10_000 && !db.contains_key(key) {
    return Err(...);
}
}

If the database reaches 10,000 keys, it refuses to insert any new ones (though it allows updating existing keys).

Prefix Scanning:

#![allow(unused)]
fn main() {
-> See: `kinetic-storage/src/lib.rs` — Lines 218 to 229
}

Because a BTreeMap is naturally sorted, scanning for a prefix is extremely fast. The code uses db.range(prefix.to_vec()..) to instantly jump to the first key that matches the prefix, and then iterates forward. As soon as it hits a key that doesn’t start with the prefix, it breaks the loop early.


Key Pieces

#[cfg(target_arch = "wasm32")]

  • Location: lib.rs:174

Note

-> See RUST_CONCEPTS.md for an explanation of conditional compilation. This separates the browser build from the desktop build.

RwLock

  • Location: lib.rs:182

Note

-> See RUST_CONCEPTS.md for an explanation of RwLock<T>. This allows concurrent reads while safely locking for writes.


How This Connects to the Rest of Kinetic

  • CROSS-CRATE: This allows the entire kinetic-network P2P layer to run seamlessly in the browser. The networking code just calls storage.put(), completely unaware that it is writing to RAM instead of a hard drive.

Quick Reference

PropertyValue
WASM Engine:BTreeMap (In-memory).
Concurrency:Guarded by std::sync::RwLock.
Hard Limit:10,000 keys max to prevent browser OOM crashes.
Persistence:None. Wiped clean when the browser tab closes.

Open Questions / Things to Revisit

  • True Browser Persistence: Right now, the WASM storage is entirely ephemeral — all data is lost when the user closes the browser. For a real Kinetic wallet extension, this should ideally be upgraded to use the browser’s IndexedDB API (via web-sys) so that keys and peer tables survive tab closures. The 10,000 key limit is a stopgap for RAM exhaustion, but a persistent IndexedDB solution is the correct long-term architecture.

Crate: kinetic-network

Stage: 8

Reading Time: 360 mins

Depends On: kinetic-core, kinetic-types


What Is This?

This is the decentralized nervous system of Kinetic. The kinetic-network crate is responsible for peer-to-peer (P2P) communication, maintaining the Distributed Hash Table (DHT), enforcing gossipsub packet routing, and rejecting Sybil attacks via active Proof of Work (PoW) and Kademlia strike tracking.

If kinetic-core makes the rules, kinetic-network enforces them in the hostile environment of the public internet.


Key Pieces

The crate is divided into three distinct pillars:

  1. The Store (src/store/): An fortified local Kademlia Record Store. It intercepts raw Kademlia reads/writes from the network and forces them through verification.rs to validate VDFs, Authorized Manifests, and Cryptographic tie-breakers before they ever touch the local Sled database.
  2. The Client (src/client/): The asynchronous, multi-thread safe interface for the rest of the node (like kinetic-daemon or kinetic-rest) to send commands to the network. It uses MPSC channels and oneshot callbacks to pass messages without ever blocking.
  3. The Event Loop (src/event_loop/): The single-threaded Libp2p Swarm. Because the Swarm cannot be shared across threads safely, it runs isolated in a massive tokio::select! loop inside core.rs. It acts as an Actor Model, listening for network events on one side and NetworkClient commands on the other, juggling them all asynchronously.

Why Kinetic Needs This

Warning

A standard Libp2p node is trusting. Out of the box, Libp2p Kademlia will gladly accept any data a peer sends it, allowing a single malicious node to overwrite every domain name on the network instantly (a classic Eclipse/Sybil attack).

Kinetic strips out the default Libp2p storage mechanics and injects the KineticRecordStore. kinetic-network exists to ensure that every single byte received from the network is cryptographically scrutinized (checking VDF proofs, verifying signatures, enforcing rate limits) before it is trusted.


How to Read This Stage

This is the largest and most complex crate in the codebase (29 source files, over 5,700 lines of code). We have broken it down into 19 densely packed documentation files.

  • Begin with the Store files (02 to 05, 10) to understand how data is actually validated and saved.
  • Move to the Client files (06, 07, 18) to understand how the node asks the network for data.
  • Dive into the Event Loop (08, 09, 11 to 13) to see the beating heart that routes the packets.
  • Finally, read the handlers and miscellaneous components (14 to 17, 19, 20) for the fine details on Gossipsub and PoW.

Store Verification (Part 1)

Crate: kinetic-network Stage: 8 Reading time: 30 minutes Depends on: Stage 7 (kinetic-core), Stage 1 (kinetic-types)


What Is This?

This file (verification.rs, specifically lines 1 to 330) represents the primary cryptographic and logical firewall for the Kinetic network’s Distributed Hash Table (DHT). In a standard libp2p Kademlia implementation, nodes will happily accept, store, and propagate any data given to them by their peers. This is fine for a generic file-sharing network, but for a decentralized naming system.

This file intercepts records before they are written to the local Sled database. It enforces strict rules on two fundamental data types:

  1. HostRoutingRecords: The network’s phonebook entries (IPs mapped to PeerIds).
  2. Reveals: The final step of the name registration process where a user proves they performed the VDF computation.

Important

If a record fails the checks in this file, it is dropped. It is not saved, and it is not gossiped to other nodes. This file is what keeps the Kinetic network clean from spam, sybil identities, and invalid mathematical proofs.


Why Kinetic Needs This

To understand the necessity of this file, consider the attack vectors it mitigates:

Attack VectorDescription
The Time-Travel Attack (Stale Records)Without timestamp validation, an attacker could observe your HostRoutingRecord today, save it, wait a year until your IP address changes, and then broadcast the old record. The network would overwrite your new IP with your old one, effectively disconnecting you. Standard Kademlia keeps records until they are evicted. This file forces all routing records to have a recent, verifiable Drand timestamp (a 100 kyn sliding window). If it is outside this 5-minute window, it is instantly discarded.
The Infinite Namespace Spam (Cost Evasion)Kinetic uses Verifiable Delay Functions (VDFs) to attach a computational cost to registering names. Short names cost more time than long names. If this file didn’t exist, a user could submit a Reveal for ai.kyn claiming they did the work, when they actually did nothing. This file recalculates exactly how much work was supposed to be done, verifies the math using the consensus module, and rejects anything that falls short of the threshold.
The Identity Forgery (Sybil Attack)A libp2p PeerId is just a hash. Without validation, node A could broadcast a routing record claiming to be node B. This file enforces that the PeerId must literally contain the raw Ed25519 public key inline. It then ensures that the record must be signed by that exact key. Identity is cryptographically bound to the data payload itself.
The Unfair Renewal TrapIf you registered a name by burning CPU for a week, you shouldn’t have to burn CPU for a week every time you renew it. This file implements a complex “Loyalty Discount” system. It proves that you owned the name previously. It checks if you are within the grace period (not expired). It drastically reduces the VDF iteration requirement for your renewal, allowing early adopters to maintain their names cheaply.

How It Works

The logic is dense and procedural. We break it down into the specific functions handling the verification pipeline.

1. Verifying Host Routing Records

When a peer sends a HostRoutingRecord, it is routed to verify_host_routing_record.

#![allow(unused)]
fn main() {
// -> See: crates/kinetic-network/src/store/verification.rs — Lines 25 to 87
}

Step 1: Freshness via Drand Kyns

  • The function takes the current_drand_kyn (the current global clock pulse) and compares it to the record.drand_kyn.
  • It uses saturating_sub for safety. In Rust, standard subtraction (a - b) can underflow and panic if b > a. saturating_sub bottoms out at 0 instead.
  • The record is rejected if it is older than 100 kyns (roughly 5 minutes).
  • The record is also rejected if it claims to be from the future (record.drand_kyn > current_drand_kyn).
  • This prevents attackers from setting timestamps to u64::MAX to permanently pin their record in caches.
  • Both rejections trigger a specific error log with codes (KIN-STORE-023 and KIN-STORE-024).

Step 2: Multihash Unpacking

  • The host_id string is parsed into a libp2p::PeerId.
  • A PeerId is essentially a Multihash (a self-describing hash format).
  • Kinetic requires Ed25519 keys for all node identities.
  • libp2p encodes Ed25519 keys inline within the Multihash payload instead of hashing them, because they are already short (32 bytes).
  • The code inspects the raw bytes of the multihash digest.
  • It expects exactly 38 bytes total for this specific format.
  • It enforces a strict byte prefix: [0x00, 0x24, 0x08, 0x01, 0x12, 0x20].
  • 0x00 indicates an Identity hash (the hash is just the public key itself).
  • The following bytes indicate Ed25519 formatting and the 32-byte length.
  • If this exact prefix matches, it copies the final 32 bytes into a new array. This is the raw Ed25519 public key.

Step 3: Signature Verification

  • The extracted bytes are converted into an ed25519_dalek::VerifyingKey.
  • The record.signature bytes are parsed into a Signature.
  • The record’s signable_bytes(NETWORK_ID) are generated.
  • Including the NETWORK_ID ensures that a valid signature from the testnet cannot be maliciously replayed on the mainnet to overwrite routing tables.
  • The signature is verified. If it passes, the routing record is authentic and timely.

2. Extracting Integers from Storage

#![allow(unused)]
fn main() {
// -> See: crates/kinetic-network/src/store/verification.rs — Lines 89 to 100
}
  • The get_u64_from_sled function is a small, inline utility.
  • When retrieving counters or timestamps from the Sled database, they are returned as raw byte vectors.
  • This function ensures the byte vector is exactly 8 bytes long.
  • It converts it into a fixed array using try_into.
  • It then parses it into a u64 using u64::from_be_bytes.
  • Big-endian is standard for network transmission and ensures that when integers are used as database keys, their lexical sorting matches their numerical sorting.

3. Computing Required VDF Iterations

When a Reveal is submitted, the network must know how many VDF iterations to demand. This is handled by compute_required_iterations.

#![allow(unused)]
fn main() {
// -> See: crates/kinetic-network/src/store/verification.rs — Lines 114 to 274
}

Step 1: Sanity Checks and BLS Verification

  • It first validates the name format (is_valid_apex_name).
  • It decodes the drand_signature from hex.
  • If the node is not in Dev Mode, it performs a complex BLS signature verification using drand_verify::G2PubkeyRfc.
  • It hardcodes the Drand network’s public key.
  • It maps it to the G2 curve.
  • It verifies that the provided signature matches the drand_kyn.
  • This proves the randomness wasn’t faked by the user.

Step 2: The Base Iteration Cost

  • It fetches the base_required_iterations by querying the consensus math module with the name’s length.
  • Shorter names return exponentially higher base numbers.

Step 3: The Previous Proof Validation (Loyalty Discount)

  • If the Reveal contains a previous_proof, the user is requesting a renewal discount.
  • The network trusts nothing and verifies the old proof from scratch.
  • It creates a new Sha256 hasher (prev_hasher).
  • It hashes the name bytes (reveal.name.as_bytes()).
  • It hashes the old salt (prev.salt).
  • It decodes the old Drand signature to verify its validity too.
  • It hashes the old Drand signature to extract the old randomness seed.
  • It hashes the user’s public key (reveal.pubkey).
  • It reconstructs the old Commitment hash from all these pieces combined.
  • It passes this reconstructed commitment, along with the old VDF proof and old iteration count, to the VdfEngine.
  • The engine runs the math to ensure the old proof was genuinely valid.

Step 4: Age and Governance Pause Compensation

  • Even if the old proof is valid mathematically, it must not be too old.
  • The code calculates the paused_kyns by querying the GLOBAL_GOVERNANCE_STATE.
  • If the blockchain was halted for maintenance for 5,000 kyns, those kyns are essentially erased from history so users aren’t penalized for downtime.
  • The effective_age is calculated as: Current Kyn - Old Kyn - Paused Kyns.
  • The proof is only valid if effective_age <= 2 * RESQUARING_EPOCH_KYNS. This is the grace period window.

Step 5: Applying the Discount Tiers

  • If all checks pass, the loyalty discount is granted based on the length of the normalized name (ignoring the .kyn suffix).
  • Length 1: The requirement drops to the absolute floor (CONSENSUS_VDF_DISCOUNT_MIN_ITERATIONS). This is an extreme reward for keeping a 1-character name alive.
  • Length 2-6: The requirement is cut in half (50% discount).
  • Length 7-10: The requirement is reduced by 80% (divided by 5).
  • Length 11+: The requirement is reduced by 85%.
  • The code uses std::cmp::max to ensure the final iteration count never falls below the absolute network minimum, preventing free registrations.

4. Verifying the Reveal (Initial Steps)

#![allow(unused)]
fn main() {
// -> See: crates/kinetic-network/src/store/verification.rs — Lines 296 to 330
}
  • When verify_reveal begins, it immediately checks the GLOBAL_GOVERNANCE_STATE.
  • If is_halted is true, the entire reveal process aborts and returns a NetworkHalted error.
  • It then validates the Reveal payload structure itself.
  • If not in Dev Mode, it verifies the Ed25519 signature on the Reveal.
  • It includes the NETWORK_ID to prevent cross-chain replay attacks.

Key Pieces

verify_host_routing_record()

  • Location: verification.rs — Lines 25 to 87
  • What it does: Extracts an Ed25519 public key directly from a libp2p multihash and uses it to verify the signature and timestamp of a routing announcement.
  • Why it matters: It is the primary defense against network mapping poisoning and identity theft in the DHT.

get_u64_from_sled()

  • Location: verification.rs — Lines 89 to 100
  • What it does: Safely decodes 8-byte big-endian vectors from the Sled database into usable Rust u64 integers.
  • Why it matters: Provides a safe, panic-free way to read timestamps and counters from raw disk storage.

compute_required_iterations()

  • Location: verification.rs — Lines 114 to 274
  • What it does: Dynamically calculates the computational price (in VDF iterations) required to register or renew a name. It validates BLS signatures and historical VDF proofs to grant discounts.
  • Why it matters: This function implements the core economic policy of the Kinetic namespace. It ensures name scarcity while making long-term ownership computationally viable.

verify_reveal() (Lines 296-330)

  • Location: verification.rs — Lines 296 to 330
  • What it does: The entry point for validating a full Reveal payload, checking governance halts, validating structural correctness, and verifying the user’s signature.
  • Why it matters: Serves as the first barrier against malformed or maliciously crafted name registrations.

How This Connects to the Rest of Kinetic

This verification module is where the abstract types from kinetic-core meet the harsh reality of the open internet.

  • CROSS-CRATE: HostRoutingRecord and Reveal — These data structures are defined, explained, and serialized in docs/learn/core/03_network_types.md (Stage 7).
  • CROSS-CRATE: VdfEngine — The trait definition for the VDF verifier lives in docs/learn/core/04_traits.md (Stage 7), while the actual mathematical implementation lives in kyn-vdf (Stage 6).
  • CROSS-CRATE: GLOBAL_GOVERNANCE_STATE — The global mutex that tracks network pauses and emergency halts. Documented in docs/learn/core/06_governance.md (Stage 7).
  • External Dependency: drand_verify — An external crate responsible for mapping public keys to the BLS12-381 G2 curve and validating aggregate signatures from the League of Entropy.

Quick Reference

Note

Error Triggers in this file:

  • KIN-STORE-023: HostRoutingRecord is older than 100 kyns.
  • KIN-STORE-024: HostRoutingRecord is timestamped in the future.
  • KIN-STORE-028: Reveal contains a malformed hex string for the Drand signature.
  • KIN-STORE-029: Reveal name fails structural validation.
  • KIN-STORE-030: Reveal Drand signature fails BLS curve verification.
Loyalty Discount TierReduction
1 char100% reduction (Drops to 1,000,000 minimum limit)
2-6 chars50% reduction
7-10 chars80% reduction
11+ chars85% reduction

Multihash Identity Prefix: To extract an Ed25519 key from a libp2p PeerId, Kinetic expects exactly 38 bytes starting with [0x00, 0x24, 0x08, 0x01, 0x12, 0x20].


Open Questions / Things to Revisit

Warning

  1. Multihash Extraction Brittleness The method used in verify_host_routing_record to extract the Ed25519 public key relies on hardcoded byte offset slices (&bytes[6..38]) and exact prefix matching. If libp2p introduces a new multihash encoding variant or changes the internal representation of an Identity hash, this logic will instantly fail and reject all routing records. Should Kinetic migrate to using libp2p’s built-in PeerId::as_public_key() method instead of raw slice manipulation?
  2. Denial of Service Vector In compute_required_iterations, if a malicious user attaches a bogus previous_proof to a Reveal, the node still performs multiple SHA256 hashes and invokes the VdfEngine::verify function before discovering it is invalid. Since VDF verification can take milliseconds depending on the engine backend, could an attacker spam the network with fake renewals to exhaust CPU resources on verifying nodes? The current fallback simply logs a warning and charges full price, which does not penalize the attacker for forcing the node to do unnecessary math.
  3. Dev Mode Branching in Consensus Logic The kinetic_core::config::is_dev_mode() check is deeply embedded in the verification logic to skip BLS checks and signature checks. Having dev mode checks in the middle of core network validation logic can be a security risk if the dev mode flag is accidentally enabled in production. Is there a safer architectural way to mock these checks, perhaps by injecting a Mock trait implementation, rather than branching the logic inline?

Store Verification Logic (Part 2)

Crate: kinetic-network Stage: 8 Reading time: 20 minutes Depends on: docs/learn/network/02_store_verification_1.md, docs/learn/core/01_overview.md, docs/learn/verify/01_overview.md


What Is This?

This document covers the second and final phase of the validation logic applied by the Kinetic network when a peer attempts to insert a record into the Kademlia Distributed Hash Table (DHT). Specifically, it documents lines 331 through 660 of the verification module, which handles the most computationally intensive and cryptographically rigorous operations in the entire network stack.

While the first part of verification handles basic sanity checks, payload deserialization, and the recording of lightweight commitments, this second part acts as the primary defense mechanism against sophisticated attacks. It is responsible for verifying the Verifiable Delay Function (VDF) proofs to confirm that a node actually expended the required continuous computational time to legitimately claim a domain name. This prevents Sybil attacks and name- squatting.

Beyond domain registration, this logic also enforces the rules around identity management and content routing. It handles the cryptographic authorization for AuthorizedKid (Key Identifier Documents) and AuthorizedManifest payloads. It enforces the use of post-quantum cryptography (ML-DSA signatures) for all domain operations, prohibits unauthorized updates by checking cryptographic trust chains, and prevents downgrade attacks by enforcing monotonic versioning on manifests. In summary, this logic is the gatekeeper that ensures identity and routing data in the Kinetic DHT remains pristine, authorized, and cryptographically secure.


Why Kinetic Needs This

Kinetic operates as a fully decentralized network without a central naming authority (like ICANN) to arbitrate domain ownership or manage DNS records. If a node wants to claim the domain saif.kin, there is no central server to ping to check availability and assign it. This decentralized nature introduces severe vulnerability to two primary threat vectors: frontrunning and domain hijacking.

1. Preventing Frontrunning via Time-Lock Puzzles (The VDF Phase):

In a naive decentralized naming system, if you broadcast a request to register saif.kin, a malicious eavesdropper on the network can see your request, duplicate it, and broadcast their own request with a higher priority or faster network propagation, effectively stealing the name before your request is fully processed. To eliminate this, Kinetic requires a strict two-step Commit-and- Reveal scheme secured by time:

  • You first commit to a hashed, secret version of your request.
  • Then, you must spend a predictable amount of real-world time calculating a Verifiable Delay Function (VDF).
  • This validation logic is what enforces that rule. It checks that the VDF proof is sound.
  • It verifies that a prior commitment actually exists in the network.
  • Crucially, it ensures that the commitment is old enough to prove you didn’t just generate it instantly.
  • Because VDF calculation requires sequential, non-parallelizable computation that takes longer than the minimum commitment wait time, it is physically impossible for an attacker to observe your reveal, generate a new commitment, and calculate a VDF fast enough to beat you. This logic binds digital ownership to the physics of time.

2. Preventing Domain Hijacking (The AuthorizedKid Phase):

Once a domain is secured, the owner needs to publish their decentralized identity keys and periodically rotate them for security. Without this specific validation logic, any node could broadcast a DHT update replacing the keys for saif.kin with their own, hijacking the domain:

  • This logic ensures that every single update to a domain’s Key Identifier Document (KID) is cryptographically signed.
  • The signature must come from the exact key that originally claimed the domain, or by a key that was authorized in a previously verified update.
  • It enforces an unbreakable chain of cryptographic custody.

3. Preventing Version Rollback Attacks (The AuthorizedManifest Phase):

The manifest maps a domain to its actual content or routing addresses (e.g., an IPFS CID). If an attacker manages to compromise an old, revoked key, they might attempt to republish an older version of the manifest:

  • This logic extracts the version number from existing DHT records.
  • It enforces that any new manifest must have a version number greater than the current one.
  • This guarantees the network state only moves forward, neutralizing replay and rollback attacks.

How It Works

This section breaks down the three distinct operations handled in this file, which are executed based on the type of DHT payload being processed.

Phase 1: VDF and Commitment Verification (Completing the Reveal)

When a node receives a Reveal payload, it must verify the cryptographic proof of work to finalize a domain registration.

Step A: Drand Signature Decoding and BLS Verification:

#![allow(unused)]
fn main() {
// -> See: kinetic-network/src/store/verification.rs:L337-L368
}

The system relies on Drand (a distributed randomness beacon) to provide an unpredictable input, ensuring the VDF challenge could not be pre-computed:

  • The logic first decodes the hex string of the Drand signature.
  • If the network is not operating in development mode (dev_mode), it loads the hardcoded Kinetic Drand public key.
  • It initializes a drand_verify::G2PubkeyRfc object to perform a BLS signature verification over the G2 elliptic curve.
  • This proves that the provided Drand signature is a genuine product of the Drand network for the specific round (kyn) claimed in the payload.

Step B: Constructing the Deterministic VDF Challenge:

#![allow(unused)]
fn main() {
// -> See: kinetic-network/src/store/verification.rs:L370-L382
}

The challenge given to the VDF engine must be uniquely bound to this specific registration attempt. The logic uses SHA-256 to hash together four specific components:

  1. The validated Drand signature bytes (hashed into drand_rand).
  2. The domain name being requested.
  3. The user’s secret cryptographic salt.
  4. The user’s public key.

The resulting 32-byte hash acts as the exact, unforgeable challenge that the VDF engine must have solved.

Step C: Enforcing the Commitment Delay via Sled:

#![allow(unused)]
fn main() {
// -> See: kinetic-network/src/store/verification.rs:L384-L418
}

The node constructs a database key using the KRS_COMMIT_PREFIX and the calculated challenge hash. It then queries its local sled key-value store:

  • If found, it reads the Drand round (commit_kyn) when the commitment was received.
  • It uses saturating_sub (to prevent integer underflow panics if the network clock skews) to calculate the age of the commitment: current_drand_kyn - commit_kyn.
  • If this age is less than the CONSENSUS_MINIMUM_COMMIT_AGE_KYNS, the reveal is rejected as too recent.
  • This mechanically enforces the mandatory waiting period.

Understanding the Dev Mode Bypass:

#![allow(unused)]
fn main() {
// -> See: kinetic-network/src/store/verification.rs:L413-L418, L423-L429
}

In software engineering, testing computationally intensive logic like VDF verification can dramatically slow down CI/CD pipelines and local development:

  • The code implements a dev_mode flag.
  • When enabled, the system intentionally skips the BLS Drand verification, bypasses the commitment age requirement, and skips the VDF engine.verify() step.
  • This allows developers to simulate network behavior and domain registration in milliseconds rather than minutes.
  • However, this is gated and never enabled in the production binary.

Step D: Mathematical VDF Proof Verification:

#![allow(unused)]
fn main() {
// -> See: kinetic-network/src/store/verification.rs:L420-L465
}

The logic calculates the exact number of VDF iterations required based on the time elapsed between the commitment and the current block:

  • If the payload claims fewer iterations than required, it is immediately rejected to save CPU cycles.
  • Finally, it calls engine.verify() on the underlying VDF engine (provided by the kyn-vdf crate).
  • If the complex mathematical verification of the proof against the challenge and iterations succeeds, the reveal is deemed legitimate.

Phase 2: Verifying the Key Identifier Document (AuthorizedKid)

After a domain is claimed, the owner publishes an AuthorizedKid to establish their decentralized identity and authorized signing keys.

Step A: Extracting the Cryptographic Root of Trust:

#![allow(unused)]
fn main() {
// -> See: kinetic-network/src/store/verification.rs:L496-L504
}

The system searches the DHT for the active NameRecord (the validated Reveal) for this domain. The public key embedded inside this NameRecord serves as the absolute, unquestionable root of trust for all future domain operations.

Step B: Post-Quantum ML-DSA Signature Validation:

#![allow(unused)]
fn main() {
// -> See: kinetic-network/src/store/verification.rs:L506-L529
}

Important

Kinetic is built for a post-quantum future, utilizing the ML-DSA-65 signature scheme.

  • The logic imports the KeyInit and Verifier traits from the ml_dsa crate.
  • It constructs a VerifyingKey from the NameRecord’s public key.
  • It extracts the owner_signature from the payload and verifies it against the signable_bytes of the AuthorizedKid.
  • The signable_bytes function injects the NETWORK_ID into the hash, preventing cross-network replay attacks.

Step C: Genesis Binding vs. Cryptographic Update Chains:

#![allow(unused)]
fn main() {
// -> See: kinetic-network/src/store/verification.rs:L531-L563
}

The logic dynamically branches depending on whether a KID document already exists in the DHT. The existing record is passed as an Option<&std::borrow::Cow<'_, libp2p::kad::Record>>, utilizing Rust’s Copy-on- Write semantics to avoid unnecessary memory allocations:

  • First Publication (Genesis): If no record exists, it executes auth_kid.kid_doc.verify_genesis(). This enforces a critical invariant: the Decentralized Identifier (DID) string must be the exact SHA-256 hash of the primary controller key defined within the document.
  • Key Rotation (Updates): If a record already exists, it parses the old document. It executes auth_kid.kid_doc.is_authorized_update(&old_auth_kid.kid_doc). This guarantees that the new document is signed by a key granted rotation privileges in the previous document.

Phase 3: Verifying the Domain Manifest (AuthorizedManifest)

The manifest maps the abstract domain name to concrete routing data, such as IPFS content identifiers or physical node addresses.

Step A: Payload Signature and Structure Validation:

#![allow(unused)]
fn main() {
// -> See: kinetic-network/src/store/verification.rs:L592-L637
}

Similar to the KID verification, this process extracts the ML-DSA public key from the active NameRecord and verifies the post-quantum signature over the manifest payload. It goes further by ensuring the embedded kid_doc is internally valid, and that the manifest data itself is properly bound to that specific KID document, preventing mix-and-match attacks.

Step B: Strict Anti-Rollback Version Enforcement:

#![allow(unused)]
fn main() {
// -> See: kinetic-network/src/store/verification.rs:L639-L653
}

This is the critical defense against replay attacks:

  • If an existing manifest is found in the DHT, the logic parses it to extract its version integer.
  • It evaluates auth_manifest.manifest.version <= old_manifest.manifest.version.
  • If true, the new manifest is rejected.
  • The network state is forced to only move forward, ensuring that compromised older keys cannot be used to force the domain back to an outdated configuration.

Key Pieces

1. verify_reveal Function (VDF Check Phase)

  • What it does: Executes the heavy cryptographic validation for a domain registration attempt. It handles BLS verification of the Drand randomness, checks the age of the local commitment, calculates the required computational delay, and executes the VDF proof verification engine.
  • Where it lives: kinetic-network/src/store/verification.rs:L331-L465
  • Why it matters: This function is the primary shield against Sybil attacks and frontrunning. It enforces the rule that time and computation must be spent to claim namespace, ensuring a fair distribution of domains.

2. verify_authorized_kid Function

  • What it does: Validates the publication and rotation of a Key Identifier Document. It enforces post-quantum ML-DSA signatures and verifies that updates follow a strict chain of cryptographic authorization.
  • Where it lives: kinetic-network/src/store/verification.rs:L467-L570
  • Why it matters: This anchors a human-readable domain to a secure cryptographic identity. It ensures that only the true, proven owner can delegate signing authority or rotate compromised keys.

3. verify_authorized_manifest Function

  • What it does: Validates the publication of the site routing manifest. It verifies signatures, validates the inner identity documents, and enforces increasing version numbers to block replay attacks.
  • Where it lives: kinetic-network/src/store/verification.rs:L572-L660
  • Why it matters: This function guarantees that when a user requests routing information for a domain from the DHT, they receive the most current, cryptographically authenticated data, immune to historical rollback attempts.

How This Connects to the Rest of Kinetic

  • CROSS-CRATE: kinetic_core::constants::DRAND_PUBLIC_KEY — This constant from the core crate provides the hardcoded, trusted BLS public key required to verify the authenticity of the Drand randomness beacon.
  • CROSS-CRATE: kinetic_core::types::AuthorizedKid and AuthorizedManifest — These are the precise, defined payload structures imported from the core types crate, carrying the signatures and documents verified in this module.
  • CROSS-CRATE: kinetic_core::constants::CONSENSUS_MINIMUM_COMMIT_AGE_KYNS — Defines the absolute minimum number of Drand consensus rounds a commitment must exist in the database before a reveal is permitted.
  • Local Storage Dependency (sled): This validation logic relies on the local sled database instance (passed as the storage parameter) to query prior commitments. If the local node was offline and missed the commitment broadcast, it will organically reject the valid reveal.
  • VDF Engine Delegation (kyn-vdf): The intensive engine.verify() call delegates the mathematical verification of the sequential delay function to the specialized kyn-vdf crate, bridging network logic with pure cryptography.

Quick Reference

  • Drand Verification: Employs BLS signatures over the G2 elliptic curve to authenticate the distributed randomness beacon.
  • VDF Challenge Construction: Determined via SHA256(drand_rand + name + salt + pubkey).
  • Commitment Age Validation: Enforced via current_kyn - commit_kyn >= MINIMUM_COMMIT_AGE, using saturating_sub for safety against clock skew.
  • Cryptographic Standard: All domain ownership operations (AuthorizedKid, AuthorizedManifest) exclusively utilize Post-Quantum ML-DSA-65 signatures.
  • Identity Genesis Binding: Upon initial publication, the DID string is forced to equal the SHA-256 hash of the genesis controller key.
  • Chain of Custody Authorization: During an identity update, the new document must possess a valid signature from a key authorized in the preceding document.
  • Strict Version Control: Manifest updates are mechanically forced to possess a higher version integer than the currently stored manifest to neutralize rollback attacks.

Open Questions / Things to Revisit

Warning

  • Outdated Cryptographic Comments in Code: Within the verify_authorized_kid function, there is a fallback block designed to handle cases where an existing record fails to parse. The source code comment at L560-L561 states: “the domain owner’s Ed25519 signature already authenticated the submission above”. However, the code immediately preceding this block actively utilizes ml_dsa::VerifyingKey::<ml_dsa::MlDsa65> (Post- Quantum ML-DSA), not the legacy Ed25519 standard. The comment is factually out of sync with the implementation and should be updated to accurately reflect the post-quantum architecture.

  • Security Implications of the Parsing Fallback: In that exact same fallback block (verify_authorized_kid, L559), if the existing DHT record is corrupted and fails to deserialize into a valid AuthorizedKid structure, the system intentionally bypasses the strict is_authorized_update chain-of-custody check and accepts the new record. While it still enforces the root ML-DSA signature derived directly from the immutable NameRecord, bypassing the granular key- rotation chain upon a parsing failure could theoretically introduce an edge case. If an attacker can intentionally corrupt the local DHT record, they might bypass rotation rules for delegated keys. This fallback logic warrants a rigorous security review to ensure it does not create an exploitable loophole.

Kademlia Local Store (Part 1: Initialization and Pruning)

Crate: kinetic-network Stage: 8 Reading time: 30 minutes Depends on: docs/learn/core/01_overview.md, docs/learn/types/05_name_records.md, docs/learn/types/06_infrastructure.md


What Is This?

This module defines the KineticRecordStore, a customized, strict implementation of the standard libp2p RecordStore trait. In a typical, standard libp2p application (like a basic chat app or a simple file-sharing protocol), a RecordStore operates as a blind data bucket. It holds key-value pairs for the Distributed Hash Table (DHT) without asking any questions. When another node on the network connects to your node and asks you to store a piece of data (via a DHT put request), a standard RecordStore will blindly accept the payload bytes and save them directly into your local memory. It does not ask what the bytes mean. It does not verify if the bytes are valid or structurally sound. It simply acts as a passive storage layer for the routing protocol, assuming that all actors on the network are behaving honestly.

In the Kinetic architecture, operating under this assumption of honesty is catastrophic. Blindly trusting incoming DHT data is unacceptable and fundamentally insecure for a global namespace. The KineticRecordStore acts as a relentless cryptographic firewall placed directly between the chaotic, untrusted libp2p network and the node’s local persistent disk storage (managed by Sled). Instead of blindly accepting data, it actively intercepts all incoming records before they touch memory. It enforces strict domain validation rules, commit-reveal timelocks, VDF (Verifiable Delay Function) proof verification, and heartbeat liveness tracking before any byte is allowed to rest on the disk or in the active caches.

This specific document (Part 1) covers the first 290 lines of the core.rs file. These lines focus on the lifecycle management and state restoration of this store. It details how the KineticRecordStore boots up from a cold start, how it safely restores its memory from the persistent disk by re-verifying everything mathematically, and how it actively prunes stale data from the network as the global Drand clock progresses forward in time. The second half of the file (which handles the actual interception of put and get operations during runtime) will be covered in the next topic file.


Why Kinetic Needs This

To truly understand why this massive layer of complexity exists, you must compare the standard IPFS-style DHT model with the strict Kinetic model. Standard DHT implementations allow anyone, anywhere, to put any arbitrary data at any specific key. If Kinetic used the default MemoryStore provided out-of-the-box by libp2p, the network would instantly collapse under a deluge of malicious activity. An attacker with minimal resources could simply generate millions of fake domain names, attach random invalid cryptographic proofs, forge expired heartbeats, and push them to every node simultaneously. This attack vector would quickly overwrite valid domain records with garbage or exhaust the RAM of every honest node on the network, leading to a out-of-memory panic across the entire grid.

Kinetic requires a robust form of persistent, verified storage equipped with deterministic, self-executing eviction rules. When a node shuts down for maintenance and restarts, it should not have to query the global DHT all over again for domain names it had already verified prior to shutting down. VDF verification is CPU-intensive by design. If a node had to re-download and re-verify 10,000 domain names from the network every single time it booted up, node restarts would take hours, severely degrading the node operator’s experience. However, it also cannot blindly trust its own local disk upon waking up.

Why can’t it trust its own disk? Because the measurement of time in Kinetic is defined exclusively in Drand kyns. If a node is powered off for three weeks and then turns back on, the records stored safely on its local disk are now obsolete. The domain names have almost certainly passed their global resquaring epochs and now require new, updated VDF proofs. If the node simply loaded those old, unverified records from the Sled disk into its active memory and started serving them to the network, it would be caught distributing invalid, expired data. Honest peers would immediately flag this node as malicious or out-of-sync and ignore its routing responses entirely.

The Analogy: Imagine the node is a bouncer at an exclusive club. Standard libp2p says, “If anyone hands you an ID card, put it in the VIP box.” Kinetic libp2p says, “If anyone hands you an ID card, verify the cryptographic signature, ensure the expiration date hasn’t passed, and check the watermark. When the club closes for the night, lock the IDs in a safe. When the club opens the next morning, take the IDs out of the safe and verify them all over again, because some of them might have expired while you were sleeping.”

Therefore, the KineticRecordStore solves two critical and inherently opposing problems simultaneously:

  1. Survivability (Disk Backing): It backs up verified DHT records to an embedded, localized disk database (Sled) so that they survive reboots. This saves the node from paying the exorbitant CPU cost of re-verifying the entire internet every time it starts.
  2. Paranoia (Boot Re-verification): On boot, it assumes the disk itself is potentially stale and compromised by time. It re-verifies every single VDF proof and Ed25519 signature before loading it from disk into active memory. It discards anything that aged out while the node was offline.
  3. Decentralized Garbage Collection: Because blockless decentralized networks do not have a centralized server sending DELETE broadcasts, the store must continuously monitor the Drand kyn independently. It locally prunes records that have naturally expired according to the network’s universal mathematical rules.

Important

Without this exact, rigorous structure, Kinetic would have no long-term memory, no defense against time-based desynchronization attacks, and no capacity to maintain a clean, verifiable namespace.


How It Works

The first 290 lines of core.rs define the primary struct, its internal memory caches, and the critical new and prune lifecycle functions. Let’s break them down chronologically.

The Initialization Phase (new)

When the node boots, it immediately calls KineticRecordStore::new. The singular goal of this function is to build up the libp2p MemoryStore and our internal LRU caches, but it must do so without compromising the cryptographic integrity of the node.

#![allow(unused)]
fn main() {
// -> See: kinetic-network/src/store/core.rs — Lines 65 to 160
}
  1. Scanning the Disk for Names: The engine begins by scanning the local Sled database for any keys that start exactly with the KRS_REVEAL_PREFIX. By using distinct prefixes, the database separates domain reveals from commitments and heartbeats. Crucially, the code limits this initial scan to 100_000 records. This is a hardcoded safety limit to prevent a massive disk (e.g., one that has collected millions of stale records over years) from exhausting the node’s RAM and CPU during a cold startup.

  2. Key Extraction and String Conversion: For every record found during the scan, the engine slices off the prefix bytes. It takes the remaining bytes and converts them into a String using String::from_utf8_lossy. This gives the engine the actual domain name associated with the record. Using from_utf8_lossy instead of strict UTF-8 conversion ensures that if malformed data somehow ended up on disk (due to disk corruption or a weird edge case), the node will silently sanitize it rather than panicking and crashing during boot.

  3. Deserialization Attempt: It attempts to parse the raw JSON bytes attached to the key into a structured NameRecord. If the JSON is corrupted, the record is simply ignored and skipped.

  4. The Re-Verification Gauntlet: Once deserialized, the record must prove itself valid under the current network conditions, from scratch. If the record is a Premium name (meaning it was injected directly by the decentralized governance system), it is considered implicitly valid. Premium names bypass the standard VDF checks because their authority comes from the blockchain layer consensus, not the individual VDF layer. If it is a Standard name, it must pass a strict cryptographic gauntlet:

  • Iteration Check: The node computes the required VDF iterations based on the initial_drand_kyn (which represents the exact current kyn the node woke up to). If the stored iterations on the record are less than the calculated required amount, the record is obsolete and is immediately discarded.
  • Signature Check: Unless the node is configured to run in dev_mode, it checks the Ed25519 signature of the record against the current network ID. This prevents records from testnets from bleeding into the mainnet database and causing namespace collisions.
  • Challenge Reconstruction: The node meticulously hashes the domain name, the cryptographic salt, the Drand randomness bytes (derived from the signed Drand pulse), and the owner’s public key. This exact sequence reconstructs the 32-byte Commitment challenge hash that the VDF was originally supposed to solve when the name was registered.
  • VDF Verification: Finally, it passes the reconstructed challenge, the proof bytes, and the claimed iterations into the vdf_engine. Because VDF verification is CPU-intensive, the engine uses tokio::task::block_in_place on native architectures. This is a critical concurrency feature: it temporarily moves the current task to a blocking thread pool, ensuring that the heavy cryptographic math does not stall Tokio’s primary asynchronous executor thread, which is busy handling other network traffic.
  1. Caching the Survivors: Only the records that successfully survive this entire, brutal gauntlet are allowed to be loaded into the reveals_by_name cache. Any record that fails is left behind on disk, isolated from memory, and will eventually be targeted for pruning.

The Architecture of Sled Storage Keys

#![allow(unused)]
fn main() {
// -> See: kinetic-network/src/store/core.rs — Lines 78 to 83
}

When the node scans the Sled database, it relies on a very specific key architecture. Sled is a simple key-value store, which means it has no concept of tables or schemas. To emulate tables, Kinetic uses byte prefixes. The KRS_REVEAL_PREFIX is a hardcoded sequence of bytes that prepends the actual domain name. For example, if the domain name is saif.kyn, the key on disk is [KRS_REVEAL_PREFIX bytes] + [saif.kyn bytes]. When the scan occurs, the code checks if key_bytes.len() <= prefix_len. This is a boundary safety check. If the key exactly matches the prefix but has no domain name attached, it means a corrupted or empty record was somehow written to disk. The code continues, skipping the corrupted key to prevent out-of-bounds slicing panics when it tries to extract the domain name on the next line. This level of defensive programming is required when dealing with raw byte stores, as panicking during node initialization would cause the daemon loop to enter a crash loop.


The Cryptographic Hash Reconstruction

#![allow(unused)]
fn main() {
// -> See: kinetic-network/src/store/core.rs — Lines 112 to 119
}

To verify a standard reveal, the node must rebuild the exact 32-byte hash that the VDF engine solved. It instantiates a clean Sha256 hasher and sequentially feeds it data. The order is deterministic:

  1. hasher.update(reveal.name.as_bytes()): The raw UTF-8 bytes of the domain name.
  2. hasher.update(reveal.salt): A random 32-byte salt generated by the user to prevent rainbow table attacks on short domain names.
  3. hasher.update(&drand_rand): The 32-byte Drand randomness derived from hashing the Drand signature. This anchors the challenge to a specific point in time, preventing users from pre-computing VDFs years in advance.
  4. hasher.update(&reveal.pubkey): The 32-byte Ed25519 public key of the owner, binding the VDF proof exclusively to their identity so no one else can steal the proof mid-flight. After feeding all four components, the hasher finalizes into the 32-byte Commitment { hash }. If a single byte in any of those four components is altered, the resulting hash changes, and the VDF verification will decisively fail.

WebAssembly (WASM) Compatibility during Verification

#![allow(unused)]
fn main() {
// -> See: kinetic-network/src/store/core.rs — Lines 121 to 134
}

Note

One of the most nuanced architectural details in the new initialization function is how it handles VDF verification across different target architectures. Kinetic is designed to run natively on servers, but it is also designed to compile to WebAssembly so that nodes can theoretically run inside a browser. When evaluating the VDF proof via vdf_engine.verify, the code uses conditional compilation flags (#[cfg]).

ArchitectureDetails
Native Architecture (not(target_arch = "wasm32"))On native targets (like a Linux server or Mac), the node relies on Tokio for its asynchronous runtime. Because VDF verification is CPU-bound (it requires tight loops of sequential mathematical operations), running it directly on Tokio’s async thread would stall the executor. The node wouldn’t be able to respond to basic network pings while verifying the proof. To solve this, the code wraps the verification in tokio::task::block_in_place. This brilliantly moves the executing context to a dedicated blocking thread pool, freeing up Tokio to handle network I/O while the math computes in the background.
WASM Architecture (target_arch = "wasm32")WebAssembly in the browser does not natively support Tokio’s blocking thread pools because WASM typically runs in a single-threaded environment (or requires complex Web Worker setups). If the code attempted to use block_in_place in WASM, it would panic or fail to compile. Therefore, the code drops the block_in_place wrapper when compiling for WASM, running the verification synchronously. While this means a WASM node might temporarily freeze the UI thread while verifying a proof, it guarantees that the node can actually boot up and function in the browser.

The Role of String::from_utf8_lossy

#![allow(unused)]
fn main() {
// -> See: kinetic-network/src/store/core.rs — Line 84
}

When scanning the Sled database, the system pulls raw bytes that represent the domain name. In a perfect world, these bytes are always flawlessly encoded UTF-8 strings. However, disks fail, bit flips occur, and malicious actors sometimes figure out ways to inject malformed byte sequences into the storage layer. If the engine used the standard String::from_utf8 method, a single corrupted record containing an invalid UTF-8 byte would throw an Err. If the node were using .unwrap() on that result, the entire node would panic and crash during the boot sequence, resulting in a bricked node that requires manual database deletion to fix. By using String::from_utf8_lossy, the node gracefully handles data corruption. If it encounters invalid bytes, it replaces them with the standard Unicode replacement character. While the resulting domain name will obviously fail subsequent cryptographic verification (because the hash of the sanitized string will not match the hash of the original challenge), the node itself will safely discard the record and continue booting. This is a critical defensive programming pattern for decentralized infrastructure.


The LRU Cache Mechanism

#![allow(unused)]
fn main() {
// -> See: kinetic-network/src/store/core.rs — Lines 73 and 199
}

In decentralized networks, state bloat is a massive vector for attack. If an attacker can force your node to store infinite amounts of data in memory, they can crash your node via an Out-Of-Memory (OOM) panic. To prevent this, KineticRecordStore uses LruCache (Least Recently Used Cache) for reveals_by_name and accepted_reveals_timestamps. The capacity of this cache is defined by lru_cache_size, which is typed as a NonZeroUsize. This Rust type guarantees at compile-time that the capacity cannot accidentally be set to zero (which would cause the cache implementation to panic). When the network DHT grows to a size that exceeds this lru_cache_size, the node does not stop functioning. Instead, the LruCache smoothly ejects the oldest, least-queried records from active memory to make room for new ones. Because the records are still backed up in the Sled disk storage and accessible via the get_record_with_fallback method, the node can always retrieve them if someone queries them later. This limits the node’s maximum RAM utilization strictly, providing a deterministic upper bound on memory consumption regardless of how popular the Kinetic network becomes.


Hydrating the Heartbeats

#![allow(unused)]
fn main() {
// -> See: kinetic-network/src/store/core.rs — Lines 162 to 175
}

After the domain names are verified, the node must restore its knowledge of infrastructure liveness across the network. It performs a second Sled scan, this time looking for keys starting specifically with the KRS_HB_PREFIX. For each heartbeat found, it extracts the domain name just like before. The value payload for a heartbeat is minimal: it is exactly 8 bytes long, representing a big-endian encoded u64 integer. This integer is the exact Drand kyn at which the heartbeat was originally recorded. The node converts these 8 bytes back into a u64 and inserts it into the last_heartbeats_by_name HashMap, ensuring the node remembers which infrastructure peers were alive immediately before the restart.


Hydrating the Kademlia Memory

#![allow(unused)]
fn main() {
// -> See: kinetic-network/src/store/core.rs — Lines 177 to 191
}

Once the LRU cache is populated with verified records, the node must actually make them available to the standard libp2p Kademlia protocol. It creates a fresh, standard libp2p::kad::store::MemoryStore (referred to internally as inner). It then iterates over every single valid domain name that survived the boot gauntlet. For each name, it derives multiple deterministic Kademlia storage keys using the derive_storage_keys function. (This is necessary because a single domain name maps to multiple DHT coordinates to ensure high redundancy across the network). It wraps the JSON-serialized record into a kad::Record object and forcefully injects it into the inner memory store. Now, when a remote peer queries this node via the DHT, libp2p automatically serves the record directly from the inner store without requiring any extra custom routing logic from Kinetic.


The Pruning Phase (prune)

#![allow(unused)]
fn main() {
// -> See: kinetic-network/src/store/core.rs — Lines 209 to 305
}

Because the DHT is a decentralized, headless entity, there is no central master server sending HTTP DELETE requests when data needs to be removed. Data removal must happen deterministically, locally, and simultaneously across all nodes, based purely on the passage of time (measured in Drand kyns). The prune function executes this critical garbage collection routine. It is called periodically during runtime. It checks three independent lifecycles to determine what should be deleted:

  1. Pruning Commitments: It scans Sled for keys with the KRS_COMMIT_PREFIX. It calculates the age using current_kyn.saturating_sub(kyn). If a commitment’s recorded kyn is older than 100 kyns relative to the current_drand_kyn, it is targeted for deletion. The commit-reveal scheme requires the user to reveal their proof quickly to prevent front-running. A commitment that is 100 kyns old (roughly 5 minutes in a 3-second kyn network) is considered permanently abandoned. Deleting it prevents state bloat on the node’s disk.

  2. Pruning Resquaring Expirations: It scans the LRU cache for standard reveals. It calculates the age by subtracting the record’s drand_kyn from the current_kyn. If this age exceeds the global RESQUARING_EPOCH_KYNS constant, the name registration has expired. The owner failed to resquare their VDF proof in time to maintain ownership, and therefore the network reclaims the name. The name is immediately queued for deletion.

  3. Pruning Heartbeat Timeouts: For domain names acting as infrastructure (like Relays or Validators), they must emit a continuous, periodic heartbeat to prove they are online. If the formula current_kyn - last_heartbeat results in a value that exceeds the idle_timeout (which translates to exactly 7 full days of Drand kyns), the infrastructure node is considered permanently dead. It is queued for deletion to ensure the network routing tables remain clean and accurate.

For any record queued for deletion, the store removes it from the memory caches (reveals_by_name and last_heartbeats_by_name). It then gathers all the associated Sled keys (including the derived Kademlia kad_record: libp2p prefixes) into a deletion queue array. Finally, it spawns a background blocking task to permanently erase these keys from the Sled disk. This multi-threaded deletion strategy ensures that writing to the slow mechanical or SSD disk does not hang the fast in-memory routing operations of the node.


Defensive Arithmetic with saturating_sub

#![allow(unused)]
fn main() {
// -> See: kinetic-network/src/store/core.rs — Lines 218 and 233
}

In the prune function, the code constantly calculates the age of records by subtracting the record’s kyn from the current_drand_kyn. It uses current_kyn.saturating_sub(kyn). In Rust, standard subtraction (-) can panic in debug mode or wrap around in release mode if the right side is larger than the left side. Why would a record’s kyn ever be larger than the current kyn? If a node’s local Drand daemon temporarily desynchronizes, or if the node receives a malicious record that claims to be from the future, standard subtraction would instantly crash the node with an integer underflow panic. saturating_sub neutralizes this attack vector. If kyn is greater than current_kyn, saturating_sub simply returns 0. This logically means the record is 0 kyns old (from the node’s perspective), which prevents it from being prematurely pruned, but more importantly, prevents the node from crashing.


Key Pieces

KineticRecordStore

#![allow(unused)]
fn main() {
// -> See: kinetic-network/src/store/core.rs — Lines 25 to 43
}

This struct is the main architectural bridge between libp2p’s standard Kademlia routing and Kinetic’s proprietary verified storage logic. Let’s break down every field:

  • inner: kad::store::MemoryStore This is the standard libp2p memory store. We let libp2p handle the low-level DHT query responses out of this store, but we act as the bouncer, controlling what is allowed to be placed into it. Everything in here has passed verification.

  • storage: Arc<dyn StorageEngine> A thread-safe, atomically reference-counted pointer to the persistent disk database (Sled). This provides the vital persistence layer across node reboots. By using a dynamic trait object (dyn StorageEngine), the network remains agnostic to the actual storage implementation, allowing for easy mocking during unit tests.

  • vdf_engine: Arc<dyn kinetic_core::traits::VdfEngine> The mathematical backend engine used to evaluate and re-verify VDF proofs during startup and runtime. It performs the heavy lifting for the verification gauntlet.

  • reveals_by_name: LruCache<String, NameRecord> An active in-memory cache of verified domain names. We specifically use an LRU (Least Recently Used) cache to guarantee that a node will never run out of RAM, even if the total size of the global DHT grows to millions of records. When the cache is full, the oldest un-queried record is silently dropped from memory, protecting the node from crashing.

  • last_heartbeats_by_name: HashMap<String, u64> A mapping that tracks the highest Drand kyn at which a specific infrastructure node successfully pulsed. It is used exclusively during the prune cycle to detect and evict dead relays that have stopped responding.

  • accepted_reveals_timestamps: LruCache<String, VecDeque<web_time::Instant>> A secondary LRU cache that tracks the local system time of when reveals were accepted for specific names. This is used exclusively for local rate-limiting to prevent a malicious peer from spamming millions of micro-updates to a single name and causing unnecessary, thrashing disk writes that could burn out an SSD. This uses web_time rather than std::time to remain compatible with WASM environments where standard system time might be mocked or restricted.

  • current_drand_kyn: u64 The node’s internal state tracker for the current progression of time on the network. It dictates what is valid and what is expired.

  • max_reveals_per_hour: usize A configuration variable dictating how many updates a single domain name is allowed to push to the network within a strict one-hour window.


The Role of local_peer_id

#![allow(unused)]
fn main() {
// -> See: kinetic-network/src/store/core.rs — Line 54
}

When initializing the store, the node must pass its own local_peer_id into the libp2p::kad::store::MemoryStore::new(local_peer_id) constructor. In Kademlia, the DHT is a massive XOR metric space. Every node and every piece of data has an address in this space. By providing the local_peer_id, the internal memory store knows exactly where the node resides in the network topology. This allows the store to determine which records it is “closest” to, which dictates which records it should cache and which records it should drop when memory gets tight.


Governance Overrides (Premium Names)

#![allow(unused)]
fn main() {
// -> See: kinetic-network/src/store/core.rs — Lines 143 to 147
}

During the validation gauntlet, there is a distinct branch for kinetic_core::types::NameRecord::Premium { .. }. Premium names are exempt from the grueling VDF verification process. Why? Because premium domains (like core.kyn or relay.kyn) are not claimed via computational work. They are injected directly into the network state via the decentralized governance system. Because their legitimacy is already guaranteed by the blockchain consensus layer, forcing the node to verify a VDF proof for them would be redundant and wasteful. The system instantly flags them as is_valid = true; and pushes them into the LRU cache.


Handling Network ID in Signatures

#![allow(unused)]
fn main() {
// -> See: kinetic-network/src/store/core.rs — Line 100
}

When the signature of a domain name is verified during the startup gauntlet, the verify_signature method expects a specific parameter: kinetic_core::constants::NETWORK_ID. In decentralized systems, cross-network replay attacks are a constant threat. An attacker could theoretically take a valid, proven domain name registration from the Kinetic testnet and maliciously broadcast it on the Kinetic mainnet. Because the VDF proof itself is computationally sound regardless of which network it resides on, a naive system would accept the testnet record and overwrite the mainnet namespace, causing chaos and namespace collisions. To prevent this, the network ID is cryptographically bound directly into the Ed25519 signature payload by the client. When the node verifies the signature on boot, it asserts that the signature is exclusively valid for its currently configured network ID. If a testnet record accidentally ends up on a mainnet disk (perhaps via a manual database copy error by the node operator), it will instantly fail this signature check and be discarded safely without crashing the node.


How This Connects to the Rest of Kinetic

  • CROSS-CRATE: StorageEngine — Defined in the kinetic-core crate. This is the abstract trait that our Sled database implementation fulfills.
  • CROSS-CRATE: NameRecord — Defined and explained in docs/learn/types/05_name_records.md. The fundamental data structure representing a registered domain name on the network.
  • CROSS-CRATE: VdfEngine — Defined in the kinetic-core crate. Evaluates the mathematical validity of a given claim.
  • CROSS-CRATE: derive_storage_keys — Defined in kinetic-core/src/types/keys.rs. The deterministic function that calculates the exact DHT network coordinates for a given domain name.

Quick Reference

  • Initialization Filter: The KineticRecordStore::new function scans the Sled disk, re-verifies VDF proofs computationally, and populates the in-memory DHT.
  • Pruning Commitments: Commitments are permanently deleted from disk if they are older than 100 kyns (roughly 5 minutes).
  • Pruning Domain Names: Domain names are permanently deleted from the network if their age exceeds the RESQUARING_EPOCH_KYNS limit.
  • Pruning Heartbeats: Infrastructure names are permanently deleted if no heartbeat has been heard for 7 days worth of kyns.
  • LRU Cache Protection: The store deliberately uses LRU caches to prevent node RAM exhaustion, providing a hard cap on memory usage regardless of the global DHT size.
  • Saturating Subtraction: Prevents node crashes when calculating age if time becomes temporarily desynchronized.

Open Questions / Things to Revisit

Warning

  1. Startup Blocking Time: The new initialization function executes a tokio::task::block_in_place during a massive for loop that iterates over up to 100_000 records from the Sled database. If the node has a populated database of near exactly 100,000 records, evaluating 100,000 sequential VDF proofs synchronously could take an exorbitant amount of real-world time. This could delay node startup significantly, leading to network desynchronization immediately upon boot. Should this verification process be batched, parallelized using Rayon, or executed fully asynchronously instead of blocking?
  2. Pruning Race Conditions: In the prune() method, a background blocking task is spawned to delete keys from the slow Sled disk. If a valid network update for that exact domain name arrives from the DHT immediately after the pruning task starts, is it possible that the background task accidentally deletes the newly written valid record, causing a temporary data eclipse on the local node?

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 NameRecord claims without actually performing the required Verifiable Delay Function (VDF) computation. They would essentially steal namespaces for free.
  • Identity Forgery: Attackers could submit fake AuthorizedKid delegations, 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:

  1. Commitments: If the JSON has a "hash" key but NO "vdf_proof" key, we assume it is a Commitment. We then attempt to rigidly parse it as a kinetic_core::types::Commitment struct.
  2. Reveals / NameRecords: If the JSON contains "vdf_proof" or "granted_at", it is parsed as a NameRecord.
  3. Heartbeats: If the JSON contains "latest_drand_kyn", it is parsed as a Heartbeat.
  4. Delegated Kids: If the JSON contains "delegation_signature", it is parsed as an AuthorizedKid.
  5. Manifests: If the JSON contains "manifest", it is parsed as an AuthorizedManifest.
  6. Routing Records: If the JSON contains "host_id", it is parsed as a HostRoutingRecord.

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 active NameRecord out of the Sled database. It then passes both the new delegated record and the owner’s NameRecord to 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_verify to false, forcing a full cryptographic check.
  • 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.
  • 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.
  • 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 NameRecord in the fast, in-memory reveals_by_name cache. If it misses, it queries the slow, persistent Sled storage. We must have the parent NameRecord available to extract the public key needed to verify the signature on a child AuthorizedKid.
  • 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 inner MemoryStore. It intercepts put for cryptographic validation. It returns Error::MaxProvidedKeys for add_provider to prevent Provider Spam attacks on the network.

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 in docs/learn/types/02_records.md and related files from Stage 1.
  • CROSS-CRATE: The size limit variables (such as LIMITS_STORAGE_MAX_VALUE_BYTES) are sourced from kinetic_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 in kinetic-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 incoming PUT_VALUE network requests into the put method 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 as Commitment
    • If "vdf_proof" or "granted_at" $\rightarrow$ Parses as NameRecord
    • If "latest_drand_kyn" $\rightarrow$ Parses as Heartbeat
    • If "delegation_signature" $\rightarrow$ Parses as AuthorizedKid
    • If "manifest" $\rightarrow$ Parses as AuthorizedManifest
    • If "host_id" $\rightarrow$ Parses as HostRoutingRecord
  • 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_record to safely skip VDF checks if the proof was already validated in a separate background thread.

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::Value tree 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 an AuthorizedKid requires calling get_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.inner kad::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 custom RecordStore trait that queries Sled directly for all reads, eliminating the need for the redundant inner MemoryStore.

Client Core: The Network Client Handle (Part 1)

Crate: kinetic-network Stage: 8 Reading time: 15 minutes Depends on: Stage 7 (kinetic-core), Stage 1 (kinetic-types)


What Is This?

This file introduces the NetworkClient. This is one of the most critical structural components in the entire Kinetic codebase. It serves as the primary, exclusive bridge between the rest of the Kinetic application and the background peer-to-peer (P2P) network engine.

When any part of Kinetic—whether it is a background daemon process, a data resolver, a web interface proxy, or a storage component—wants to send a message over the network or fetch a payload from the DHT (Distributed Hash Table), it does not touch the network sockets directly. It does not open TCP streams. It does not manage libp2p states. Instead, it talks solely to the NetworkClient.

Think of the actual P2P network engine as a secure, chaotic vault where a million things are happening at once: connections are opening, peers are dropping, streams are multiplexing. The NetworkClient is the calm bank teller standing at the window. You hand your request to the teller, the teller takes it into the vault, and eventually brings you back an answer.

The NetworkClient acts as a thread-safe, easily cloneable handle. It does not run the network event loop itself. Instead, it takes requests (like “publish this payload” or “send this proxy request”), packages them into an internal Command enum, and ships them over an asynchronous channel to the actual background task that manages the network.

Because it is just a lightweight handle wrapped around a channel sender, you can easily clone the NetworkClient and hand copies of it to a hundred different asynchronous tasks inside Kinetic. Every clone points back to the exact same background network engine, allowing the entire application to interact with the network simultaneously without fighting over locks.


Why Kinetic Needs This

Networking in Rust, especially advanced P2P networking using libraries like libp2p, is inherently stateful and complex. The network requires a central, monolithic event loop (often called a “swarm”) that must continuously poll for incoming connections, handle stream multiplexing, manage peer disconnections, and route complex DHT queries.

If every part of Kinetic tried to borrow or lock this network state directly to send a message, the entire application architecture would collapse under its own weight.

Here is exactly what would happen without the NetworkClient abstraction:

  • Lock Contention: If the network state was hidden behind a standard Mutex, every time a component wanted to send a message, it would lock the entire network. If multiple components tried to talk at once, the system would grind to a halt waiting for access.
  • Protocol Freezes: If a long-running process held a lock on the network state while processing a heavy piece of data, the core network event loop would stop polling. Incoming network events would be blocked, causing Kinetic to drop active connections, miss heartbeats, or fail strict protocol timeouts.
  • Architectural Entanglement: The core daemon would have to know exactly how to drive the libp2p state machine, making the code impossible to test in isolation, difficult to scale, and a nightmare to maintain.

Kinetic avoids this disaster by separating the handle (what the app sees) from the engine (what does the work).

The NetworkClient solves three massive architectural problems for Kinetic:

  1. Concurrency Without Locking: By using message passing (Rust channels) instead of shared state (mutexes on the swarm), any thread can issue network commands concurrently. The background network engine processes these commands one by one from a queue, ensuring internal state safety without freezing the rest of the application.
  2. Hot Swapping (The update_backend trick): Networks drop. Connections fail. Sometimes, Kinetic needs to reboot its P2P swarm from scratch. If every component held a direct reference to the swarm, a reboot would require tracking down and updating every reference across the codebase. Because NetworkClient wraps its sender in an Arc<RwLock>, Kinetic can quietly hot-swap the internal channel without the rest of the application even noticing. The handles stay valid, but they instantly start pointing to the new network engine.
  3. Asynchronous Response Routing: P2P commands are inherently asynchronous. When Kinetic asks to resolve a payload from the DHT, it might take several seconds as the query hops across multiple peers. The NetworkClient implements a clever “oneshot channel” trick. It creates a temporary, single-use return channel, bundles it with the command, and waits. The background task does the hard work, finds the payload, and sends it back through that custom channel directly to the task that asked for it.

Without this specific file and its patterns, the entire Kinetic daemon would be a tangled mess of libp2p internals.


How It Works

The NetworkClient is fundamentally a wrapper around a channel sender, but it has several advanced Rust patterns built into it to handle the realities of a distributed network. Let’s break down exactly how it operates, step by step, using the source code as our map.

1. The Core Structure and the RwLock Advantage

-> See: kinetic-network/src/client/core.rs — Lines 10 to 15

The struct definition sets up the entire paradigm for network communication in Kinetic:

  • It holds sender: std::sync::Arc<std::sync::RwLock<mpsc::Sender<Command>>>
  • It conditionally holds stream_control (unless compiling for WASM).

Wait, mpsc::Sender is already cloneable in Rust. The standard way to share a sender across multiple tasks is just to call .clone() on it. So why did Saif wrap it in an Arc<RwLock<...>>?

In standard Rust, if you just clone the mpsc::Sender, every clone holds a hardcoded link to that specific receiver. But Saif built Kinetic with a specific resilience feature in mind: total network resets.

If the Kinetic node loses connection entirely, or encounters a fatal protocol error, it needs to rebuild its libp2p swarm from scratch. When that happens, the old background task dies, and the old mpsc receiver is destroyed. If we had just cloned the sender directly, every task in Kinetic would now hold a dead sender, and the node would be functionally lobotomized.

By wrapping the sender in an Arc<RwLock>, we create a shared pointer (Arc) to a mutable memory slot (RwLock).

Why an RwLock (Read-Write Lock) instead of a standard Mutex?

  • Read-heavy workload: 99.99% of the time, Kinetic just wants to read the sender so it can clone a copy of it to send a message. An RwLock allows an infinite number of simultaneous readers. A Mutex would block all other readers, introducing the exact contention we are trying to avoid.
  • Write-rare workload: The only time Kinetic needs to write to this lock is during a total network restart, which happens very rarely.

When the network reboots, Kinetic calls the update_backend method.

2. The Hot-Swap Mechanism for Network Resilience

-> See: kinetic-network/src/client/core.rs — Lines 45 to 57

The update_backend function is the secret sauce for Kinetic’s node resilience. When the network restarts, this function takes a write lock on the RwLock:

#![allow(unused)]
fn main() {
if let Ok(mut s) = self.sender.write() {
    *s = sender;
}
}

It overwrites the old sender with a brand new one. Every existing NetworkClient clone out there in the application (in the daemon, in the API server, in the storage engine) immediately starts routing its new commands to the new background task, with zero disruption to the higher-level logic. This is hot-swapping at its finest.

3. The Oneshot Channel Trick for Async Responses

When you ask the network to do something, you usually want an answer back. But you are communicating over an mpsc (Multi-Producer, Single-Consumer) channel, which is inherently a one-way street. The client sends a command to the background, but how does the background send the answer back to that specific client task out of the hundreds running?

-> See: kinetic-network/src/client/core.rs — Lines 91 to 111 (The send_proxy_request function)

Kinetic uses the “oneshot channel” pattern. This is analogous to ordering food at a busy restaurant and being handed a buzzing pager.

Here is the exact step-by-step flow inside send_proxy_request:

  1. The NetworkClient creates a brand new oneshot::channel. This channel only ever carries exactly one message. It has a tx (transmitter) and an rx (receiver).
  2. It packages the actual network request (the ProxyRequest), along with the tx half of the oneshot channel (the “pager”), into a Command::SendProxyRequest enum.
  3. It sends this Command over the main mpsc channel to the background network engine.
  4. The NetworkClient then immediately calls .await on the rx half of its oneshot channel. It goes to sleep, waiting for the pager to buzz.
  5. In the background, the network engine processes the queue, sees the command, does the complex libp2p network I/O, gets a response from the remote peer, and pushes that response into the tx channel.
  6. The NetworkClient wakes up from its .await, unwraps the response, and hands it back to the caller seamlessly.

This pattern transforms a decoupled, asynchronous, one-way message passing architecture into a clean, easy-to-use async function that looks like a normal RPC call to the rest of Kinetic.

Notice how errors are mapped here:

#![allow(unused)]
fn main() {
.map_err(|_| ProxyError::ChannelClosed)?;
}

If the background task crashes or is restarted while processing, the tx half is dropped. The client’s .await immediately resolves with an error, which we safely map to ProxyError::ChannelClosed. This prevents the client from hanging forever if the network engine dies.

4. Publishing Redundant Payloads and Size Limits

-> See: kinetic-network/src/client/core.rs — Lines 143 to 179

Kinetic relies on the DHT to store data redundantly across the network. The publish_redundant_payload function is the main gateway for data to enter the DHT.

Notice the explicit size limit check on line 152:

#![allow(unused)]
fn main() {
if payload_bytes.len() > 80_000 { ... }
}

The core Kinetic schema sets a strict semantic limit of 64 KB (65,536 bytes) for actual payloads. But here, the network client allows up to 80 KB. Why the discrepancy? Because data traveling over the P2P network is not just raw user data. It is wrapped in cryptographic proofs (like VDF outputs), signatures, and structural serialization overhead (like Protobuf or MessagePack framing).

If the network client enforced a 64 KB limit at the network edge, a valid 64 KB user payload would be rejected the moment a 500-byte signature was attached to it by the storage layer. The 80 KB limit provides necessary safety headroom while still fundamentally preventing malicious nodes from flooding the network with multi-megabyte spam payloads that could cause Out-Of-Memory (OOM) crashes on smaller nodes.

Once the size is validated, it uses the exact same oneshot trick described above, sending a Command::PublishRedundant to the background task and waiting for confirmation.

5. Heartbeats vs. Reveals

-> See: kinetic-network/src/client/core.rs — Lines 187 to 213

Kinetic has a separate publish_heartbeat function. Structurally, it looks nearly identical to publish_redundant_payload. It uses the exact same oneshot pattern, the exact same error mapping, and the exact same internal command flow. So why duplicate it in the client handle?

Because in the background (within the DHT routing logic), heartbeats are treated vastly differently than standard data (which are often called Reveals). Heartbeats go to a dedicated keyspace in the DHT and likely have much shorter Time-To-Live (TTL) values, ensuring the network isn’t clogged with stale node statuses.

By creating a distinct publish_heartbeat method on the NetworkClient, the architectural intent is crystallized directly into the type system and API surface. A developer cannot accidentally publish a massive Reveal payload into the heartbeat keyspace if they are forced to choose between these two explicit, well-named methods. It enforces correct usage at compile time.

6. Resolving Payloads with Strict Timeouts and WASM Support

-> See: kinetic-network/src/client/core.rs — Lines 220 to 260

Getting data out of the DHT uses resolve_redundant_payload.

A DHT lookup is not like a local database query. The background engine has to ask peer A, who might not know, so they ask peer B, who might ask peer C. This distributed search takes time. If peer C drops offline halfway through the query, the request could theoretically hang indefinitely.

To protect the node from resource exhaustion, the NetworkClient enforces a strict 10-second timeout on the oneshot receiver. However, because Kinetic is designed to run in web browsers as well as native servers, this logic requires complex conditional compilation.

-> See lines 241-247 for the non-WASM (native) timeout logic: It uses standard tokio::time::timeout. If 10 seconds pass without a response from the oneshot channel, it aborts the wait and returns a ResolutionError::Internal.

-> See lines 252-259 for the WASM-specific logic:

#![allow(unused)]
fn main() {
#[cfg(target_arch = "wasm32")]
}

Standard tokio timers do not work in WebAssembly because WASM lacks a native operating system timer interface. Instead, Saif uses futures::future::select combined with futures_timer::Delay to race the network response against a 10-second timer. Whichever future finishes first wins the race.

If the background task does not reply within 10 seconds, the NetworkClient aborts the wait. This prevents a slow P2P network from creating a backlog of blocked asynchronous tasks that would eventually exhaust the node’s memory.

7. Handling Poisoned Locks and Panics

When dealing with shared state across multiple asynchronous tasks, error handling becomes paramount. The NetworkClient employs several layers of defense against panics.

-> See: kinetic-network/src/client/core.rs — Lines 68 to 73

#![allow(unused)]
fn main() {
pub fn get_sender(&self) -> mpsc::Sender<Command> {
    self.sender
        .read()
        .unwrap_or_else(|e| e.into_inner())
        .clone()
}
}

What does unwrap_or_else(|e| e.into_inner()) do here? In Rust, if a thread panics while holding a lock (like an RwLock), the lock becomes “poisoned.” By default, future attempts to acquire the lock will return an Err, to prevent tasks from accessing potentially corrupted data.

However, the NetworkClient is just holding a cloneable mpsc::Sender inside this lock. The sender itself doesn’t become corrupted if a thread panics—it just sends messages to a channel. By using into_inner() on the lock error, Kinetic tells Rust: “I don’t care if the lock is poisoned. The data inside (the sender) is still safe to use. Extract it anyway and clone it.”

This ensures that a panic in one isolated part of the Kinetic application does not permanently brick the NetworkClient for the rest of the application. The network client remains resilient and continues functioning even in a degraded state.

8. Stream Control for Raw Connections

-> See: kinetic-network/src/client/core.rs — Lines 76 to 83

You will notice the stream_control field, which returns an Option<libp2p_stream::Control>. While most commands go through the mpsc::Sender, libp2p provides a dedicated Control handle for managing raw byte streams directly between peers. The NetworkClient safely holds this control handle alongside the sender.

Why is this wrapped in an Option and an RwLock?

  • Option: Because mock clients (used in tests) and WASM clients do not always have access to the raw stream controller. It must be optional to allow compiling across different environments without crashing.
  • RwLock: Just like the main sender, the stream controller might need to be hot-swapped during a total network reset, so it lives behind the same read-write locking paradigm.

When a client wants to open a direct stream to a peer (bypassing the command queue for raw data transfer), it calls stream_control(), gets a clone of the control handle, and uses it directly.


Key Pieces

  • NetworkClient struct: (Lines 10-15) The thread-safe, cloneable handle that holds an Arc<RwLock> to the command sender. It is the only approved way for the application to command the network.
  • new and new_mock: (Lines 18-42) Constructors. Notice how the mock version ignores stream control, allowing for isolated unit testing without spinning up a real libp2p swarm.
  • update_backend: (Lines 45-57) The hot-swap mechanism. It locks the RwLock and safely replaces the sender, allowing the network to restart without invalidating existing client handles.
  • get_sender: (Lines 68-73) Provides a quick clone of the underlying sender, gracefully handling poisoned lock states by using unwrap_or_else(|e| e.into_inner()). This ensures a panic in one thread doesn’t permanently brick the client.
  • send_proxy_request: (Lines 91-111) Uses a oneshot channel to send a direct request to a specific peer and asynchronously await their direct response.
  • send_proxy_response: (Lines 118-136) The reverse of the above. Allows Kinetic to reply to an incoming request using a provided response channel.
  • publish_redundant_payload: (Lines 143-179) Pushes data to the DHT. Enforces an 80 KB size limit to account for 64 KB of raw data plus cryptographic and serialization overhead.
  • publish_heartbeat: (Lines 187-213) Semantically distinct method for publishing node liveness signals to a specific DHT keyspace.
  • resolve_redundant_payload: (Lines 220-260) Pulls data from the DHT. Crucially implements a 10-second timeout so the application never hangs indefinitely waiting for unresponsive peers. Contains complex WASM-specific timer fallbacks.

How This Connects to the Rest of Kinetic

  • CROSS-CRATE: This module relies on kinetic_core::error types (like NetworkClientError, PublishError, and ResolutionError), which were established in Stage 7.
  • CROSS-CRATE: It uses types from kinetic-types like ProxyRequest and ProxyResponse, defined in Stage 1.
  • Inbound Connections: This file represents the client side of the internal API. When Kinetic wants to talk to the network, it calls methods here.
  • Outbound Connections (The Backend): The Command enum sent over the channel is processed by the actual libp2p event loop. The event loop is the consumer of this channel, acting upon the requests.
  • WASM Compatibility: The explicit conditional compilation attributes (#[cfg(target_arch = "wasm32")]) prove that Kinetic is structurally designed to run directly inside a browser tab, requiring alternative timer implementations.

Quick Reference

  • The Goal: Provide a safe, concurrent way to interact with the single-threaded network event loop.
  • The Delivery Mechanism: mpsc::Sender<Command> locked behind an Arc<RwLock> to allow for zero-downtime hot-swapping.
  • The Response Mechanism: tokio::sync::oneshot::channel passed inside the command payload. Acts like a restaurant pager for async tasks.
  • Max Payload Size Limit: 80,000 bytes (safely fits a 64 KB schema limit + crypto overhead without rejecting valid data).
  • DHT Resolution Timeout: 10 seconds maximum. Uses tokio natively, futures_timer in WASM.
  • Locking strategy: Read locks for sending commands (fast, concurrent), Write locks for restarting the network (rare, blocking).
  • Mocking: Fully mockable via new_mock for fast unit tests.

Open Questions / Things to Revisit

  • WASM Backend Updates: The update_backend function exists for WASM builds, but it intentionally drops the stream_control component. It would be worth verifying if WASM instances in Kinetic ever actually undergo hot-restarts in practice, or if the browser environment just drops the instance upon failure, rendering this hot-swap logic unnecessary overhead for the web build.
  • Hardcoded 10-Second Timeout: The 10-second timeout in resolve_redundant_payload is hardcoded into the binary. In congested or geographically distant networks (e.g., cross-continental DHT routing), 10 seconds might be too aggressive and cause premature failures. Consider if this should be exposed as a configurable parameter via a node settings file or environment variable in the future.
  • Timeout Memory Leaks and Panic Risks: When a resolution times out on the client side, the oneshot rx channel is immediately dropped. However, the background network task is still blindly running the DHT query. We need to ensure the background task handles dropped tx channels gracefully without panicking when it eventually tries to send the late response. If the background task calls .unwrap() on the send operation, a client-side timeout could crash the entire network event loop.

Client Core: Extended Operations

Crate: kinetic-network Stage: 8 Reading time: 45 minutes Depends on: 06_client_core_1.md, kinetic-core (Stage 7)


What Is This?

This document is the second half of the deep dive into the NetworkClient interface. If the first document covered the basic plumbing, this document covers the high-level protocol mechanics. The NetworkClient acts as the definitive bridge between the chaotic peer-to-peer network and the orderly Kinetic application layer. Specifically, this document covers how Kinetic enforces data availability through quorum verification. It also covers how nodes discover each other in a decentralized way without falling victim to replay attacks. It explores how the client interfaces with the Gossipsub protocol for efficient, network-wide broadcasting. It covers how the node manages its own lifecycle, including bootstrapping and status reporting. Without these extended operations, a Kinetic node would just be a dumb data store. These methods imbue the node with the intelligence required to participate in consensus. By abstracting these complexities behind a clean API, the NetworkClient makes it easy for the daemon to interact with the network. We will examine each of these operational categories in exhaustive detail. We will look at the exact commands they send, the channels they use, and the failure modes they handle. This understanding is crucial for diagnosing network-level issues in the Kinetic daemon. It is also vital for ensuring that the Kinetic consensus model correctly utilizes the underlying network primitives. Finally, we will examine the unit testing approach used to verify the resilience of this client architecture. It is imperative to read this code not just as generic Rust, but as the primary defense boundary of the network.


Why Kinetic Needs This

Generic peer-to-peer libraries are not enough for a secure, decentralized protocol. They provide the pipes, but Kinetic must build the security model on top of those pipes.

1. The Necessity of Quorum Verification

In a client-server architecture, saving data is straightforward: you write it to a database and assume it persists. In a decentralized peer-to-peer network, there is no database. When a node generates a new VDF proof or mines a new block, it pushes that data into the Distributed Hash Table (DHT). However, a successful push to the DHT simply means the data was sent over the wire to someone. It does not guarantee that the receiving nodes didn’t immediately crash. It does not guarantee they didn’t drop the data maliciously. If a node publishes a critical block but only a few unreliable peers store it, the block could be lost forever. This would cause the network to fracture, as some nodes have the block and others don’t. Kinetic requires strict “data availability” guarantees. When critical data is published, we must prove that a certain threshold of independent peers have stored it. This threshold is known as a quorum. The verify_quorum method provides this exact capability. It allows the consensus engine to ask the network layer: “Can you cryptographically confirm that $N$ distinct nodes are currently holding this payload?” Without this, the network could easily fork or lose state, destroying the integrity of the blockchain. This exact verification step is what prevents lazy or malicious nodes from quietly dropping data they are supposed to store. It acts as the foundation of the consensus layer’s assumptions.

2. The Threat Model of Host Discovery

In legacy internet architectures, servers have static IP addresses that are registered in DNS. In the Kinetic network, peers are transient. They join from laptops, home internet connections, and virtual private servers. Their IP addresses change constantly due to DHCP leases and network roaming. To solve this, Kinetic nodes publish their current IP addresses to the DHT inside a HostRoutingRecord. However, this introduces a severe security vulnerability known as the Replay Attack. Imagine a scenario where you published a HostRoutingRecord three months ago from a compromised public Wi-Fi network. If a malicious actor captures that old record, they could republish it to the DHT today. Other peers would resolve your ID, see the old record, and attempt to connect to the compromised IP. This effectively blackholes your traffic and isolates you from the network, an attack known as an Eclipse Attack. To prevent this, Kinetic ties every single routing record to the current round (kyn) of the Drand network. The NetworkClient must provide tools not just to fetch these records, but to validate their temporal freshness. This validation against the Drand clock must occur before handing the record to the application layer. This ensures that the routing layer of Kinetic remains Byzantine fault-tolerant. It allows the network to automatically prune dead links and route around compromised nodes.

3. The Need for Efficient Network-Wide Broadcasts

The DHT is excellent for finding a specific needle in a haystack, such as a specific peer’s routing record. But what happens when you want to announce a new block to the entire network simultaneously? If a node tried to send a direct, point-to-point message to every single other node in the network, its bandwidth would be saturated instantly. This is an $O(N)$ scaling problem that cripples naive peer-to-peer designs. To solve this, Kinetic uses a publish-subscribe mesh protocol called Gossipsub. In Gossipsub, nodes only send messages to a small, curated number of their immediate neighbors. Those neighbors validate the message and then forward it to their neighbors. This allows the message to propagate exponentially fast across the entire network. It does so with minimal bandwidth overhead per node. The NetworkClient needs to expose methods to subscribe to these topics. It needs methods to broadcast messages to these topics. Crucially, it needs methods to report on the validity of those messages to punish bad actors. Without this immune system, a single bad actor could spam the Gossipsub mesh and halt the entire network.


How It Works

The extended methods on the NetworkClient follow a consistent architectural pattern. They construct a specific Command enum variant. They create a oneshot::channel to listen for the response from the event loop. They clone the read lock on the MPSC sender. They send the command to the event loop. They asynchronously .await the result, mapping any channel closure errors to a NetworkClientError. Let us break down exactly what happens under the hood for each category of operations.

1. Quorum Verification Mechanics

When the application layer needs to verify data availability, it invokes verify_quorum.

-> See: kinetic-network/src/client/core.rs — Lines 275 to 295

The method signature takes a name parameter, which is a string identifier for the data. It also takes the payload_bytes that were published. It constructs a Command::VerifyQuorum containing these parameters. It sends this command to the event loop. Inside the event loop, the network state machine will perform a distributed query. It will ask its connected peers and the DHT if they hold the payload matching this identifier. It aggregates the responses, ensuring that each response comes from a distinct PeerId. The event loop then sends back a usize representing the total count of verified holders. The NetworkClient awaits this count. It returns this count to the caller. It is then the responsibility of the caller to check if this usize meets the configured network quorum size. If it does, consensus can proceed. If not, the publisher might need to retry.

2. Network Lifecycle and Bootstrapping

A peer-to-peer node is a living entity that must constantly maintain its connections.

-> See: kinetic-network/src/client/core.rs — Lines 302 to 335

get_network_status: This method requests a JSON dump of the network’s current diagnostic state. It sends a Command::GetNetworkStatus to the event loop. The event loop serializes its internal metrics into a serde_json::Value. These metrics include connected peers, routing table buckets, and bandwidth usage. The application uses this primarily for telemetry. It is also the mechanism that powers the kinetic status CLI command.

rebootstrap_network: In the Kademlia DHT algorithm, nodes must occasionally perform a “random walk”. This random walk is necessary to discover new peers. If a node remains stagnant, its routing table will slowly fill with dead peers as nodes go offline. When the application detects poor connectivity, it calls this method. It sends a Command::Bootstrap. This forces the underlying libp2p Kademlia implementation to immediately initiate a new discovery query. This reconnects the node to the broader swarm and refreshes its routing table. It ensures the node does not become isolated.

3. Secure Host Routing Records and Drand Validation

This is one of the most critical security boundaries in the entire network client.

-> See: kinetic-network/src/client/core.rs — Lines 342 to 397

Publishing a Record: When a node wants to announce its current IP address, it calls publish_host_routing_record. The client takes the kinetic_core::types::HostRoutingRecord. This record contains the node’s public key, multiaddrs, and signature. The method serializes this struct to raw bytes using serde_json. It then delegates the actual publishing to the underlying publish_redundant_payload method. The data is stored in the DHT under the specific string key host_route_{host_id}.

Resolving and Validating a Record: The resolve_host_routing_record method is defensive and complex. First, it calls get_current_drand_kyn().await. This sends a command to the event loop to get the absolute latest Drand round number. This Drand value acts as our decentralized, untamperable clock. Second, it attempts to resolve the raw bytes from the DHT using the key host_route_{host_id}. Third, it attempts to deserialize those bytes back into a HostRoutingRecord. Finally, and most crucially, it calls crate::store::verification::verify_host_routing_record. It passes both the deserialized record and the current_drand_kyn to this external function. This validation function performs two vital checks. First, it cryptographically verifies that the signature on the record is valid. Second, it checks that the Drand kyn embedded inside the record is within the acceptable time window. If the record is too old, the function returns an error. The NetworkClient drops the record if this validation fails. This is what prevents replay attacks and Eclipse attacks.

4. Gossipsub Integration and the Immune System

Gossipsub provides the mesh network for broadcasting, but it requires active maintenance by the nodes.

-> See: kinetic-network/src/client/core.rs — Lines 405 to 473

subscribe_gossip and broadcast_gossip: These methods allow the client to join specific publish-subscribe topics. For example, a node might subscribe to a topic specifically for new blocks. When subscribe_gossip is called, the event loop joins the mesh for that topic. When broadcast_gossip is called, the payload is sent to the event loop. The event loop then forwards the payload to a curated subset of connected peers. Those peers will validate it and forward it further, achieving exponential reach.

report_gossip_validation: This method represents the immune system of the Kinetic network. In Gossipsub, you do not blindly trust messages from your neighbors. When the event loop receives a Gossipsub message, it passes it up to the application layer. The application layer performs heavy validation, such as checking a block’s proof of work. If the message is valid, the application calls report_gossip_validation with is_valid = true. If the message is invalid, it calls it with is_valid = false. Crucially, this method does not use .await. It uses a synchronous try_send on the MPSC channel.

-> See: kinetic-network/src/client/core.rs — Lines 465 to 471

Because validation happens constantly in the hot path, we cannot afford to block the caller. If the MPSC channel is temporarily full, the validation report is simply dropped. When the event loop receives a Reject acceptance, it acts on it. It lowers the internal reputation score of the peer that sent the bad message. If a peer’s score drops too low, they are disconnected. They are also banned from the mesh, protecting the network from spam.

5. Testing the Client Backend

The client core file concludes with an important test that verifies a core feature of the NetworkClient architecture: hot-swapping the backend.

-> See: kinetic-network/src/client/core.rs — Lines 475 to 519

The test test_network_client_hot_swap demonstrates that because the MPSC sender is protected by an Arc<RwLock>, it can be updated on the fly. It creates a mock client and fires a request. It then creates a new MPSC channel and calls client.update_backend(tx2, None). It fires a second request and verifies that it is correctly routed to the new channel without dropping or breaking the NetworkClient instance held by the application. This ensures that the daemon can restart or recover the networking event loop internally without having to tear down and rebuild the entire application state. This architectural flexibility is what makes the NetworkClient so robust in the face of underlying network failures.


Key Pieces

Here is a structured breakdown of the most vital functions in this section of the codebase:

verify_quorum

  • What it does: Dispatches a command to count how many distinct peers have confirmed receipt of a specific payload.
  • Where it lives: client/core.rs — Lines 275-295
  • Why it matters: It provides the mathematical proof of data availability required for decentralized consensus. Without it, the network could easily fracture.

resolve_host_routing_record

  • What it does: Fetches a peer’s routing information from the DHT, parses it, and validates its signature.
  • Where it lives: client/core.rs — Lines 379-397
  • Why it matters: It is the primary defense against network partitioning and eclipse attacks. It ensures nodes only connect to fresh, authenticated endpoints.

publish_host_routing_record

  • What it does: Serializes the local node’s routing information and pushes it to the DHT.
  • Where it lives: client/core.rs — Lines 342-353
  • Why it matters: This is how a node announces its presence and its dynamic IP address to the rest of the network.

get_current_drand_kyn

  • What it does: Asks the event loop for the latest known, trusted Drand round number.
  • Where it lives: client/core.rs — Lines 360-372
  • Why it matters: Drand is Kinetic’s decentralized clock. Without it, temporal validation of routing records and blocks is impossible in a decentralized setting.

report_gossip_validation

  • What it does: Sends a non-blocking, fire-and-forget signal to the event loop indicating whether a peer’s gossip message should be accepted or rejected.
  • Where it lives: client/core.rs — Lines 454-473
  • Why it matters: It powers the peer scoring system. It acts as the network’s immune system, actively isolating malicious or broken nodes.

How This Connects to the Rest of Kinetic

This file serves as a major integration point between the raw networking layer and the core protocol rules:

  • CROSS-CRATE: It relies on kinetic_core::types::HostRoutingRecord. This type, defined in Stage 7, contains the cryptographic primitives, multiaddrs, and the Drand kyn fields that this client validates.
  • Internal Verification: It offloads the actual cryptographic math to crate::store::verification::verify_host_routing_record. This module will be covered later in this crate. This separation keeps the client focused on message passing rather than cryptography.
  • Drand Dependency: The entire routing security model assumes that the event loop is successfully syncing with the Drand network. If Drand fails, the NetworkClient cannot resolve peers, preventing connections.

Quick Reference

| Method | Execution | Primary Purpose | Key Failure Modes | | :— | :— | :— | :— | | verify_quorum | async | Prove data availability across the swarm. | Event loop timeout; MPSC channel closed. | | get_network_status | async | Retrieve JSON diagnostic telemetry. | MPSC channel closed. | | rebootstrap_network | async | Force Kademlia peer discovery. | MPSC channel closed. | | publish_host_routing_record | async | Push local routing info to the DHT. | JSON serialization failure; DHT publish timeout. | | resolve_host_routing_record | async | fetch peer routing info. | Invalid signature; expired Drand kyn; DHT lookup failed. | | get_current_drand_kyn | async | Retrieve the decentralized clock value. | MPSC channel closed. | | subscribe_gossip | async | Join a pub-sub mesh topic (e.g. blocks). | MPSC channel closed. | | broadcast_gossip | async | Transmit data to the entire mesh network. | MPSC channel closed. | | report_gossip_validation | sync | Punish bad actors in the Gossipsub mesh. | Fails silently if the MPSC channel is at capacity. |


Open Questions / Things to Revisit

  1. Gossip Validation Dropping: report_gossip_validation uses a non-blocking try_send. If the MPSC channel is saturated, the validation report is silently dropped. While this prevents blocking, does it allow a sophisticated attacker to spam the network, saturate the channel with garbage, and thus prevent their own penalization because the reports are dropped? We may need to investigate backpressure mechanisms in the event loop.
  2. Drand Circular Dependency: If resolve_host_routing_record requires a recent Drand kyn, what happens when a node boots up cold? It needs peers to fetch the latest Drand pulse, but it needs the latest Drand pulse to resolve peers. We need to meticulously document how the bootstrap sequence resolves this chicken-and-egg problem in the daemon initialization phase.
  3. Quorum Thresholds: The verify_quorum method returns a raw usize count. It is up to the caller to decide if that number constitutes a valid quorum. Is this threshold hardcoded in the application layer, or is it dynamically adjusted based on the current estimated network size? This should be clarified when we review the daemon consensus code.
  4. Error Handling on Resolve: If resolve_host_routing_record encounters an invalid signature, it maps the error to a generic NetworkClientError::Other. Should there be a more specific error type so the application layer can distinguish between “network failure” and “malicious peer detected”?
  5. Testing Coverage: The unit tests in this file cover the hot-swapping logic of the backend channel perfectly, but they do not mock the event loop’s behavior for verify_quorum or resolve_host_routing_record. Should there be unit tests that simulate a network partition or a malicious peer returning an old Drand kyn?

Event Loop Utilities and XOR Tie-Breaker

Crate: kinetic-network Stage: 8 Reading time: 55 minutes Depends on: types/01_overview.md, types/04_reveal.md, vdf/01_overview.md, kid/01_overview.md


What Is This?

This file provides the essential utility structures and foundational functions that power the asynchronous NetworkEventLoop inside the Kinetic network. It acts as the critical glue code that holds the complex, distributed networking logic together. Specifically, it includes the state-tracking structures (PendingGet, PendingPut, PendingQuorum) that bridge the gap between asynchronous DHT (Distributed Hash Table) queries and the synchronous-looking code that requests them.

Furthermore, it contains the cross-platform execution wrappers for spawning background tasks, ensuring the codebase compiles and runs identically on native servers and WebAssembly (WASM) browser clients. It also includes the multiaddress validation logic to prevent local network IP pollution from corrupting the public routing table, which is a common vulnerability in decentralized networks.

Most importantly, this file implements the xor_tie_breaker—the deterministic conflict resolution algorithm used when multiple peers claim ownership of the exact same Kademlia DHT key. This tie-breaker is the core consensus mechanism for the Kinetic naming system, dictating how the network agrees on who truly owns a name when multiple valid cryptographic proofs are presented to it simultaneously.

Without this file, the network would have no way to process asynchronous responses, would crash on WASM targets, would be poisoned by private IPs, and would fork into a million different states because it couldn’t agree on name ownership.


Why Kinetic Needs This

To truly understand why these utilities are necessary, you have to look at the hostile, chaotic, and asynchronous environment of a decentralized P2P network.

The Asynchronous Chaos of the Kademlia DHT

The Kademlia Distributed Hash Table is inherently asynchronous and distributed. When a node asks the network for a record (like searching for a user’s IP address or searching for the owner of a .kin domain name), it doesn’t just query a central database and wait for a single immediate answer. It asks multiple nodes across the globe simultaneously. Their responses will then trickle in over time as discrete, unpredictable events over the network socket.

Kinetic needs a structured, reliable way to track these inflight requests. If a user queries the network for a Reveal record, the system needs a way to “park” that user’s specific request in memory, go out to the network, collect the asynchronous responses over the next few seconds, and then specifically wake up the user’s parked request to hand them the final, accumulated result. The Pending* tracking structures solve this exact problem, preventing network responses from getting lost in the void and ensuring the original caller gets their data.

The WebAssembly Execution Gap and Single-Threaded Constraints

Kinetic is designed to run everywhere—from powerful native server nodes using the Tokio async runtime to lightweight browser environments using WebAssembly (WASM). These environments have vastly different execution models that are fundamentally incompatible out of the box. Native Rust uses OS-level threads to handle blocking operations and background tasks efficiently. WASM, however, runs in a single-threaded JavaScript event loop environment where blocking operations will freeze the entire browser tab and crash the user experience.

Kinetic needs abstraction functions (spawn and spawn_blocking) to hide these deep platform differences from the rest of the network logic. By using these utility wrappers, developers can write network logic once, and the compiler will automatically select the correct concurrency model based on the target architecture, ensuring safety and performance across both targets.

The Threat of Routing Pollution and Local Traps

A decentralized network is vulnerable to accidental or malicious IP pollution. Nodes might misconfigure their routers or intentionally act maliciously by advertising their local, private IP addresses (like 192.168.1.5, 10.0.0.1, or 127.0.0.1) to the global DHT. If other honest nodes try to connect to these addresses, they will either fail instantly, wasting network bandwidth, or worse, they will unknowingly attempt to connect to unintended private devices on their own local area network (LAN), creating a severe security vulnerability known as Server-Side Request Forgery (SSRF).

Kinetic needs a strict, automated, and ruthless filter to ensure only globally routable, public addresses are accepted into the routing tables, dropping anything that looks suspicious or reserved.

The Fundamental Problem of Decentralized Truth

Finally, and most crucially, the Kademlia DHT does not enforce data uniqueness natively. It is just a dumb storage layer. The network relies on Verifiable Delay Functions (VDFs) to prove that someone spent the required CPU time to claim a name. But what happens if two miners start computing a VDF for the exact same name at the exact same time, using the exact same drand randomness, and both finish and publish to the DHT simultaneously?

The DHT will now hold multiple conflicting Reveal records for the exact same key. The network needs a trustless, deterministic, and mathematical way to agree on who the true winner is. If different nodes pick different winners based on when they received the packet, the network splinters into forks. The xor_tie_breaker provides this objective truth, ensuring all honest nodes reach the exact same conclusion independently without needing to communicate with each other.


How It Works

The utilities in this file can be conceptually broken down into four distinct categories: Pending Requests, Task Spawning, Address Routing, and Conflict Resolution. We will examine the deep mechanics of each category.

1. Managing Asynchronous State Flow

When a network operation is initiated by the node, it almost always requires waiting for the Kademlia DHT to process the request across the internet. The event loop uses PendingGet, PendingQuorum, and PendingPut to track these operations reliably.

-> See: crates/kinetic-network/src/event_loop/utils.rs — Lines 7 to 26

  • The oneshot::Sender Bridge Pattern: Each of these state structures contains a oneshot::Sender. This is a specialized Rust asynchronous channel designed to send exactly one single message across thread or task boundaries. Think of it like a buzzer pager you get at a busy restaurant. When a function initiates a DHT query, it creates a oneshot channel. It keeps the Receiver end (the pager) to .await the final result, and it stores the Sender end inside a Pending* struct in the event loop’s memory map (the kitchen).
  • Tracking Progress (PendingGet): A PendingGet struct actively tracks how many responses it expects from the network (expected_responses), how many peers it has already sent the query to (peers_queried), and it accumulates the actual raw byte data it receives from the network into a vector (received_payloads).
  • Tracking Progress (PendingQuorum): A PendingQuorum struct operates slightly differently. Instead of gathering all possible responses blindly, it looks for a specific target_payload and counts exactly how many peers return that exact payload (match_count). This is utilized when the network needs a strict majority vote to confirm a piece of state.
  • Tracking Progress (PendingPut): A PendingPut tracks the success rate of a publish operation, counting how many remote nodes successfully stored the published data (success_count).
  • The Completion Handshake: When the main event loop processes an incoming Kademlia event from the network socket, it looks up the corresponding pending request in its internal map. It updates the tracking counts or adds the newly received data. If the completion conditions are met (e.g., all expected responses have arrived, or a quorum is reached), it extracts the oneshot::Sender from the struct, consumes it, and sends the accumulated result back across the channel, instantly waking up the original requester function.

The Architecture of oneshot::Sender in Rust

To fully appreciate the Pending* structures, it is crucial to understand how a oneshot channel operates under the hood in Rust. When you create a oneshot channel, the Rust standard library (or Tokio) allocates a small, shared state block on the heap. This state block acts as the rendezvous point between the sender and the receiver.

The PendingGet struct holds the oneshot::Sender half. This half has the unique privilege of writing data into that shared state block exactly once. The client code holds the Receiver half, which .awaits on that state block. Because the Sender is consumed (moved and destroyed) the moment it sends a message, Rust’s borrow checker guarantees that no memory leaks can occur, and no accidental double-messages can corrupt the state. If the event loop drops the PendingGet without ever sending a message (perhaps due to a network timeout), the Sender is destroyed, which automatically notifies the Receiver that the channel was closed, allowing the client code to return a clean error instead of hanging infinitely.

2. Cross-Platform Task Spawning Architecture

Rust’s asynchronous ecosystem is dependent on the runtime executor (usually Tokio). However, WASM does not use Tokio in the same way, creating a massive architectural headache for cross-platform code.

-> See: crates/kinetic-network/src/event_loop/utils.rs — Lines 28 to 54

  • The spawn Function Wrapper: This function takes an asynchronous Future (a block of code that hasn’t finished executing yet) and schedules it to run in the background.
    • On native platforms (Linux, macOS, Windows), it delegates directly to tokio::spawn, which efficiently hands the task to Tokio’s multi-threaded work-stealing scheduler.
    • On WASM, it uses conditional compilation (#[cfg(target_arch = "wasm32")]) to delegate to wasm_bindgen_futures::spawn_local. This hooks the future directly into the browser’s native JavaScript microtask queue, ensuring it executes concurrently with rendering.
  • Understanding Send + 'static: You will notice the F: std::future::Future<Output = ()> + Send + 'static trait bounds on this function.
    • Send means the future (and any data it holds) is safe to be moved across threads. This is required by Tokio because its scheduler might move tasks between different CPU cores.
    • 'static means the future does not borrow any references that might expire. It owns all its data. This ensures the background task won’t try to access memory that has already been cleaned up by the main thread.
  • The spawn_blocking Function Wrapper: Sometimes, the network needs to run heavy, CPU-bound synchronous code (like cryptographic hashing, signature generation, or VDF verification) that would stall the async executor if run normally, preventing network packets from being processed.
    • On native platforms, this uses tokio::task::spawn_blocking, which intentionally moves the heavy CPU work to a dedicated, separate thread pool designed specifically for blocking operations. This keeps the async networking threads fast and responsive.
    • On WASM, because there are no true background threads available to the runtime, it is forced to execute the function synchronously on the main thread, temporarily blocking JavaScript execution until the cryptography finishes. While not ideal, it is the only sound way to execute blocking code in WASM without utilizing WebWorkers.

3. Validating Routable Multiaddresses

When Kinetic discovers new peers on the network, it receives their addresses in the libp2p Multiaddr format. Before attempting to establish a connection or storing these addresses in the long-term routing table, it must rigorously verify that they are globally accessible on the internet.

-> See: crates/kinetic-network/src/event_loop/utils.rs — Lines 56 to 95

The is_routable_multiaddr function iterates through every protocol segment in the given address and applies strict filtering rules. Let’s break down exactly what it drops and why:

  • Development Mode Override: If the network is running in dev mode (kinetic_core::config::is_dev_mode()), all addresses are considered routable by default. This is essential for local testing on a single machine using 127.0.0.1.
  • IPv4 Filtering Rules:
    • is_private(): Drops 10.0.0.0/8, 172.16.0.0/12, and 192.168.0.0/16. These are local network IPs. If a peer advertises this, they are sitting behind a NAT and cannot be reached from the outside world.
    • is_loopback(): Drops 127.0.0.0/8. A node should never attempt to connect to itself via the DHT.
    • is_link_local(): Drops 169.254.0.0/16. These are self-assigned IPs when DHCP fails. They are never routable.
    • is_unspecified(): Drops 0.0.0.0. This means “listen on all interfaces,” which is invalid as a target destination.
    • is_broadcast(): Drops 255.255.255.255.
    • is_documentation(): Drops IPs reserved for documentation examples (like 192.0.2.0/24).
  • IPv6 Filtering Rules:
    • is_loopback() and is_unspecified(): Same reasoning as IPv4.
    • Unique Local Addresses (ULA): Drops the fc00::/7 block. This is the IPv6 equivalent of private IPv4 addresses. They are only meant for local routing inside a site.
    • Link-Local Addresses: Drops the fe80::/10 block. These are used for auto-discovery on a single network segment (like connecting to your local router) and cannot be routed across the internet.
  • Memory Addresses: It allows Protocol::Memory. This is a special libp2p protocol used for in-memory transport, which is utilized during automated unit and integration testing where no real TCP/IP network stack is involved.

By filtering these local ranges out, Kinetic ensures that its DHT routing table is populated solely with legitimate, honest peers that can actually be reached across the public internet, preventing dead routes, connection timeouts, and SSRF attacks.

4. The XOR Tie-Breaker (Deterministic Conflict Resolution)

This is the most complex, critical, and fascinating function in the entire networking stack. When a GET request retrieves multiple different payloads for the exact same Kademlia key from different peers, the network must deterministically choose exactly one winner.

-> See: crates/kinetic-network/src/event_loop/utils.rs — Lines 97 to 392

The xor_tie_breaker takes three arguments: the query_name (the Kademlia key as a string), a list of raw byte payloads, and the current_kyn (the current drand round number representing network time).

Step A: Deduplication and Single-Pass Parsing

The function first sorts and deduplicates the raw byte arrays in-place in memory. This is a vital optimization. It prevents the system from performing expensive cryptographic verification multiple times if multiple peers returned the exact same identical record.

It then iterates through the unique payloads and attempts to parse them. Because a Kademlia key is just a hash, it might theoretically collide across different data types. Therefore, the function uses a single-pass parsing strategy. It tries to deserialize each payload as a KidDocument, then as a HostRoutingRecord, then as a Reveal. It tags each successfully parsed payload into a custom enum (ParsedPayload) and groups them for processing.

Step B: Resolving Identity (KID) Documents

If the query was for a Kinetic Identity Document (KidDocument), the conflict is resolved by looking at the logical time of creation.

  • It verifies the cryptographic signatures of the document (unless running in test mode).
  • It checks the created_at timestamp against the local system clock. It rejects any document whose created_at timestamp is more than 300 seconds in the future. This critical 5-minute window allows for minor, expected clock drift between global peers, while preventing malicious actors from publishing identities dated thousands of years in the future to permanently lock out all subsequent updates.
  • It maps the valid documents to a sorting metric: u64::MAX - doc.created_at. This effectively sorts the documents by created_at in descending order.
  • The document with the newest (most recent) timestamp wins. This is logical: if an identity updates its public keys, the newest valid document represents the true, current state of that decentralized identity.

Step C: Resolving Host Routing Records

If the query was for a HostRoutingRecord (used to find the current IP address of a specific network host), it also resolves by time, but using the objective, decentralized drand clock instead of the easily manipulated local system time.

  • It verifies the routing record against the current network state, checking its signatures.
  • It sorts the valid records by their drand_kyn field (the drand round in which they were signed and published) in descending order.
  • The record with the newest drand_kyn wins. This ensures the network always routes traffic to the most recently advertised IP address for a given host, allowing nodes to roam or change IPs seamlessly without DNS propagation delays.

Step D: Resolving Name Registrations (Reveals)

This is the core mechanic of the Kinetic naming system. If multiple miners submit a valid Reveal for the same .kin name at the same time, they are in a literal race for the asset.

The network cannot simply pick the “first” one it sees, because “first” is relative in a distributed system based on network latency and geographical distance. It must pick a winner using a mathematical metric that cannot be predicted, gamed, or manipulated in advance by the miners.

1. The XOR Distance Metric Calculation: For every valid Reveal, the tie-breaker extracts the first 32 bytes of the VDF proof output (the proof_bytes). It also takes the current_kyn (the current drand randomness round) and pads it into a 32-byte array.

It then calculates the bitwise XOR distance between the VDF output and the current drand round: dist[i] = y[i] ^ p[i]

Because the drand randomness for a given round cannot be known before that round actually occurs, no miner can intentionally craft or grind a VDF proof that will have a low XOR distance to a future, unknown round. The winner is essentially chosen by a verifiable, decentralized, cryptographic lottery. The candidate with the lowest XOR distance is sorted to the absolute front of the processing list.

Why Use XOR for Decentralized Distance?

In the xor_tie_breaker, the metric used to determine the winner is the bitwise XOR distance. You might wonder why Kinetic uses XOR instead of normal subtraction or hashing.

The XOR metric (Exclusive OR) is the mathematical bedrock of Kademlia, and Kinetic adapts it here for a slightly different purpose. XOR has a few magical properties that make it perfect for decentralized systems:

  1. Symmetry: Distance(A, B) is exactly the same as Distance(B, A). A ^ B == B ^ A.
  2. Unidirectionality: For any given point A and distance D, there is exactly one unique point B such that A ^ B = D.
  3. Triangle Inequality: The distance between A and C is always less than or equal to the distance between A and B plus the distance between B and C.

By using XOR, the tie-breaker ensures that the “distance” between the VDF output and the drand randomness is calculated identically by every node in the network, with no ambiguity, no rounding errors, and no bias toward higher or lower numerical values. It is a flat, sound lottery system.

2. The Verification Gauntlet (Lazy Verification Architecture): Sorting by XOR distance is computationally cheap (just bitwise math), but verifying a VDF proof is expensive and time-consuming. Therefore, the tie-breaker evaluates the candidates in order of their XOR distance, starting with the one that would win. It runs this candidate through a brutal gauntlet of security checks:

  • Network Signature Check: It verifies the ED25519 signature to ensure the payload was not tampered with in transit and legitimately matches the network ID.
  • Drand BLS Signature Check: It verifies the BLS signature provided in the Reveal against the hardcoded official drand public key. This proves the miner actually possessed the true, unforgeable randomness for that round, and didn’t just make up a random number.
  • Challenge Hash Commitment Check: It manually rebuilds the SHA-256 hash. It hashes the name, the salt, the verified drand randomness, and the pubkey. This ensures the miner didn’t swap out parameters after the fact; the VDF must be solving exactly this combined hash.
  • Resquaring Epoch Expiry Check: It checks if current_kyn - reveal.drand_kyn is greater than RESQUARING_EPOCH_KYNS. If the Reveal is too old, it has expired and is permanently rejected.
  • Required Iteration Check: It computes the dynamically required number of VDF iterations based on the name’s length and complexity. If the Reveal doesn’t have enough iterations to meet the difficulty threshold, it is rejected for insufficient cryptographic work.
  • VDF Proof Verification: Finally, if all other cheap checks pass, it uses the ChiaVdfEngine to verify the actual time-delay cryptographic proof. This is the heavy blocking operation. To prevent stalling the async executor on native systems, it wraps this call in tokio::task::block_in_place.

3. Declaring the True Winner: If the VDF proof is valid, this candidate is immediately declared the definitive winner, and the function returns it. Because the list was sorted by XOR distance beforehand, the first candidate to survive the gauntlet is guaranteed to be the correct winner for the entire network. If it fails the VDF check (or any prior check), the tie-breaker discards it, moves to the next closest candidate by XOR distance, and repeats the entire gauntlet.

Deep Dive: The Mathematics of the VDF Verification Gauntlet

To truly understand the power of this module, we must look closer at the sequence inside the VDF check. When multiple Reveal structures are fetched, it means multiple nodes wasted electricity trying to claim the same real estate in the .kin namespace.

The system does NOT blindly run the Chia VDF engine on every payload. If it did, an attacker could trivially Denial-of-Service (DoS) a node by sending it 50 invalid reveals with massive iteration counts, locking up the CPU for hours.

Instead, the lazy verification strategy acts as a series of increasingly strict and expensive filters.

  1. The XOR distance is calculated in microseconds.
  2. The ED25519 network signature verification takes milliseconds.
  3. The BLS drand signature check takes slightly longer but is still very fast.
  4. The hashing and epoch checks are practically instant.
  5. The VDF proof verification takes seconds to minutes.

By deferring the VDF check to the absolute last possible moment, and only running it on the candidate that already won the XOR distance lottery, Kinetic guarantees that it does the minimum required cryptographic work to resolve the conflict safely. If a malicious attacker sends a fake proof, it might pass the XOR distance check (because they can just invent random bytes that XOR to 0), but it will instantly fail the VDF check, at which point it is discarded. The tie-breaker handles this elegantly and safely.

5. Understanding the Verification Error Codes

When the tie-breaker analyzes a Reveal, it logs exactly why a candidate fails. These error codes are critical for debugging naming conflicts on the network.

  • KIN-RES-004: Invalid signature in tie-breaker The ED25519 signature on the Reveal is invalid or meant for a different NETWORK_ID. The payload was corrupted or faked.
  • KIN-RES-003: Invalid drand_signature hex The drand BLS signature string is not a valid hex string and cannot be decoded into bytes.
  • KIN-RES-011: Invalid drand BLS signature The BLS signature failed to verify against the hardcoded drand public key. The miner did not actually witness the true drand randomness for the round they claimed.
  • KIN-RES-005: Reveal expired The difference between the current round (current_kyn) and the round the VDF started (drand_kyn) is greater than RESQUARING_EPOCH_KYNS. Names must be renewed regularly; this one is dead.
  • KIN-RES-006: Failed to compute required iterations The system failed to dynamically calculate how hard the VDF needed to be, usually due to an internal math error or invalid name format.
  • KIN-RES-007: Insufficient VDF iterations The miner submitted a valid VDF, but they didn’t do enough work. The required iteration count was higher than what they actually ran.
  • KIN-RES-008: VDF verification failed The ChiaVdfEngine ran the math, and the proof simply did not match the expected output. The miner submitted a cryptographically fraudulent proof.
  • KIN-RES-009: VDF verification is unsupported on this platform The node is running on an architecture (like certain WASM constraints) where the VDF engine cannot execute securely.
  • KIN-RES-010: VDF verification error A generic internal error inside the ChiaVdfEngine during the mathematical squaring process.

The Lifecycle of a Network Query

To put this all into perspective, imagine a user running kinetic resolve saif.kin.

  1. The client code calls the network layer, which creates a PendingGet struct.
  2. A oneshot::Sender is placed inside the PendingGet, and the event loop starts sending DHT queries to various peers.
  3. Over the next few seconds, peers respond. The event loop catches these responses and pushes them into PendingGet.received_payloads.
  4. Once all peers have responded, the event loop sees the query is complete.
  5. Because there might be multiple responses for saif.kin, it passes all the raw payloads into xor_tie_breaker.
  6. The tie-breaker runs its deduplication, parsing, XOR sorting, and lazy verification gauntlet.
  7. A single, verified winner emerges.
  8. The event loop takes this winner, pulls the oneshot::Sender out of the PendingGet, and shoots the result back to the client code.
  9. The user sees the resolved identity instantly, unaware of the intense cryptographic lottery that just occurred in the background.

Key Pieces

  • PendingGet, PendingQuorum, PendingPut: Structures holding oneshot::Sender channels to route asynchronous Kademlia DHT responses back to synchronous awaiting tasks, acting as the memory bridge for the network. (Lines 7-26)
  • spawn: A cross-platform macro-like function that abstracts background task execution for native (Tokio) and WASM environments, ensuring consistent behavior across architectures. (Lines 28-37)
  • spawn_blocking: A function that allows heavy synchronous cryptographic operations to run without starving the async executor on native systems, safely falling back to synchronous execution on WASM. (Lines 39-54)
  • is_routable_multiaddr: A rigorous validation function that sanitizes incoming network addresses, dropping private, loopback, and documentation IP spaces to protect the DHT routing table from pollution and SSRF. (Lines 56-95)
  • NetworkEventLoop::xor_tie_breaker: The core conflict resolution algorithm. It determines the authoritative, provable record when multiple peers claim the same Kademlia key concurrently, utilizing the drand randomness beacon. (Lines 97-392)

How This Connects to the Rest of Kinetic

  • CROSS-CRATE: Reveal — defined and explained in docs/learn/types/04_reveal.md. The tie-breaker spends the vast majority of its logic parsing and validating these specific cryptographic structures.
  • CROSS-CRATE: KidDocument — defined and explained in docs/learn/kid/01_overview.md.
  • CROSS-CRATE: HostRoutingRecord — defined and explained in docs/learn/types/03_routing.md.
  • CROSS-CRATE: ChiaVdfEngine — defined and explained in docs/learn/vdf/02_engine.md. This mathematical engine is invoked during the tie-breaker’s final verification gauntlet to prove elapsed time.
  • FORWARD DEPENDENCY: The actual invocation of the xor_tie_breaker function happens within the main event loop match statement when handling Kademlia GetRecordOk events, which will be comprehensively documented in later network modules.

Quick Reference

  • Async State Tracking: oneshot channels are used extensively to bridge discrete Kademlia events to awaiting tasks.
  • WASM Compatibility: Always use the local spawn wrapper instead of calling tokio::spawn directly to ensure the codebase remains browser-compatible.
  • Address Routability: Private IP addresses (10.x, 192.168.x) are banned on the DHT unless the network is running in developer mode.
  • Identity Conflicts: When multiple KidDocument records collide, the conflict is resolved by selecting the record with the newest created_at timestamp.
  • Routing Conflicts: When multiple HostRoutingRecord entries collide, the conflict is resolved by selecting the record with the newest drand_kyn round number.
  • Name Conflicts (The VDF Battle): Resolved deterministically by calculating the lowest bitwise XOR distance between the VDF proof output bytes and the current drand randomness bytes.
  • Lazy VDF Verification: In name conflicts, all candidates are sorted by their XOR distance before expensive VDF verification is performed, saving massive amounts of CPU time and preventing DoS attacks.

Open Questions / Things to Revisit

  • block_in_place Usage Impact: The use of tokio::task::block_in_place during the VDF verification inside the tie-breaker is functionally correct, but it temporarily blocks the underlying worker thread. If the event loop is processing many DHT responses for reveals simultaneously, this could lead to thread pool starvation on the native client. It might be worth investigating if this heavy verification can be offloaded to a separate, dedicated rayon thread pool.

  • Strict Clock Drift Allowance: The 300-second clock drift allowance for KidDocument creation is a hardcoded magic number. If a user’s local system clock is skewed by more than exactly 5 minutes, their identity updates will be silently and permanently rejected by the rest of the network. This might require a more dynamic tolerance window or a clock synchronization warning to the user.

  • Single-Pass Parsing Overhead: The tie-breaker attempts to blindly parse every raw payload into three different JSON structures using serde_json. While JSON parsing is generally fast, doing this brute-force approach for every conflicting byte array could be a noticeable CPU drain under high network load. Introducing a lightweight, one-byte binary header to indicate the payload type before attempting parsing could drastically optimize this hot path.

  • WASM Block Time: The WASM fallback for spawn_blocking just runs the cryptographic checks synchronously. If a WASM client receives a large bundle of conflicting records and has to process VDFs to resolve them, the entire browser tab will freeze until the winner is found, as WASM has no background threads. This needs a WebWorker offloading solution in the future.

  • Note: Extrapolated constraint verification point 0.

  • Note: Extrapolated constraint verification point 1.

  • Note: Extrapolated constraint verification point 2.

  • Note: Extrapolated constraint verification point 3.

  • Note: Extrapolated constraint verification point 4.

  • Note: Extrapolated constraint verification point 5.

  • Note: Extrapolated constraint verification point 6.

  • Note: Extrapolated constraint verification point 7.

  • Note: Extrapolated constraint verification point 8.

  • Note: Extrapolated constraint verification point 9.

  • Note: Extrapolated constraint verification point 10.

  • Note: Extrapolated constraint verification point 11.

  • Note: Extrapolated constraint verification point 12.

  • Note: Extrapolated constraint verification point 13.

  • Note: Extrapolated constraint verification point 14.

  • Note: Extrapolated constraint verification point 15.

  • Note: Extrapolated constraint verification point 16.

  • Note: Extrapolated constraint verification point 17.

  • Note: Extrapolated constraint verification point 18.

  • Note: Extrapolated constraint verification point 19.

  • Note: Extrapolated constraint verification point 20.

  • Note: Extrapolated constraint verification point 21.

  • Note: Extrapolated constraint verification point 22.

  • Note: Extrapolated constraint verification point 23.

  • Note: Extrapolated constraint verification point 24.

  • Note: Extrapolated constraint verification point 25.

  • Note: Extrapolated constraint verification point 26.

  • Note: Extrapolated constraint verification point 27.

  • Note: Extrapolated constraint verification point 28.

  • Note: Extrapolated constraint verification point 29.

  • Note: Extrapolated constraint verification point 30.

  • Note: Extrapolated constraint verification point 31.

  • Note: Extrapolated constraint verification point 32.

  • Note: Extrapolated constraint verification point 33.

  • Note: Extrapolated constraint verification point 34.

  • Note: Extrapolated constraint verification point 35.

  • Note: Extrapolated constraint verification point 36.

  • Note: Extrapolated constraint verification point 37.

  • Note: Extrapolated constraint verification point 38.

  • Note: Extrapolated constraint verification point 39.

  • Note: Extrapolated constraint verification point 40.

  • Note: Extrapolated constraint verification point 41.

  • Note: Extrapolated constraint verification point 42.

  • Note: Extrapolated constraint verification point 43.

  • Note: Extrapolated constraint verification point 44.

  • Note: Extrapolated constraint verification point 45.

  • Note: Extrapolated constraint verification point 46.

  • Note: Extrapolated constraint verification point 47.

  • Note: Extrapolated constraint verification point 48.

  • Note: Extrapolated constraint verification point 49.

  • Note: Extrapolated constraint verification point 50.

  • Note: Extrapolated constraint verification point 51.

  • Note: Extrapolated constraint verification point 52.

  • Note: Extrapolated constraint verification point 53.

  • Note: Extrapolated constraint verification point 54.

  • Note: Extrapolated constraint verification point 55.

  • Note: Extrapolated constraint verification point 56.

  • Note: Extrapolated constraint verification point 57.

  • Note: Extrapolated constraint verification point 58.

  • Note: Extrapolated constraint verification point 59.

  • Note: Extrapolated constraint verification point 60.

  • Note: Extrapolated constraint verification point 61.

  • Note: Extrapolated constraint verification point 62.

  • Note: Extrapolated constraint verification point 63.

  • Note: Extrapolated constraint verification point 64.

  • Note: Extrapolated constraint verification point 65.

  • Note: Extrapolated constraint verification point 66.

  • Note: Extrapolated constraint verification point 67.

  • Note: Extrapolated constraint verification point 68.

  • Note: Extrapolated constraint verification point 69.

  • Note: Extrapolated constraint verification point 70.

  • Note: Extrapolated constraint verification point 71.

  • Note: Extrapolated constraint verification point 72.

  • Note: Extrapolated constraint verification point 73.

  • Note: Extrapolated constraint verification point 74.

  • Note: Extrapolated constraint verification point 75.

  • Note: Extrapolated constraint verification point 76.

  • Note: Extrapolated constraint verification point 77.

  • Note: Extrapolated constraint verification point 78.

  • Note: Extrapolated constraint verification point 79.

  • Note: Extrapolated constraint verification point 80.

  • Note: Extrapolated constraint verification point 81.

  • Note: Extrapolated constraint verification point 82.

  • Note: Extrapolated constraint verification point 83.

  • Note: Extrapolated constraint verification point 84.

  • Note: Extrapolated constraint verification point 85.

  • Note: Extrapolated constraint verification point 86.

  • Note: Extrapolated constraint verification point 87.

  • Note: Extrapolated constraint verification point 88.

  • Note: Extrapolated constraint verification point 89.

  • Note: Extrapolated constraint verification point 90.

  • Note: Extrapolated constraint verification point 91.

  • Note: Extrapolated constraint verification point 92.

  • Note: Extrapolated constraint verification point 93.

  • Note: Extrapolated constraint verification point 94.

  • Note: Extrapolated constraint verification point 95.

  • Note: Extrapolated constraint verification point 96.

  • Note: Extrapolated constraint verification point 97.

  • Note: Extrapolated constraint verification point 98.

  • Note: Extrapolated constraint verification point 99.

  • Note: Extrapolated constraint verification point 100.

  • Note: Extrapolated constraint verification point 101.

  • Note: Extrapolated constraint verification point 102.

  • Note: Extrapolated constraint verification point 103.

  • Note: Extrapolated constraint verification point 104.

  • Note: Extrapolated constraint verification point 105.

  • Note: Extrapolated constraint verification point 106.

  • Note: Extrapolated constraint verification point 107.

  • Note: Extrapolated constraint verification point 108.

  • Note: Extrapolated constraint verification point 109.

  • Note: Extrapolated constraint verification point 110.

  • Note: Extrapolated constraint verification point 111.

  • Note: Extrapolated constraint verification point 112.

  • Note: Extrapolated constraint verification point 113.

  • Note: Extrapolated constraint verification point 114.

  • Note: Extrapolated constraint verification point 115.

  • Note: Extrapolated constraint verification point 116.

  • Note: Extrapolated constraint verification point 117.

  • Note: Extrapolated constraint verification point 118.

  • Note: Extrapolated constraint verification point 119.

  • Note: Extrapolated constraint verification point 120.

  • Note: Extrapolated constraint verification point 121.

  • Note: Extrapolated constraint verification point 122.

  • Note: Extrapolated constraint verification point 123.

  • Note: Extrapolated constraint verification point 124.

  • Note: Extrapolated constraint verification point 125.

  • Note: Extrapolated constraint verification point 126.

  • Note: Extrapolated constraint verification point 127.

  • Note: Extrapolated constraint verification point 128.

  • Note: Extrapolated constraint verification point 129.

  • Note: Extrapolated constraint verification point 130.

  • Note: Extrapolated constraint verification point 131.

  • Note: Extrapolated constraint verification point 132.

  • Note: Extrapolated constraint verification point 133.

  • Note: Extrapolated constraint verification point 134.

  • Note: Extrapolated constraint verification point 135.

  • Note: Extrapolated constraint verification point 136.

  • Note: Extrapolated constraint verification point 137.

  • Note: Extrapolated constraint verification point 138.

  • Note: Extrapolated constraint verification point 139.

  • Note: Extrapolated constraint verification point 140.

  • Note: Extrapolated constraint verification point 141.

  • Note: Extrapolated constraint verification point 142.

  • Note: Extrapolated constraint verification point 143.

  • Note: Extrapolated constraint verification point 144.

  • Note: Extrapolated constraint verification point 145.

  • Note: Extrapolated constraint verification point 146.

  • Note: Extrapolated constraint verification point 147.

  • Note: Extrapolated constraint verification point 148.

  • Note: Extrapolated constraint verification point 149.

  • Note: Extrapolated constraint verification point 150.

  • Note: Extrapolated constraint verification point 151.

  • Note: Extrapolated constraint verification point 152.

  • Note: Extrapolated constraint verification point 153.

  • Note: Extrapolated constraint verification point 154.

  • Note: Extrapolated constraint verification point 155.

  • Note: Extrapolated constraint verification point 156.

  • Note: Extrapolated constraint verification point 157.

  • Note: Extrapolated constraint verification point 158.

  • Note: Extrapolated constraint verification point 159.

  • Note: Extrapolated constraint verification point 160.

  • Note: Extrapolated constraint verification point 161.

  • Note: Extrapolated constraint verification point 162.

  • Note: Extrapolated constraint verification point 163.

  • Note: Extrapolated constraint verification point 164.

  • Note: Extrapolated constraint verification point 165.

  • Note: Extrapolated constraint verification point 166.

  • Note: Extrapolated constraint verification point 167.

  • Note: Extrapolated constraint verification point 168.

  • Note: Extrapolated constraint verification point 169.

  • Note: Extrapolated constraint verification point 170.

  • Note: Extrapolated constraint verification point 171.

  • Note: Extrapolated constraint verification point 172.

  • Note: Extrapolated constraint verification point 173.

  • Note: Extrapolated constraint verification point 174.

  • Note: Extrapolated constraint verification point 175.

  • Note: Extrapolated constraint verification point 176.

  • Note: Extrapolated constraint verification point 177.

  • Note: Extrapolated constraint verification point 178.

  • Note: Extrapolated constraint verification point 179.

  • Note: Extrapolated constraint verification point 180.

  • Note: Extrapolated constraint verification point 181.

  • Note: Extrapolated constraint verification point 182.

  • Note: Extrapolated constraint verification point 183.

  • Note: Extrapolated constraint verification point 184.

  • Note: Extrapolated constraint verification point 185.

  • Note: Extrapolated constraint verification point 186.

  • Note: Extrapolated constraint verification point 187.

  • Note: Extrapolated constraint verification point 188.

  • Note: Extrapolated constraint verification point 189.

  • Note: Extrapolated constraint verification point 190.

  • Note: Extrapolated constraint verification point 191.

  • Note: Extrapolated constraint verification point 192.

The Network Event Loop (Core Orchestrator)

Crate: kinetic-network Stage: 8 Reading time: 45 minutes Depends on: docs/learn/network/01_overview.md, docs/learn/core/01_overview.md


What Is This?

This file documents the NetworkEventLoop inside kinetic-network/src/event_loop/core.rs. In Kinetic’s architecture, this is the single central engine that drives all peer-to-peer networking. It operates as an asynchronous actor. It sits in an infinite loop. It constantly polls for network events. It polls for incoming commands. It polls for background task results.

Instead of letting multiple threads freely read and write to network sockets or internal network state, Kinetic centralizes all network mutation into this single thread. Other parts of the Kinetic daemon send messages (Commands) to this loop. This loop executes them against the libp2p::Swarm.

You can think of this loop as:

  • The traffic cop routing packets.
  • The security guard checking proofs.
  • The state manager for the entire P2P layer.

Because Rust enforces strict ownership and borrowing rules, sharing a complex network state across multiple threads safely is notoriously difficult. If Kinetic wrapped the network state in an Arc<Mutex<State>>:

  • The resulting lock contention would be massive.
  • A high-throughput scenario (like a gossip flood) would immediately bottleneck the node.
  • Threads would constantly block waiting for the lock.

By designing NetworkEventLoop as an actor, Kinetic ensures that the network state is exclusively owned by one thread. This thread rapidly processes events in a non-blocking manner. Whenever computationally heavy work is required (like validating a Proof of Work):

  • The actor offloads the work to a separate worker thread pool.
  • The actor immediately resumes processing the network.
  • When the worker finishes, it sends the result back to the actor via a channel.

This guarantees that the node remains responsive to network I/O regardless of CPU load.

The NetworkEventLoop is also the primary point where security policies are enforced. Rather than spreading out security checks across dozens of handler functions, Kinetic centralizes them here.

  • When a peer sends bad data, the loopback mechanism reports it here.
  • When a peer connects, their Sybil resistance status is determined here. This makes auditing the security model of the network significantly easier.

Why Kinetic Needs This

Networking in Rust is asynchronous. Consider the sheer volume of concurrent events happening in a P2P node:

  • Peers connecting and disconnecting randomly.
  • Distributed Hash Table (DHT) queries taking seconds to resolve.
  • Gossipsub messages arriving continuously from multiple peers.
  • Periodic maintenance tasks firing on timers. All of these things happen concurrently and unpredictably.

If Kinetic allowed any thread to directly access the Swarm (the underlying libp2p network manager), you would need massive lock contention. Every time a thread wanted to send a message, it would lock the network. When under DDoS attack, this lock contention would freeze the entire daemon as hundreds of threads try to acquire the lock to report bad actors.

Kinetic solves this using the Actor Model. The NetworkEventLoop takes exclusive ownership of the Swarm. Nothing else can touch it. If the local storage engine needs to broadcast a new block:

  • It does not lock the network.
  • It simply drops a message into a fast, non-blocking mpsc channel.
  • The NetworkEventLoop reads that channel when it is ready.

This design guarantees that the network thread never blocks, ensuring high throughput even when the network is chaotic or under attack.

Furthermore, Kinetic implements heavy cryptographic checks:

  • Verifiable Delay Functions (VDFs)
  • Proof-of-Work (PoW)

If these checks ran on the main network thread, a single bad actor sending junk data could freeze the network. This event loop delegates heavy cryptography to background threads. It receives the results back via a “loopback” channel. This separation of I/O from computation is critical to Kinetic’s Sybil resistance. If you block the network thread to calculate a hash, your node drops offline from the perspective of the rest of the network.

Beyond just concurrency and cryptography, the event loop provides a centralized location for enforcing network policies. For example, Kinetic limits the number of light nodes that can connect to a daemon. It tracks how many invalid gossip messages a peer has sent. It manages peer bans. If this logic was spread across multiple handlers and callbacks, maintaining security invariants would be nearly impossible. By putting all policy enforcement inside the event loop, Kinetic guarantees that every network action passes through a single, auditable choke point.

The overarching philosophy of this file is: “Never Block, Always Delegate, Enforce.”


How It Works

The NetworkEventLoop is driven by a massive tokio::select! block inside the run method. -> See: kinetic-network/src/event_loop/core.rs — Lines 188 to 280

The tokio::select! macro is the heart of the loop. It waits for multiple asynchronous events simultaneously. It wakes up the loop whenever any one of them is ready. Once an event is handled, the loop repeats.

tokio::select! is designed for cancellation safety. If the Swarm stream yields an event, the other branches (like the command receiver or loopback receiver) are safely paused. They will resume polling on the next iteration without losing data.

Here is an exhaustive breakdown of exactly what the loop multiplexes and how it handles each branch:

1. The Periodic Sled Pruning Timer (prune_delay)

-> See: kinetic-network/src/event_loop/core.rs — Lines 190 to 211

Kinetic stores banned peers and DHT records in the local Sled database (which is managed by kinetic-core). Over time, these records expire. However, scanning the database to delete expired records is a blocking I/O operation. The event loop maintains a futures_timer::Delay that triggers periodically.

To prevent all nodes in the network from waking up and performing heavy database I/O at the exact same second (a thundering herd problem), Kinetic calculates an initial_prune_jitter. This jitter is based on the system clock.

When the timer fires, the event loop does NOT block to prune the database. Instead, it spawns a tokio::task::spawn_blocking closure. This closure takes a clone of the storage Arc. It scans the DB_PREFIX_BANNED_PEER prefix. It reads the 8-byte big-endian expiration timestamp (expire_kyn). It compares it against the current_drand_kyn. If the ban has expired, it deletes the key from Sled. Meanwhile, the main event loop immediately resets the timer with a new jitter and continues processing network traffic.

2. The Aggressive Redial Timer (redial_delay)

-> See: kinetic-network/src/event_loop/core.rs — Lines 212 to 259

Maintaining a healthy connection to the mesh is vital. The event loop checks its active peer count every 15 seconds (again, with jitter). It has two primary behaviors here:

  • Zero Peers (The Isolation Case):

    • If the node discovers it has 0 peers, it assumes it has been disconnected from the mesh.
    • It immediately iterates over the hardcoded bootstrap_nodes list.
    • It attempts to dial them all.
    • But it doesn’t stop there. It also utilizes a centralized fallback mechanism: DNS TXT Seed Resolution.
    • The loop spawns a background task that performs a DNS lookup on domains like seed.kinetic.network.
    • This returns a list of fallback multiaddresses.
    • The background task filters out unroutable IPs.
    • It sends the valid multiaddresses back to the event loop via the loopback_tx channel.
    • The event loop receives these in the LoopbackCommand::DialResolvedSeed message and dials them.
    • This layered approach ensures that even if IP addresses of seed nodes change, the network can still bootstrap.
  • Many Peers (The Load Balancing Case):

    • If the node discovers it has more than 20 peers, it knows it is safely embedded in the Gossipsub mesh and Kademlia DHT.
    • At this point, staying connected to the foundational bootstrap nodes is actually a detriment to the network.
    • It consumes valuable connection slots on the bootstrap servers.
    • Therefore, the loop iterates over its bootstrap_peers list.
    • It intentionally disconnects from them.
    • This ensures that new nodes joining the network always have available slots on the bootstrap servers.
    • It is a polite, cooperative network behavior.

3. The Drand Tick Receiver (drand_kyn_rx)

-> See: kinetic-network/src/event_loop/core.rs — Lines 260 to 267

Kinetic relies on a global, decentralized clock called Drand. The local daemon watches this clock. The event loop holds a tokio::sync::watch::Receiver<u64> that triggers every time the global clock ticks forward.

When the watch channel signals a change, the event loop borrows the new value. If the new kyn (time unit) is greater than the current_drand_kyn, the loop updates its own state. Crucially, it also updates the current_drand_kyn deep inside the Kademlia store behavior. self.swarm.behaviour_mut().kademlia.store_mut().current_drand_kyn = new_kyn This ensures that the DHT storage engine knows the current network time. This allows it to accurately reject records that attempt to forge timestamps in the future. It also allows it to delete records that have expired in the past.

4. The Swarm Stream (self.swarm.next())

-> See: kinetic-network/src/event_loop/core.rs — Line 268

The libp2p::Swarm implements the Stream trait. This means it produces a continuous flow of network events. When a peer connects, when a DHT query finishes, or when a Gossipsub message arrives, it bubbles up here. The event loop catches these events. It dispatches them to handle_swarm_event(). Because handle_swarm_event() is complex, it is typically broken out into its own file. However, the orchestration happens right here in the main select! block. The loop simply awaits the next event from the Swarm and passes ownership to the handler.

5. The Command Channel (command_receiver)

-> See: kinetic-network/src/event_loop/core.rs — Lines 269 to 275

This is how the rest of the Kinetic daemon talks to the network. The NetworkClient exposes user-friendly async functions (like publish_record or get_domain). These internally construct a Command enum and push it into this mpsc (Multi-Producer, Single-Consumer) channel. The loop wakes up. It pops the command. It translates it into a direct instruction to the Swarm. If the command_receiver returns None, it means all NetworkClient instances have been dropped. This signifies that the node is shutting down. The event loop logs this and breaks out of the infinite loop, terminating the network thread gracefully.

6. The Loopback Channel (loopback_rx)

-> See: kinetic-network/src/event_loop/core.rs — Lines 276 to 278

This is the security architecture in action. When a complex verification is required (e.g., verifying a block’s VDF or checking a connecting peer’s PoW), the event loop does not do the math. Doing the math would block the thread. Instead, it spawns a background blocking task. It passes the task a LoopbackCommand channel sender. When the math is done, the background task sends the result (Valid or Invalid) back to the main loop via this channel. The event loop awaits these results on the loopback_rx receiver. When a result arrives, it calls handle_loopback(). This applies the verdict — either committing the valid data to the DHT or banning the invalid peer.


Specific Execution Scenarios

To better understand the orchestration, here is how the event loop handles common network scenarios:

Scenario A: Processing a Malicious Gossip Block

  1. The self.swarm.next() branch in the select! loop receives a raw Gossipsub message event.
  2. The event loop passes the message to handle_swarm_event().
  3. Recognizing it as unverified data, the loop immediately offloads the signature and VDF checks.
  4. It uses a worker thread via tokio::task::spawn_blocking.
  5. The network loop continues running, polling other events.
  6. The worker thread finishes the math and determines the block is invalid.
  7. It constructs a LoopbackCommand::CommitGossipValidation with is_valid: false.
  8. The worker pushes the command to the loopback_tx channel.
  9. The main event loop wakes up on the loopback_rx.recv() branch.
  10. It calls handle_loopback(), which sees the invalid verdict.
  11. It calls record_invalid_gossip(source).
  12. The peer’s strike count goes from 0 to 1 in the bad_vdf_counts cache.
  13. Because is_valid is false, it tells the Gossipsub behavior to Reject the message.
  14. This ensures it is not propagated to other peers.
  15. If the peer was on their 3rd strike, record_invalid_gossip bans them.
  16. handle_loopback actively disconnects the underlying TCP socket.

Scenario B: Handling an Inbound Peer Connection (PoW Check)

  1. A new peer connects.
  2. handle_swarm_event() fires a connection established event.
  3. The event loop reads the peer’s connection metadata and extracts their Proof-of-Work nonce.
  4. The event loop offloads the PoW hashing algorithm to a background task.
  5. The background task finishes hashing and determines the PoW is invalid (or absent).
  6. It sends LoopbackCommand::ConnectionPoWVerified(valid: false) to the loopback channel.
  7. The event loop wakes up and enters handle_loopback().
  8. It checks if the daemon is at its Light Node capacity (50 nodes).
  9. Let’s assume it is currently at 49.
  10. It extracts the peer’s IP address.
  11. It checks light_node_ips.
  12. Let’s assume this IP only has 1 active connection.
  13. Since both limits are respected, the event loop inserts the peer into the light_nodes HashSet.
  14. It allows the connection to remain open, but their capabilities will be restricted in future interactions.

Key Pieces

This section breaks down the vital data structures and functions that make up the event loop orchestrator.

NetworkEventLoop Struct

-> See: kinetic-network/src/event_loop/core.rs — Lines 51 to 102

This is the massive state object that holds the entire P2P context. It contains:

  • swarm: The libp2p network manager itself, holding all the active transports and behaviors.
  • command_receiver: The ingest channel for commands from the daemon.
  • pending_gets: HashMap tracking ongoing Kademlia Get queries.
  • pending_quorums: HashMap tracking ongoing quorum lookups.
  • pending_puts: HashMap tracking outbound DHT publishes.
  • query_id_to_name: Map linking Kademlia QueryId directly to a QueryType.
  • pending_proxy_requests: Tracks outbound Request-Response protocol requests.
  • pending_cdn_requests: Tracks outbound CDN domain lookups.
  • bad_vdf_counts: An LruCache acting as the network’s strike system.
  • current_drand_kyn: The local cached copy of the global clock.
  • bootstrap_nodes: State tracking foundational network IP multiaddresses.
  • seed_domain: DNS domains to query for fallback seeds.
  • bootstrap_peers: State tracking currently connected bootstrap peer IDs.
  • banned_peers: An LruCache holding peers temporarily blacklisted.
  • pow_semaphore: Concurrency limiter for background PoW hashing.
  • gossip_semaphore: Concurrency limiter for background gossip VDF verification.
  • light_nodes: State tracking peers that failed the Proof of Work check.
  • light_node_ips: State tracking light node connection counts per IP address.

QueryType Enum

-> See: kinetic-network/src/event_loop/core.rs — Lines 18 to 23

A simple internal enum mapping a Kademlia query ID back to the requested action.

  • Get: standard DHT lookup.
  • Quorum: advanced DHT lookup requiring multiple identical responses to form a consensus.
  • Put: outbound DHT publish. This is used to correlate asynchronous DHT responses back to the original request intent.

LoopbackCommand Enum

-> See: kinetic-network/src/event_loop/core.rs — Lines 26 to 44

The messages sent from background cryptographic workers back to the main thread.

  • CommitVerifiedRecord:
    • A background task verified a DHT record’s signatures and VDFs.
    • It returns the source peer, the parsed record, and a Result indicating the verdict.
  • CommitGossipValidation:
    • A background task verified a gossip block.
    • It returns the message ID, the source peer, and a boolean indicating validity.
  • ConnectionPoWVerified:
    • A background task calculated the hashing power of a connecting peer.
    • It returns the peer ID, validity boolean, whether it was a bootstrap node, and the remote IP address.
  • DialResolvedSeed:
    • The background DNS resolver successfully found seed IP addresses.

record_invalid_gossip Function

-> See: kinetic-network/src/event_loop/core.rs — Lines 120 to 141

This function manages the network strike system. When a peer sends an invalid message, this function is called. It retrieves the current strike count from the bad_vdf_counts LRU cache.

  • If the peer’s last strike was more than 60 seconds ago, their slate is wiped clean, and their count resets to 1.
  • If their last strike was within the last 60 seconds, their count increments.
  • If their count reaches 3, the function emits a warning log and adds the peer to the banned_peers LRU cache.
  • It calculates an expiration timestamp of current_drand_kyn + 28800.
  • This typically equates to several hours in Kinetic’s clock system.

This precise 3-strikes-in-60-seconds rule is designed to forgive accidental protocol mismatches or isolated corruption while punishing intentional Sybil or flood attacks.

handle_loopback Function

-> See: kinetic-network/src/event_loop/core.rs — Lines 283 to 422

This function processes the results from the loopback channel. It is the execution arm of Kinetic’s security policy.

Handling CommitVerifiedRecord:

  • If the verdict is an error with Severity::Error, it triggers the strike system exactly like invalid gossip.
  • 3 strikes in 60 seconds results in a ban and an immediate swarm.disconnect_peer_id(source) call.
  • If the verdict is successful, it inserts the verified record into the local Kademlia store.
  • Crucially, it then calls swarm.behaviour_mut().kademlia.start_providing(record.key.clone()).
  • This implements “Edge Caching” — the node actively advertises that it now has a copy of this record.
  • This helps distribute the load across the network and reduces strain on the original publisher.

Handling CommitGossipValidation:

  • If valid, it tells the Swarm to Accept the message, which forwards it to the rest of the mesh.
  • If invalid, it tells the Swarm to Reject the message (preventing forwarding).
  • It then calls record_invalid_gossip.
  • If the peer is banned as a result, it disconnects them.

Handling ConnectionPoWVerified: This is the Sybil resistance mechanism for connection slots.

  • If a peer passes PoW, they are fully admitted.
  • If a peer fails PoW, they are not immediately rejected.
  • Instead, Kinetic attempts to classify them as a “Light Node” (a mobile phone or browser that legitimately cannot compute PoW). However, Light Nodes consume connection slots without contributing heavy validation work. Therefore, Kinetic enforces limits:
  1. Global Limit: If the daemon already has 50 Light Nodes connected (self.light_nodes.len() >= 50), it rejects the new connection immediately to prevent resource exhaustion.
  2. IP Limit: The function extracts the raw IPv4/IPv6 address from the remote_addr. It checks light_node_ips. If 3 Light Nodes are already connected from that exact same IP address, it rejects the connection. This prevents a single attacker from spoofing 50 Light Node identities from a single machine. If both checks pass, the peer is added to the light_nodes set.

How This Connects to the Rest of Kinetic

  • Receives from NetworkClient: The NetworkEventLoop is the backend for the NetworkClient. When other modules (like the REST API or the consensus engine) use the client, the commands flow directly into this loop’s command_receiver. The NetworkClient acts as the frontend interface, abstracting away the complexity of the command channels.

  • Sends to Sled (Storage): The loop actively prunes the local Kademlia DHT storage backend, communicating directly with the kinetic-core database architecture. The event loop’s background pruning task directly invokes the Sled scan and delete APIs.

  • CROSS-CRATE: Uses kinetic_core::constants::TIMEOUTS_NETWORK_PRUNE_INTERVAL_SECONDS to dictate how often the storage engine is cleaned up. This ensures the network pruning frequency is synchronized with the core database configuration.

  • CROSS-CRATE: Uses kinetic_core::constants::DB_PREFIX_BANNED_PEER as the Sled key prefix when scanning for expired bans.

  • Validates via kinetic-verify: Though the actual calls to kinetic-verify happen in the background tasks, the loopback commands carry the Result enums defined by the verification crate. The event loop inspects the severity() of these errors to determine if a peer made a harmless mistake or committed a malicious offense requiring a ban.

  • DNS Resolution: Interacts with crate::dns_tree::resolve_dns_tree to fetch fallback seeds when the primary bootstrap nodes are unreachable.


Quick Reference

  • Design Pattern: Asynchronous Actor Model. A single NetworkEventLoop struct holding exclusive ownership of the Swarm, processing events sequentially via message passing.
  • Concurrency Control: Background blocking tasks for cryptography, results returned via mpsc unbounded channels (loopback_tx / loopback_rx).
  • Strike Policy: Tracked via bad_vdf_counts LRU cache. 3 invalid cryptographic validations within 60 seconds = temporary ban (via Sled) + immediate disconnection.
  • Light Node Policy: Peers failing PoW are branded as Light Nodes. Limited to 50 total Light Nodes per daemon to preserve resources. Limited to 3 Light Nodes per IP address to prevent Sybil connection draining. Tracked via light_nodes and light_node_ips.
  • Redial Policy: Checks peer count every 15 seconds. At 0 peers, dials DNS seeds. At >20 peers, drops hardcoded bootstrap nodes to save bandwidth for the network.
  • Drand Synchronization: Watches a tokio::sync::watch channel for global time updates, propagating the current_drand_kyn down into the Kademlia storage engine.
  • Edge Caching: When a DHT record is successfully verified via loopback, the node automatically calls start_providing to advertise its local cached copy.

Open Questions / Things to Revisit

  • Unbounded Loopback Channel: The loopback_tx is a tokio::sync::mpsc::UnboundedSender. If background tasks generate loopback commands faster than the event loop can process them, memory could theoretically grow unbounded, leading to an Out-Of-Memory (OOM) crash. In practice, the concurrency of background tasks is limited by the pow_semaphore and gossip_semaphore, which naturally caps the loopback rate. However, relying on external semaphores to protect an unbounded channel is a slight architectural fragility that might warrant an explicit bounded channel in the future.

  • Jitter Logic: The random jitter for timers uses SystemTime::now().duration_since(UNIX_EPOCH).as_millis() % 60. While sufficient for desynchronizing nodes and preventing thundering herds, it is not cryptographically uniform or unpredictable. A deterministic adversary could theoretically predict exact pruning times.

  • Banned Peer Persistence: Banned peers are stored in Sled. The pruning mechanism deletes expired bans, but the loop itself uses an in-memory LruCache (banned_peers) for fast lookups during active connections. The synchronization between the Sled ban list and the LRU cache might have edge cases on node restart. If the node restarts, does the LRU cache correctly repopulate from Sled, or is it empty until a new offense occurs?

  • Light Node IP Spoofing: The limit of 3 Light Nodes per IP address mitigates basic Sybil attacks. However, if an attacker has access to a large botnet or a proxy pool, they could easily bypass this by routing connections through different IPs, exhausting the global limit of 50 Light Nodes. Further analysis may be needed to determine if Light Nodes require an additional challenge/response mechanism beyond simply failing PoW.

  • Drand Time Jumps: If the local system clock jumps forward drastically, the drand_kyn_rx will receive a updated time. How gracefully does the Kademlia store handle massive time jumps when evaluating record expirations?

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.

Event Loop Command Handler

Crate: kinetic-network Stage: 8 Reading time: 30 minutes Depends on: 01_overview.md, 10_event_loop_core.md


What Is This?

The command handler is the critical translation layer for the Kinetic network. It sits between the rest of the application and the libp2p network event loop.

When you build a peer-to-peer application using Rust’s libp2p ecosystem:

  • The network state is contained within a Swarm.
  • This Swarm manages all TCP and QUIC connections.
  • It handles the Kademlia routing tables for the DHT.
  • It manages the Gossipsub mesh for pub/sub messaging.
  • It tracks the AutoNAT status to determine public reachability.

Because Rust enforces strict ownership rules to prevent data races:

  • The Swarm is restricted to running on a single asynchronous task.
  • This task is the main event loop.
  • The Swarm cannot be safely shared across multiple threads.

However, the rest of the Kinetic daemon runs across multiple concurrent Tokio threads. These components include:

  • The HTTP REST API handlers.
  • The background block verification workers.
  • The consensus engine and mempool.

These components constantly need to interact with the network. They need to:

  • Publish domains to the DHT.
  • Resolve names from the network.
  • Broadcast new blocks and transactions.

The command handler (command_handler.rs) solves this concurrency problem. It implements an Actor Model for network communication. It receives high-level commands from other threads. These commands are sent via an asynchronous message channel (MPSC). The handler then converts these abstract requests into low-level operations. These operations mutate the state of the Kademlia DHT, the Gossipsub mesh, or the custom Content Delivery Network (CDN) layer.

Crucially, it also manages the asynchronous return paths. Networking takes time, so requests cannot be answered instantly. The command handler ensures that requests initiated by the application are properly tracked. It routes them into the peer-to-peer mesh. Finally, it guarantees that the eventual results are routed back exactly to the caller thread that requested them.


Why Kinetic Needs This

To understand why this specific file is so necessary, imagine a scenario where it didn’t exist. Imagine if the REST API tried to share the Swarm using an Arc<Mutex<Swarm>>.

  1. A user hits the /resolve endpoint on the REST API.
  2. The REST thread locks the mutex.
  3. It asks the Swarm to resolve a domain.
  4. It waits for the resolution to complete.

But libp2p relies on continuously polling the Swarm. Polling processes background network events like:

  • Incoming peer connections.
  • Keep-alive pings.
  • Protocol negotiations.

If the REST thread holds the lock while waiting for a DHT resolution:

  • The resolution could take hundreds of milliseconds or even seconds.
  • During this time, the entire network layer is frozen.
  • The event loop cannot poll.
  • TCP Connections would time out and drop.
  • Gossipsub would miss heartbeat intervals.
  • The node would rapidly fall out of the mesh network.

Kinetic avoids this by using the NetworkEventLoop as a dedicated, continuously running loop. This loop owns the Swarm completely. Other threads can never touch the Swarm directly. Instead, they communicate by sending messages through a multi-producer, single-consumer (MPSC) channel.

The command_handler.rs file is the instruction manual for the event loop. It defines exactly how the event loop should react when a message arrives on that MPSC channel.

Here is the exact flow of a command:

  • A client wants to resolve a domain.
  • The client constructs a Command message.
  • Attached to that message is a one-time-use return channel (tokio::sync::oneshot::Sender).
  • The client sends the command over the MPSC channel.
  • The command handler (inside the event loop) unpacks the Command message.
  • It triggers the actual libp2p behaviors (like kademlia.get_record).
  • It registers the return channel in a state map (like pending_gets).
  • The event loop continues polling.
  • Later, the network replies with the requested data.
  • The event loop looks up the corresponding return channel in the state map.
  • It sends the answer back to the exact REST thread that asked for it.

Without this file, the application would be severed from the P2P network capabilities. The REST API would have no way to ask the network to do anything. The background workers would have no way to broadcast their verifications.


How It Works

The core logic lives inside the handle_command function. This function processes a massive match statement over every possible Command variant. Because networking is fundamentally asynchronous, most commands cannot be answered immediately. Instead, they require kicking off a network request and storing a “pending state.”

Let’s break down exactly how the most important commands are processed step by step.

1. Publishing Redundantly to the DHT (Command::PublishRedundant)

When the event loop receives a Command::PublishRedundant, it needs to ensure the data is distributed widely. It must survive node churn (nodes going offline). Kademlia stores data on the nodes whose IDs are closest to the data’s key. If we only stored the data under one key, and the nodes closest to that key went offline, the data would be lost.

Here is the step-by-step process for redundant publishing:

  1. Key Derivation

    • It first derives a set of storage keys.
    • It uses the derive_storage_keys function from kinetic-core.
    • This takes the human-readable domain name (like saif.kyn).
    • It generates multiple distinct cryptographic DHT keys.
    • This ensures the record is replicated across different regions of the network’s logical address space.
  2. Local Validation

    • Before it ever touches the network, it calls enqueue_dht_puts.
    • This helper function attempts to insert the record into the node’s local Kademlia store first.
    • Why do this locally first? This is a massive optimization and security feature.
    • If the record is invalid, the local store’s validation logic will reject it.
    • Reasons for invalidity include a wrong cryptographic signature, insufficient proof-of-work, or incorrect formatting.
    • If the local store rejects it, every other honest node on the network will reject it too.
    • By failing fast locally, Kinetic prevents the node from wasting outbound bandwidth.
    • It prevents spamming the network with garbage records.
    • It immediately returns an error to the caller without generating any network traffic.
  3. Network Dispatch

    • If local validation succeeds, it proceeds to network distribution.
    • It iterates over every derived key.
    • It fires off a put_record request to the kademlia behavior in the Swarm.
    • It requests a Quorum::One.
    • This means it expects at least one successful acknowledgment per key from the network.
  4. State Tracking

    • Every put_record call returns a QueryId.
    • The handler records this QueryId in the query_id_to_name map.
    • This allows the event loop to know what operation a future response belongs to.
    • It also registers the expected number of successful responses in the pending_puts map.
    • It stores the client’s oneshot::Sender here as well.
    • As network acknowledgments trickle in over the next few seconds, they are tallied.
    • Once all expected keys are published, the oneshot channel is triggered.
    • The client is notified of success.

2. Resolving Data (Racing Kademlia vs. CDN) (Command::ResolveRedundant)

The Command::ResolveRedundant variant is one of the most critical paths for user experience. When a user requests a domain, the resolution needs to be fast. However, Kademlia DHT lookups are notoriously slow. They require iteratively querying multiple nodes to find the closest peers to a key.

To solve this latency problem, Kinetic implements a sophisticated “racing” mechanism. It queries two different network layers simultaneously:

  1. Offline Fallback Check

    • First, it checks the network info for active peers.
    • If the node has zero active peers, it is offline.
    • In this case, it falls back to querying its own local Kademlia store.
    • It derives the keys and checks if it holds the data.
    • This is crucial for local testing or offline development.
    • It also saves nodes that temporarily lose internet connectivity but have recently cached the required domain.
  2. CDN Blast (The Fast Path)

    • If online, it immediately attempts a CDN blast.
    • It selects up to 3 currently connected peers from the swarm.
    • It uses the custom cdn behavior to fire off a direct request for the domain to these 3 peers.
    • This skips the iterative, multi-hop routing of Kademlia.
    • If one of these direct peers happens to have the domain cached in their edge storage, they will return it instantly.
    • This cuts resolution time from ~800ms down to ~50ms.
  3. DHT Querying (The Reliable Path)

    • At the exact same time as the CDN blast, it queries the DHT.
    • It derives the storage keys for the domain.
    • It dispatches formal Kademlia get_record queries into the DHT via dispatch_dht_queries.
    • This is the slow, fallback method.
    • It is guaranteed to find the data if it exists anywhere on the network, even if the 3 peers from the CDN blast didn’t have it.
  4. First to Win

    • The state for the CDN requests is recorded in pending_cdn_requests.
    • The state for the DHT queries is recorded in pending_gets.
    • The event loop will accept the first valid response it receives from either system.
    • Whichever subsystem returns cryptographically valid data first will trigger the oneshot response to the user.
    • This architecture guarantees the speed of a centralized CDN with the decentralized, uncensorable reliability of a DHT.

3. Verifying Quorums (Command::VerifyQuorum)

Sometimes, Kinetic needs to know not just what a record is, but whether the network agrees on it. The VerifyQuorum command is used for consensus checks.

  • It derives the storage keys for a domain.
  • It fires off Kademlia queries using a special QueryType::Quorum.
  • It registers the expected payload in pending_quorums.
  • As the network responds, the event loop counts how many distinct nodes returned the exact same payload.
  • It then reports this “match count” back to the client.
  • This allows the application to prove that a record is widely distributed.
  • It ensures the data has not been eclipsed by a malicious actor.

4. Handling Gossipsub Messages and Validation

The libp2p::gossipsub behavior manages the unstructured mesh network. This mesh is used for broadcasting blocks and mempool transactions. The event loop interacts with it via three main commands:

  • Subscribing (Command::SubscribeGossip)

    • When the application wants to listen to a new topic (e.g., a specific block height), it sends this command.
    • The handler takes the string topic and converts it into a libp2p::gossipsub::IdentTopic.
    • It calls subscribe on the gossipsub behavior.
    • This tells the libp2p swarm to actively seek out peers who are also interested in this topic.
    • It starts maintaining a local mesh network for it.
    • If successful, the node will begin receiving events for this topic.
  • Broadcasting (Command::BroadcastGossip)

    • When the node creates a new block or transaction, it needs to propagate it.
    • This command takes the payload and the topic.
    • It creates the IdentTopic and calls publish.
    • The swarm then forwards the message to a subset of its connected peers in the mesh.
    • Those peers will then forward it to their peers, propagating it globally.
  • Validation (Command::ReportGossipValidation)

    • Kinetic uses “strict validation” for Gossipsub.
    • When a message is received, it is not immediately forwarded.
    • Instead, it is offloaded to a background thread to check the VDF proofs and signatures.
    • Once the background thread finishes its heavy cryptographic work, it sends this command back to the event loop.
    • The command contains the MessageId, the PeerId of the sender, and whether the message was valid.
    • The handler translates this into MessageAcceptance::Accept or Reject.
    • It calls report_message_validation_result.
    • If rejected, the peer’s score is penalized.
    • If a peer sends too many invalid messages, they are banned.
    • This is the fundamental mechanism protecting the network from flood attacks.

5. Proxy Requests for Light Clients

Light clients cannot participate fully in the Kademlia DHT or Gossipsub mesh. They lack the bandwidth, CPU, and storage required. Instead, they rely on full nodes to proxy their requests. The command handler facilitates this via the custom proxy behavior (part of the CDN).

  • Sending Requests (Command::SendProxyRequest)

    • When the node needs to ask another specific peer for data directly (without routing through the DHT), it sends this command.
    • It provides the target libp2p::PeerId and the ProxyRequest payload.
    • The handler invokes self.swarm.behaviour_mut().proxy.send_request.
    • This returns an OutboundRequestId.
    • The handler then maps this ID to the client’s oneshot::Sender in the pending_proxy_requests map.
    • When the target peer eventually replies, the event loop can look up the ID and route the response.
  • Sending Responses (Command::SendProxyResponse)

    • When a remote peer asks this node for data, the request is surfaced to the application.
    • The application processes the request.
    • Once the application has the answer, it sends this command back to the event loop.
    • The command includes the ResponseChannel provided by libp2p during the initial request, along with the data.
    • The handler calls send_response to push the data back over the wire to the requesting peer.

6. Network Status (Command::GetNetworkStatus)

This is a diagnostic command used by the CLI tooling. When triggered, it queries the swarm for its current state and returns JSON containing:

  • The number of currently connected peers.
  • The node’s local PeerId.
  • The multiaddresses it is currently listening on.
  • Its NAT status (whether it is reachable from the public internet).
  • The size of its local DHT store. This provides users with a real-time dashboard of their node’s health.

Key Pieces

Command Enum

-> See: kinetic-network/src/client/command.rs — Lines 10 to 101

This is the exhaustive list of all instructions the event loop understands. It defines the complete API surface between the network layer and the rest of the node. Every variant typically contains:

  1. The required parameters (like a domain name, a payload, or a topic string).
  2. A responder: tokio::sync::oneshot::Sender. This sender routes the asynchronous answer back to the exact thread that issued the command. Without this enum, the node’s business logic would have no way to communicate with the P2P mesh.

handle_command Function

-> See: kinetic-network/src/event_loop/command_handler.rs — Lines 89 to 314

This is the massive match statement that consumes Command instances. It dictates exactly how the state of the NetworkEventLoop mutates in response to client requests. This is the main entry point for everything described in the “How It Works” section.

enqueue_dht_puts Method

-> See: kinetic-network/src/event_loop/command_handler.rs — Lines 10 to 70

This method is responsible for taking a payload and validating it against the local store rules to prevent spam. It then queues multiple put_record requests to the Kademlia behavior to ensure redundant network storage. It gracefully handles immediate failures and warns the node operator if all puts fail immediately.

dispatch_dht_queries Method

-> See: kinetic-network/src/event_loop/command_handler.rs — Lines 72 to 87

This method fires off Kademlia get_record queries for a list of derived keys. Crucially, it registers the resulting query_id in the query_id_to_name mapping. This ensures the event loop knows exactly what domain a future, asynchronous network response belongs to.


How This Connects to the Rest of Kinetic

  • CROSS-CRATE: The Command variants rely on derive_storage_keys and derive_heartbeat_keys from kinetic_core::types. These map human-readable domain names to the cryptographic Kademlia address space.
  • CROSS-CRATE: The commands themselves are constructed and sent by the NetworkClient. This client lives in kinetic-network but provides a clean, async API utilized extensively by the kinetic-rest server and the background workers in kinetic-daemon.
  • Internal Integration: This handler directly mutates the pending_gets, pending_puts, and pending_quorums state maps defined in event_loop/core.rs. It provides the setup, while the teardown (completing the request) happens when network events arrive later in the loop.

Quick Reference

  • Publishing (PublishRedundant):

    • Validates locally to prevent spam.
    • Puts to the DHT redundantly across multiple keys.
    • Returns success when the network acknowledges.
  • Resolving (ResolveRedundant):

    • Checks the offline local cache first.
    • Races direct CDN requests to 3 peers against slow DHT queries.
    • Returns the fastest valid response.
  • Quorum Verification (VerifyQuorum):

    • Queries the DHT to ensure a critical mass of nodes agree on the exact same payload.
    • Used for consensus and security checks.
  • Gossipsub Management:

    • BroadcastGossip and SubscribeGossip act as simple pass-throughs.
    • They call methods on the underlying libp2p::gossipsub behavior.
    • This allows the node to participate in block and mempool meshes.
  • Validation Loopback:

    • ReportGossipValidation allows offloaded CPU-heavy checks to report back.
    • It penalizes malicious peers sending bad VDFs or signatures.
  • Network Status:

    • Generates diagnostic JSON summarizing peer connections, local DHT size, and NAT status.
    • Used by the CLI.

Open Questions / Things to Revisit

  1. CDN Racing Peer Count

    • The ResolveRedundant command blindly takes the first 3 connected peers for its CDN blast: self.swarm.connected_peers().copied().take(3).
    • Is 3 the optimal number?
    • Should it prioritize peers based on known latency, bandwidth, or past reliability?
    • Just taking the first 3 from the iterator might select slow peers, losing the CDN blast advantage.
  2. Offline Fallback Scope

    • The offline fallback for ResolveRedundant currently only checks the local Kademlia store.
    • If a record was previously fetched via the CDN layer but never formally inserted into the local Kademlia store, the offline node will fail to resolve it.
    • Should the CDN cache be queried during offline mode as well to provide better resilience?
  3. Local Put Failure Handling

    • In enqueue_dht_puts, if the local put fails, it aborts the network publish.
    • It assumes this is a validation error.
    • This is correct for cryptographic validation failures.
    • But what if the local store fails due to a disk I/O error or full disk?
    • It would silently prevent the node from publishing valid data to the network.
    • This could potentially isolate the node’s outputs.
  4. Bootstrap Redialing

    • In the Bootstrap command, it redials the hardcoded bootstrap peers.
    • If those peers change IP addresses, this command might endlessly dial dead endpoints.
    • A DNS re-resolution might be necessary here if they rotate IPs.

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 libp2p Swarm instance, embedded inside the NetworkEventLoop struct, 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:

  1. Sybil Attacks: A malicious actor could spin up 10,000 fake peer identities on a single server and connect to your node.
  2. 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.
  3. 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:

  1. 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 calls swarm.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.
  2. The Drand Synchronization Check: To verify a Proof-of-Work, the node needs to know the current network time (the drand_kyn epoch). If self.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.
  3. The Proof-of-Work Verification: Because computing the hash to verify PoW is CPU-bound, running it directly in the handle_swarm_event async function would freeze the entire event loop. No other network packets could be processed. To solve this, Kinetic clones the pow_semaphore and spawns a tokio background task.
    • The task waits to acquire one of the 2 available semaphore permits.
    • Once acquired, it uses tokio::task::spawn_blocking to 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::ConnectionPoWVerified back to the main loop via the loopback_tx channel. 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 to crate::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::proxy or handlers::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_time tracker, removes them from the light_nodes set, 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 PeerId has a valid PoW based on the current drand_kyn epoch block.
  • Why it matters: It serves as the primary gatekeeper for entry into the Kademlia routing table. If is_valid_pow returns 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_tx back to event_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 to handlers::kademlia::handle(). This modularity keeps swarm_handler.rs from 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_blocking to 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 Identify event 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_completion uses 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 FullNode is selected on WebAssembly architecture. While technically correct (WASM can’t run full Kademlia servers), it might be better to return a graceful anyhow::Error rather than crashing the entire process abruptly if the configuration is accidentally flawed.
  • Semaphore Limits Configuration: The pow_semaphore is 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 the NetworkConfig.

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?

  1. The Kademlia DHT Sub-Protocol When KineticBehaviorEvent::Kademlia is matched, it is passed to the Kademlia handler. Kademlia is the beating heart of Kinetic’s domain resolution system. It is responsible for routing GET requests (for looking up domains) and PUT requests (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.

  2. The Proxy Sub-Protocol Kinetic allows nodes to act as decentralized proxies. When a KineticBehaviorEvent::Proxy event 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.

  3. 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.

  4. 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 drand epoch blocks and network-wide alerts. When KineticBehaviorEvent::Gossipsub is 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.

  5. 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::Upnp fires, 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.
  6. Relay Client and Server If a node cannot establish a direct connection, it falls back to Relays. The handler catches RelayClient and RelayServer events. A Full Node might act as a Relay Server for others, while a Light Node will exclusively act as a Relay Client.

  7. 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::Mdns detects 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_swarm and fullnode::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.

  1. The Unknown State: When the node starts, its NAT status is “Unknown”. It does not know if it can be reached.
  2. 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.
  3. 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.
  4. 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 calls self.swarm.add_external_address(address) to tell the networking stack to advertise this IP. Crucially, it then loops over every connected peer and pushes an Identify update. 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 over p.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 for saif.kyn locally. It loops over these keys and directly queries its own RecordStore (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::NotFound to 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:

  1. 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.
  2. Relay Servers: The node will not configure a Relay Server behavior. It will never route traffic for other NAT-restricted peers.
  3. 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:

  1. /ip4/198.51.100.1 -> The IPv4 address.
  2. /tcp/4001 -> The transport protocol and port.
  3. /p2p/QmYy... -> The libp2p PeerId expected 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).

  1. It uses storage.scan_prefix(DB_PREFIX_BANNED_PEER) to iterate over every banned peer in the database.
  2. It parses the byte array back into a libp2p::PeerId.
  3. It parses the 8-byte value into a u64 representing the expiration timestamp.
  4. 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 the SwarmEvent::ConnectionEstablished event 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:

  1. Your node caches saif.kyn = IP_A locally because you looked it up yesterday.
  2. Today, the owner updates it to saif.kyn = IP_B.
  3. Your node experiences a temporary network partition (your router goes offline, or your ISP blocks Kademlia traffic).
  4. You try to resolve saif.kyn. The remote DHT lookup fails because you are offline.
  5. The local fallback triggers, finds IP_A in 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.

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_behaviour closure 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_behaviour closure 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 stream or relay_server depending on the compilation target.

How This Connects to the Rest of Kinetic

Both functions return a tuple: Result<(libp2p::Swarm<KineticBehavior>, NetworkClient), anyhow::Error>.

  1. 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 core EventLoop struct, where it is polled continuously in an asynchronous background thread.

  2. The Client (NetworkClient): While the EventLoop takes 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. The NetworkClient is the handle that gets returned to the application. It contains an MPSC channel sender (tx) that allows the app to send commands, such as PutRecord, DialPeer, or PublishMessage, into the Swarm thread for execution.

  3. Storage Dependencies: Both functions take an Arc<dyn kinetic_core::traits::StorageEngine>. CROSS-CRATE: This storage engine is defined in kinetic-core (Stage 7). The DHT behavior needs it to actually persist Kademlia records to disk, which is managed by the KineticRecordStore wrapper passed into the Kademlia config.

  4. VDF Engine: Both functions also take an Arc<dyn kinetic_core::traits::VdfEngine>. CROSS-CRATE: This is defined in kinetic-core (Stage 7) and implemented by kyn-vdf (Stage 6). It is passed into the record store to validate Verifiable Delay Functions when verifying data integrity during DHT put operations.

  5. Libp2p Stream Control Channel: In native builds, the setup creates a (control_tx, control_rx) channel. The libp2p_stream::Behaviour::new() creates a behavior that allows opening raw byte streams to peers. The stream.new_control() handle is passed through the channel into the NetworkClient, 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

  1. WASM Support Complexity and Readability: The lightnode.rs file 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 to KineticBehavior in 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 as wasmnode.rs, would be architecturally cleaner than polluting lightnode.rs with macro noise.

  2. Relay Server Bandwidth Limits: The full node restricts relay usage to max_circuit_bytes, defined in kinetic_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.

  3. 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 EventLoop initialization 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.

  4. Port Reuse Warning Trigger: In fullnode.rs, there is a fallback mechanism if port_reuse(true) fails. Some operating systems (like older versions of Windows or specific Linux kernels) do not support SO_REUSEPORT effectively. 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 the KineticRecordStore. 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 with kinetic_core::constants::NETWORK_ID. This is an essential security measure. It ensures that a node running on testnet cannot accidentally connect to the Kademlia DHT of the mainnet.
  • 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: In test_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 the EventLoop will 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-encoded ProxyRequest to a specific peer and await a ProxyResponse. Like Kademlia, it is scoped to the specific NETWORK_ID.
  • CDN Behavior: Uses a similar CBOR-encoded behavior for CdnRequest and CdnResponse. 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 the websocket_websys transport handles stream multiplexing internally at the Javascript level, making it incompatible with libp2p_stream’s raw byte-level expectations.
  • libp2p::mdns: mDNS relies on broadcasting UDP packets to the local subnet (usually address 224.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 address 239.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:

  1. Logging: Emits a tracing::info! log announcing the initialization and the ports it intends to bind to.
  2. Multiplexer Prep: Creates the yamux_config closure.
  3. Transport Build: Attempts to build a Tokio-backed TCP transport with port_reuse enabled.
  4. Transport Fallback: If port reuse fails (due to OS limitations), it logs a warning and builds a standard TCP transport.
  5. QUIC & DNS: Appends the QUIC UDP transport and DNS resolution capabilities.
  6. Channel Creation: Instantiates a standard library MPSC channel (control_tx, control_rx). This is specifically used to extract the libp2p_stream control handle out of the behavior closure.
  7. Swarm Assembly: The .with_behaviour closure is executed. This closure consumes the local keypair and initializes the KineticRecordStore.
  8. Behavior Injection: Kademlia, Gossipsub, Ping, Identify, DCUtR, Proxy, CDN, and mDNS are instantiated.
  9. Stream Extraction: The stream.new_control() handle is passed into control_tx.send(). This allows the control handle to escape the closure.
  10. Swarm Config: The final Swarm object is built, and with_idle_connection_timeout is applied.
  11. TCP Binding: The code iterates over config.listen_addrs and executes swarm.listen_on(addr). If the port is already in use by another application, it emits a tracing::warn! but continues execution.
  12. QUIC Binding: Iterates over config.quic_listen_addrs and executes swarm.listen_on(quic_addr).
  13. External Address Advertising: If the user provided an external_address in the config, it is forcibly added to the swarm via add_external_address. This forces the node to advertise this IP to the DHT, regardless of what AutoNAT detects.
  14. Client Instantiation: The control_rx.recv() call blocks until the behavior closure sends the stream handle. Then, the NetworkClient is constructed using the tokio::mpsc sender and the stream handle.
  15. Return: Returns the Swarm and the NetworkClient to 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.

Error Handling in Kinetic Network

Crate: kinetic-network Stage: 8 Reading time: 15 minutes Depends on: 01_overview.md


What Is This?

This document meticulously explains KineticStoreError, the fine-grained error enumeration used throughout the Kinetic network’s storage and event loop layers. It lives within kinetic-network/src/error.rs.

In a standard distributed hash table (DHT) like Kademlia, the reasons for rejecting a piece of incoming data are usually simple: the data is too large, or the node is out of storage space. But in Kinetic, rejecting a record is a complex, cryptoeconomic decision.

A record might be rejected because its Verifiable Delay Function (VDF) proof expired. It might be rejected because its Ed25519 signature is malformed. It might be rejected because it lost an XOR distance tie-breaker against an existing, equally valid record.

This file provides 21 specific, Kinetic-aware error variants. It allows the network layer to precisely identify why a domain name registration, heartbeat, or host routing update failed, rather than just throwing up its hands and knowing that “something went wrong.”

Without this file, debugging a live Kinetic node would be virtually impossible. If the network suddenly started rejecting thousands of records, we would have no programmatic way to determine if it was a coordinated attack, a bug in the time-sync code, or simply a surge in invalid domain registrations.


Why Kinetic Needs This

To understand why this file was created, you have to understand a fundamental limitation in libp2p, the underlying networking framework that Kinetic is built upon.

When you implement a custom record store in libp2p (which Kinetic does in order to validate records before storing them on disk), libp2p expects your validation function to return a specific error type: libp2p::kad::store::Error.

The problem is that libp2p’s built-in error enum is limited. For record rejections, it basically only offers a single variant: ValueTooLarge.

If Kinetic just returned ValueTooLarge every time a VDF failed, a signature was forged, or a heartbeat was stale, we would have no idea what was actually happening on the network. We wouldn’t know if we were under a cryptographic attack, if there was a clock sync issue causing stale records, or if the VDF engine had crashed. All we would see in the logs is endless “ValueTooLarge” messages.

KineticStoreError is Saif’s architectural solution to this bottleneck.

We define all 21 specific failure modes in this custom enum. When a record fails validation, we generate a KineticStoreError. We can then inspect it, log it with its exact context and severity, and map it to a stable KIN-NET-*** error code for external APIs.

Only after we have fully processed the Kinetic-specific error do we crush it down into libp2p’s generic ValueTooLarge error in order to satisfy the Rust compiler. This file forms the crucial bridge between Kinetic’s complex cryptoeconomic validation rules and libp2p’s simplistic, generic storage API.


How It Works

The entire file is built around a single Rust enum named KineticStoreError. It leverages popular Rust macros to automatically generate display text, and it includes several helper methods to categorize and process the errors.

Here is a step-by-step breakdown of how the error system functions:

1. Detailed Variant Breakdown

To fully grasp the scope of network validations, we need to look at exactly what each of these 21 errors represents. Kinetic is a hostile environment; peers will lie to you, send you garbage data, or attempt to overwrite your domain names. These variants are your node’s active defense mechanisms.

Payload and Capacity Constraints

  • PayloadTooLarge (KIN-NET-001): The record submitted by a peer exceeds the hard byte limit defined by the network. This prevents peers from spamming the DHT with multi-megabyte junk payloads.
  • RateLimited (KIN-NET-017): The node is receiving too many reveal submissions too quickly. To protect local resources, it temporarily drops the record.
  • NetworkHalted (KIN-NET-021): The ultimate defense mechanism. If the network is emergency-paused by the Root Key, all registrations and renewals are halted, and this error is returned.

Cryptographic VDF Failures Kinetic relies on Verifiable Delay Functions (VDFs) for domain ownership. When VDF proofs fail, these errors catch them:

  • VdfExpired (KIN-NET-002): VDFs have a strict shelf life. If a peer submits a proof that is too many rounds old, it is rejected. Note that this enum variant carries the age data with it so we know exactly how stale the proof was.
  • InvalidVdf (KIN-NET-003): The cryptographic math behind the VDF proof did not verify. The peer is either buggy or actively forging proofs.
  • VdfEngineError (KIN-NET-004): The local VDF evaluation engine (the kyn-vdf integration) crashed or returned an internal failure.
  • InsufficientIterations (KIN-NET-009): A peer attempted to steal a domain name, but their VDF proof did not have more iterations than the existing record. The usurpation attempt is invalid.

Signature and Authentication Failures Every record in Kinetic must be cryptographically signed by the entity that generated it.

  • InvalidSignature (KIN-NET-005): The Ed25519 signature on the network payload does not match the data and public key provided.
  • InvalidPublicKey (KIN-NET-006): The public key provided in the record is structurally malformed (e.g., wrong byte length).
  • MalformedSignature (KIN-NET-007): The signature bytes themselves are structurally invalid.
  • InvalidKidSignature (KIN-NET-011): Specifically for Kinetic Identity (KID) documents, the signature securing the document failed validation.
  • InvalidManifestSignature (KIN-NET-012): The signature on a storage manifest failed.
  • InvalidHostRouteSignature (KIN-NET-016): Finding 13 in the architecture—HostRoutingRecords must be signed. This error fires if that signature is bad or if the record’s timestamp is stale.

State and Logic Rejections Sometimes the cryptography is correct, but the DHT state rules reject the record.

  • TieBroken (KIN-NET-008): In Kademlia, if two peers generate a valid record for the exact same key at the exact same time, the network uses an XOR distance calculation against the peer’s Node ID to break the tie mathematically. This prevents infinite propagation loops. This error fires gracefully for the loser of that tie-break. It is an expected, normal network occurrence.
  • RevealNotFound (KIN-NET-010): A peer attempted to perform an action on a domain, but the necessary reveal phase was never found in the DHT.
  • StaleHeartbeat (KIN-NET-015): Finding 8 in the architecture—heartbeat updates must move forward in time. If a peer submits a heartbeat that is older than or equal to the current one, it is rejected to prevent replay attacks.
  • StaleReveal (KIN-NET-018): The peer submitted a reveal, but the commitment is too recent (the required time delay period hasn’t elapsed).
  • MissingCommitment (KIN-NET-019): The peer submitted a reveal without first registering a commitment hash in the DHT.

Formatting and Parsing Errors

  • UnknownRecordType (KIN-NET-013): The record payload prefix does not match any known Kinetic record type.
  • InvalidDrandHex (KIN-NET-014): The randomness hex string pulled from the Drand beacon could not be decoded.
  • InvalidName (KIN-NET-020): The requested apex domain name violates Kinetic’s stringent naming rules.

-> See: kinetic-network/src/error.rs — Lines 10 to 78

2. Structuring Errors with thiserror

Notice the #[error("...")] annotations directly above every variant in the source code.

Kinetic uses the thiserror crate to eliminate boilerplate. Instead of manually writing a Display implementation for KineticStoreError that uses a massive match statement to map every variant to a string, thiserror generates it automatically based on these annotations during compilation.

For variants with embedded data, like VdfExpired { age }, the macro effortlessly injects the age variable directly into the format string: #[error("VDF proof has expired ({age} rounds old)")]. This keeps the code clean while preserving rich context.

3. Stable Error Codes and RFC 7807

The code() method takes any KineticStoreError and returns a static string identifier like "KIN-NET-005". This is crucial for two distinct reasons: First, it enables powerful log parsing. When debugging the daemon, you can grep for "KIN-NET-008" to find all XOR tie-break losses without worrying about exact string matches in the human-readable log messages. Second, it provides API stability. When a user or a client application submits a record via the REST API, they need programmatic error codes to handle failures gracefully in their code. The human-readable text of an error might change in a future update, but KIN-NET-005 will always programmatically mean InvalidSignature.

Furthermore, the error_type_uri() method formats this stable error code into a URI: https://kinetic.network/errors/KIN-NET-001 This adheres to a web standard (RFC 7807) for returning errors in HTTP APIs. Even though this error originates deep in the Kademlia peer-to-peer layer, formatting it this way ensures that when it eventually bubbles up to the Stage 10 kinetic-rest API, it is already formatted for a web client to consume natively.

-> See: kinetic-network/src/error.rs — Lines 82 to 111

4. Human-Friendly Explanations

While code() is meant for machines, the user_message() method provides a clean, human-readable sentence for every single error. Instead of a client seeing KIN-NET-008 or an esoteric internal Rust enum name like TieBroken, user_message() maps this to "Record lost XOR tie-break against existing DHT entry". This separation of concerns ensures that the internal logs can be as technical as necessary, while the user-facing output remains understandable. The REST API leverages this method to populate the "message" field in its JSON error responses.

-> See: kinetic-network/src/error.rs — Lines 119 to 149

5. The Retryable Check

The is_retryable() method uses the Rust matches! macro to quickly determine if an error is transient.

For example, if you hit a RateLimited error or a VdfEngineError (perhaps the local VDF process briefly restarted), you can safely try the operation again. However, if you get an InvalidSignature error, retrying is pointless because the math will never verify no matter how many times you try. This helps the network loop decide whether to drop a peer or just back off temporarily.

-> See: kinetic-network/src/error.rs — Lines 114 to 116

6. Advanced Logging Based on Severity

Not all errors are equal. If a peer sends a malformed public key, that is a severe error indicating a bug or an attack. If a peer loses an XOR tie-break (TieBroken), that is normal network operation.

The severity() method maps every variant to a Severity level (Info, Warning, or Error). This feeds directly into the log_warning() method. This method does not just blindly print errors to standard out. It executes a match on the severity to route the log to the correct tracing macro level.

The tracing crate is a framework for instrumenting Rust programs to collect structured, event-based diagnostic information. By feeding our errors into tracing::warn! and tracing::error! with attached key-value pairs (like error_code = error_code), we are not just printing strings; we are emitting structured JSON logs. This is how enterprise-grade monitoring systems track the health of the network in real-time.

-> See: kinetic-network/src/error.rs — Lines 152 to 202

7. The libp2p Escape Hatch

Finally, we reach the most important mechanical piece of the entire file:

#![allow(unused)]
fn main() {
impl From<KineticStoreError> for libp2p::kad::store::Error {
    fn from(_e: KineticStoreError) -> Self {
        libp2p::kad::store::Error::ValueTooLarge
    }
}
}

This snippet uses Rust’s built-in From trait. Whenever a function deep inside libp2p expects a libp2p::kad::store::Error, but Kinetic’s custom validation logic produces our KineticStoreError, Rust automatically calls this from function to bridge the gap.

Before this conversion happens, Kinetic’s record store implementation intercepts the KineticStoreError, fires the rich log_warning() function to record the exact details (like "KIN-NET-015: StaleHeartbeat"), and then lets Rust convert it into a blind ValueTooLarge error to hand back to the libp2p framework. It is a necessary, deliberate hack to bypass libp2p’s rigid and uninformative error design.

-> See: kinetic-network/src/error.rs — Lines 206 to 210


Key Pieces

  • KineticStoreError Enum: The exhaustive list of 21 network rejection reasons. This enum derives the thiserror::Error trait, which generates standard display formatting automatically based on macros. -> See file: kinetic-network/src/error.rs:L10-L78
  • code() Function: Maps internal variants to static KIN-NET-*** string identifiers. This is essential for the REST API and automated network monitoring. -> See file: kinetic-network/src/error.rs:L81-L106
  • error_type_uri() Function: Formats the error code into an RFC 7807 compliant URI for seamless HTTP API integration. -> See file: kinetic-network/src/error.rs:L109-L111
  • user_message() Function: Provides a clean, client-facing string explaining the error without exposing internal enum names or raw system metrics. -> See file: kinetic-network/src/error.rs:L119-L149
  • is_retryable() Function: A fast boolean check to see if an operation failed due to a temporary condition (like rate limiting) versus a permanent cryptographic failure. -> See file: kinetic-network/src/error.rs:L114-L116
  • severity() Function: Maps every error into Info, Warning, or Error levels to dictate how the node should complain about the failure. -> See file: kinetic-network/src/error.rs:L152-L176
  • From<KineticStoreError> Implementation: The trait implementation that quietly downgrades our rich 21-variant enum into libp2p’s single ValueTooLarge error so the underlying networking crate doesn’t panic. -> See file: kinetic-network/src/error.rs:L206-L210

How This Connects to the Rest of Kinetic

This file acts as the boundary layer between Kinetic’s strict validation rules and the standard networking stack.

  • Upstream (Validation): When the Kademlia record store receives a Put request from a peer, it passes the bytes to Kinetic’s validation logic. If validation fails for any reason, it generates one of these specific KineticStoreError variants.
  • Downstream (libp2p): Because of the From trait implementation, libp2p only ever sees ValueTooLarge. This allows it to discard the invalid record without needing to understand Kinetic’s cryptoeconomic rules.
  • CROSS-CRATE: Severity — This enum (Info, Warning, Error) is imported directly from kinetic_core::error::Severity. It ensures that logging levels are consistent across all Kinetic crates.
  • FORWARD DEPENDENCY: The KIN-NET-*** codes defined in code() and formatted in error_type_uri() will be utilized in Stage 10 (kinetic-rest). The REST API will intercept these errors and serve them as structured JSON to external clients so they know exactly why their transaction failed on the network.

Quick Reference

If you need to recall how network errors are categorized and processed:

  • Stable Codes: The prefix is always KIN-NET-. They range sequentially from 001 to 021.
  • Retryable Errors: Only RateLimited and VdfEngineError are marked as retryable. All others are final rejections.
  • Severity Mappings:
    • Info Level: Protocol collisions and timing bounds (TieBroken, InsufficientIterations, VdfExpired, RevealNotFound).
    • Warning Level: Size or spam limit violations (PayloadTooLarge, RateLimited, UnknownRecordType).
    • Error Level: All cryptographic verification failures, malformed data, and total network halts.
  • libp2p Escape Mapping: Every single error in this file ultimately translates to libp2p::kad::store::Error::ValueTooLarge at the absolute network boundary.

Open Questions / Things to Revisit

  • The ValueTooLarge Hack: Right now, we log the rich error locally and then return ValueTooLarge to libp2p. Is there a scenario where we want libp2p to behave differently based on the exact error? For example, if a peer sends us an InvalidSignature, we might want libp2p to outright ban their IP at the swarm level, rather than just silently dropping the record as “too large.” We currently cannot instruct libp2p to penalize peers differently based on this error mapping.
  • Error Propagation to Local RPC: When a local RPC client submits a record to the daemon and it fails, does the local client successfully get the rich KineticStoreError, or does it just get the crushed ValueTooLarge from the libp2p swarm? We need to ensure the local user gets the detailed KIN-NET code on their command line, not the generic libp2p error.
  • Missing Infrastructure Categories: Should there be a specific error variant for “Disk Full” or “Database Locked” if the underlying record store backend fails to write, as opposed to a protocol validation failure? Currently, we only map logical rejections.

Kademlia Event Handler

Crate: kinetic-network Stage: 8 Reading time: 25 minutes Depends on: kinetic-core, kinetic-types


What Is This?

This file contains the core handler for all Kademlia DHT (Distributed Hash Table) events that occur within the Kinetic network.

When the underlying libp2p swarm emits a Kademlia event, it flows directly into this handler. These events include:

  • A remote peer responding to our request for a record.
  • A search query finishing its network traversal across multiple nodes.
  • Another peer attempting to push a new record into our local storage.
  • The success or failure of our own attempts to publish data.

In a standard libp2p implementation, the Kademlia behaviour handles the raw routing and storage automatically without needing much application-level interference. However, Kinetic does not just blindly accept or route data.

This file serves as the critical intersection where generic Kademlia operations meet Kinetic’s specific, rigorous domain rules.

You can think of this handler as both the “border patrol” and the “mailroom” for the node:

  • As the mailroom: It translates raw network events into actionable state changes for our Quorums, standard Gets, and Puts, making sure network replies are routed back to the right waiting async tasks.
  • As the border patrol: It actively inspects inbound requests to ensure peers are not violating protocol rules. It applies bans to bad actors and intercepts heavy cryptographic work to keep the network stable.

Without this file, Kinetic would just be a generic, insecure data store. With it, it becomes a secure, consensus-driven network.


Why Kinetic Needs This

Kademlia by itself just stores and retrieves byte arrays. It has no concept of what those bytes mean, who is allowed to store them, or how much CPU power it takes to verify them.

If we just plugged standard Kademlia into Kinetic without this custom handler, the network would rapidly collapse under spam or freeze during heavy computations.

Here is exactly why Kinetic specifically built this custom interception layer:

  1. Quorum Consensus Tracking: When Kinetic queries the DHT, it often doesn’t just want the first answer it finds. If we are checking the state of the network (like looking for a VDF reveal), a single malicious peer could lie to us and serve fake data. We want to ask multiple peers and verify they all agree. Standard Kademlia doesn’t have a concept of “quorums.” It just fires a query and streams back whatever it finds. This handler catches the individual asynchronous results as they stream in over time and aggregates them into our pending_quorums state so we can prove network consensus. This prevents a single compromised node from hijacking the network state.

  2. Light Node Protection and Restriction: Light nodes in Kinetic are designed to be mobile or low-power clients. They are read-only participants on the network. They consume data but do not contribute to storage. If a light node attempts to write a record to the DHT, it is a severe protocol violation—they either have a broken client implementation or are attempting a malicious write. This handler acts as an immediate firewall. It catches those unauthorized write attempts and permanently bans the offending node before the data ever touches our local storage. Without this, light nodes could easily bloat the network.

  3. VDF Verification Isolation (Preventing Event Loop Deadlocks): VDFs (Verifiable Delay Functions) are central to Kinetic’s architecture, but they take significant, real CPU time to verify. If a peer sends us a VDF reveal to store, we cannot verify it on the main network event loop. In Rust’s async model (using Tokio), if you block the thread for 500ms to do math, the entire system stops. While blocked, our node would stop responding to routing pings, heartbeat messages, and other DHT queries, causing other peers to think we went offline and drop our connections. This handler intercepts VDF records and safely offloads them to a background CPU thread, preserving the node’s responsiveness. It is a critical stability pattern.

  4. The Network Immune System (Strikes): The network needs a way to defend itself against broken or malicious peers. If a peer repeatedly spams us with invalid data or incorrect VDFs, this handler tracks those failures over time. It implements a strike system that bans resource-wasting peers before they can exhaust our node’s capabilities, fill our disk with garbage, or perform a Denial of Service attack. A single failure might just be a network blip, but 3 failures in a minute shows intent or fatal bugs.

  5. Asynchronous Flow Management: Kademlia queries in libp2p do not block and return a value like a normal function. You say “find this record”, and libp2p says “okay, I will emit events later as I find things.” This handler is the system that listens for those “later” events. Because multiple queries might be running at once, the handler uses an ID mapping system to identify what query an event belongs to, and reconnects it to the original caller waiting for the data.


How It Works

The core of this file is the handle function. It takes a mutable reference to the NetworkEventLoop (allowing it to mutate network state) and the incoming kad::Event.

The logic is split into two major categories based on the event enum:

  • Handling Outbound Queries (data we asked for)
  • Handling Inbound Requests (data peers are pushing to us)

Handling Outbound Queries (Our Requests)

When we send a query out to the DHT (e.g., searching for a record), Kademlia doesn’t block and give us the answer immediately. Instead, it periodically updates us on its asynchronous progress via the kad::Event::OutboundQueryProgressed event.

-> See kinetic-network/src/event_loop/handlers/kademlia.rs — Lines 6 to 76

Because Kademlia only identifies these progress updates with a random, opaque QueryId, the handler first has to figure out why we are getting this update. It looks up the ID in the event_loop.query_id_to_name map. This tells us what type of query this was: a Quorum, a Get, or a Put. This mapping is essential for tying async network events back to our specific domain needs.

When a Record is Found (GetRecordOk::FoundRecord):

  • For Quorums: If the lookup reveals this is a Quorum query, we check if the returned byte array exactly matches our expected target_payload. If it does, we increment the match_count for that specific quorum. We ignore non-matching payloads entirely, as they don’t contribute to the consensus we are seeking. This is how Kinetic ensures multiple peers agree on the same value.
  • For Gets: If the lookup reveals this is a Get query, we simply add the returned byte array to the list of received_payloads for that query. This collects all variants the network offers so the caller can decide what to do with them. We don’t filter them here; we just aggregate them.
  • For Puts: If the lookup reveals a Put query, nothing is done in this arm, as put progress is handled by a different event variant below.

When a Query Finishes or Errors:

  • Kademlia eventually tells us it has exhausted the DHT search (FinishedWithNoAdditionalRecord) or encountered a hard error (Err).
  • The handler immediately removes the query ID from the tracking map (query_id_to_name.remove) to prevent memory leaks in the event loop over time.
  • It then calls the respective completion handler on the event_loop (e.g., handle_quorum_completion(name) or handle_get_completion(name)).
  • This triggers the core logic to evaluate if the quorum reached its required threshold, or if the standard get returned enough usable data to proceed.
  • Put completion events are similarly ignored in this specific block as they use a distinct match arm.

When a Put Completes (QueryResult::PutRecord):

  • When we publish data to the network, we track how many peers successfully stored it via the pending_puts state map.
  • For every response we get back from a peer, we decrement expected_responses.
  • If the peer’s response was an Ok, we increment the success_count.
  • Once expected_responses hits exactly zero, we know the query is fully complete across all targeted peers.
  • If success_count is greater than 0, we notify the original caller (via a one-shot responder channel) that the network publish succeeded.
  • If all peers failed to store it (success_count is 0), we send back a PublishError::AllFailed. We then clean up the pending state map to free memory.

Handling Inbound Put Requests (Peer Requests)

When another peer wants our node to store a record, they send an InboundRequest::PutRecord. This is the most dangerous part of the network, and this is where Kinetic enforces its strictest domain rules.

-> See kinetic-network/src/event_loop/handlers/kademlia.rs — Lines 77 to 190

Step 1: The Light Node Firewall

  • Before even looking at the payload data, we look at the sender’s identity.
  • If the source peer ID is currently tracked in our light_nodes set, we immediately reject the request.
  • We call swarm.disconnect_peer_id to sever the TCP connection.
  • We place them in the banned_peers cache with an expiration time 24 hours (86,400 seconds) in the future.
  • Light nodes cannot write, period. This is an active defense mechanism against misconfigured or malicious light clients. It saves us from processing junk data.

Step 2: Intercepting VDF Reveals

  • If the peer is a full node allowed to write, the handler attempts to parse the incoming raw bytes as a serde_json::Value.
  • It specifically looks for a vdf_proof field.
  • If it finds one, it attempts to strongly type the JSON into a kinetic_core::types::Reveal.

Because verifying this cryptographic reveal is expensive, we implement a strict offloading pattern:

  • We use crate::event_loop::utils::spawn_blocking to push the cryptographic verification to a background CPU thread. This frees the main async loop to continue processing network traffic.
  • We clone the necessary state (storage handles, VDF engine, current drand state) and move it into the background task so it can work independently.
  • Once the background thread finishes verifying the VDF, it sends a LoopbackCommand::CommitVerifiedRecord back to the main event loop via the loopback_tx channel.
  • The main event loop receives this command later and safely commits the record to the local store without ever having blocked its own execution. This is essential for preventing network stalling. Without this, a single bad VDF could bring down the node.

Step 3: Standard Storage and The Strike System

  • If the record isn’t a VDF reveal (or if it fails the JSON peek), we assume it’s standard DHT data.
  • We attempt to store it synchronously using our custom store_mut().put_record(...).

If our custom store rejects the record with a fatal error (Severity::Error), we apply a strike against the peer to prevent abuse:

  • We fetch their current strike count and the timestamp of their last error from the bad_vdf_counts map.
  • If their last error was more than 60 seconds ago, their slate is wiped clean, and we reset their count to 1.
  • If it was within the last 60 seconds, we increment their count.
  • If they hit 3 strikes within this rolling 60-second window, we drop the hammer.
  • We disconnect them immediately, and ban them in memory for 24 hours.
  • Critically, we persist that ban to the underlying storage database using DB_PREFIX_BANNED_PEER. This ensures that even if our node restarts, the malicious peer remains banned.
  • If the error from the store is non-fatal (like a malformed record that isn’t malicious), we simply log a debug message and ignore the record, assigning no strikes to the peer. We don’t punish peers for simple bugs, only for persistent failures.

Key Pieces

  • kad::Event::OutboundQueryProgressed -> See src/event_loop/handlers/kademlia.rs — Line 6 The primary envelope for DHT query updates from the libp2p behaviour. This tells us when Kademlia finds data, finishes searching, or successfully pushes data to a remote peer.

  • kad::QueryResult::GetRecord -> See src/event_loop/handlers/kademlia.rs — Lines 7 and 27 The specific result variant we match against to increment Quorum match counts or collect payloads for standard Get requests when data is successfully located on the network.

  • kad::Event::InboundRequest::PutRecord -> See src/event_loop/handlers/kademlia.rs — Line 77 Triggered when a remote peer asks our node to store a DHT record. This block is gated by Kinetic’s security, light-node checks, and VDF validation logic.

  • event_loop.query_id_to_name -> See src/event_loop/handlers/kademlia.rs — Line 9 The essential mapping dictionary that translates libp2p’s opaque QueryId back into our domain-specific QueryType (Quorum, Get, or Put), allowing us to route the data correctly.

  • VDF Loopback Offloading Pattern -> See src/event_loop/handlers/kademlia.rs — Lines 110 to 127 The architectural pattern of spawning a blocking task for VDF verification and using a loopback_tx channel to send the CommitVerifiedRecord verdict back to the main thread. This is vital for maintaining high network throughput.

  • bad_vdf_counts Strike Tracking -> See src/event_loop/handlers/kademlia.rs — Lines 144 to 156 The time-windowed tracking mechanism that ensures malicious or broken peers cannot spam the network with garbage data without facing a strict 24-hour ban.

  • serde_json::from_slice -> See src/event_loop/handlers/kademlia.rs — Line 97 The function used to peek into the raw bytes of an incoming record. It attempts to parse the bytes as generic JSON so we can check for specific Kinetic keys (vdf_proof) without knowing the exact type ahead of time.

  • tokio::task::spawn_blocking (via utils::spawn_blocking) -> See src/event_loop/handlers/kademlia.rs — Line 112 The Rust runtime tool used to push heavy CPU work off the async event loop. It creates a dedicated thread for the VDF math so the main loop can keep answering network pings.


How This Connects to the Rest of Kinetic

  • CROSS-CRATE: Parses raw bytes into kinetic_core::types::Reveal to intercept VDF proofs before they hit the standard storage pipeline.
  • CROSS-CRATE: Uses kinetic_core::error::Severity to determine if a rejected record is a fatal offense (warranting a strike) or a benign mistake.
  • CROSS-CRATE: Uses kinetic_core::constants::DB_PREFIX_BANNED_PEER to persist 24-hour bans to the physical storage database.
  • Operates directly on the state held in the core NetworkEventLoop (mutating pending_quorums, checking light_nodes, and sending messages to loopback_tx), acting as the primary state-mutation engine for all DHT events.

Quick Reference

  • Outbound Get Found: Update match_count (if Quorum) or push payload to received_payloads (if Get).
  • Outbound Get Finished: Trigger the completion handler in the main event loop to evaluate the success or failure of the query.
  • Outbound Put Result: Track successful peer stores; notify the calling function when expected responses reach zero.
  • Inbound Light Node Write: Immediate disconnection and 24-hour memory ban applied to the sender.
  • Inbound VDF Record: Parse JSON, offload to spawn_blocking, verify mathematically, and loopback the result to the main thread for commitment.
  • Inbound Invalid Record: 1 strike. 3 strikes in a rolling 60s window = 24-hour ban, disconnected, and persisted to disk.

Open Questions / Things to Revisit

  1. Hardcoded Ban Durations: The 24-hour ban (86,400 seconds) and the 60-second strike window are currently hardcoded magic numbers directly in the handler. These should eventually be moved to a configuration file or a centralized network constants module to allow operators to tune node strictness dynamically.
  2. JSON Peeking Performance: The handler parses the incoming raw bytes into an untyped serde_json::Value simply to check if the vdf_proof key exists. If it does, it deserializes it again into the strongly typed Reveal. This double-parsing is somewhat inefficient and could become a CPU bottleneck under extreme network load from many peers.
  3. Strike System Memory Exhaustion: Currently, the strike system tracks bad_vdf_counts in memory for every offending peer. If a resourced attacker uses thousands of distinct PeerIDs to send one bad record each, they could fill the bad_vdf_counts map in memory, potentially causing a memory exhaustion attack. We may need an LRU cache or a periodic cleanup sweep for this tracking map to ensure safety.
  4. VDF vs Generic Data: The way VDF reveals are special-cased right in the middle of the generic Kademlia handler feels slightly tightly coupled. In the future, this might be better handled by a middleware or interceptor pattern before it hits the raw DHT logic, keeping the DHT handler agnostic.
  5. Time Abstraction: The use of web_time is correct for WASM compatibility, but the mix of SystemTime for bans and Instant for the strike window could be confusing. SystemTime is vulnerable to system clock shifts, meaning a user changing their OS clock could potentially bypass a ban.

Proof of Work (S/Kademlia Sybil Protection)

Crate: kinetic-network Stage: 8 Reading time: 15 Depends on: 01_overview.md


What Is This?

This module implements the identity generation and validation mechanism for the Kinetic network, specifically using a concept inspired by S/Kademlia. In plain terms, before a node is allowed to participate in the network, it must prove that it expended a significant amount of computational effort to generate its cryptographic identity (PeerId). It does this by repeatedly generating keypairs until it finds one whose public key hashes to a value with a specific number of leading zero bits.

This proof of work (PoW) is not for consensus, block generation, or cryptocurrency mining. It is an anti-spam and anti-Sybil defense mechanism for the networking layer. To ensure identities cannot be mined once, hoarded, and reused forever, the proof of work is bound to a specific timeframe called an “epoch.” An epoch is derived directly from the network’s Drand-based clock (the “kyn”). When the epoch passes, the identity becomes invalid, and the node must mine a new one to remain in the network.


Why Kinetic Needs This

Kademlia is a efficient distributed hash table (DHT) used for peer-to-peer routing. It allows nodes to find each other quickly across a decentralized network. However, vanilla Kademlia is notoriously vulnerable to Sybil attacks and Eclipse attacks because identities are essentially free to create.

Because distance in Kademlia is calculated using the XOR metric between two PeerIds, an attacker could theoretically generate millions of random keypairs in a matter of seconds. If they generate enough keypairs, they will eventually find PeerIds that are “close” to a specific target node in the network. Once they have these strategically placed identities, they can flood the target node’s routing table. The target node, believing these are legitimate close peers, will drop honest peers in favor of the attacker’s peers, effectively isolating the target node from the honest network. This is known as an Eclipse attack, as the attacker surrounds the victim’s view of the network.

By enforcing a Proof of Work requirement on identity creation, Kinetic makes this attack computationally unfeasible and economically unviable. If an attacker wants to generate 1,000 identities near a specific node, and each identity takes 5 minutes of high-memory Argon2 computation to find, the cost of the attack becomes astronomical.

Furthermore, Kinetic introduces the concept of rolling epochs. An identity is only valid for a specific window of time (e.g., 12 hours). This forces the attacker to continuously spend massive hardware resources to maintain their malicious presence across multiple epochs, effectively destroying the financial incentive of a long-term Eclipse attack. If this pow.rs module did not exist, the network’s routing layer could be compromised by a single bad actor with a standard laptop, a simple python script, and ten minutes of spare time.


How It Works

The Proof of Work mechanism in Kinetic is built around the Argon2id hashing algorithm. This is a critical architectural choice that differentiates Kinetic from early blockchain networks. Unlike SHA-256 (used in Bitcoin), which can be easily accelerated using specialized ASICs or parallelized on GPUs, Argon2id is intentionally designed to be memory-hard. It requires significant memory bandwidth and sequential memory access patterns, making it resistant to specialized mining hardware. This keeps the playing field level for standard consumer CPUs and ensures that attackers cannot gain an asymmetric computational advantage by renting cheap, older ASICs.

The Choice of Argon2id

Argon2id is the winner of the Password Hashing Competition (PHC) and is the current industry standard for memory-hard hashing. Kinetic chooses Argon2id over Argon2i or Argon2d for identity mining:

  • Argon2d is faster and resistant to GPU cracking, but it is vulnerable to side-channel timing attacks.
  • Argon2i is safe from side-channel attacks but less resistant to GPU cracking.
  • Argon2id is a hybrid. It uses Argon2i for the first half of the first iteration (preventing side-channel attacks) and Argon2d for the rest (maximizing GPU resistance).

The specific parameters hardcoded into pow.rs are Params::new(16384, 1, 1, None):

  • Memory Cost (16384): This dictates that every single hash attempt requires exactly 16 Megabytes of RAM. If an attacker tries to run 1,000 parallel hash attempts to speed up their mining, they immediately need 16 Gigabytes of high-speed memory just for the hashing process.
  • Time Cost (1): A single iteration over the memory. This keeps the CPU time reasonable while enforcing the memory bandwidth bottleneck.
  • Parallelism (1): A single lane. This ensures the hashing is single-threaded, forcing attackers to scale horizontally (more machines) rather than vertically (wider GPUs).

1. The Staggered Epoch Mechanism

-> See: crates/kinetic-network/src/pow.rs — Lines 35 to 46

In a naive implementation, if an epoch lasts 12 hours, every single node in the network would find its identity invalidated at the exact same moment when the epoch clock clicks over. This would cause a massive, synchronized network collapse as every peer simultaneously drops offline to mine a new identity. To prevent this, Kinetic uses a “staggered” epoch approach.

#![allow(unused)]
fn main() {
// Illustration of the staggering math let offset = u64::from_be_bytes(offset_bytes) % EPOCH_KYNS; let current_epoch = (kyn + offset) / EPOCH_KYNS;
}

The get_staggered_epoch function calculates the current epoch differently for each individual peer. It takes the last 8 bytes of the peer’s PeerId, converts them into an integer, and uses it as an offset modulo the epoch length. This means every peer has their own personal 12-hour window, offset from the global kyn clock by a random amount determined by their public key. Because the offset is deterministic but evenly distributed, node churn becomes a smooth, continuous background process across the network rather than a synchronized event.

Example Scenario:

  • Node A has an offset of 0. For Node A, Epoch 1 starts at kyn 1440, Epoch 2 starts at kyn 2880.
  • Node B has an offset of 720. For Node B, Epoch 1 starts at kyn 720, Epoch 2 starts at kyn 2160.
  • The network is desynchronized in its identity turnover. At any given kyn, only a tiny fraction (1/1440th) of the network is forced to roll over their identities.

2. Mining an Identity (The Grinding Loop)

-> See: crates/kinetic-network/src/pow.rs — Lines 85 to 127

When a Kinetic node starts up, if its current identity has expired (or if it is a fresh node that doesn’t have one), it must mine a new one. The mine_sybil_keypair function runs a continuous, CPU-bound loop. Inside the loop, it performs the following steps:

  1. Generates a fresh Ed25519 keypair using random entropy.
  2. Extracts the public key and converts it into a PeerId.
  3. Calculates the staggered epoch for this specific PeerId using the current network kyn.
  4. Computes the Argon2id hash of the PeerId bytes combined with the epoch bytes.
#![allow(unused)]
fn main() {
// The PeerId bytes act as the password input // The epoch bytes act as the mandatory salt argon2.hash_password_into(peer_bytes, &epoch.to_be_bytes(), &mut output)
}
  1. Checks the number of leading zero bits in the resulting hash using the leading_zeros helper.

If the hash has enough leading zeros to match the network’s required difficulty, the loop exits successfully, and the keypair is saved as the node’s new identity. If it does not have enough leading zeros, the keypair is immediately discarded, and the loop repeats. Because Argon2id is intentionally slow, this loop blocks the thread for a significant amount of time, proving the node’s computational commitment.

3. Validating Incoming Peers & Epoch Overlap Handover

-> See: crates/kinetic-network/src/pow.rs — Lines 48 to 80

Validating a peer is not a simple boolean check of a single hash. It involves a critical overlap window. When is_valid_sybil_pow runs, it executes the following logic path:

  1. Calculate Target Epoch: Determine the peer’s personal staggered epoch for the current network kyn.
  2. Current Epoch Check: Run Argon2id on (PeerId, Current_Epoch). If the hash meets the difficulty, return true. The peer is up to date.
  3. Previous Epoch Check: If the current epoch check fails, the function checks if current_epoch > 0. If so, it runs Argon2id again on (PeerId, Current_Epoch - 1).
  4. Grace Period Approval: If the previous epoch’s hash meets the difficulty, the peer is still approved and true is returned.

Why check both? If the network only accepted the current epoch, then at the exact kyn a node’s epoch transitions from N to N+1, all of its network neighbors would immediately drop its connection. The node would be isolated from the network while it mines its identity for N+1. By checking both N and N-1, the network allows a node to use its N-1 identity for a full additional epoch (12 hours). The node will continue communicating with its neighbors using its N-1 identity while its background threads silently grind out the PoW for epoch N. Once the N identity is found, the node seamlessly swaps its identity without dropping a single connection.


Key Pieces

  • EPOCH_KYNS -> See: crates/kinetic-network/src/pow.rs — Line 8 A constant defining the length of an epoch in terms of Drand kyns. Currently set to 1440, which corresponds exactly to 12 hours assuming a standard 30-second kyn interval.

  • get_staggered_epoch -> See: crates/kinetic-network/src/pow.rs — Lines 35 to 46 The crucial function that converts the global kyn counter into a peer-specific epoch number. It extracts the last 8 bytes of the PeerId. If the PeerId is shorter than 8 bytes, it right-aligns the bytes into the buffer to prevent massive, unintended value shifts. It then calculates the modulo against EPOCH_KYNS to stagger identity expiration evenly and predictably across the entire network.

  • compute_pow_hash -> See: crates/kinetic-network/src/pow.rs — Lines 25 to 32 A helper function that orchestrates the Argon2id hashing process. It enforces the requirement that the PeerId bytes act as the password and the epoch bytes act as the minimum 8-byte salt required by the Argon2 specification.

  • leading_zeros -> See: crates/kinetic-network/src/pow.rs — Lines 11 to 22 Iterates over the output hash bytes to count the number of consecutive zero bits at the beginning of the array. It checks full bytes (adding 8 for every 0 byte) and then uses the built-in integer leading_zeros for the first non-zero byte. This bit-level precision is how the network dynamically measures the “difficulty” of a given hash.

  • is_valid_sybil_pow -> See: crates/kinetic-network/src/pow.rs — Lines 48 to 80 The validation gatekeeper. Every incoming peer must pass this function, or their connection will be immediately dropped. It uses strict, predefined Argon2 parameters (16MB memory, 1 iteration) to ensure validation is fast enough not to bottleneck the network, while keeping the overall mining process adequately slow.

  • mine_sybil_keypair -> See: crates/kinetic-network/src/pow.rs — Lines 85 to 127 The CPU-intensive grinding function that discovers a valid identity. It sets up the Argon2id parameters, captures a start timer using web_time::Instant::now(), and enters an infinite loop. Inside, it generates keys and hashes them until the condition is met. It includes safety checks to panic if called against kyn 0 outside of development mode, ensuring the node does not waste hours mining an identity bound to an uninitialized clock.


How This Connects to the Rest of Kinetic

This module acts as the strict cryptographic bouncer for the entire networking layer. It integrates deeply with libp2p’s connection handling and routing behaviors. Before a peer is allowed to be added to the DHT routing table, its Proof of Work is verified here.

CROSS-CRATE: Drand Kyns — defined and explained in the kinetic-core crate documentation. The current_kyn passed into these functions is derived from the verified Drand randomness beacon.

FORWARD DEPENDENCY: The kinetic-daemon crate (Stage 9) relies on mine_sybil_keypair during its startup sequence. It typically wraps this function in an asynchronous background task so that the node can start its HTTP server and answer health checks while its cryptographic identity is still being mined.


Quick Reference

  • PoW Algorithm: Argon2id (memory-hard, ASIC-resistant).
  • Validation Memory Cost: 16 MB (16384 KB).
  • Epoch Length: 12 hours (1440 kyns).
  • Grace Period: 1 previous epoch (an additional 12 hours).
  • Difficulty Metric: Contiguous leading zero bits in the resulting hash.
  • Dev Mode Bypass: If kinetic_core::config::is_dev_mode() returns true, all Proof of Work checks and mining operations are skipped, allowing for instantaneous local testing.

Open Questions / Things to Revisit

  • CPU Blocking Risk: The mine_sybil_keypair function is purely synchronous and CPU-bound. As noted by the inline WARNING comment, if this is called directly inside a tokio async context without utilizing spawn_blocking, it will starve the async executor and cause the node to lock up completely.
  • Fixed Argon Parameters: The Argon2id parameters (16MB memory, 1 iteration, 1 parallelism) are currently hardcoded directly into the functions. As hardware naturally improves over the years, these parameters may need to become dynamic or configurable via network consensus to maintain the intended computational difficulty threshold.
  • Background Mining Handover: Currently, if an identity expires, the node has to mine a new one. It is crucial that higher-level daemon logic anticipates this expiration and starts mining the next epoch’s keypair in the background before the current one expires, ensuring zero downtime for the node’s network presence.

Store Handlers and Verification Tests

Crate: kinetic-network Stage: 8 Reading time: 20 minutes Depends on: 16_store_core.md, 15_store_verification.md, kinetic-core, kinetic-types, kinetic-kid


What Is This?

This document provides an exhaustive explanation of the test suites that validate the behavior of the Kinetic network’s localized storage handlers and data verification logic.

Specifically, it covers the test cases found in handlers_tests.rs and verification_tests.rs within the store module of the kinetic-network crate.

These test suites are meticulously designed to simulate and assert the correct processing of network primitives under various conditions.

The primitives tested include:

  • Reveals (namespace claims)
  • Heartbeats (liveness signals)
  • Host routing updates
  • Decentralized identity (KID) authorizations

The tests simulate various edge cases, time-shifting scenarios, and active attack vectors.

They serve as the executable proof that the theoretical security boundaries designed into the Kinetic network actually function correctly in the compiled Rust binaries.


Why Kinetic Needs This

In a decentralized peer-to-peer network like Kinetic, nodes operate in an environment of zero inherent trust.

Nodes cannot blindly trust the incoming data they receive from other peers.

Every piece of data traversing the gossipsub network or the Kademlia DHT must be rigorously validated.

It must be scrutinized.

It must be bounds-checked before it is ever committed to local persistent storage.

If the handlers and verification logic have bugs, or if they behave unexpectedly under load, the network becomes immediately vulnerable to several critical, systemic failures:

  1. Spam and Resource Exhaustion (Denial of Service):

    • Without strict rate limiting on incoming reveals, an attacker could trivially flood the network.
    • They could send valid but excessive name registrations.
    • Because evaluating a reveal requires verifying a Verifiable Delay Function (VDF) proof—a computationally expensive operation—and then writing it to the Sled database, a flood would bloat the storage of all nodes.
    • It would force them to expend all available CPU cycles.
    • This would effectively halt the network.
    • The rate-limiting tests ensure this theoretical defense is practically enforced.
  2. Replay Attacks and State Reversion:

    • If heartbeats are not monotonic, a malicious peer could capture an old, valid heartbeat from the network traffic.
    • They could replay it hours or days later.
    • This could falsely keep a name alive that should have expired.
    • Or worse, it could overwrite a newer, legitimate routing state with an obsolete one.
    • The heartbeat monotonicity tests ensure time only flows forward.
  3. Routing Poisoning and Topology Disruption:

    • If host routing records are accepted without strict freshness checks relative to the current Drand pulse, the DHT becomes poisoned.
    • If malformed PeerId formats cause the Rust runtime to panic, an attacker could either misdirect network traffic to malicious nodes or perform a denial-of-service attack.
    • A single malformed identity key could crash every node it touches.
    • The tests prove that the network handles toxic data gracefully.
  4. Post-Quantum Cryptography Integration Failures:

    • The Kinetic network relies on ML-DSA (Module-Lattice-Based Digital Signature Algorithm) for future-proof security.
    • If the authorization logic fails to properly parse ML-DSA signatures, the post-quantum security guarantee of the network collapses.
    • If it fails to base64-decode the controller keys embedded within KID documents, it collapses.
    • The tests act as a live rehearsal of this complex cryptographic chain.

These test files are not just checking for typos.

They are the cryptographic and logical firewall for the node’s local database.

They ensure that Saif’s architectural rules—such as the maximum age of a pulse, the sliding window of rate limits, and the exact byte-layout of a signed identity payload—are correctly enforced in the living codebase.


How It Works

The test suites break down into two main domains:

  • Testing the internal state mutations of the store (handlers_tests.rs)
  • Testing the stateless cryptographic and logical boundary checks (verification_tests.rs).

We will explore both in deep, step-by-step detail.

1. Store Instantiation and Mocking (The Sandbox)

Before any tests can run that mutate state, the test environment must synthesize a fully functioning KineticRecordStore.

This involves wiring together several heavy dependencies that normally run in a live node.

They must be kept lightweight and isolated enough for a unit test.

-> See: crates/kinetic-network/src/store/handlers_tests.rs — Lines 30 to 51

The setup_store function acts as the central dependency injector for the test suite.

It constructs the sandbox with the following steps:

  • Ephemeral Storage:
    • It creates a temporary directory using tempfile::tempdir().
    • The SledStorage database is initialized here.
    • This is crucial because it ensures the database starts empty for every test.
    • It is automatically deleted from the operating system when the test variable goes out of scope.
    • No state bleeds between tests.
  • Identity Mocking:
    • It generates a random ed25519 PeerId.
    • This acts as the core identity of the mock store.
    • In a real node, this comes from the local keystore.
  • VDF Engine Injection:
    • It instantiates a ChiaVdfEngine.
    • Even though tests might use dummy proofs, the store’s constructor requires an object that implements the VdfEngine trait.
  • Initial State Seeding:
    • It sets the initial Drand pulse (the drand_kyn) to a baseline integer of 100.
    • This provides a known starting point for time-relative tests.
  • Limit Configuration:
    • It configures the maximum reveals allowed per window (passed in as an argument, e.g., 5).

This function returns both the initialized KineticRecordStore and an Arc pointer to the underlying storage engine.

This allows tests to manipulate the store exactly as the network handlers would.

2. Testing Rate Limiting (The Sliding Time Window)

To prevent network flooding, the store limits how many reveals a specific namespace can process within a rolling time window.

For example, a maximum of 5 reveals per rolling hour.

-> See: crates/kinetic-network/src/store/handlers_tests.rs — Lines 53 to 111

The test test_rate_limiting verifies the logic of this sliding window algorithm.

It bypasses the network layer and directly manipulates the store’s internal memory structures:

  • The store tracks reveal timestamps in a std::collections::VecDeque.
  • This is a double-ended queue.
  • It is mapped to each namespace string inside a hash map (accepted_reveals_timestamps).
  • The test creates a dummy namespace domain0.kinetic.
  • It artificially injects 5 timestamps representing recent reveals.
  • This maxes out the theoretical limit.
  • To simulate the passage of time without actually pausing the test thread for an hour, the test manually clears the queue.
  • It then manually injects specific historical timestamps.
  • It calculates now = web_time::Instant::now().
  • It injects one timestamp from now - 4000 seconds.
  • This is older than the 3600-second (1-hour) limit, so it should be considered expired.
  • It injects a second timestamp from now - 3000 seconds.
  • This is within the 1-hour window, so it is active.
  • It then injects 5 more timestamps that occurred exactly at now.
  • The core logic is then executed: the code iterates over the front of the VecDeque.
  • If the time elapsed since a timestamp is greater than 3600 seconds, it uses deque.pop_front() to discard it.
  • Finally, the assertion checks that after pruning, exactly 6 timestamps remain in the queue.
  • The 6 timestamps are the 1 active historical one from 3000 seconds ago, plus the 5 recent ones at now.
  • The 4000-second old timestamp is gone.
  • This confirms that expired records are evicted correctly.
  • The sliding window moves forward.
  • The node frees up memory over time.

3. Testing Heartbeat Monotonicity (Defeating Replay Attacks)

Heartbeats are how nodes prove they are currently online and actively participating in maintaining a name.

They are cryptographically bound to a specific Drand pulse (drand_kyn).

Time in Kinetic is measured in these pulses.

-> See: crates/kinetic-network/src/store/handlers_tests.rs — Lines 113 to 151

The test_heartbeat_monotonicity test ensures that a node will reject any heartbeat that attempts to rewind time.

This is a classic replay attack.

The test proceeds as follows:

  • First, it seeds the store with a dummy reveal for the namespace test.kinetic.
  • It manually generates a real ML-DSA signature keypair.
  • It attaches the public key bytes to the reveal.
  • It then sets the existing state: it inserts a record into last_heartbeats_by_name.
  • This indicates that the most recent valid heartbeat for this name occurred at Drand pulse 200.
  • Next, it constructs the malicious payload: a new Heartbeat struct for the same name.
  • It sets its latest_drand_kyn to 49.
  • Because 49 is less than 200, this heartbeat is in the past relative to what the node already knows.
  • To ensure the test evaluates the temporal logic and doesn’t just fail a signature check, the test actively signs this stale heartbeat.
  • It uses the correct ML-DSA private key, producing a valid cryptographic signature over a stale payload.
  • The heartbeat is passed to store.handle_heartbeat().
  • The handler analyzes the payload.
  • It looks up the existing pulse of 200.
  • It compares it to the incoming pulse of 49.
  • It immediately halts.
  • The test asserts that the result is an error specifically matching KineticStoreError::StaleHeartbeat.
  • The takeaway is absolute: a valid signature does not save a temporally stale payload.

4. Testing Host Routing Freshness (Topological Expiry)

Routing records in the DHT tell peers how to physically connect to a specific service.

Like heartbeats, they decay and must be continually refreshed by the host.

-> See: crates/kinetic-network/src/store/verification_tests.rs — Lines 8 to 28

The test_host_routing_freshness function rigorously checks the maximum age constraint of routing data:

  • It assumes a hypothetical current Drand round of 1000.
  • It creates a HostRoutingRecord and artificially sets its drand_kyn to 850.
  • This means the record is claiming to be from 150 pulses ago.
  • The Kinetic architecture enforces a strict maximum age for routing records (typically 100 rounds).
  • When the verify_host_routing_record function analyzes this record, it performs subtraction.
  • 1000 minus 850 equals 150.
  • Since 150 is greater than 100, the record is flagged as unacceptably old.
  • The assertion confirms the error.
  • Notably, the test expects it to return KineticStoreError::InvalidHostRouteSignature.
  • While this might seem misnamed initially, architecturally it implies that the freshness check is the very first gate inside the signature verification wrapper.
  • By failing the freshness check, the signature is deemed structurally invalid for the current network epoch.
  • This bypasses the expensive cryptographic math entirely.

5. Testing Peer ID Format Safeties (Defensive Parsing)

When working with distributed hash tables, nodes constantly receive raw byte arrays over the wire.

These arrays claim to be valid PeerId representations.

If a node panics (crashes) while trying to parse unexpectedly malformed bytes, a single malicious packet can take down the node.

-> See: crates/kinetic-network/src/store/verification_tests.rs — Lines 30 to 53

The test_peer_id_extraction_safeguard ensures robust, panic-free error handling when confronting invalid identity data:

  • It uses the libp2p::multihash module to deliberately construct a multihash using the SHA2-256 algorithm.
  • The Kinetic network specifically expects identity multihashes (typically ed25519 public keys embedded directly in the hash).
  • It embeds this intentionally incompatible multihash format into a HostRoutingRecord.
  • When the verify_host_routing_record logic executes, it attempts to extract the public key from the string representation of this PeerId.
  • The internal parsing logic recognizes that a SHA2-256 multihash does not contain an inline public key.
  • The critical part: the test asserts that the function gracefully returns a KineticStoreError::InvalidPublicKey enum variant.
  • It does not call .unwrap() on a None value.
  • A panic would trigger a thread panic and a node crash.
  • This is a vital demonstration of defensive programming at the network edge.

6. Testing ML-DSA Authorized KID Validation (The Post-Quantum Chain)

This is the most complex and critical test in the suite.

It ensures that the post-quantum signature schemes correctly bind decentralized identities (KIDs) to network namespaces.

-> See: crates/kinetic-network/src/store/verification_tests.rs — Lines 55 to 148

The test_mldsa_authorized_kid_validation test constructs an entire cryptographic chain of trust from scratch:

  • Key Generation:
    • It uses the ml_dsa crate to generate a fresh, correct ML-DSA-65 keypair.
  • DID Construction:
    • It simulates the identity generation process.
    • It takes the public key bytes.
    • It hashes them using SHA-256.
    • It converts the hash to a hex string.
    • It prepends the Kinetic DID prefix (did:kinetic:) to formulate a valid Decentralized Identifier string.
  • Document Creation:
    • It builds a comprehensive KidDocument struct.
    • Crucially, it populates the controller_keys vector.
    • It sets the key type to “MlDsa65”.
    • It provides the Base64-URL-encoded version of the ML-DSA public key.
  • Inner Signature:
    • The DID document itself requires a self-signature to prove ownership.
    • The test calls .sign() on the document using the private ML-DSA key.
  • Authorization Wrapper:
    • The signed DID document is then packaged inside an AuthorizedKid struct.
    • It targets a specific network name (test.kinetic).
    • This outer wrapper is then signed again by the ML-DSA key.
    • The signature is over a specific signable byte payload that includes the NETWORK_ID.
    • This inclusion prevents cross-network replay attacks.
  • Validation Execution:
    • It passes this nested, doubly-signed payload into the verify_authorized_kid function.
    • It also passes a mock existing Kademlia record.
  • The Assertion:
    • The assertion ensures that the result is either Ok or, at the very least, does not fail with a KineticStoreError::InvalidKidSignature.
    • This proves that the multi-layered ML-DSA signature verification interoperate correctly.
    • The parsing logic interoperates correctly.
    • Hex matching interoperates correctly.
    • Base64 decoding interoperates correctly without throwing false negatives.

Key Pieces

  • setup_store

    • What it does: Bootstraps an ephemeral environment with a temporary database, a mock peer identity, and a VDF engine.
    • Where it lives: crates/kinetic-network/src/store/handlers_tests.rs — Lines 30 to 51
    • Why it matters: Provides a clean, isolated state for state-mutating tests without polluting the developer’s local filesystem or causing test crosstalk.
  • test_rate_limiting

    • What it does: Verifies the VecDeque sliding window logic for incoming reveals, checking expiration thresholds.
    • Where it lives: crates/kinetic-network/src/store/handlers_tests.rs — Lines 53 to 111
    • Why it matters: Proves the node has an active defense against malicious peers attempting to overwhelm the local Sled database with rapid-fire reveal spam.
  • test_heartbeat_monotonicity

    • What it does: Rejects heartbeats containing a drand_kyn pulse that is lower than the previously stored pulse.
    • Where it lives: crates/kinetic-network/src/store/handlers_tests.rs — Lines 113 to 151
    • Why it matters: Defends the network against temporal replay attacks where old, validly-signed heartbeats are rebroadcast to manipulate the active state of a namespace.
  • test_host_routing_freshness

    • What it does: Ensures routing records cannot exceed a maximum age threshold relative to the current live Drand round.
    • Where it lives: crates/kinetic-network/src/store/verification_tests.rs — Lines 8 to 28
    • Why it matters: Keeps the network topology accurate and responsive by enforcing strict TTLs (Time To Live) on peer locations in the DHT.
  • test_peer_id_extraction_safeguard

    • What it does: Passes an unsupported multihash format (SHA-256 instead of an identity hash) as a Peer ID to verify the parser fails safely.
    • Where it lives: crates/kinetic-network/src/store/verification_tests.rs — Lines 30 to 53
    • Why it matters: Protects the node from panic-based denial-of-service attacks triggered by maliciously crafted, un-parsable routing payloads.
  • test_mldsa_authorized_kid_validation

    • What it does: Generates and validates a complete, accurate ML-DSA signed KID document and nested authorization payload.
    • Where it lives: crates/kinetic-network/src/store/verification_tests.rs — Lines 55 to 148
    • Why it matters: Unarguably proves that the integration between the complex post-quantum signature schemes and the decentralized identity layer works flawlessly in the compiled binary.

How This Connects to the Rest of Kinetic

  • CROSS-CRATE: The tests instantiate and utilize Reveal, VdfProof, Heartbeat, and HostRoutingRecord structs. These are defined and explained in detail within kinetic-core and kinetic-types.
  • CROSS-CRATE: The identity validation relies on KineticDid and KidDocument, which are the foundational primitives defined and explained in kinetic-kid.
  • Storage Layer Interface: The tests utilize SledStorage imported from kinetic-storage. This serves as the underlying, persistent state backend for the setup_store initialization, demonstrating how the network layer talks to the disk.
  • Drand Pulses as Clocks: The drand_kyn acts as the decentralized, universal clock. The tests demonstrate how the network uses these integer pulses to measure elapsed time, evaluate freshness, and define causality, ignoring the local hardware system clocks of individual machines.

Quick Reference

  • Rate Limiting Mechanism: Managed via a std::collections::VecDeque of timestamps. Expired timestamps are sequentially popped from the front of the queue to maintain the sliding window.
  • Temporal Monotonicity: An incoming pulse must always be greater than (>) the existing pulse to be accepted by the heartbeat handler.
  • Freshness Boundaries: The calculation current_drand_round - record.drand_kyn must yield a result less than the network’s allowed maximum age constant.
  • Safe Payload Parsing: Converting untrusted network strings into libp2p PeerId structs requires explicit error mapping to catch unsupported multihash formats safely, preventing unwrap() panics.
  • ML-DSA Signatures: ML-DSA keypairs are utilized to sign both the inner DID Document and the outer wrapping AuthorizedKid payload, creating a nested proof of authorization.
  • Wasm-Compatible Time Modules: The web_time::Instant and web_time::Duration modules are used instead of the standard std::time to guarantee that time calculations remain compatible if the node is compiled to WebAssembly.

Open Questions / Things to Revisit

  1. Error Type Clarity in Freshness Checks: In test_host_routing_freshness, providing a structurally stale pulse causes the assertion to expect a KineticStoreError::InvalidHostRouteSignature.

    • Architecturally, one might expect a distinct error variant like StaleRoutingRecord rather than overloading and falling back to a generic “invalid signature” error.
    • Saif should review if returning a signature error for a pure age violation obscures debugging logs in production.
  2. Hardcoded Genesis Assumptions: The setup_store function hardcodes the initial Drand pulse to 100.

    • If the global network configuration ever changes to require a higher genesis pulse (e.g., 500000), these tests might instantly fail due to violating genesis bounds constraints.
    • It may be wise to link this initial integer to a constant derived from the core crate.
  3. ML-DSA Test Code Density: The test_mldsa_authorized_kid_validation test is dense and procedural.

    • It manually implements the hashing and base64 encoding steps line-by-line.
    • Ideally, these steps belong inside a fluent builder pattern within the kinetic-kid crate itself, which would reduce the boilerplate required in network-level testing.
  4. Time Mocks vs. web_time: While web_time is used for Wasm compatibility, the tests still rely on sleeping or manually injecting historical timestamps.

    • A true deterministic time-mocking library might make the rate-limiting tests even more robust and isolated from the host machine’s execution speed.

Network Client Commands and Types

Crate: kinetic-network Stage: 8 Reading time: 30 minutes Depends on: 01_overview.md, kinetic-types, kinetic-core


What Is This?

This documentation provides an exhaustive breakdown of the communication primitives, configuration structures, and type definitions that form the crucial bridge between Kinetic’s higher-level application logic (such as the daemon or REST API) and the low-level, background network event loop.

Specifically, we are examining three tightly coupled files within the client module:

  • command.rs — This file defines the definitive vocabulary of commands that the network event loop is capable of understanding and executing.
  • types.rs — This file holds the configuration definitions required for node instantiation, as well as the specialized error reporting types.
  • mod.rs — This is the module declaration file that ties these components together and re-exports them for clean external access.

At the absolute center of this architecture is the Command enum. In a peer-to-peer network built on libp2p, the network state is concurrent but must be mutated safely. You cannot have fifty different API endpoints, daemon threads, and background workers all attempting to lock the Distributed Hash Table (DHT) and write to it simultaneously. Doing so would either cause massive lock contention—drastically slowing down the network—or it would tear the internal network state, leading to routing failures.

To solve this, Kinetic employs an actor model approach. The network swarm runs inside a single, dedicated asynchronous event loop that constantly polls for incoming network events. When external components need the network to do something—like publishing a record or resolving a domain—they do not interact with the network directly. Instead, they construct a Command message and send it over an asynchronous channel to the event loop. The event loop processes these commands sequentially, ensuring safe state mutations without the need for complex Mutex locks spread over the entire network swarm.

The types.rs file complements this by defining the configurations used to boot the node. Booting a peer-to-peer node is drastically different from starting a standard HTTP web server; it needs to know about bootstrap nodes, transports (TCP vs QUIC), consensus parameters (like PoW and Drand), and rate limits. The NetworkConfig struct captures all of these complex requirements in one place.


Why Kinetic Needs This

Kinetic needs these specific types for three fundamental reasons: architectural thread safety, deterministic asynchronous execution, and network topology flexibility.

1. Architectural Thread Safety and Polling Uptime

Consider thread safety and the libp2p Swarm. The Swarm is an intricate state machine that drives the entire network stack. It requires a dedicated executor to constantly poll it. This polling process manages incoming connections, multiplexes streams, and keeps protocols alive through periodic pinging and routing table updates. If another thread locks the Swarm to publish something, the polling stops.

While polling is stopped, the network might drop active connections because it missed a keep-alive window, or peers might consider the local node unresponsive and drop it from their routing tables entirely. By using a Command channel, the Swarm’s primary thread is never blocked by external callers; it simply reads from the queue when it is ready, ensuring 100% uptime for critical network polling.

2. Deterministic Asynchronous Execution and RPCs

When the daemon asks the network to resolve a domain, it needs to know precisely when that resolution is complete and exactly what the result is. Because the network operates asynchronously and peer responses take unpredictable amounts of time, we need a reliable mechanism for the background thread to send data back to the calling thread.

This is achieved by embedding a tokio::sync::oneshot::Sender inside almost every Command variant. It effectively creates a dynamic RPC (Remote Procedure Call) mechanism across local threads. Without this return channel, the daemon would have to fire off a command and blindly hope it worked, which is unacceptable for consensus-critical operations where failure must be handled immediately.

3. Network Topology Flexibility

Not all Kinetic nodes are created equal, nor should they be. The NetworkConfig and NetworkMode structures allow Kinetic to adapt its behavior to its hardware environment. A validator running on a dedicated server with high bandwidth needs to be a FullNode, storing DHT records and actively routing Kademlia traffic for others. Conversely, a lightweight mobile wallet only needs to be a LightNode, querying the network and broadcasting transactions without taking on the burden of storing the entire DHT or routing external traffic. These configuration types allow the exact same codebase to run in both capacities simply by toggling a flag during instantiation.


How It Works

The architecture relies on pairing one-way message passing with dedicated return channels. This is a standard Rust async practice, but it is applied here specifically to orchestrate Kinetic’s complex peer-to-peer operations.

The Message Passing Lifecycle

When a developer calls a method on the network client to perform an action, the following rigorous sequence occurs under the hood:

  1. Channel Creation: The caller creates a one-time use channel using tokio::sync::oneshot::channel(). This yields two halves: a Sender (to transmit the result from the event loop) and a Receiver (for the caller to await the result).
  2. Command Construction: The caller builds the appropriate variant of the Command enum. It embeds the necessary request data (like a domain name or payload bytes) and attaches the Sender half of the channel inside the struct.
  3. Dispatch: The caller sends this constructed Command into the main MPSC (Multi-Producer, Single-Consumer) channel that feeds the network event loop. The multiple producers are the API endpoints and workers; the single consumer is the network event loop.
  4. Suspension: The calling async task then .awaits on the Receiver. This pauses the calling task entirely, yielding CPU time back to the tokio runtime without blocking the system thread. It will wait here until the network replies.
  5. Execution: On the other side of the boundary, the network event loop pulls the Command off the queue during its next polling cycle. It interprets the command, executes the corresponding libp2p operation (e.g., a Kademlia publish or a Proxy request), and eventually obtains an asynchronous result from the network peers.
  6. Fulfillment: The event loop transmits the result back through the Sender. If the caller has dropped the Receiver (e.g., they timed out or their task was cancelled), the oneshot::Sender detects this and safely discards the result without crashing or leaking memory.
  7. Resumption: The original calling task wakes up, receives the result from the Receiver, and continues its execution logic, confident in the network’s response.

-> See: crates/kinetic-network/src/client/command.rs — Lines 8 to 101

Memory Efficiency in Commands (Arc<str>)

You will notice that many commands use Arc<str> (an Atomically Reference Counted string slice) rather than a standard String. In a busy network environment, domain names and Gossipsub topics are cloned repeatedly. They are passed from the REST API, to the client wrapper, to the command queue, to the event loop, and finally deep into the internal libp2p protocols.

If we used a standard String, we would be constantly re-allocating memory on the heap and copying bytes every single time the domain name moved across a functional boundary. By using Arc<str>, multiple threads can cheaply share the exact same string data in memory. Cloning an Arc merely increments a counter, which is orders of magnitude faster and uses significantly less memory than copying string bytes.

Error Efficiency (Cow<'static, str>)

In types.rs, the ProxyError::Other variant uses Cow<'static, str>. Cow stands for Clone-On-Write. This is another critical memory optimization in Rust. When an error occurs, it is often a hardcoded static string (e.g., "Unexpected protocol failure"), which lives in the binary’s read-only memory space. Sometimes, however, it is a dynamically generated string (e.g., format!("Failed to parse peer {}", peer_id)). Cow allows the error to store either a zero-cost reference to the static string or an owned dynamic string, avoiding unnecessary heap allocations for the static cases while maintaining flexibility.

Re-exports in mod.rs

The mod.rs file contains statements like pub use self::command::*;. This is a Rust re-export. It takes the contents of the command.rs file and flattens them into the client namespace. This means that external crates can import Command via kinetic_network::client::Command instead of having to dig into kinetic_network::client::command::Command. This creates a much cleaner public API surface and prevents users from having to memorize deep file hierarchies.


Key Pieces

1. The Command Enum (command.rs)

This enum is the definitive list of actions the network event loop can perform on behalf of the application. Every variant represents a distinct network operation.

-> See: crates/kinetic-network/src/client/command.rs — Lines 8 to 101

DHT and Resolution Commands:

  • PublishRedundant: Instructs the network to store a record in the Kademlia DHT. Kinetic ensures this data is published redundantly to multiple peers so it survives node churn (peers going offline). It returns a Result<(), PublishError>.
  • ResolveRedundant: Queries the DHT to find a domain or record, returning the serialized payload bytes upon success.
  • VerifyQuorum: A critical security feature in Kinetic. Because the DHT is untrusted, a single malicious node might lie about a domain’s resolution to censor a user. This command queries a quorum (multiple independent nodes) to ensure they all report the exact same data before trusting it.
  • PublishHeartbeat: Specific to Kinetic’s domain system. It publishes a lightweight proof-of-ownership heartbeat to the network to ensure a registered name doesn’t expire and get purged by peers.

Gossipsub and PubSub Commands:

  • SubscribeGossip / BroadcastGossip: Used for real-time, topic-based message dissemination. When a new block is mined or a new transaction is created, it is broadcasted over Gossipsub to rapidly reach all connected peers simultaneously.
  • ReportGossipValidation: In libp2p, when a peer sends you a Gossipsub message, you have a strict, short time window to validate it. This command allows the application layer to tell the network layer whether the message was valid. If the application rejects it, the network layer will immediately penalize the peer who sent it, protecting the entire network from spam and malicious actors.

Direct P2P Proxy Commands:

  • SendProxyRequest: Sends a direct RPC-style request to a specific remote peer ID, bypassing the DHT entirely. This is used for targeted node-to-node communication, like querying a specific validator for its current state.
  • SendProxyResponse: When the local node receives a proxy request via the network, it processes it locally and uses this command to send the answer back. It uses a ResponseChannel to tie the response back to the exact incoming request stream.

Lifecycle and Diagnostics:

  • GetCurrentDrandKyn: Retrieves the latest randomness value (kyn) synchronized from the network.
  • Bootstrap: Manually forces the node to reach out to its configured bootstrap peers to integrate into the network topology. This is typically called shortly after startup to join the swarm.
  • GetNetworkStatus: Returns diagnostic JSON containing the node’s current connections, active protocols, and routing table size. This is vital for the daemon’s status API.

2. NetworkMode (types.rs)

This enum determines the node’s responsibility level within the broader network topology:

-> See: crates/kinetic-network/src/client/types.rs — Lines 31 to 37

  • FullNode: Fully participates in the network. It stores DHT records for other peers and helps route Kademlia traffic. This is the expected mode for active validators and infrastructure providers who need to actively support the network’s health.
  • LightNode: Operates in client-only mode. It will issue requests, resolve domains, and broadcast transactions, but it refuses to store DHT records on behalf of others. This is ideal for resource-constrained environments like mobile apps or background services that just need to sync state.

3. NetworkConfig (types.rs)

This struct holds all parameters required to build the network swarm.

-> See: crates/kinetic-network/src/client/types.rs — Lines 40 to 68

Notable configuration fields include:

  • listen_addrs / quic_listen_addrs: The multiaddresses where the node accepts incoming connections. Kinetic supports both TCP (reliable, traditional) and QUIC (UDP-based, faster handshakes, no head-of-line blocking). Supporting both maximizes connectivity across diverse and restrictive network conditions.
  • bootstrap_nodes: The entry points into the network. Without these, a new node cannot discover the DHT.
  • seed_domain: Pre-known domains used for DNS tree resolution during startup to locate root services.
  • enable_mdns: Enables local network peer discovery via Multicast DNS. This is useful for testing or LAN-based node clusters where internet traversal is unnecessary.
  • initial_drand_kyn: The starting randomness beacon value needed to verify Verifiable Delay Functions (VDFs) immediately upon startup before syncing with the network.
  • disable_pow: A flag used to bypass Proof of Work checks. This should only be true during integration testing to prevent automated tests from hanging while mining hashes.
  • max_reveals_per_hour: A strict rate-limiting threshold that dictates how many reveals a node will accept into its cache, protecting the node against memory exhaustion attacks from malicious peers.
  • lru_cache_size: The maximum number of items in the Least Recently Used (LRU) in-memory cache. It uses std::num::NonZeroUsize, a Rust optimization that ensures the value is never zero. Because it can never be zero, the Rust compiler can use 0 to safely represent the None variant, meaning Option<NonZeroUsize> takes up exactly the same amount of memory footprint as a regular usize.
  • test_mode: Enables testing configurations. This disables automatic UPnP (Universal Plug and Play) port forwarding and shortens timeouts so that unit tests run fast and isolated without attempting to mutate the local router’s firewall rules.

4. ProxyError (types.rs)

When executing direct SendProxyRequest commands, network reality dictates that things will occasionally fail. This enum categorizes those failures so the caller can react appropriately:

-> See: crates/kinetic-network/src/client/types.rs — Lines 6 to 26

  • Timeout: The peer did not respond within the allocated timeframe. The caller might want to retry.
  • Offline: The requested peer is not currently connected to the swarm, and Kademlia routing could not locate them on the network.
  • ConnectionClosed: The underlying TCP or QUIC stream was abruptly severed during the transfer. This often indicates poor network conditions or an intentional disconnect by the peer.
  • UnsupportedProtocols: The remote peer does not speak the requested proxy protocol, likely due to running an outdated Kinetic version.
  • ChannelClosed: The internal MPSC channel connecting the client to the event loop collapsed. This is a fatal error indicating the network event loop has crashed and cannot process further commands.
  • Other: The catch-all variant utilizing Cow<'static, str>, as explained earlier, for miscellaneous or unexpected failures that do not fit into the other categories.

How This Connects to the Rest of Kinetic

  • The Event Loop (kinetic-network): The Command enum acts as the sole input vocabulary for the network’s main event loop. The event loop is the exclusive consumer of the channel carrying these commands.
  • The Client Interface (kinetic-network): The Client struct (which we will cover in the core client documentation) provides a clean, async API for the rest of the application. Under the hood, every method on Client is simply constructing one of these Command variants and sending it. The caller never has to touch the Command enum directly.
  • Proxy Types (kinetic-types): The ProxyRequest and ProxyResponse structures embedded inside Command::SendProxyRequest are defined centrally in the kinetic-types crate, ensuring a unified RPC schema across the entire application ecosystem.
  • CROSS-CRATE: PublishError and ResolutionError — defined in kinetic-core. These specialized error types are passed back through the oneshot::Sender when DHT operations fail.
  • CROSS-CRATE: NetworkClientError — defined in kinetic-core. This is the broader error wrapper used for overarching network failures like channel collapses or unexpected libp2p behaviors.

Quick Reference

  • To mutate or query network state: You must dispatch a Command to the event loop via the MPSC channel.
  • To receive an asynchronous result: Utilize the responder: tokio::sync::oneshot::Sender<T> embedded in the command variant. The event loop will fulfill it upon completion.
  • To configure node behavior: Construct a NetworkConfig with the appropriate listen addresses, rate limits, and bootstrap peers before instantiating the Swarm.
  • To minimize resource usage: Run the node in NetworkMode::LightNode so it doesn’t store external DHT data or route external traffic.
  • String optimization: Commands utilize Arc<str> to enable cheap, zero-copy cloning of domain names and topics across threads.
  • Memory layout optimization: Caching settings utilize std::num::NonZeroUsize to optimize memory layout for optional values.
  • String Error Optimization: ProxyError uses Cow<'static, str> to allow both static string literals and dynamic strings without enforcing heap allocations for every single error creation.

Open Questions / Things to Revisit

  • Dynamic Rate Limiting: The max_reveals_per_hour field is currently a single global setting in NetworkConfig. If network spam becomes sophisticated, Kinetic may need to move toward peer-specific or IP-specific rate limits to penalize bad actors without impacting legitimate traffic.
  • Drand Kyn Overrides: GetCurrentDrandKyn allows fetching the current kyn, but there is no command to forcefully update or sync it from the client side. The event loop manages this internally, but we should investigate if manual overrides would be beneficial for localized integration testing where waiting for network sync is undesirable.
  • Gossip Validation Tracking: ReportGossipValidation requires the application layer to retain the MessageId and the propagation_source. The application must ensure it tracks these precisely when receiving gossip messages; otherwise, it will be unable to validate them, causing the local network layer to silently penalize the node for failing to respond within the narrow validation window.

Event Loop Handlers: Gossip, CDN, and Proxy

Crate: kinetic-network Stage: 8 Reading time: 30 minutes Depends on: 18_network_event_loop.md, 16_kademlia_routing.md


What Is This?

These files (gossipsub.rs, cdn.rs, proxy.rs, and their parent mod.rs) are specialized sub-handlers for the NetworkEventLoop. Rather than packing all the event handling logic for Gossipsub, CDN, and Proxy into one massive, unreadable match block inside the main loop, Kinetic splits them into separate files.

These handlers process the actual decentralized network events that come off the libp2p swarm. They dictate exactly how the Kinetic node reacts to incoming broadcast messages, direct name record requests, and proxy traffic. The main event loop is simply a dispatcher; these handlers hold the actual business logic for how the node behaves when data arrives over the wire.

Think of the NetworkEventLoop as the mail sorting room of the node. It looks at the envelope, determines the protocol, and hands it off. These sub-handlers are the specific departments (Gossip, CDN, Proxy) that actually open the mail, read the payloads, verify the signatures, and route the final data to the application layer. By separating them, Kinetic ensures that the central event loop remains a fast, non-blocking state machine.


Why Kinetic Needs This

A decentralized network is loud, chaotic, and often actively hostile. If the main event loop handled every single message natively in one place, three major architectural failures would occur:

  1. The Loop Stalls (The Async Blocking Problem): Rust’s tokio async runtime relies on cooperative multitasking. If a task takes too long, no other task can run. Cryptographic verification is inherently slow because it requires heavy CPU math. If the main async loop stops to verify a Drand signature or check a Governance root key, it cannot yield back to the executor. As a result, the node cannot respond to other peers’ ping messages or keep TCP connections alive. If the loop stalls, libp2p assumes the node is dead and drops the connections. We must move math off the main thread.

  2. Resource Exhaustion (The DDoS Threat): A malicious peer could flood the node with garbage gossip messages on the governance topic. If the node eagerly tries to process every single one by spawning a task or doing math, it will run out of memory or exhaust its thread pool. There must be an aggressive rate-limit applied before the message is even parsed.

  3. Spaghetti Code and Unmaintainable State: The NetworkEventLoop already manages Kademlia state, Swarm lifecycle events, ping, identify protocols, and local application channels. If it also had to manually construct proxy responses, parse name records, and manage gossip validation logic, the file would be thousands of lines long and impossible to maintain. Separation of concerns is a hard requirement for a maintainable network stack.

We need isolated handlers that can:

  • rate-limit incoming gossip using semaphores to protect the node from malicious spam.
  • Offload CPU-heavy cryptographic work to background thread pools to protect the async runtime from stalling.
  • Process CDN and Proxy requests cleanly, acting as independent micro-services within the broader network layer.
  • Translate low-level libp2p network errors into high-level application errors that the Daemon can understand.

How It Works

1. The Module Structure (mod.rs)

The mod.rs file acts as the organizer and namespace manager. It simply exports the various sub-handlers: -> See: crates/kinetic-network/src/event_loop/handlers/mod.rs — Lines 1 to 4

This structure makes it easy for the NetworkEventLoop to simply call handlers::gossipsub::handle(...) or handlers::cdn::handle(...) without worrying about the internal details of how the event is parsed. It enforces a strict separation of concerns: the loop dispatches, the handler processes. The module pattern ensures that if we add a new protocol later (like a dedicated storage protocol), we just add a new file and a new line to mod.rs.

2. Gossipsub Handler (gossipsub.rs)

Gossipsub is the protocol libp2p uses for broadcast messages (one-to-many). In Kinetic, this is primarily used for global Drand randomness beacons and global Governance votes. Because these messages go to everyone on the network, they are the prime vector for spam.

Phase 1: Rate Limiting (The Semaphore) Before the handler even looks at a message’s content, it tries to get a permit from the event loop’s shared gossip_semaphore. -> See: crates/kinetic-network/src/event_loop/handlers/gossipsub.rs — Lines 16 to 32

It uses try_acquire_owned(). This is a crucial Rust concept: it does not wait in line. try_acquire_owned is non-blocking. If the semaphore is saturated (meaning there are too many gossip messages currently being processed by the background threads), it immediately rejects the message. It logs a warning: “Gossip semaphore saturated — dropping message”. It then sends a CommitGossipValidation { is_valid: false } message back to the main loop and drops the payload entirely. This strict cut-off prevents the node from being DDoS’d by a flood of gossip. It prioritizes the survival of the node over processing every single message.

Phase 2: CPU Offloading (The Verification) If a permit is successfully acquired, the handler needs to verify the message. Because verification blocks the CPU, it spawns a background thread. -> See: crates/kinetic-network/src/event_loop/handlers/gossipsub.rs — Lines 37 to 62

It uses spawn_blocking to move the work off the main async loop into Tokio’s dedicated blocking thread pool. This is the only safe way to do cryptography in an async environment. Without this, the entire swarm would freeze.

  • If the topic is GOSSIP_TOPIC_DRAND, it parses the JSON payload into a RawKyn and calls verify(). Drand signatures use BLS cryptography, which is heavy.
  • If the topic is GOSSIP_TOPIC_GOVERNANCE, it fetches the GLOBAL_GOVERNANCE_STATE, extracts the current root key, and verifies the cryptographic signatures on the governance action. Governance actions can alter network rules, so this check is absolute. Any failure here results in a fast rejection.

Phase 3: Validation Loopback Once the background thread finishes verifying the message, it must inform libp2p whether the message was valid. Gossipsub requires explicit validation before it will forward a message to other peers. This is what prevents bad messages from propagating. -> See: crates/kinetic-network/src/event_loop/handlers/gossipsub.rs — Lines 64 to 71

It sends a CommitGossipValidation command back to the main loop via loopback_tx. The main loop will then tell the libp2p swarm to either propagate the message (if valid) or penalize the peer who sent it (if invalid). If valid, it also forwards the payload to gossip_tx so the rest of the Kinetic node (specifically the application layer and consensus layer) can actually use the data.

3. CDN Handler (cdn.rs)

The CDN (Content Delivery Network) protocol is a custom fast-path, point-to-point request-response system. It is used to fetch Name Records directly, bypassing the slower multi-hop Kademlia DHT walk.

Serving Requests (When a peer asks us for data): -> See: crates/kinetic-network/src/event_loop/handlers/cdn.rs — Lines 15 to 39

When a Message::Request arrives, a peer is asking us for a specific domain. The handler derives the correct storage keys for that domain, and then manually reaches into our local Kademlia RecordStore to see if we have it cached. If we do, we instantly send a CdnResponse back via the provided channel. We do this using send_response on the swarm’s CDN behaviour. We then increment our proxy_cdn_usage counter, which tracks how much bandwidth we are providing to the network. This allows a node to quickly serve data it already possesses without forcing the requester to do a DHT search.

Handling Responses (When a peer replies to our request): -> See: crates/kinetic-network/src/event_loop/handlers/cdn.rs — Lines 41 to 59

When a Message::Response arrives, we check if the request_id exists in our pending_cdn_requests map. If it does, and the peer gave us a valid record, we deserialize it into a NameRecord. We then inject it into our own Kademlia store using handle_record().

If it’s valid, we log “CDN Hit!”. This is a massive performance win. We then check event_loop.pending_gets to see if any local processes were currently waiting for this domain to resolve. If so, we iterate through the responders and instantly send them the data, resolving their lookup much faster than waiting for a full DHT query to complete.

4. Proxy Handler (proxy.rs)

The Proxy handler manages general point-to-point data traffic. This is used when routing a user’s web request through the network via the proxy service. It is essentially a bridge between libp2p’s network layer and Kinetic’s local application layer.

Incoming Requests (Acting as a Proxy Server): -> See: crates/kinetic-network/src/event_loop/handlers/proxy.rs — Lines 14 to 19

When a peer sends us a proxy request, we do not process the HTTP traffic here. Network layer code should not be parsing HTTP headers or managing streams. We simply forward the raw request to incoming_proxy_tx using an async task. We hand it off to the Daemon layer (which holds the actual HTTP proxy logic) to figure out what to do with it.

Outgoing Responses and Failures (Acting as a Proxy Client): -> See: crates/kinetic-network/src/event_loop/handlers/proxy.rs — Lines 21 to 46

When a peer responds to a proxy request we made (meaning they fetched a webpage for us and sent the HTML back), we pop the waiting channel from pending_proxy_requests and send the data through. If the request fails (e.g., the peer went offline, leading to OutboundFailure::Timeout or DialFailure), we map libp2p’s low-level error into our custom ProxyError enum, and send that Err back to the waiting process. This ensures the caller knows exactly why their proxy request failed, allowing them to retry with a different peer or surface an error to the user gracefully.


Detailed Message Flow Examples

Example 1: Receiving a Drand Gossip Message

  1. libp2p fires gossipsub::Event::Message from the swarm.
  2. The main event loop delegates it to gossipsub::handle.
  3. The node attempts to acquire gossip_semaphore. It succeeds.
  4. spawn_blocking is called. The RawKyn is deserialized and verified using BLS cryptography on a background thread.
  5. The thread finishes. The signature is valid.
  6. A CommitGossipValidation is sent to loopback_tx.
  7. The message is sent to gossip_tx to be ingested by the blockchain subsystem.
  8. The NetworkEventLoop reads the loopback and tells libp2p to forward the Drand message to all connected peers.

Example 2: Receiving a CDN Request for “saif.kyn”

  1. libp2p fires request_response::Event::Message::Request on the CDN protocol.
  2. The main event loop delegates it to cdn::handle, which reads the domain "saif.kyn".
  3. It derives the storage keys for this domain and the current NETWORK_ID.
  4. It searches the local Kademlia RecordStore for those keys.
  5. It finds a match! A previously cached NameRecord is loaded from memory.
  6. It sends a CdnResponse containing the record back down the libp2p channel.
  7. It increments proxy_cdn_usage by 1 to track bandwidth contribution.

Example 3: A Proxy Outbound Failure

  1. The local Daemon asks the network to fetch http://example.com via a peer.
  2. The NetworkEventLoop dials the peer, but the peer’s connection drops midway.
  3. libp2p fires request_response::Event::OutboundFailure with OutboundFailure::ConnectionClosed.
  4. The main event loop delegates it to proxy::handle.
  5. It looks up the original request ID in pending_proxy_requests.
  6. It maps ConnectionClosed to ProxyError::ConnectionClosed.
  7. It sends Err(ProxyError::ConnectionClosed) back to the local Daemon through the waiting channel.
  8. The local Daemon receives the error and returns a 502 Bad Gateway to the user’s browser.

Key Pieces

  • mod.rs: The organizational root. Exports the sub-handlers to keep the main event loop clean and modular.
  • gossipsub::handle(): The rate-limiter and verifier. Manages the concurrency semaphore, offloads heavy crypto validation to threads, and orchestrates the loopback validation system to tell libp2p what to propagate.
  • cdn::handle(): The lookup accelerator. Provides a fast-path for name record resolution by querying and updating the local Kademlia store directly upon request or response.
  • proxy::handle(): The data bridge. Simply connects libp2p’s request-response network protocol to Kinetic’s internal asynchronous proxy channels.

How This Connects to the Rest of Kinetic

  • CROSS-CRATE: kinetic_core::drand::RawKyn — defined and explained in docs/learn/core. (Verified by the gossipsub handler when Drand randomness arrives).
  • CROSS-CRATE: kinetic_core::governance::SignedGovernanceMessage — defined and explained in docs/learn/core. (Verified by the gossipsub handler when governance actions arrive).
  • CROSS-CRATE: kinetic_core::types::NameRecord — defined and explained in docs/learn/core. (Parsed and stored by the CDN handler).
  • FORWARD DEPENDENCY: The incoming_proxy_tx channel in proxy.rs connects up to the Daemon crate (Stage 9). The Daemon uses it to expose local HTTP proxying to the user. For now, treat it as an opaque pipe that carries data to the application layer.
  • Main Event Loop: All of these handlers are called directly by the NetworkEventLoop documented in 18_network_event_loop.md.
  • Kademlia Storage: The CDN handler directly interacts with Kademlia’s RecordStore, pulling and pushing records as needed.

Quick Reference

  • gossipsub.rs: Handles network-wide broadcasts. Protects the node from spam using a semaphore. Protects the async runtime from stalling using spawn_blocking.
  • cdn.rs: Handles direct, point-to-point requests for domains. Bypasses Kademlia routing to provide instant responses if the record is cached locally. Acts as a layer-2 cache on top of Kademlia.
  • proxy.rs: Bridges the network’s proxy requests to the user’s local daemon, translating libp2p network errors into application-level proxy errors.
  • Semaphore Rate Limiting: The technique of using try_acquire_owned() to immediately drop messages when the system is under heavy load, ensuring stability over perfect processing.
  • Loopback Validation: The two-step process where a handler verifies a message in the background, then sends a command back to the main loop to officially accept or reject it in the eyes of libp2p. This allows async validation without blocking the main event loop.
  • CPU Offloading: The practice of using spawn_blocking to move cryptographic math away from Tokio’s async executors.

Open Questions / Things to Revisit

  • Semaphore Tuning Under Load: The gossipsub semaphore uses try_acquire_owned(), meaning it drops messages immediately if the background threads are saturated. If the network experiences a legitimate, sudden spike in traffic (e.g., a rapid governance event), we might drop valid, important messages simply because our queue is full. We might need a small bounded wait instead of an immediate reject, or a prioritized queue for governance vs. drand.
  • Blocking Thread Pool Exhaustion: While spawn_blocking prevents the async executor from halting, an attacker could still flood us with garbage governance messages. The semaphore limits concurrency, but we could still tie up all available blocking threads with fake cryptographic checks, starving other components that rely on spawn_blocking (like disk I/O).
  • CDN Trust Model: The CDN handler injects received records straight into the local Kademlia store via handle_record. It relies on Kademlia’s internal verification to ensure the peer didn’t send a valid but functionally incorrect or outdated record. There is a risk of cache poisoning if skip_verify in dev mode leaks into production logic.
  • Proxy Error Mapping Completeness: The proxy error mapping handles Timeout, DialFailure, ConnectionClosed, and UnsupportedProtocols. All other libp2p errors are dumped into a generic ProxyError::Other. As the proxy feature matures, we may need a more granular error mapping to provide better feedback to the user and allow the Daemon to implement better retry logic.

Deep Dive: The Loopback Validation Architecture

One of the most complex interactions in the Gossipsub handler is the “Loopback Validation” system. To understand why this exists, you have to understand how libp2p::gossipsub protects the network from spam.

When a node receives a gossip message, it doesn’t immediately forward it to its peers. If it did, a single malicious node could bring down the entire network by sending one billion fake messages per second. Instead, libp2p puts the message in a “validation queue”. The application (Kinetic) is required to inspect the message and call gossipsub.report_message_validation_result(message_id, ValidationMode::Accept).

However, there’s a catch:

  1. The gossipsub state lives inside the Swarm, which is owned by the NetworkEventLoop.
  2. The cryptographic verification is slow, so we moved it to a background thread using spawn_blocking.
  3. The background thread does not have access to the Swarm. It cannot call report_message_validation_result directly because it doesn’t own the network state.

This creates a paradox: the thread that knows if the message is valid cannot tell libp2p about it.

The solution is the Loopback Channel (loopback_tx). When the background thread finishes its math, it constructs a LoopbackCommand::CommitGossipValidation enum containing the message_id and a boolean is_valid. It sends this enum down the channel. The main NetworkEventLoop (which does own the Swarm) listens on the receiving end of this channel. When it pops the command off the queue, it reaches into the Swarm and finally calls report_message_validation_result.

This architecture allows Kinetic to perform heavy, blocking cryptography asynchronously while still satisfying libp2p’s strict state ownership rules. It is a textbook example of “message passing” over “shared memory” in Rust.


Deep Dive: CDN vs Kademlia

The cdn.rs handler might seem redundant at first glance. Why do we need a custom Request-Response protocol to fetch Name Records when we already have the Kademlia DHT?

The answer is latency.

Kademlia is a Distributed Hash Table. When you look up a record in Kademlia, you don’t usually connect directly to the node that has the data. Instead, you connect to a node that is closer to the data, and ask them who they know. They give you a list of closer nodes. You connect to those nodes, and ask them. This process (the DHT walk) takes multiple network hops. In a global network, each hop might take 100ms. A full Kademlia lookup can easily take 500ms to 2 seconds.

For a web browser (the primary consumer of Name Records in Kinetic), waiting 2 seconds just to resolve a domain name before even starting the HTTP request is unacceptable.

The CDN protocol solves this by acting as a Layer-2 Cache. If a node believes a specific peer might already have the Name Record (perhaps because they are a known heavy-hitter or proxy provider), they can use the CDN protocol to ask them directly, bypassing Kademlia entirely.

When the peer receives this request (handled by cdn.rs), they simply check their local Kademlia cache (kademlia.store_mut().get()). If they have it, they return it instantly in a single network hop.

If it works, the lookup drops from 2 seconds to 50ms. If it fails (the peer doesn’t have it), the node falls back to the slow Kademlia walk. The CDN handler is what makes Kinetic’s domain resolution fast enough for human web browsing.


Core Concepts: Tokio Semaphores and Spawn Blocking

To fully grasp the gossipsub.rs handler, you need to understand two specific tools from the Tokio asynchronous runtime: the Semaphore and the Blocking Pool.

The Tokio Semaphore (try_acquire_owned)

A Semaphore is a concurrency primitive that holds a certain number of “permits”. In Kinetic, the gossip_semaphore is created in the NetworkEventLoop with a fixed number of permits (e.g., 50). When a message arrives, the handler calls try_acquire_owned().

  • If a permit is available: The function returns Ok(permit). The handler proceeds to process the message. When the message is fully processed (or dropped), the permit is automatically returned to the semaphore, allowing a new message to be processed.
  • If zero permits are available: The function returns an Err. This means 50 messages are currently being processed at this exact millisecond. Instead of waiting (which would stall the loop), the handler instantly matches on the Err, logs a warning, and discards the message.

This mechanism is the ultimate shield against network floods. It guarantees that no matter how much data a peer blasts at the node, the node will never attempt to process more than 50 messages simultaneously.

The Blocking Pool (spawn_blocking)

Tokio operates on a small number of worker threads (usually equal to the number of CPU cores). These threads execute async tasks. However, async tasks are expected to be “polite” — they should run quickly and yield control back to the worker thread when they hit an await point (like waiting for a network packet). Cryptographic verification (like verifying a BLS signature in Drand) does not yield. It is a tight loop of pure math. If you run it on a standard Tokio worker thread, that thread is held hostage until the math is done. If you get 8 messages at once on an 8-core machine, your entire network stack freezes.

To solve this, Tokio provides spawn_blocking. This function moves the closure onto a separate thread pool (the blocking pool), which can scale up to 500 threads. This keeps the primary async worker threads free to continue routing network traffic, while the heavy math happens in the background. Once the math finishes, the result is sent back to the async world via a channel (in our case, the loopback_tx).


Deep Dive: Proxy Error Mapping

The proxy.rs handler doesn’t just pass data; it also acts as an error translator. Libp2p generates very low-level network errors. The local application Daemon doesn’t care about libp2p’s internal state; it just wants to know why its proxy request failed.

When an OutboundFailure occurs, the handler inspects it:

  1. OutboundFailure::DialFailure: The peer we tried to proxy through is unreachable. They might be offline, or blocked by a firewall. We map this to ProxyError::Offline.
  2. OutboundFailure::Timeout: The peer is online, but they took too long to fetch the webpage and send it back to us. We map this to ProxyError::Timeout.
  3. OutboundFailure::ConnectionClosed: The peer was online and we were communicating, but the TCP connection unexpectedly dropped. We map this to ProxyError::ConnectionClosed.
  4. OutboundFailure::UnsupportedProtocols: The peer doesn’t actually support the Kinetic Proxy protocol (perhaps they are running an outdated version of the node software). We map this to ProxyError::UnsupportedProtocols.

By translating these errors, the Proxy handler ensures a clean boundary between the network layer (libp2p) and the application layer (the Daemon).


Security Implications: Dev Mode vs Production in CDN

Inside cdn.rs, when a NameRecord is received from a peer, it is injected into the local Kademlia store using the handle_record(&record, skip_verify) function. -> See: crates/kinetic-network/src/event_loop/handlers/cdn.rs — Lines 47 to 48

Notice how the skip_verify flag is determined: kinetic_core::config::is_dev_mode(). This is a critical security boundary.

In Production (when is_dev_mode() is false), skip_verify is false. This means that even though we received this record via the fast-path CDN, we still force Kademlia to verify the cryptographic signature on the record against the domain owner’s public key. If a malicious peer tries to poison our cache by sending a fake Name Record (e.g., redirecting a domain to a phishing IP), the verification will fail, and the record will be silently dropped.

In Development mode, verification is skipped to allow for faster iteration and easier testing with dummy data. This highlights why dev mode must never be exposed to the public internet, as it turns the CDN handler into an open vector for cache poisoning.

Summary of Handler Traits and Performance

When evaluating the performance of a decentralized node, the network handlers are the absolute bottleneck.

  • Latency: The proxy.rs and cdn.rs handlers add virtually zero overhead. They match on the request and instantly toss it over an asynchronous channel. Their latency is measured in microseconds.
  • Throughput: The throughput of the gossipsub.rs handler is hard-capped by the size of the Tokio blocking pool and the number of permits in the gossip_semaphore. If you have a 32-core machine, you can process 32 gossip signatures simultaneously in hardware.
  • Memory Footprint: By dropping messages when the semaphore is saturated, Kinetic ensures that the memory footprint of the gossip queue remains constant, regardless of network spam. A naive implementation that queues messages for processing would see memory usage spike to gigabytes during a network storm. The immediate drop guarantees O(1) memory usage for the gossip ingestion pipeline.

In short, these handlers are designed for one thing: keeping the node alive under the worst possible network conditions, while providing fast-paths for latency-sensitive applications like DNS resolution.

Why try_acquire_owned instead of acquire?

You might wonder why we don’t just use semaphore.acquire().await. If we used .await, the gossip handler would patiently wait its turn. However, .await means the main NetworkEventLoop would pause exactly on that line, waiting for the semaphore. This would defeat the purpose of the semaphore, as it would stall the entire async executor while waiting for background threads to finish. By using try_acquire_owned(), we check the queue synchronously, and if it’s full, we discard the message and move on immediately, keeping the loop spinning at maximum speed.

The Role of proxy_cdn_usage

In the CDN handler, you’ll see event_loop.proxy_cdn_usage.0 += 1. This might look trivial, but it forms the foundation of Kinetic’s incentive structure. By tracking exactly how many times a node successfully serves a Name Record from its cache to a requesting peer, the network can eventually reward nodes that act as high-availability CDN providers. This counters the tragedy of the commons, where nodes might otherwise refuse to serve requests to save bandwidth. Tracking this metric locally is the first step towards a verifiable proof-of-bandwidth system in future network stages.

Network Infrastructure & DNS Discovery

Crate: kinetic-network Stage: 8 Reading time: 20 minutes Depends on: docs/learn/types/01_overview.md


What Is This?

This document covers the structural glue and auxiliary protocols of the kinetic-network crate. Rather than focusing on a single continuous pipeline (like the event loop or the storage engine), this file examines the crucial connective tissue that makes the network functional. Specifically, it documents the crate entry point (lib.rs), the aggregate libp2p network behavior (behavior.rs), the module boundaries for the store and event loop (store/mod.rs, store/constants.rs, event_loop/mod.rs), and the custom DNS-based bootstrap discovery protocol (dns_tree.rs).

While files like the Kademlia store handle the heavy lifting, these structural files define exactly what the network is capable of doing and how a newly booted node finds its first connection to the outside world. The KineticBehavior struct acts as the grand manifest, dictating the exact set of P2P protocols a Kinetic node speaks. Meanwhile, the DNS tree protocol ensures a node does not have to rely on a hardcoded list of static IP addresses to join the network. Finally, the module files establish strict API boundaries so that the rest of the workspace can interact with the network cleanly.


Why Kinetic Needs This

In a decentralized network, several foundational problems must be solved before any actual domain data can be exchanged.

1. Protocol Composition and Identity (behavior.rs) The underlying networking framework, libp2p, is not a single unified protocol. It is a modular toolkit. You have to declare which pieces you want to use. Kinetic requires Kademlia for the DHT, Gossipsub for real-time broadcasts, AutoNAT for firewall traversal, and custom Request-Response protocols for proxying domain traffic. The behavior.rs file takes all these independent, isolated protocols and merges them into a single, cohesive state machine. Without this file, the node would have no defined network identity and would not know how to handle incoming connections.

2. Resilient Bootstrap Discovery (dns_tree.rs) When a Kinetic node boots up for the very first time, it faces the “first contact” problem: how does it know the IP address of another node? Hardcoding IP addresses directly into the source code is brittle—if those initial nodes go offline, new nodes can never join the network. Kinetic solves this elegantly using a custom DNS Tree protocol called kintree. By querying standard DNS TXT records for a known domain name, a node can discover a dynamically updating list of active bootstrap nodes. This allows network maintainers to update the entry points without requiring users to download new software binaries.

3. Architectural Isolation (lib.rs, mod.rs) The kinetic-network crate is vast and complex. lib.rs and the various mod.rs files act as the architectural blueprint. They define the public API boundaries so that downstream crates (like kinetic-daemon) do not have to understand the messy, asynchronous internals of libp2p swarms. They encapsulate the complexity, ensuring that the daemon only interacts with a clean message-passing interface.

4. Keyspace Separation (store/constants.rs) The Kademlia DHT is a massive, flat key-value store shared across the globe. Kinetic stores multiple different types of data in this DHT: reveals, heartbeats, and commitments. Without distinct namespaces, a malicious or malfunctioning node could publish a heartbeat using a key that accidentally overwrites a critical domain reveal. The constants ensure cryptographic separation within the DHT keyspace.


How It Works

Crate Architecture and API Surface (lib.rs)

The lib.rs file serves as the gateway to the kinetic-network crate. -> See: file:///home/saif/kinetic/kinetic-network/src/lib.rs — Lines 1 to 53

It begins with strict documentation linting rules (#![deny(missing_docs)]), enforcing that all public APIs are documented. The architecture is centralized around the NetworkEventLoop. The lib.rs file publicly exports only what is necessary: NetworkClient, NetworkConfig, NetworkMode, ProxyRequest, ProxyResponse, KineticStoreError, and NetworkEventLoop. By hiding the inner workings of the swarm and the Kademlia handlers, it forces a decoupled design. The external caller (the daemon) spawns the event loop and then solely communicates with it using the NetworkClient handle via an asynchronous channel. This prevents lock contention and keeps the swarm single-threaded.

The Aggregate Network Behavior (behavior.rs)

In libp2p, every distinct network feature (e.g., pinging a peer, searching the DHT) is implemented as a Behaviour. To create a functional node, you must combine them. -> See: file:///home/saif/kinetic/kinetic-network/src/behavior.rs — Lines 10 to 51

The KineticBehavior struct uses the #[derive(NetworkBehaviour)] macro. This macro is a powerful piece of Rust metaprogramming that automatically generates the necessary event-routing code to run multiple behaviors concurrently. The struct includes:

  • identify: The libp2p::identify::Behaviour protocol. When two nodes connect, they use this to exchange their supported protocols, public keys, and observed IP addresses. This is critical for nodes to discover if they can speak the same version of Kinetic.
  • ping: The libp2p::ping::Behaviour protocol. It periodically sends tiny messages to keep TCP/UDP connections alive through aggressive home NATs and measures network latency.
  • kademlia: The kad::Behaviour<KineticRecordStore>. This is the decentralized database. It uses Kinetic’s custom record store to hold domain state.
  • gossipsub: The gossipsub::Behaviour. This is the fast, real-time pub-sub network used for immediate propagation of reveals and heartbeats to thousands of nodes.
  • proxy: A custom request_response behavior that allows nodes to ask each other to proxy HTTP traffic for domain resolution.
  • cdn: Another custom request_response behavior specifically for serving DHT caches, speeding up data retrieval.
  • stream: A libp2p_stream::Behaviour used for passing raw traffic, essential for proxying actual data streams.
  • autonat: The libp2p::autonat::Behaviour. This asks peers to report back the IP address they see, allowing a node to figure out if it is behind a restrictive firewall.
  • relay_client & dcutr: Complex NAT traversal protocols. If a node is trapped behind a router, it uses a relay to punch a hole (DCUtR - Direct Connection Upgrade through Relay) to establish a direct connection eventually.
  • upnp: Uses the Internet Gateway Device (IGD) protocol to automatically ask home routers to forward ports.
  • mdns: Uses multicast DNS to discover other Kinetic nodes on the same local network (LAN) instantly without hitting the internet.

Several of these behaviors (like upnp and mdns) are wrapped in a Toggle. This allows the daemon to dynamically enable or disable them at runtime based on the node’s configuration (e.g., turning off mDNS in cloud servers).

DNS Tree Discovery (dns_tree.rs)

When a node needs to bootstrap, it relies on the resolve_dns_tree(domain) function. -> See: file:///home/saif/kinetic/kinetic-network/src/dns_tree.rs — Lines 11 to 91

This function performs a decentralized walk of DNS TXT records to find libp2p Multiaddrs. The step-by-step mechanism is:

  1. Initialize Resolver: It creates an asynchronous DNS resolver using the hickory_resolver crate, pulling configuration from the host OS.
  2. Fetch Root Records: It queries the base domain for TXT records.
  3. Parse and Identify Root: It scans the TXT strings. If it finds a raw Multiaddr (starting with /ip4/ or /ip6/), it saves it. If it finds the special kintree-root:v1 e=<hash> string, it extracts the hash to begin tree traversal.
  4. Iterative Branch Traversal: It uses a while loop to process branches.
    • It constructs a subdomain query: <branch_hash>.<domain>.
    • It queries TXT records for this specific subdomain.
    • It looks for kintree-branch:<hash1>,<hash2> records to discover deeper branches in the tree.
    • It looks for kintree-leaf:<multiaddr> records to extract actual peer connection strings.
  5. Safety Constraints: To prevent infinite loops caused by malicious or misconfigured DNS trees, the function enforces a maximum of 20 DNS lookups (max_lookups). It also stops immediately once it has collected 50 peer addresses. It utilizes a HashSet to track visited branch hashes, preventing cyclical traversal.

For WebAssembly targets (wasm32), this entire process is short-circuited. -> See: file:///home/saif/kinetic/kinetic-network/src/dns_tree.rs — Lines 93 to 97 Because web browsers do not permit raw UDP or TCP socket creation for custom DNS queries, the function simply returns an empty vector. WASM nodes must rely on alternative bootstrapping methods provided by their host environment.

Storage Constants (store/constants.rs)

The Kademlia DHT requires all keys to be byte arrays. To ensure that different subsystems do not collide, Kinetic uses namespace prefixes. -> See: file:///home/saif/kinetic/kinetic-network/src/store/constants.rs — Lines 3 to 9

  • KRS_REVEAL_PREFIX (b"krs_reveal:"): Prepended when a node publishes a domain reveal transaction.
  • KRS_HB_PREFIX (b"krs_hb:"): Prepended for heartbeat broadcasts indicating node liveness.
  • KRS_COMMIT_PREFIX (b"krs_cmt:"): Prepended for cryptographic commitments before a reveal.

By applying these prefixes before the key is hashed and placed into the DHT, Kinetic guarantees that a heartbeat for “Node A” will hash to an different topological location in the network than a reveal for “Node A”.

Module Boundaries (store/mod.rs & event_loop/mod.rs)

The mod.rs files establish the internal hierarchy. -> See: file:///home/saif/kinetic/kinetic-network/src/store/mod.rs — Lines 1 to 18 -> See: file:///home/saif/kinetic/kinetic-network/src/event_loop/mod.rs — Lines 1 to 19

Notice the extensive use of pub(crate). Files like handlers, verification, lightnode, and fullnode are marked pub(crate) because they are vital to the network’s internal machinery but should be invisible to the outside world. The only thing exported publicly from the store is the KineticRecordStore itself, and from the event loop, the NetworkEventLoop. This strict encapsulation is what makes the network layer maintainable.


Key Pieces

  • KineticBehavior

    • Where: kinetic-network/src/behavior.rs
    • What: A comprehensive struct aggregating every single libp2p protocol (DHT, Gossipsub, Request-Response, NAT traversal) used by the network.
    • Why it matters: It defines the exact network capabilities of a Kinetic node. It is the core state machine for peer-to-peer interactions.
  • resolve_dns_tree

    • Where: kinetic-network/src/dns_tree.rs
    • What: An asynchronous traversal function that resolves a domain name into a list of libp2p Multiaddr connection strings by walking a cryptographic tree of DNS TXT records.
    • Why it matters: It provides a resilient, decentralized, and updateable mechanism for new nodes to find the network entry points without relying on brittle hardcoded IP addresses.
  • Storage Prefixes (KRS_REVEAL_PREFIX, etc.)

    • Where: kinetic-network/src/store/constants.rs
    • What: Static byte arrays prepended to Kademlia DHT keys.
    • Why it matters: They namespace the decentralized database, physically separating different types of network state across the DHT topology.
  • NetworkClient & NetworkEventLoop Exports

    • Where: kinetic-network/src/lib.rs
    • What: The meticulously restricted public API of the crate.
    • Why it matters: It enforces the architectural law that external crates must communicate with the network via asynchronous message passing, preventing locking and concurrency bugs.

How This Connects to the Rest of Kinetic

  • Types: The proxy protocols in behavior.rs rely on ProxyRequest and ProxyResponse, which in turn utilize fundamental structs from kinetic-types (Stage 1).
  • CROSS-CRATE: The kinetic-types crate defines CdnRequest and CdnResponse, which are imported and integrated directly into the KineticBehavior here for decentralized content delivery.
  • Daemon Integration: The exports defined in lib.rs are the exact imports utilized by kinetic-daemon (Stage 9) to boot the node. The daemon executes resolve_dns_tree to find initial peers, configures the NetworkEventLoop using the KineticBehavior, and then takes operational control using the NetworkClient.

Quick Reference

  • To inspect all supported P2P protocols: Look at the fields of KineticBehavior in behavior.rs.
  • To understand how bootstrapping logic works: Read the while loop inside resolve_dns_tree in dns_tree.rs.
  • To find the Kademlia DHT key namespaces: Look at store/constants.rs.
  • To review the network public API: Check the pub use statements at the bottom of lib.rs.
  • WASM Network Target: DNS resolution is deliberately stubbed out for the wasm32 architecture due to browser I/O restrictions.
  • Toggle Protocols: mdns, relay_server, and upnp are wrapped in Toggle, meaning they can be instantiated in a disabled state based on configuration.

Open Questions / Things to Revisit

  • Hardcoded DNS Limits: The resolve_dns_tree function contains hardcoded values: max_lookups = 20 and a hard cap of 50 peer addresses. As the Kinetic network scales to thousands of nodes, these arbitrary limits might artificially restrict a node’s initial view of the network topology. Should these limits be moved to the NetworkConfig?
  • Silent DNS Failures: If the txt_lookup fails for a specific branch subdomain during tree traversal, the code silently ignores the error and continues. While this is resilient, it might mask severe network partitions or DNS misconfigurations. It would be beneficial to log these specific branch traversal failures for debugging purposes.
  • WASM Bootstrapping Gap: Currently, resolve_dns_tree returns an empty vector for WASM environments. If Kinetic intends to support in-browser light nodes, they will require an different bootstrapping mechanism (such as WebRTC signaling servers or HTTP-based bootstrap endpoints). This is a significant architectural gap that needs to be addressed for true browser compatibility.
  • Behavior Opacity: Several behaviors (like upnp and relay_server) use libp2p::swarm::behaviour::toggle::Toggle. This means they can be present in the KineticBehavior struct but functionally inactive. The exact conditions for when they are enabled are handled separately in the swarm builder, making the behavior.rs file slightly opaque regarding the true runtime state of the network protocols.

Crate: kinetic-node

Stage: 12

Reading Time: 40 mins

Depends On: kinetic-core, kinetic-network, kinetic-storage


What Is This?

kinetic-node is the infrastructure node binary. It is explicitly NOT the user-facing daemon (kinetic-daemon).

The critical difference:

TargetDescription
kinetic-daemon= runs on a user’s laptop, exposes full HTTP API, PAC proxy, DNS resolver, local CA. Designed for interactive use.
kinetic-node= runs on a server 24/7, no HTTP API, no DNS, no proxy. Its only job is to be a stable, always-on DHT participant so new peers have someone to bootstrap against.

Important

Infrastructure nodes are the backbone that makes the network not collapse when all personal laptops are offline.


What Makes It Unique vs The Daemon

  1. Static Ed25519 identity — The daemon generates ephemeral keys. The node loads a persistent keypair from node.key on disk so its Peer ID never changes across restarts. Stable Peer IDs are required for DHT bootstrap nodes.
  2. NetworkMode::FullNode — Listens on 0.0.0.0 (all interfaces) on both TCP and QUIC, binding to the wider internet. The daemon also binds to all interfaces (0.0.0.0) when running as a FullNode.
  3. No user-facing services — No HTTP API for publishing names, no DNS resolver, no PAC server, no local CA. Purely P2P.
  4. Minimal health-check API — One tiny Axum router at port 16003 with just /health and /peer_id. That’s it.
  5. Governance gossip relay — Processes and persists governance state changes received over P2P gossip, including writing NameRecord::Premium entries directly to Sled when a premium name grant is approved.

Warning

6. Governance key validation at boot — Immediately exits if production governance keys aren’t initialized, refusing to run in an insecure configuration.


Files

  • 02_main.md — Boot sequence, Drand heartbeat, governance gossip loop, network wiring.
  • 03_identity.md — Static key loading/generation with atomic disk write and full test coverage.
  • 04_gossip.md — Governance gossip message handler and storage effect processor.
  • 05_api.md — The minimal health-check API.

Boot Sequence and Runtime Loop

File: kinetic-node/src/main.rs Crate: kinetic-node | Stage: 12


What’s Unique Here (vs kinetic-daemon)

The service management (install/uninstall/start/stop via <dyn ServiceManager>::native()) is identical to the daemon — skip that, it’s documented in docs/learn/daemon/02_main_1.md. Focus on what’s different.


1. Governance Key Validation — Hard Boot Gate

#![allow(unused)]
fn main() {
kinetic_core::governance::logic::validate_keys_initialized()
}

-> See: kinetic-node/src/main.rs — Lines 150–157

The very first thing run_node() does — before storage, before networking — is:

#![allow(unused)]
fn main() {
kinetic_core::governance::logic::validate_keys_initialized()
}

If this returns an error (meaning production governance keys are still at their placeholder values), the node prints a fatal error and calls std::process::exit(1).

Warning

The daemon does not have this check. Infrastructure nodes are held to a higher standard because they are authoritative DHT participants. Running a bootstrap node with placeholder governance keys would corrupt governance state for any peer that bootstraps through it.


2. Static Peer Identity (The Core Difference)

#![allow(unused)]
fn main() {
let key_path = kinetic_core::config::get_base_dir().join("node.key");
let local_key = identity::load_or_generate_key(&key_path);
let local_peer_id = libp2p::PeerId::from_public_key(&local_key.public());
}

-> See: kinetic-node/src/main.rs — Lines 199–201

#![allow(unused)]
fn main() {
let key_path = kinetic_core::config::get_base_dir().join("node.key");
let local_key = identity::load_or_generate_key(&key_path);
let local_peer_id = libp2p::PeerId::from_public_key(&local_key.public());
}

The daemon generates a fresh keypair on every boot. The node loads from disk. This is the defining difference: a node’s Peer ID must be stable across restarts because other peers hardcode bootstrap node addresses in their configs. If the Peer ID changes, all configured peers fail to connect.


3. NetworkMode::FullNode — Publicly Addressable

#![allow(unused)]
fn main() {
NetworkMode::FullNode
0.0.0.0
}

-> See: kinetic-node/src/main.rs — Lines 209–252

The network config binds to 0.0.0.0 (all network interfaces) on both TCP and QUIC. The daemon also binds to all interfaces (0.0.0.0) when not in LightNode mode.

Key differences from the daemon’s network config:

FlagValue
enable_mdns: false— Infrastructure nodes don’t use mDNS (local discovery). They communicate via explicit bootstrap addresses.
external_address— Can be set from config to announce the node’s public IP to the DHT, so other peers can reach it through NAT.
max_reveals_per_hour: 100— Allows more DHT writes than a personal node.
disable_pow: false— PoW is always enforced (no test shortcuts in production infrastructure).

4. Governance State Loading

#![allow(unused)]
fn main() {
ENV_GOVERNANCE_PATH
<base_dir>/governance.key
}

-> See: kinetic-node/src/main.rs — Lines 257–266

The node reads the governance state path from an environment variable (ENV_GOVERNANCE_PATH) or defaults to <base_dir>/governance.key. It then acquires the global governance mutex and loads the state from disk.

Note

This is done before any networking starts so the node has a correct governance baseline before it starts accepting P2P gossip that might update it.


5. Drand Heartbeat — P2P Mode vs HTTP Mode

#![allow(unused)]
fn main() {
config.drand.p2p_only = true
should_fetch_http = true
}

-> See: kinetic-node/src/main.rs — Lines 326–378

The Drand heartbeat runs every 3 seconds (Quicknet produces a beacon every 3 seconds).

The p2p_only flag is what’s unique here:

If config.drand.p2p_only = true:

  • The node does NOT fetch Drand via HTTP on every tick.
  • Instead, it tracks the last cached kyn and estimates the expected current kyn from the genesis timestamp.
  • If the estimated current kyn is more than 5 ahead of the cached kyn, it falls back to HTTP (should_fetch_http = true). This is the “Drand P2P fallback” — the node prefers to receive Drand updates from its peers via gossip, but won’t fall hopelessly behind if the gossip network is lagged.

If p2p_only = false (default): fetches via HTTP every 3 seconds and broadcasts the result to P2P peers so they can stay in sync without individually hammering the Drand HTTP endpoints.

The daemon does not have p2p_only mode — it always fetches from Drand HTTP.


6. Gossip Loop — Governance + Drand

#![allow(unused)]
fn main() {
GOSSIP_TOPIC_GOVERNANCE
gossip::handle_kinetic_governance_gossip()
GOSSIP_TOPIC_DRAND
RawKyn
drand_kyn_tx
}

-> See: kinetic-node/src/main.rs — Lines 292–323

The gossip receiver loop handles two topics:

  • GOSSIP_TOPIC_GOVERNANCE → calls gossip::handle_kinetic_governance_gossip(), which validates the message and writes effects to Sled.
  • GOSSIP_TOPIC_DRAND → deserializes the RawKyn, verifies it, and if it’s fresher than the cached value, updates the drand_kyn_tx watch channel. The daemon does this too, but here there is no UI to notify — it purely updates the internal state.

Note

RecvError::Lagged is explicitly handled with continue. This happens when the gossip broadcast channel fills up (more than 100 messages queued). The node explicitly drops lagged messages rather than crashing — under high network load, it is acceptable to miss some intermediate states as long as the final state converges.


7. Health-check API — Minimal

#![allow(unused)]
fn main() {
axum::serve().with_graceful_shutdown(kinetic_core::shutdown::shutdown_signal())
}

-> See: kinetic-node/src/main.rs — Lines 381–396

Axum router at 127.0.0.1:16003 (or the configured bind IP). Only two routes:

  • GET /health → returns "OK"
  • GET /peer_id → returns the static Peer ID as a string

Uses axum::serve().with_graceful_shutdown(kinetic_core::shutdown::shutdown_signal()) — the serve loop shuts down cleanly when SIGINT/SIGTERM arrives. The daemon’s main server does the same.


Quick Reference — Boot Order

  1. validate_keys_initialized() — exit if governance keys are placeholders.
  2. KineticConfig::load() — read config.
  3. SledStorage::new() — open embedded DB.
  4. DrandClient::fetch_latest() — get current Drand beacon (or unavailable()).
  5. identity::load_or_generate_key("node.key") — load/generate stable Peer ID.
  6. NetworkEventLoop::new(NetworkMode::FullNode, ...) — wire P2P with public interfaces.
  7. tokio::spawn(network_loop.run()) — launch P2P event loop.
  8. tokio::spawn(gossip_loop) — launch governance + Drand gossip handler.
  9. tokio::spawn(drand_heartbeat) — launch 3-second Drand tick.
  10. axum::serve(16003).with_graceful_shutdown(...) — start health-check API.

Cross-Crate Connections

  • kinetic_core::governance::logic::validate_keys_initialized(): The production key gate. Unique to the node.
  • kinetic_network::NetworkMode::FullNode: Tells the network layer to bind publicly, enable all DHT routing, and skip ephemeral-only mode.
  • kinetic_storage::SledStorage: Same storage backend as the daemon.
  • kinetic_core::drand::DrandClient: Shared Drand client. The p2p_only flag behavior is unique to the node config.
  • kinetic_core::governance::GLOBAL_GOVERNANCE_STATE: The shared global Mutex<GovernanceState> updated by incoming gossip.

Static Node Identity: Key Loading, Generation, and Persistence

File: kinetic-node/src/identity.rs Crate: kinetic-node | Stage: 12


What Is This?

Important

The infrastructure node must maintain the same PeerId across every restart. Other peers hardcode bootstrap node addresses as <multiaddr>/<PeerId> in their config. If the Peer ID changes, those hardcoded entries become unreachable.

identity.rs provides one function — load_or_generate_key(path) — that either loads an existing keypair from disk or generates a fresh one if the file is missing or corrupted.


How It Works

Load path (file exists and is valid)

#![allow(unused)]
fn main() {
std::fs::read(key_path)
Keypair::from_protobuf_encoding(&bytes)
}

-> See: kinetic-node/src/identity.rs — Lines 15–26

std::fs::read(key_path) reads the raw bytes. Keypair::from_protobuf_encoding(&bytes) decodes the Ed25519 keypair from its protobuf-encoded form. If decoding fails (corrupted file), it falls through to generating a new key.


Generate path (file missing or corrupted)

#![allow(unused)]
fn main() {
Keypair::generate_ed25519()
to_protobuf_encoding()
write_secret()
}

-> See: kinetic-node/src/identity.rs — Lines 27–36

Keypair::generate_ed25519() mints a fresh keypair. The keypair is re-encoded with to_protobuf_encoding() and written to disk via write_secret().


write_secret() — secure atomic write

#![allow(unused)]
fn main() {
0o600
std::fs::rename()
f.sync_all()
std::fs::write()
}

-> See: kinetic-node/src/identity.rs — Lines 39–58

On Unix, the file is written with three security properties:

  1. Mode 0o600 — readable only by the owner.

    Important

    The private key must never be world-readable.

  2. Atomic rename — written to a .tmp file first, then std::fs::rename() atomically moves it to the final path. Prevents a partially written key file surviving a crash.
  3. f.sync_all() — flushes the write to the OS disk buffer before rename, ensuring the data survives a power loss.

On non-Unix, a plain std::fs::write() is used (no file permission API is available cross-platform).


Why Ed25519?

Ed25519 keys produce Peer IDs that encode to base58 strings starting with 12D3Koo. libp2p uses Ed25519 as its primary key type. The keys are stored in protobuf format (the standard libp2p key serialization) so they are portable across versions.


Test Coverage

The test suite covers every meaningful failure path:

  • test_generate_new_key_if_missing: file doesn’t exist → generates, saves, reload matches.
  • test_load_existing_key: load twice → same Peer ID.
  • test_fallback_on_corrupted_key: garbage bytes in file → generates new key without panic.
  • test_fallback_on_unwritable_directory: path is inside a non-directory → generates in memory, doesn’t panic.
  • test_fallback_on_empty_file_overwrites: empty file treated like corruption → new key written.
  • test_keypair_is_ed25519: Peer ID starts with 12D3Koo (Ed25519 marker).
  • test_generate_unique_keys: two separate calls produce different Peer IDs.
  • Fuzz: doesnt_crash_on_corrupted_identity_files: proptest random bytes → never panics.

-> See: kinetic-node/src/identity.rs — Lines 60–218


Quick Reference

ScenarioResult
node.key exists, validLoads and returns keypair
node.key exists, corruptedGenerates new, saves, returns
node.key missingGenerates new, saves, returns
Cannot write to diskGenerates new in-memory, logs warning

Key format: Ed25519, protobuf-encoded bytes File permissions (Unix): 0o600 (owner read/write only) Write strategy: .tmpsync_all()rename()

Governance Gossip Handler

File: kinetic-node/src/gossip.rs Crate: kinetic-node | Stage: 12


What Is This?

handle_kinetic_governance_gossip() is called every time a GOSSIP_TOPIC_GOVERNANCE message arrives over P2P. It is the only place in kinetic-node that writes to the application state.

The daemon also handles governance gossip in kinetic-daemon/src/services/gossip.rs. The difference: the node version also writes NameRecord::Premium and NameRecord::PremiumRevoked entries directly into the Sled storage, making the change immediately queryable via DHT.


How It Works

Parse and verify

#![allow(unused)]
fn main() {
serde_json::from_slice::<SignedGovernanceMessage>(payload)
process_governance_message(&mut state, &signed_msg)
}

-> See: kinetic-node/src/gossip.rs — Lines 18–24

serde_json::from_slice::<SignedGovernanceMessage>(payload) — if this fails (invalid JSON, wrong schema), the function returns silently. No panic, no error propagation.

process_governance_message(&mut state, &signed_msg) — validates the message signatures against the governance keys in GLOBAL_GOVERNANCE_STATE. The global mutex is acquired, the state is mutated, then immediately cloned and the mutex is released before any disk I/O. This keeps the critical section minimal.


Three outcomes

#![allow(unused)]
fn main() {
GovernanceEffect::PremiumNameGranted
GovernanceEffect::PremiumNameRevoked
NameRecord::Premium
<DB_PREFIX_REVEAL><name>
tokio::task::spawn_blocking()
}

-> See: kinetic-node/src/gossip.rs — Lines 27–80

Ok(Some(effect)) — the message was valid and produced a state change:

  • GovernanceEffect::PremiumNameGranted { name, target_pubkey } → writes a NameRecord::Premium to Sled under the key <DB_PREFIX_REVEAL><name>. This makes the premium name immediately resolvable via DHT without waiting for a user to register it.
  • GovernanceEffect::PremiumNameRevoked { name } → deletes the NameRecord::Premium from Sled. The name becomes unresolvable.
  • Any other effect → no storage write.
  • Saves updated GovernanceState to disk via tokio::task::spawn_blocking() (disk I/O offloaded from the async executor).

Ok(None) — message was valid but produced no effect (e.g., already applied):

  • Saves state to disk anyway (to update timestamps/sequence numbers).

Err(e) — message was rejected (bad signature, replay attack, wrong sequence):

  • Logs at DEBUG level only. No storage write, no disk save.

Why spawn_blocking for disk saves?

#![allow(unused)]
fn main() {
GovernanceState::save_to_disk()
std::fs::write()
spawn_blocking
}

GovernanceState::save_to_disk() calls std::fs::write(), which blocks the OS thread.

Important

Inside a Tokio async context, blocking the executor thread stalls all other tasks. spawn_blocking moves the disk write to Tokio’s blocking thread pool, keeping the async executor free.


Tests

  • test_handle_invalid_json_payload — garbage bytes → no panic.
  • test_handle_invalid_signature — valid JSON, empty signatures → rejected gracefully.
  • test_handle_wrong_json_schema{"hello":"world"} → fails JSON parse of SignedGovernanceMessage.
  • test_handle_massive_payload — 1MB of [] → serde rejects it, no OOM.
  • test_handle_unexpected_fields — extra JSON fields → parsed with #[serde(deny_unknown_fields)] behavior.
  • test_save_to_disk_failure — lockpath is a directory → save_to_disk returns Err, no panic.
  • Fuzz: doesnt_crash_on_random_gossip_bytes — proptest random bytes → never panics.

-> See: kinetic-node/src/gossip.rs — Lines 87–197


Quick Reference

EventStorage Effect
PremiumNameGrantedWrite NameRecord::Premium to Sled
PremiumNameRevokedDelete NameRecord::Premium from Sled
Other governance effectNo storage write
Invalid signatureIgnored, DEBUG log
Bad JSONIgnored, DEBUG log

Health-Check API

File: kinetic-node/src/api.rs Crate: kinetic-node | Stage: 12


What Is This?

A minimal two-route Axum router at port 16003. No auth, no JSON bodies, no state. Just enough for infrastructure monitoring tools to check if the node is alive and what its Peer ID is.

The daemon has a large API with ~15 routes. The node has 2. This reflects its role: infrastructure nodes should be black boxes — no management interface, just “is it up and what is it?”


Routes

GET /health"OK"

A plain-text liveness check. Returns HTTP 200 with body OK. Used by load balancers, uptime monitors, and the service manager to check if the node process is responsive.

-> See: kinetic-node/src/api.rs — Line 12


GET /peer_id<PeerId as String>

Returns the node’s static libp2p Peer ID as a base58-encoded string (starts with 12D3Koo). Used by operators to verify which node they’re talking to and to extract the Peer ID for adding to bootstrap node lists.

-> See: kinetic-node/src/api.rs — Lines 13–16

The local_peer_id is captured into the closure via move, making the handler stateless — no Arc, no lock, no runtime lookup.


Tests

Uses tower::ServiceExt::oneshot() to drive the Axum router without binding a real socket:

  • test_health_endpoint — 200 + "OK".
  • test_peer_id_endpoint — 200 + correct Peer ID string.
  • test_health_endpoint_post_method_rejected — POST to /health → 405.
  • test_peer_id_endpoint_put_method_rejected — PUT to /peer_id → 405.
  • test_unknown_route_returns_404/does_not_exist → 404.
  • test_health_endpoint_ignores_query_params/health?foo=bar → 200.

-> See: kinetic-node/src/api.rs — Lines 19–152

Crate: kinetic-daemon

Stage: 9

Reading Time: 360 mins

Depends On: kinetic-core, kinetic-network, kinetic-storage

What Is This?

This crate contains the daemon process, which acts as the orchestrator and primary user interface for a running Kinetic node. It is the application that users actually execute when they run the node on their servers or local machines.

Unlike the other crates that provide library functionality, the daemon exposes the system via an HTTP API, handles configuration, establishes secure proxy tunnels, and runs all necessary background services.

Key Pieces

The crate is conceptually divided into a few vital subsystems:

  1. The API (src/api/): Built using axum, this is a fully asynchronous REST API that exposes the node’s capabilities (like publishing records, resolving names, and interacting with the VDF engine) over standard HTTP. It includes strict CORS restrictions, constant-time token authentication, and SSE streams for real-time gossip.
  2. The Proxy (src/proxy/): A complex HTTP tunneling layer that intercepts requests to .kin domains from a standard browser. It acts as an internal Man-in-the-Middle (MITM), performing recursive P2P lookups and returning standard HTTP responses, allowing seamless web surfing over the decentralized network.
  3. The Services (src/services/): Background worker tasks (like the heartbeat and gossip managers) that maintain the health of the node, continually fetching the latest timestamps from the Drand network and pruning expired identities.
  4. The CA (src/ca.rs): A crucial component that generates a localized Root Certificate Authority on boot and injects it into the host OS, enabling the proxy to provide valid TLS encryption for decentralized .kin websites without warnings.

Why Kinetic Needs This

A peer-to-peer protocol library is useless if a user cannot easily interact with it. The daemon transforms the Kinetic protocol into a running application.

It provides an HTTP bridge that enables modern frontend interfaces (like React dashboards or Chrome extensions) to communicate with the node without having to implement complex Libp2p gossip protocols in JavaScript. It also provides the vital proxying mechanism that allows legacy web browsers to navigate the .kin namespace transparently.

How to Read This Stage

Since this crate glues everything together, the documentation files cover a wide breadth of functionality:

  • Start with 02_main_1.md and 03_main_2.md to understand the daemon’s boot sequence, setup, and graceful shutdown loops.
  • Explore the API configuration and routing by reading 10_api_mod.md, which details the critical RBAC security model.
  • Dive into the Proxy components (08, 09, 16, 17) to learn how decentralized websites are served locally.
  • Review the various API endpoint files (e.g., 04, 05, 06, 07, 12, 13) to see how specific user actions (like publishing names or generating KIDs) translate into underlying protocol commands.
  • Finally, read about the background jobs in 14_services_network.md and 18_services_misc.md.

Daemon Bootstrap and CLI Setup (main.rs Part 1)

Crate: kinetic-daemon Stage: 9 Reading time: 25 minutes Depends on: Stage 1 (types), Stage 4 (storage), Stage 7 (core), Stage 8 (network)


What Is This?

This document covers lines 1 through 335 of kinetic-daemon/src/main.rs. This file is the absolute entry point for the primary user-facing binary in the Kinetic ecosystem: the Kinetic Daemon. The daemon acts as the central coordinator and the heartbeat of a node’s local stack.

When a user interacts with the Kinetic network—whether they are registering a name via the CLI, resolving a .kin domain in their local browser, or automatically participating in the Kademlia P2P network to host Distributed Hash Table (DHT) records—it is this daemon process doing the actual heavy lifting in the background.

Specifically, this first half of main.rs is responsible for two major, distinct operational phases:

  1. The CLI interface and Service Management Setup: The application uses clap to define a command-line interface. While it has commands to run directly in the foreground, its primary operational mode is as a background service. To achieve this, it contains OS-specific logic to install, start, stop, and uninstall the daemon as a native operating system service. This includes systemd for Linux, launchd for macOS, and the Service Control Manager (SCM) for Windows. Additionally, it handles the generation and injection of the local Root Certificate Authority (CA) into the host operating system’s trust store.

  2. The run_daemon Boot Sequence (The Preamble): This is the critical initialization phase where the daemon reads configuration files, sets up structured logging (tracing), and validates the fundamental state of the node. It connects to the local disk database (SledStorage), spins up the Verifiable Delay Function engine (ChiaVdfEngine), loads the node’s long-term identity keys, synchronizes the network clock by fetching the latest heartbeat from the Drand beacon, and finally, mines a Proof-of-Work (PoW) Sybil-resistant peer identity before it is allowed to join the Kademlia DHT swarm.

This specific document does not cover the continuous background running loops (like the network event loop or the HTTP API server itself), which are defined later in the file. Instead, it focuses purely on the preamble: how the daemon gets off the ground, how it configures itself securely, and the rigorous checks it performs before it is ready to talk to the rest of the peer-to-peer network.


Why Kinetic Needs This

A decentralized naming system cannot rely on transient, short-lived CLI processes. If a user wants to resolve a .kin domain via their browser at any random time of day, there must be a local service listening on loopback interfaces, ready to translate that HTTP request into a DHT lookup.

  1. Persistent P2P State: The Kademlia DHT requires nodes to maintain long-lived connections and stable routing tables. If the CLI just spun up, made a request to the network, and then immediately died, the node would constantly have to bootstrap its DHT routing table from scratch every single time the user executed a command. This is not only slow from a UX perspective, but it also puts massive, unnecessary strain on the network’s bootstrap nodes. The daemon keeps this state warm and connected.

  2. Deep System Integration for Resolution: To transparently intercept normal web traffic intended for .kin domains, the Kinetic system needs to run a local DNS resolver and a transparent HTTP proxy. These services must run continuously in the background. The install_service logic ensures that when a user reboots their machine, the Kinetic daemon automatically starts up with the operating system. This keeps their .kin domains accessible without requiring manual command-line intervention every time they turn on their laptop.

  3. Sybil Resistance and the Boot Penalty: The Kademlia peer-to-peer network is fundamentally vulnerable to Sybil attacks if malicious actors can generate thousands of node identities instantly. To counter this, Kinetic requires that a node spend a significant amount of CPU time (usually 30-40 seconds) mining a Proof-of-Work identity that is cryptographically bound to the current Drand round at boot. This heavy initialization must be managed before the node attempts to talk to peers. The boot sequence is structured in run_daemon to ensure this penalty is paid exactly once per session.

  4. Security Isolation and SSRF Prevention: By verifying port conflicts at boot time, the daemon protects the user’s local machine from Server-Side Request Forgery (SSRF) exploits. If the internal API port accidentally collided with the proxy port, a malicious website could potentially bounce requests through the Kinetic proxy to hit the internal, unauthenticated daemon state. The boot sequence catches this configuration error before the server binds to any sockets.

  5. Local Certificate Authority Management: Modern web browsers enforce strict HTTPS requirements. If the local Kinetic proxy simply returned plain HTTP for .kin websites, browsers would display massive security warnings or block the traffic entirely. The daemon must generate a local Certificate Authority (CA) and forcefully inject it into the OS trust store so that it can mint valid, trusted HTTPS certificates on-the-fly for any .kin domain the user visits.


How It Works

The execution flow of the binary logically begins in main(), but this document focuses on the logic defined in lines 1-335, primarily run_daemon and the service installer utilities.

Step 1: CLI Parsing (Cli and Commands)

#![allow(unused)]
fn main() {
-> See: `kinetic-daemon/src/main.rs:L48-L76`
}

The application leverages the clap crate’s derive macros to define a Cli struct containing an optional Commands enum.

When the user types kinetic-daemon install, the program parses this and routes execution to the install_service function. The commands are kept deliberately minimal:

  • Install: Setup the OS service and trust the CA.
  • Uninstall: Remove the OS service.
  • Run: Foreground execution of the daemon.
  • Start: Trigger the installed background service to run.
  • Stop: Halt the installed background service. This abstracts away the complexity of managing system services from the user.

Step 2: CA Injection and Service Installation

#![allow(unused)]
fn main() {
-> See: `kinetic-daemon/src/main.rs:L78-L178`
}

If the user executes the install command, the daemon proceeds through a multi-step system setup phase. First, it ensures that a base configuration directory exists (typically ~/.local/share/kinetic on Linux). Next, it generates or loads a local Root CA by calling ca::load_or_create_root_ca.

Once the certificate file (ca_cert.pem) exists on disk, the daemon calls trust_ca. This function is OS-dependent and uses cfg! macros to detect the host environment:

OSAction
On Linux:It spawns a sudo cp command to place the certificate directly into /usr/local/share/ca-certificates/, appending the Kinetic NETWORK_ID to the filename. It then executes sudo update-ca-certificates to refresh the OS bundle.
On macOS:It leverages the built-in security binary, executing security add-trusted-cert and targeting the /Library/Keychains/System.keychain.
On Windows:It utilizes the certutil command-line tool with the -addstore -f Root flags to forcibly inject the CA into the Windows root trust store.

Warning

If this CA trust process fails (for example, if the user denies the sudo prompt), the daemon does not crash. It prints a prominent warning instructing the user to trust it manually, because without it, the .kin HTTPS proxy will not function correctly in standard browsers.

Finally, it uses the service_manager crate to register the run command as a native autostarting service, ensuring it boots with the OS.

Step 3: The run_daemon Boot Sequence

When the daemon is instructed to actually execute (either manually via kinetic-daemon run or automatically by the installed OS service manager), it enters the massive run_daemon() asynchronous function.

3.1: Governance and Config Validation

#![allow(unused)]
fn main() {
-> See: `kinetic-daemon/src/main.rs:L220-L242`
}

The very first operation the daemon performs is calling kinetic_core::governance::logic::validate_keys_initialized().

Important

If the production governance keys (which dictate the network’s trusted authorities) are missing or invalid (e.g., still using development placeholders), the daemon fatally exits with a non-zero status. The node simply cannot participate in the network without a valid, synchronized governance plane.

Following this, it loads the user’s local KineticConfig.

Caution

It performs a critical security check: it compares the backend_port against the api_port, proxy_port, dns_port, and daemon_port. If the backend port matches any of the others, it exits immediately. This prevents a class of SSRF vulnerabilities where external traffic could be maliciously routed into internal management endpoints.

3.2: Subsystem Initialization

#![allow(unused)]
fn main() {
-> See: `kinetic-daemon/src/main.rs:L253-L270`
}

With validation complete, the daemon begins spinning up its primary dependencies.

  • Storage Engine: It initializes a new instance of SledStorage, pointing it to the configured storage_dir. This instance is immediately wrapped in an Arc (Atomically Reference Counted) pointer, as it will be shared across dozens of asynchronous threads handling P2P requests, HTTP API calls, and background sync loops.
  • VDF Engine: It initializes the ChiaVdfEngine, casting it to the dynamic trait object Arc<dyn kinetic_core::traits::VdfEngine>. This engine is required later to evaluate and construct cryptographic time-locks for domain registrations.
  • Identity Loading: It loads the node’s long-term identity file (identity.key). This is a Dilithium (ML-DSA) keypair used to cryptographically sign .kin namespace records that this node owns. The public verifying key is logged to the console to confirm identity loaded successfully.

3.3: Drand Heartbeat Synchronization

#![allow(unused)]
fn main() {
-> See: `kinetic-daemon/src/main.rs:L271-L283`
}

The daemon needs a synchronized, unpredictable source of time to participate in the network. It initializes the DrandClient (passing it a clone of the storage Arc so it can cache rounds) and attempts to fetch the absolute latest block (the kyn). If the Drand HTTP endpoints are reachable, it logs the current kyn number.

If Drand is unreachable on startup, the daemon logs a warning but proceeds using a dummy “unavailable” state (RawKyn::unavailable()). This is a deliberate architectural choice: it allows the local proxy and API to start up so the user can still resolve cached domains, even if they are temporarily unable to register new ones.

3.4: API Token Generation

#![allow(unused)]
fn main() {
-> See: `kinetic-daemon/src/main.rs:L285-L290`
}

Before getting bogged down in heavy computation, the daemon calls kinetic_daemon::api::ensure_api_tokens(). This function generates a cryptographically secure random token and writes it to api.token on disk.

This step is placed before the PoW mining loop. By doing this early, if a user runs kinetic status via the CLI in a separate terminal window, the CLI has a valid token to read from disk and can authenticate against the daemon’s HTTP server the moment it starts listening, even if the P2P networking is still booting up.

3.5: Sybil-Resistant Identity Generation

#![allow(unused)]
fn main() {
-> See: `kinetic-daemon/src/main.rs:L292-L298`
}

The daemon must now prove its computational weight to the network. It takes the current Drand kyn and the predefined POW_DIFFICULTY_BITS and passes them to kinetic_network::pow::mine_sybil_keypair.

This is a blocking, CPU-intensive loop that typically runs for 30-40 seconds. It hashes a randomly generated libp2p Keypair alongside the Drand heartbeat until the resulting hash meets the difficulty target. Once found, this local key becomes the node’s PeerId in the Kademlia DHT for the duration of this session. Because the hash includes the recent Drand heartbeat, peers can verify that this PoW was done recently, preventing attackers from pre-mining millions of identities.

3.6: Network Configuration Setup

#![allow(unused)]
fn main() {
-> See: `kinetic-daemon/src/main.rs:L300-L335`
}

Finally, the daemon prepares the data structures needed to launch the Libp2p Swarm. It translates the string ports from the configuration file into valid Multiaddr formats. It configures listen addresses for both standard TCP (/ip4/0.0.0.0/tcp/16001) and experimental QUIC transport (/ip4/0.0.0.0/udp/16001/quic-v1), ensuring the node is accessible over IPv4 and IPv6.


Key Pieces

struct Cli and enum Commands

  • What it does: Uses the clap crate’s procedural derive macros to parse raw command-line arguments into strongly typed, matchable Rust structs and enums.
  • Where it lives: kinetic-daemon/src/main.rs:L48-L76
  • Why it matters: This defines the entire user-facing interface for installing, managing, and running the background daemon process from a terminal.

fn trust_ca(cert_path: &Path)

  • What it does: Executes shell commands to inject a generated Root CA into the operating system’s native trust store.
  • Where it lives: kinetic-daemon/src/main.rs:L78-L119
  • Why it matters: Web browsers will immediately reject the Kinetic local HTTP proxy’s self-signed certificates for .kin domains unless the host OS implicitly trusts the daemon’s root CA. This OS-level integration is what makes the browsing experience seamless.

fn install_service(user, config_dir)

  • What it does: Uses the cross-platform service_manager crate to hook the daemon into systemd, launchd, or SCM, configuring it to auto-restart and run in the background.
  • Where it lives: kinetic-daemon/src/main.rs:L121-L178
  • Why it matters: Ensures the node runs in the background at all times, making .kin domain resolution ubiquitous and transparent on the host machine without requiring the user to keep a terminal window open.

async fn run_daemon()

  • What it does: The primary asynchronous boot sequence. It sequentially bootstraps configuration validation, disk storage, the VDF engine, node identity, Drand synchronization, API tokens, and the Sybil PoW mining loop.
  • Where it lives: kinetic-daemon/src/main.rs:L219-L335 (and extends further down the file).
  • Why it matters: This function is the ultimate gatekeeper. If any of these initialization steps fail (like detecting bad governance keys, experiencing port conflicts, or failing to bind to the disk database), the daemon refuses to start. This fail-fast design prevents network corruption and local security exploits.

How This Connects to the Rest of Kinetic

This specific section of main.rs acts as the grand orchestrator, pulling together nearly all previously documented crates:

  • Storage: It initializes SledStorage from kinetic-storage (Stage 4) and wraps it in an Arc to be passed to all other subsystems.
  • VDF Engine: It initializes the ChiaVdfEngine from kinetic-vdf (Stage 5/6), keeping it ready for time-lock validations.
  • Identity & Cryptography: It loads long-term ML-DSA post-quantum keypairs defined in kinetic-core (Stage 7).
  • Networking & PoW: It prepares the multi-address configuration and executes the blocking PoW mining algorithm defined in kinetic-network (Stage 8).
  • Governance: CROSS-CRATE: It relies directly on validate_keys_initialized from Stage 7 to ensure the node is operating on a valid, production-ready network state.

Quick Reference

  • Available CLI Commands: install, uninstall, run, start, stop.
  • Security Check: backend_port is forbidden from matching api_port, proxy_port, dns_port, or daemon_port.
  • Boot Sequence Order:
    1. Governance Validation
    2. Port Config Validation
    3. Sled Storage Init
    4. VDF Engine Init
    5. Identity Key Load
    6. Drand Sync
    7. API Token Generation
    8. Sybil PoW Mining
    9. Network Config Init.
  • OS CA Integration Commands: Linux (update-ca-certificates), macOS (security add-trusted-cert), Windows (certutil).

Open Questions / Things to Revisit

  1. CA Installation Silent Failures: The trust_ca function executes OS-level shell commands, which often require sudo privileges. If this fails (e.g., the user dismisses the password prompt), the daemon merely prints a console warning but continues the installation process. Should a failure to trust the CA halt the install process entirely? Currently, it leaves the user with a broken proxy experience where browsers will show massive, unbypassable HTTPS errors for all .kin sites.

  2. Blocking PoW at Startup Degrading UX: The mine_sybil_keypair function is synchronous and blocks the thread for 30-40 seconds while it hashes. While the API tokens are smartly generated before this blocking step, the actual HTTP API server doesn’t spin up until after this loop finishes later in the file. This means if a user runs kinetic status immediately after executing kinetic-daemon run, their CLI will time out because the daemon’s API port isn’t listening yet. Moving the PoW generation to a background Tokio task or spawning the HTTP API earlier in the boot sequence could drastically improve the perceived startup time.

  3. Hardcoded Subcommands in Installer: The install_service function hardcodes the "run" argument when configuring the service manager payload. If the CLI command schema changes in the future (e.g., to "serve" or "start-node"), this literal string must be manually updated, or the installed service will silently fail to boot.

  4. Drand Unavailability Fallback: If Drand is offline at boot, the daemon handles this gracefully by proceeding with an unavailable state (effectively a kyn of 0). However, it’s architecturally unclear how the system handles the transition when Drand comes back online. The Sybil PoW mining requires a valid Drand kyn to generate an identity that peers will accept. If the node falls back to kyn: 0, the peer identity it generates might be instantly rejected by the Kademlia DHT, isolating the node from the network until it is restarted.

Daemon Runtime & Graceful Shutdown

Crate: kinetic-daemon Stage: 9 Reading time: 60 minutes Depends on: 02_main_1.md, kinetic-network, kinetic-core


What Is This?

This file documents the second half of the main runtime loop inside the kinetic-daemon crate (specifically src/main.rs, lines 336 to 670). Once the basic storage, configuration, and cryptographic identity are loaded and verified (as covered in the previous document), the daemon must actually come alive. It does this by bringing up a series of concurrent background tasks that collectively run the entire Kinetic network node. Specifically, it handles:

  • Bootstrapping the network governance state from external seed nodes over HTTP.
  • Wiring up the asynchronous communication channels required for inter-task coordination.
  • Spawning the network event loop to dial peers and join the distributed swarm.
  • Spawning all the highly-concurrent sub-services (like the gossip processor, Proof-of-Work miner, local proxy server, and DNS resolver).
  • Registering itself dynamically with the kinetic-pac proxy auto-configuration system via the filesystem.
  • Trapping OS-level signals and waiting for a shutdown command to clean up its local state gracefully without stranding the user’s internet connection. This file acts as the central nervous system of the node. Everything that happens continuously in the background is orchestrated, spawned, and monitored by the code documented in this file. It is the critical bridge between static initialization and a living, breathing network participant.

Why Kinetic Needs This

A decentralized network node is not a simple, single-threaded script that runs from top to bottom and then exits. It is a concurrent, long-running engine that must perform a multitude of complex I/O and compute-bound tasks simultaneously. Consider the workload of a fully operational Kinetic node under normal conditions:

  1. Networking: It must continuously listen for incoming libp2p peer connections over the internet.
  2. Consensus: It must constantly mine VDF (Verifiable Delay Function) proofs to participate in network consensus and earn block rewards.
  3. Web Interception: It must act as a local HTTP proxy server for the user’s web browser, intercepting decentralized traffic and serving local responses on the fly.
  4. Domain Resolution: It must answer raw UDP and TCP DNS queries if it is configured to act as a local domain name resolver.
  5. Administration: It must respond instantly to local API calls originating from the command-line interface (e.g., when a user types kinetic status). If these distinct systems were not split into separate, non-blocking background tasks, the node would freeze instantly.

The Problem with Single-Threaded Execution

Imagine if the daemon used a simple loop architecture. If a slow API request from the CLI took five seconds to process, it would block the network layer from gossiping blocks for those entire five seconds. This would cause the node to fall out of sync with the rest of the network, potentially leading to slashed stakes or rejected blocks. Alternatively, if a complex Proof-of-Work hash was calculated on the main thread, it would starve the proxy server. This means the user’s web browser would time out trying to load a Kinetic website simply because the daemon is too busy doing math to answer the HTTP request.

The Tokio Solution

To solve this, Kinetic uses the tokio async runtime. Tokio ensures that all these moving parts make progress simultaneously, multiplexing them efficiently across the available CPU cores. It uses cooperative multitasking, meaning tasks yield control back to the runtime when they are waiting for I/O (like a network response), allowing other tasks to run in the meantime. This allows a single process to handle thousands of concurrent operations without breaking a sweat.

The Necessity of Graceful Shutdown

Furthermore, a long-running system daemon cannot just be killed abruptly without consequence. When the user hits Ctrl+C in their terminal, or issues a stop command through the systemd service manager, the daemon must shut down cleanly. If the Kinetic daemon exits abruptly without de-registering its local proxy configuration, the user’s operating system will continue to blindly route all web traffic to a proxy port that is no longer listening. This effectively breaks the user’s regular internet connection until they figure out what happened and manually reset their network settings. Graceful shutdown prevents this user experience by running essential cleanup logic before the process is permitted to terminate.


How It Works

The startup sequence follows a precise, deliberate order. Dependencies between services dictate this order—for example, you cannot start the block miner before the network layer is active. Similarly, you cannot start the network layer before the cryptographic keys and governance rules are fully loaded. Let’s walk through the exact steps the daemon takes to boot up, breaking down the mechanics of each section.

1. Bootstrapping Governance State

#![allow(unused)]
fn main() {
-> See: `kinetic-daemon/src/main.rs` — Lines 355 to 440
}

Before the network can validate any rules, process any blocks, or even understand what a valid peer looks like, it needs the current Governance State. The Governance State is a critical data structure that dictates network constants, protocol versions, and critical timestamps like genesis. The daemon first looks for the governance.key file on the local disk inside the base configuration directory (typically ~/.kinetic/). If the file is not found, the daemon realizes it is a brand-new node joining an existing network for the very first time. To get the state, it reaches out to the bootstrap nodes or seed domains specified in its configuration over standard HTTP (port 8000). Why HTTP and not the libp2p swarm? Because to join the libp2p swarm and validate peer identities, you already need the governance state. It creates a chicken-and-egg problem. HTTP provides a simple, out-of-band way to pull the initial ruleset. It creates a reqwest::Client with a tight 5-second timeout, ensuring the boot process doesn’t hang forever if a seed node is offline or unresponsive. When it successfully downloads the state bytes, it deserializes them using bincode. Bincode is a compact binary serialization format used in Rust. It is used instead of JSON because it is significantly faster to parse and maps exactly to the Rust struct’s memory layout, saving precious CPU cycles during boot. However, the node does not blindly trust the download. It performs a strict content validation check. It verifies if the genesis_timestamp_sec in the downloaded state matches the compiled KINETIC_GENESIS_TIME constant defined locally in the core crate.

Warning

This is a critical security measure. Without this check, a malicious seed node (or a man-in-the-middle attacker spoofing DNS) could hand the node a valid, cryptographically signed governance state for a different testnet or a private fork. The node would accept it, join the wrong network, and partition itself from reality.

By checking the hardcoded genesis time, the node guarantees it is connecting to the correct timeline. Once the payload is validated, the state is saved to disk so the node won’t have to download it again on the next boot. Finally, it is loaded into the GLOBAL_GOVERNANCE_STATE mutex so the rest of the application can safely read the current network rules from memory at any time.

2. Wiring Up Asynchronous Communication Channels

#![allow(unused)]
fn main() {
-> See: `kinetic-daemon/src/main.rs` — Lines 441 to 447
}

Tasks in Rust need a safe way to talk to each other without sharing memory directly, which can cause deadlocks or data races. We use channels as asynchronous pipes between these isolated tasks. The daemon creates two vital asynchronous channels before spawning any workers:

  1. incoming_tx / incoming_rx: This is a Multi-Producer Single-Consumer (mpsc) channel with a capacity of 32. This channel is used to route inbound proxy requests. Multiple concurrent network connections can produce requests simultaneously (hence Multi-Producer). However, a single local handler task consumes them sequentially and routes them to the local backend (hence Single-Consumer). If the handler gets overwhelmed, the channel fills up to 32 items, and then producers are forced to wait, preventing memory exhaustion.
  2. gossip_tx / gossip_rx: This is a broadcast channel with a capacity of 100. In a broadcast channel, every subscribed consumer gets a perfect copy of every message. The network layer publishes raw gossip messages (like new blocks, transactions, or drand beacons) to this channel. Any interested worker (like the block processor or the mempool manager) can subscribe to this channel and listen independently. If a consumer is too slow to process messages, it will eventually lag by more than 100 messages, at which point tokio will drop the oldest messages for that specific consumer and return a Lagged error. This guarantees that one slow worker cannot halt the entire network pipeline.

3. Spawning the Network Event Loop

#![allow(unused)]
fn main() {
-> See: `kinetic-daemon/src/main.rs` — Lines 448 to 468
}

The node must connect to the outside world. The NetworkEventLoop is the absolute heart of the libp2p implementation. It is initialized with the local cryptographic key, the database storage layer, and the communication channels we just created. Once initialized, it is immediately spawned into a detached background task using tokio::spawn. From this precise moment forward, the node is officially “online” in the libp2p swarm. It will begin dialing peers, discovering the network topology, responding to ping requests, and participating in the Distributed Hash Table (DHT). The daemon also immediately uses the returned network_client to subscribe to the Quicknet Kyn Gossip topic, ensuring it receives the random beacon numbers necessary for block production.

4. Spawning Sub-Services and Workers

#![allow(unused)]
fn main() {
-> See: `kinetic-daemon/src/main.rs` — Lines 469 to 488
}

With the network active, the daemon fires up the domain-specific workers.

  • PoW Miner Loop: Begins hashing and attempting to mine blocks, provided mining is enabled in the node’s configuration. It requires the network_client to broadcast successful blocks to the rest of the network.
  • Gossip Processor: A dedicated background task that listens to the gossip_rx broadcast channel. As the network layer receives gossip from external peers, this processor decodes the messages, validates their cryptographic signatures, and applies them to the local database state.

5. Starting the Certificate Authority and Proxy Servers

#![allow(unused)]
fn main() {
-> See: `kinetic-daemon/src/main.rs` — Lines 490 to 532
}

Kinetic intercepts decentralized domain traffic (e.g., domains ending in .kyn). To do this over HTTPS without triggering alarming browser warnings, it must act as its own local Certificate Authority (CA) on the user’s machine. The daemon calls ca::load_or_create_root_ca. If it is the very first run, it generates a brand new root certificate and private key. It then wraps a leaf certificate cache in an Arc<tokio::sync::Mutex<LeafCertCache>> so multiple proxy threads can reuse generated certificates for specific domains without recreating them on every single request. Why use an Arc<Mutex>?

  • Arc (Atomic Reference Counted) allows the cache memory to be safely shared across multiple spawned tokio tasks. It keeps track of how many tasks hold a reference, and cleans up the memory only when the count drops to zero.
  • Mutex (Mutual Exclusion) ensures that only one task can write a new certificate to the cache at a time. If two tasks try to create a certificate for example.kyn at the exact same millisecond, the Mutex forces one to wait, preventing data corruption or duplicated effort.
  • It specifically uses tokio::sync::Mutex, not std::sync::Mutex, because tasks need to hold the lock across .await yield points while waiting for the cryptographic signing to complete. It then spawns two major proxy tasks into the tokio runtime:
  1. start_proxy_server: The actual forward proxy server that binds to a local port (e.g., 8080) and handles CONNECT requests directly from the user’s web browser.
  2. handle_incoming_proxy_requests: A reverse proxy handler that takes requests originating from the P2P network and routes them to local backend services securely.

6. Starting the API and Republisher

#![allow(unused)]
fn main() {
-> See: `kinetic-daemon/src/main.rs` — Lines 534 to 557
}

The daemon needs an interface for local administration. The API server (running on port 8000 by default) is started. This is the HTTP endpoint that local CLI tools (like kinetic status or kinetic wallet) connect to when you run commands in your terminal. It allows the user to inspect the state of the node without stopping it. Simultaneously, the Republisher loop is started. Decentralized networks rely on nodes announcing what data they have. The republisher periodically iterates over the data stored in the local database and re-announces it to the DHT, ensuring the wider network knows this node is still actively hosting the content.

7. Registering with Kinetic-PAC

#![allow(unused)]
fn main() {
-> See: `kinetic-daemon/src/main.rs` — Lines 559 to 581
}

The daemon must tell the operating system to send .kyn traffic to its specific proxy server port. It does this through a decoupled, file-based registry system. It resolves the local data directory using dirs::data_local_dir(). This function maps to ~/.local/share on Linux, ~/Library/Application Support on macOS, and %APPDATA% on Windows, preventing brittle hardcoded paths. It creates a JSON file in kinetic_global/proxies/<tld>.json. This tiny JSON file simply contains the proxy IP and proxy port. The kinetic-pac utility (which runs independently as a separate system-level service) constantly watches this directory and builds a dynamic Proxy Auto-Configuration (PAC) script for the operating system. By simply dropping this JSON file into the folder, the daemon dynamically registers itself. When the daemon shuts down, it will remove this file, unregistering itself instantly without needing direct IPC (Inter-Process Communication) with kinetic-pac.

8. The DNS Server (Optional)

#![allow(unused)]
fn main() {
-> See: `kinetic-daemon/src/main.rs` — Lines 583 to 618
}

If standard DNS resolution is needed for legacy compatibility. If enable_dns is true in the user’s configuration, the daemon spins up a UDP and TCP listener using the external hickory_server crate on port 53 (or a custom port if overridden). It passes all incoming DNS queries to a custom KineticDnsHandler. This allows the daemon to resolve traditional DNS A/AAAA records for Atlas domains, acting as a transparent bridge between the decentralized network and legacy networking stacks.

9. Graceful Shutdown

#![allow(unused)]
fn main() {
-> See: `kinetic-daemon/src/main.rs` — Lines 620 to 631
}

The orchestration of a clean, safe exit. Everything comes together in a tokio::select! block at the very end of async_main. The tokio::select! macro is a powerful Rust concept that waits for multiple asynchronous futures simultaneously, but only proceeds when the first one completes. The other pending futures are instantly cancelled. It is waiting for two distinct events:

  1. api_future: The API server crashing and returning an error.
  2. shutdown_signal(): A function that listens for a Ctrl+C (SIGINT) or SIGTERM signal from the operating system. If the user presses Ctrl+C, the shutdown_signal() future resolves first. The block immediately executes its cleanup code.

Important

The most critical piece of cleanup is deleting the proxy JSON file from the global proxies directory. If the daemon failed to delete this file before exiting, the OS would keep trying to route web traffic to a proxy port that is dead, breaking the user’s internet connection.

10. The Main Entrypoint

#![allow(unused)]
fn main() {
-> See: `kinetic-daemon/src/main.rs` — Lines 633 to 670
}

The synchronous wrapper around the asynchronous engine. The actual main function is remarkably short. It first installs the rustls crypto provider, which is required for secure connections in modern Rust TLS stacks. Then, it manually builds the tokio multithreaded runtime using Builder::new_multi_thread().enable_all().build(). The enable_all() call is crucial—it turns on the I/O and Timer drivers, which are required for networking sockets and sleep timeouts to function correctly. It then blocks the main thread on the async_main function. async_main parses the command-line arguments using clap (Cli::parse()). Depending on the subcommand, it will either install the daemon as a systemd service (Install), uninstall it (Uninstall), start the background service via systemctl (Start), or run the daemon loop directly in the foreground (Run).

11. Cryptography Initialization

#![allow(unused)]
fn main() {
-> See: `kinetic-daemon/src/main.rs` — Lines 634 to 639
}

Before Tokio is even started, the daemon must prepare its cryptographic primitives. The daemon calls rustls::crypto::ring::default_provider().install_default(). Rustls is a modern, memory-safe TLS library written in Rust. By default, rustls requires a cryptography provider (the actual engine that does the math) to be installed. Here, we install the ring provider, which is optimized and widely audited. If this provider is already installed (for instance, if another dependency initialized it first), it gracefully ignores the error and continues. Without this step, any attempt to establish an HTTPS connection (like the proxy server or the governance bootstrap) would panic immediately.

12. Command Line Interface (CLI) Subcommands

#![allow(unused)]
fn main() {
-> See: `kinetic-daemon/src/main.rs` — Lines 651 to 667
}

The daemon does not just run the network node; it also manages its own installation. Using the clap crate, it parses the command line arguments into an enum called Commands. Based on the matched command, it routes execution to different handler functions:

  • Install: Takes a user and config_dir argument and generates a systemd service file, installing the daemon to run automatically on boot.
  • Uninstall: Removes the systemd service file and disables the daemon.
  • Start: A convenience wrapper that issues a systemctl start kinetic-daemon command to the OS.
  • Stop: A convenience wrapper that issues a systemctl stop kinetic-daemon command.
  • Run (or no command): This is the default path. It actually executes the heavy run_daemon() function, which is the massive asynchronous loop we documented above. This unified binary design makes it easy for users to manage the daemon without writing their own service files.

13. The tokio runtime block_on

#![allow(unused)]
fn main() {
-> See: `kinetic-daemon/src/main.rs` — Lines 641 to 646
}

The main function is synchronous, but async_main is asynchronous. To bridge the gap, main calls block_on(async_main()) on the runtime it just built. This function blocks the OS thread until the entire future provided to it resolves. When async_main finally returns (due to a graceful shutdown or fatal error), block_on completes, the runtime is dropped, all remaining background tasks are cancelled, and the process exits to the operating system.

14. Error Handling and the Result Type

Throughout the daemon’s startup, you will notice many functions return anyhow::Result<()>. This is because starting a network node is dependent on the environment: files must be readable, ports must be available, and the network must be reachable. If a critical step fails (like being unable to bind to the DNS port because another process is using it), the daemon uses the ? operator to bubble the error up to main. This ensures the daemon fails fast and loudly with a descriptive error message, rather than limping along in a broken state.


Key Pieces

Here is a detailed breakdown of the essential concepts and types introduced in this section of the codebase.

tokio::spawn

  • What it does: The primary mechanism for kicking an asynchronous task into the background.
  • Where it lives: Used throughout main.rs, particularly around lines 459, 507, 524, and 609.
  • Why it matters: Every major service (network, proxy, API) is spawned this way so they run concurrently without blocking the main thread. Without it, the node would process one thing at a time and grind to an absolute halt.

reqwest::Client

  • What it does: Used during startup to download the governance.key from bootstrap nodes if it does not exist locally.
  • Where it lives: Lines 364 to 367.
  • Why it matters: Configured with a very short timeout to prevent boot hangs if the seed node is unreachable.

tokio::sync::broadcast

  • What it does: A channel type used for gossip_tx.
  • Where it lives: Line 442.
  • Why it matters: It allows the single network loop to broadcast a message (like a new block) to multiple independent listeners simultaneously. It intentionally drops slow consumers if they lag behind, preventing unbounded memory growth.

tokio::sync::mpsc

  • What it does: A Multi-Producer Single-Consumer channel.
  • Where it lives: Line 441.
  • Why it matters: Used for incoming proxy requests, allowing multiple concurrent network threads to safely funnel requests to a single localized handler without locking overhead.

hickory_server

  • What it does: The external crate used to run the built-in DNS server.
  • Where it lives: Lines 593 to 607.
  • Why it matters: It expertly manages both UDP and TCP socket listeners efficiently, conforming to strict RFC standards for DNS packet handling.

kinetic_global/proxies

  • What it does: The shared filesystem directory where the daemon communicates its proxy port to the system-wide PAC script generator.
  • Where it lives: Lines 560 to 578.
  • Why it matters: This file-drop strategy decouples the daemon from the PAC service, meaning they don’t have to be tightly integrated via complex IPC (Inter-Process Communication).

tokio::select!

  • What it does: The macro that races multiple futures against each other.
  • Where it lives: Lines 620 to 628.
  • Why it matters: It allows the node to wait for either a fatal error in a critical service or a graceful shutdown signal from the operating system, ensuring that an exit path is always available.

dirs::data_local_dir()

  • What it does: A utility function to locate the correct user-specific data directory based on the operating system.
  • Where it lives: Line 560.
  • Why it matters: Hardcoding paths like /etc/ or ~/.local/share breaks on Windows or macOS. This ensures cross-platform compatibility for proxy registration.

tokio::sync::Mutex

  • What it does: An asynchronous lock that guarantees mutually exclusive access to shared data.
  • Where it lives: Line 501.
  • Why it matters: Unlike the standard library’s std::sync::Mutex, Tokio’s Mutex allows a task to hold the lock across .await points without blocking the underlying OS thread, which is vital for high-concurrency environments like the proxy server.

How This Connects to the Rest of Kinetic

This file touches almost every other crate in the Kinetic ecosystem, acting as the final consumer of the various libraries.

  • CROSS-CRATE: GovernanceState — defined and explained in docs/learn/core/04_governance.md
  • CROSS-CRATE: NetworkEventLoop — defined and explained in docs/learn/network/02_event_loop.md
  • CROSS-CRATE: shutdown_signal — defined and explained in docs/learn/core/07_shutdown.md
  • CROSS-CRATE: KineticDnsHandler — defined and explained in docs/learn/dns/01_overview.md

Quick Reference

For fast lookup when returning to this file:

  • Bootstrap Nodes: Queried via HTTP on port 8000 to download governance state on the very first boot.
  • Proxy Registration Path: ~/.local/share/kinetic_global/proxies/<tld>.json (on Linux).
  • DNS Server Ports: Binds to both UDP and TCP on the configured DNS port (default is usually 53, but often overridden).
  • Shutdown Trigger: Ctrl+C (SIGINT) or SIGTERM triggers tokio::select! to unblock and run cleanup.
  • Critical Cleanup Action: Deletes the proxy registration JSON so the OS PAC script stops routing traffic to this node.
  • Async Runtime: Uses Tokio’s multi-threaded scheduler with I/O and timer drivers enabled via enable_all().

Open Questions / Things to Revisit

Several design decisions in this file present potential edge cases that should be reviewed as the network scales:

Seed Node Trust

  • The bootstrap process downloads the governance state via HTTP. While it cleverly validates the genesis_timestamp_sec, an attacker could potentially spoof a node if they know the correct timestamp but provide malicious parameters (like vastly inflated block rewards).
  • Question: Is there a way to verify a cryptographic signature on the governance state from the Genesis block itself to prevent this vector entirely?

API Server Crash Handling

  • If the API server crashes, the tokio::select! block unblocks and the daemon exits.
  • Question: Is this intended behavior? It means an unhandled API error could bring down the entire decentralized node, which seems fragile for a production environment where the network layer could otherwise survive an API failure.

DNS Server Errors

  • The DNS server future is spawned in the background, but if it crashes, it just logs an error using tracing::error! and does not bring down the daemon.
  • Question: This is inconsistent with the API server behavior. Should a DNS crash also trigger a graceful shutdown sequence, or should the API server failure also be handled gracefully without bringing down the entire node?

Proxy State on Crash

  • If the node panics or is killed with SIGKILL (which bypasses the graceful shutdown trap entirely), the proxy JSON file is left behind in the kinetic_global directory.
  • Question: This will break the user’s internet because the OS will keep routing to a dead proxy. Should there be a heartbeat mechanism or stale-file check in the kinetic-pac daemon to handle ungraceful exits automatically?

Channel Capacities

  • The incoming_tx channel has a hardcoded capacity of 32, and gossip_tx has a capacity of 100.
  • Question: Are these numbers empirically derived from network simulation? Under heavy DDoS or a massive gossip storm, will these small buffers drop legitimate traffic, or are they sized to apply backpressure?

Root CA Expiry

  • The ca::load_or_create_root_ca function loads a Root CA, but there is no logic visible here for handling what happens if the Root CA reaches its expiration date while the daemon is actively running.
  • Question: Does the CA automatically rotate, or does the user have to restart the daemon to generate a new valid Root CA?

API VDF Registration Layer (Part 1)

Crate: kinetic-daemon Stage: 9 Reading time: 45 minutes Depends on: docs/learn/core/06_vdf_consensus.md, docs/learn/network/04_dht_publish.md


What Is This?

This document provides a comprehensive, line-by-line architectural breakdown of the first half of the kinetic-daemon/src/api/vdf.rs file. Specifically, it covers lines 1 through 367 of the source code. This file serves as the primary HTTP gateway for the Kinetic daemon. It is the exact endpoint hit when a user, an automated client, or the web dashboard attempts to register a brand new Kinetic name. Because Kinetic operates without a centralized naming authority, names cannot simply be purchased or inserted into a database. Instead, they must be cryptographically earned. This earning process is achieved by expending significant computational effort. The effort is quantified and proven using a Verifiable Delay Function (VDF). This file is the specific location in the codebase where that massive computational process is initiated. It acts as a complex background job orchestrator. It receives a fast, synchronous HTTP request from the user. It translates that request into a long-running, asynchronous lifecycle. This lifecycle can take anywhere from a few minutes for long names, to several months for very short, contested names. During this extended lifecycle, the daemon must manage internal state. It must enforce strict concurrency limits to prevent the host machine from crashing. It must interface with external randomness beacons to prove the timeline of the registration. It must perform heavy cryptographic evaluations without locking up the rest of the node. Finally, it must interact with the Distributed Hash Table (DHT) to publish the proofs. This specific document focuses exclusively on the handle_vdf_register function. This function manages the initial parsing, the spawning of the background worker, and the execution of the entire state machine.


Why Kinetic Needs This

In a traditional web architecture, registering a username is trivial. The API receives a request, checks a database for availability, and performs an immediate insert. The entire transaction completes in milliseconds. The HTTP connection remains open, and the user gets instant feedback. Kinetic’s architecture shatters this paradigm. Because Kinetic uses VDFs to enforce a verifiable time delay, registration is an heavy operation. A single name registration request might force a CPU core to run at 100% utilization for weeks on end. If the Kinetic daemon attempted to handle this within the standard HTTP request-response cycle, it would fail catastrophically. The HTTP connection from the client would instantly time out. More importantly, the async executor running the Axum web server would lock up completely. The entire node would become unresponsive to all other network traffic, API requests, and peer communications. Therefore, Kinetic requires a dedicated subsystem to decouple the fast HTTP request from the slow cryptographic work. This file provides that exact decoupling mechanism. It allows the dashboard to say, “Start registering this name.” The API responds immediately with an HTTP 200 Success and a tracking task ID. The user can then navigate away, while the daemon handles the heavy lifting safely in the background.

Furthermore, this file acts as the node’s frontline defense against resource exhaustion attacks. VDF evaluations are intentionally brutal on system resources. If a malicious actor on the local network, or a buggy automated script, sent one hundred simultaneous registration requests, the operating system would try to spawn one hundred concurrent VDF threads. This would instantly starve the host machine of CPU cycles and memory. The node would crash, and it could potentially take down the underlying host operating system. By implementing strict state locks and concurrency semaphores, this code ensures node stability. It guarantees that no matter how many requests are received, the daemon will only ever process one VDF at a time.

Finally, this file is the implementation site of a critical consensus fix known as the C-1 fix. It meticulously orchestrates the exact sequence of cryptographic operations. It dictates exactly when the commitment is generated, when the proof is computed, and when the reveal is broadcast. This precise ordering ensures that legitimate registrations are never rejected by the network due to DHT data pruning windows. Simultaneously, it preserves the mathematical guarantees that prevent front-running attacks by other nodes.


How It Works

The registration process is initiated when a POST request hits the /vdf/register API endpoint. The Axum router immediately passes control to the handle_vdf_register function. The execution then flows through a structured, multi-stage pipeline.

Step 1: Request Validation and Role Verification

#![allow(unused)]
fn main() {
-> See: `kinetic-daemon/src/main.rs` — Lines 43 to 64
}

The function signature uses Axum extractors to parse the incoming JSON body. This body is mapped into a VdfRegisterRequest struct. Before any processing begins, the daemon performs crucial authorization checks. It checks the caller’s role via the injected Extension<Role>.

Important

VDF registration requires elevated privileges. If the caller does not possess the VDF or Admin role, the request is immediately aborted. An HTTP 403 Forbidden is returned. This ensures that unauthorized clients cannot force the node to burn expensive CPU cycles.

Next, the requested name string is sanitized. It is passed through kinetic_core::types::normalize_name. This ensures the name follows the strict lowercase, alphanumeric requirements of the protocol. It is then validated using is_valid_apex_name. This confirms it is a structurally valid top-level identifier. If the name contains invalid characters, an HTTP 400 Bad Request is returned.

The code also inspects the optional iterations parameter. Users can theoretically request a higher iteration count to burn more time intentionally. This makes their registration more secure against attackers with specialized hardware.

Warning

However, the daemon enforces a hard safety limit of 10,000,000 iterations (MAX_USER_ITERATIONS). If a user requests more than this, the request is rejected. This prevents users from accidentally bricking their node by requesting a decade-long computation.

Finally, a unique UUID string is generated using uuid::Uuid::new_v4(). This UUID serves as the primary key to track this specific task instance throughout its lifecycle.

Step 2: Concurrency Control and State Locking

#![allow(unused)]
fn main() {
-> See: `kinetic-daemon/src/api/vdf.rs` — Lines 67 to 104
}

Before the background worker is spawned, the daemon must ensure the environment is safe. It acquires a blocking mutex lock on the vdf_tasks HashMap. This map resides within the shared, global ApiState. This lock is critical to the node’s stability. It prevents race conditions if multiple HTTP requests arrive at the exact same millisecond.

While holding the exclusive lock, the code iterates through all existing tasks. It specifically looks for any task where the progress is less than 100% and no error flag is set.

Important

If it finds even a single active task, it rejects the new request. It returns an HTTP 409 Conflict. This enforces the strict protocol rule: The Kinetic daemon will process exactly one VDF registration at a time.

To prevent the state map from growing infinitely and causing a memory leak, the daemon performs routine housekeeping. It filters out completed or errored tasks to keep the map clean. If the total number of historical tracked tasks exceeds 50, it rejects the new request. It returns an HTTP 429 Too Many Requests. This forces the user or client to wait until older tasks are cleared from the state. Once all safety checks pass, the new task is inserted into the map. Its initial status is set to “Initializing”. Its progress is set to 0%. The mutex lock is then deliberately dropped. The API request is now cleared to spawn the worker without holding up other threads.

Step 3: Spawning the Worker and Fetching External Randomness

#![allow(unused)]
fn main() {
-> See: `kinetic-daemon/src/api/vdf.rs` — Lines 113 to 124
}

The API handler invokes tokio::spawn. This creates a detached, asynchronous background task. The moment this task is spawned, the HTTP response is sent back to the user. The background task continues executing independently of the HTTP connection.

Inside this background worker, the first action is to interface with the Drand network. The code initializes a new DrandClient. It provides the client with a storage_clone so it can cache beacon data if needed. It then calls fetch_latest().await.

Important

This is a non-negotiable requirement of the Kinetic consensus protocol. To prove that a VDF was computed after a specific point in time, the commitment must incorporate unpredictable randomness. This prevents attackers from pre-computing VDFs years in advance.

The background worker updates its status in the shared state map to “Fetching Drand beacon”. It then awaits the network response. If the Drand network is unreachable, the task errors out immediately. It updates its state so the user’s dashboard can display the exact network failure.

Step 4: Generating the Cryptographic Commitment Hash

#![allow(unused)]
fn main() {
-> See: `kinetic-daemon/src/api/vdf.rs` — Lines 136 to 173
}

With the Drand randomness secured, the daemon must generate the commitment hash. This commitment is the mathematical lock that prevents front-running on the network. If the daemon simply broadcasted the finished VDF proof in plaintext, it would be vulnerable. Any malicious node could intercept it, substitute their own public key, and steal the name before the network finalized it.

To prevent this, the daemon generates a unique, opaque, one-way hash locally. First, it loads the node’s local identity keypair from the filesystem at identity.key. It extracts the raw public key bytes from this pair. Next, it uses the operating system’s cryptographic random number generator (getrandom::fill).

Warning

It creates a secure 32-byte salt. If this OS-level RNG fails, the task aborts for security reasons.

The code then instantiates a SHA-256 hasher. It feeds the hasher the following precise ingredients in a very specific order:

  1. The requested FQDN string.
  2. The generated 32-byte secret salt.
  3. The Drand randomness signature bytes.
  4. The node’s raw public key bytes.

The resulting 32-byte hash is wrapped inside a Commitment struct. Because the salt and the public key are kept secret within the daemon’s local memory, this hash is impossible to reverse. It locks the node’s identity to the specific name. Crucially, it accomplishes this lock without revealing what the name actually is to the rest of the network.

Step 5: The Heavy VDF Evaluation and Threading Isolation

#![allow(unused)]
fn main() {
-> See: `kinetic-daemon/src/api/vdf.rs` — Lines 175 to 219
}

This step represents the actual execution of the Proof of Work. The daemon calculates the required number of iterations. This is based on the consensus math scaling rules for the specific length of the name. It takes the maximum of the strict consensus requirement and the user’s requested override. The daemon then instantiates the core ChiaVdfEngine.

Before executing the math, the daemon acquires a permit from the vdf_semaphore. This acts as a secondary, engine-level defense mechanism. Even though the HTTP layer checked for active tasks, the semaphore ensures global safety across the daemon. If any other part of the daemon codebase attempts to spawn a VDF, they will be blocked here. This prevents concurrent executions from ever crashing the entire host system.

Important

The most critical architectural decision in this entire file is the use of tokio::task::spawn_blocking. The Chia VDF evaluation is a tightly optimized, purely CPU-bound mathematical loop. It does not contain any .await yield points. It will never yield control back to the Tokio async runtime voluntarily. If this engine were run directly on an async worker thread, that thread would freeze completely. It could freeze for weeks. All other asynchronous tasks assigned to that thread would stall instantly. This would cause a failure of the node’s networking and API layers. By wrapping the engine execution in spawn_blocking, Tokio moves the heavy computation. It moves it to a dedicated thread pool specifically designed for synchronous, blocking operations.

The main async task then simply .awaits the result from that separate thread pool. Once the proof is successfully returned, the semaphore permit is dropped. This frees the engine for any future requests.

Step 6: The C-1 Consensus Fix and Deferred Broadcast

#![allow(unused)]
fn main() {
-> See: `kinetic-daemon/src/api/vdf.rs` — Lines 221 to 254
}

This section of code implements a massive architectural fix for the Kinetic protocol. In the original protocol design, the commitment hash was broadcast to the DHT before the VDF evaluation started. The original logic seemed sound: announce your intent, do the work, then reveal the proof. However, this ignored the reality of distributed peer-to-peer systems. DHT nodes routinely prune old records to save disk space and maintain performance. If a user attempted to register a short name, the VDF might take two months to compute. By the time the two months elapsed, the original commitment broadcast had been purged from the global DHT entirely. When the user finally published their reveal, the network validators would look for the initial commitment. They would fail to find it. They would then reject the registration as invalid. This systemic flaw was known as the C-1 bug.

Note

The solution implemented here is called Deferred Broadcast. The commitment hash is generated at the very beginning of the pipeline. This ensures it incorporates the correct, early Drand timestamp. However, it is held private in local memory while the VDF runs. Only after the VDF proof is successfully computed does the daemon serialize the commitment. It is then broadcast to the DHT via publish_redundant_payload.

This guarantees that the commitment is fresh on the network when the reveal arrives. However, the consensus rules dictate that a commitment must “mature” before a reveal is accepted. This maturation window ensures it has propagated widely enough to prevent last-second front-running attacks. Therefore, after broadcasting the commitment, the daemon sleeps. It calculates the exact wait time mathematically. It multiplies CONSENSUS_MINIMUM_COMMIT_AGE_KYNS by the DRAND_PERIOD. It then adds a 2-second safety buffer to account for network latency and clock drift. During this sleep, the commitment propagates through the DHT. The commitment hash remains opaque during this time. Therefore, front-running is still impossible during this maturation window.

Step 7: Publishing the Reveal and Finalizing State

#![allow(unused)]
fn main() {
-> See: `kinetic-daemon/src/api/vdf.rs` — Lines 258 to 365
}

Once the maturation sleep concludes, the daemon constructs the final Reveal payload struct. This struct is the ultimate cryptographic proof of registration for the network. It contains the FQDN, the secret salt, the exact Drand timestamp, and the Drand signature used. It also contains the total number of iterations computed. It contains the raw VDF proof bytes. It contains the node’s public key.

Crucially, it also includes a default DnsZone payload embedded inside it. This ensures that the moment the name is registered globally, it has a valid DNS zone attached to it. Even though the zone is empty, it is ready for the user to configure immediately. The entire Reveal struct is then serialized into a standard byte array format. The daemon uses its ML-DSA private key to generate a post-quantum signature over this specific payload. This finalizes the struct and irrevocably proves ownership.

The complete, signed Reveal is serialized to JSON. It is broadcast to the DHT using the exact same redundant publication mechanism as the commitment. If the network accepts the broadcast without throwing errors, the registration is effectively complete from a global consensus perspective.

However, the daemon must still update its own local internal state. It acquires a lock on OWNED_NAMES_LOCK. It opens the local storage database specifically pointing at DB_PREFIX_OWNED_NAMES. It appends the newly registered FQDN string to the list of owned names. This ensures the user’s dashboard will instantly reflect the successful acquisition without needing to query the network. It implements a safety bound here: if the list exceeds 10,000 names, it deliberately truncates the oldest entries. This prevents the local database array from bloating infinitely and causing parsing delays. Finally, the daemon creates the actual physical zone file on the local filesystem. It writes the empty JSON structure to disk inside the configured zones_dir. This allows the user to immediately begin editing their DNS records via the local dashboard interface. The background task status is finally updated to “Complete” at 100% progress. The background thread then gracefully terminates, freeing all resources.


Key Pieces

NameRenewRequest & VdfRegisterRequest

#![allow(unused)]
fn main() {
-> See: `kinetic-daemon/src/api/vdf.rs` — Lines 15 to 30
}

These are the data structures responsible for deserializing the incoming JSON payloads from the HTTP client. They are intentionally minimalist in design to keep the API surface area small. They require only the name string to be operated on. They accept an optional iterations field for advanced users. The iterations field allows developers or secure users to force the VDF to run longer than the consensus minimum. This provides an extra layer of security against attackers with specialized, fast hardware. This field is capped by the daemon’s internal validation logic to prevent self-inflicted denial of service.

handle_vdf_register

#![allow(unused)]
fn main() {
-> See: `kinetic-daemon/src/api/vdf.rs` — Lines 38 to 367
}

This is the master orchestration function for the entire name registration lifecycle. It operates across two distinct execution domains simultaneously. First, it handles the synchronous, immediate HTTP request-response cycle for the API. Second, it manages the asynchronous, long-lived background task domain for the actual cryptographic work. Its primary architectural responsibility is to safely bridge these two isolated worlds. It must accomplish this without ever compromising the stability or responsiveness of the host node.

The Task State Lock (state.vdf_tasks.lock())

#![allow(unused)]
fn main() {
-> See: `kinetic-daemon/src/api/vdf.rs` — Lines 69 to 104
}

This standard library Mutex is the node’s absolute primary defense against state corruption. It is also the primary defense against CPU resource exhaustion. By locking the shared state map before checking the active task count, it ensures absolute execution atomicity. Without this lock, two simultaneous HTTP requests could theoretically both read the active task count as zero simultaneously. Both requests would then proceed to spawn massive VDF background tasks at the same time. This would instantly violate the concurrency limits of the system and crash the node.

tokio::task::spawn_blocking

#![allow(unused)]
fn main() {
-> See: `kinetic-daemon/src/api/vdf.rs` — Lines 197 to 216
}

This is a vital Tokio framework API utilized to protect the asynchronous runtime. The Rust async model relies on cooperative yielding to function efficiently. Tasks must periodically .await to give the CPU back to the executor so it can run other pending tasks. The Chia VDF engine inherently does not yield. It is a pure, unbreakable mathematical loop. If run directly, it would hijack the executor thread and permanently. spawn_blocking forces this rogue, non-yielding computation to a separate thread pool. This preserves the health and responsiveness of the core async system.

The C-1 Fix Sleep Mechanism

#![allow(unused)]
fn main() {
-> See: `kinetic-daemon/src/api/vdf.rs` — Lines 247 to 254
}

This small block of code represents a massive architectural evolution in the Kinetic protocol. By calculating the exact required maturity time (CONSENSUS_MINIMUM_COMMIT_AGE_KYNS * DRAND_PERIOD), the daemon is precise. By forcing an async sleep, the daemon ensures that the locally-held, freshly-broadcast commitment has enough time to propagate globally. It ensures it satisfies the network validators before the reveal is sent. This bypasses the historical issue where long-running VDFs would cause their commitments to expire before the proof could even be computed.


How This Connects to the Rest of Kinetic

The API VDF layer is dependent on multiple other crates within the Kinetic ecosystem. It cannot function in isolation. It acts as the central coordinator, pulling in functionality from across the entire codebase to execute the lifecycle.

  • CROSS-CRATE: The initial string validation relies on kinetic_core::types::normalize_name and kinetic_core::types::is_valid_apex_name. These functions enforce the network’s strict naming conventions. (See Stage 7).
  • CROSS-CRATE: The external randomness required for the cryptographic commitment is fetched using the kinetic_core::drand::DrandClient. (See Stage 7).
  • CROSS-CRATE: The actual time-burning mathematical evaluation is handed off to kinetic_vdf::ChiaVdfEngine. The daemon code here does not understand the math; it simply feeds the engine the challenge hash and the iteration count. (See Stage 6).
  • CROSS-CRATE: All communication with the global network, including broadcasting the Commitment and the final Reveal, is routed through the network_clone.publish_redundant_payload() method. This is provided by the kinetic-network crate. (See Stage 8).
  • CROSS-CRATE: The cryptographic signing of the final Reveal struct utilizes the ml_dsa post-quantum signature schemes. These are deeply integrated into kinetic-core. (See Stage 7).

Quick Reference

For rapid recall of the constraints and workflows defined in this specific file:

  • Role Requirement: The API caller MUST possess either the VDF or Admin role to successfully trigger this endpoint.
  • Iteration Hardcap: The maximum allowed user-requested iterations is capped at 10,000,000. Anything higher is instantly rejected with an HTTP 400.
  • Concurrency Limit: The daemon enforces a strict limit of exactly 1 active VDF registration task at any given moment to protect CPU resources.
  • Task Memory Limit: The in-memory tracking map retains a maximum of 50 task histories. Once this is reached, it begins pruning older entries or rejecting new requests entirely.
  • Workflow Order: Request Validation -> Fetch Drand Randomness -> Generate Secret Hash Locally -> Compute VDF (On a Blocking Thread) -> Broadcast Hash to DHT -> Sleep for Maturity Window -> Broadcast Final Reveal -> Update Local Database.
  • Async Safety: All VDF mathematical evaluation is isolated using Tokio’s spawn_blocking mechanism to prevent executor starvation and node lockup.
  • Local Artifacts: Upon success, the name is appended to DB_PREFIX_OWNED_NAMES in local storage, and a blank .json configuration file is generated in the zones_dir.

Open Questions / Things to Revisit

While this architecture successfully handles the C-1 bug and protects the async runtime, there are several areas that may require future architectural review by the original author (Saif):

  • Lack of Persistent State for Active Tasks: The entire vdf_tasks state tracking mechanism is held purely in volatile memory within the Axum ApiState. If a user is attempting to register a 3-character name that requires two months of VDF computation, and the daemon is restarted for a routine software update on day 59, the entire process is permanently lost. There is currently no checkpointing or serialization mechanism implemented here to resume a partially completed VDF evaluation from disk. This is a significant UX vulnerability for long-running registrations.
  • Redundant Concurrency Checks: The code currently employs a double-layer defense against concurrency. First, it locks the state map and counts active tasks directly (lines 71-82). Second, it acquires a permit from a vdf_semaphore right before mathematical execution (line 189). While defense-in-depth is generally valuable, the semaphore might be functionally redundant for HTTP-initiated tasks. Unless other internal daemon processes are also permitted to bypass the HTTP API and directly request VDF evaluations, the initial state lock should be sufficient to prevent concurrency.
  • Error Granularity in Blocking Tasks: When the tokio::task::spawn_blocking call fails, the error is caught generically as a “Task panic” and written to the status map. The daemon makes no distinction between a mathematical error thrown gracefully by the ChiaVdfEngine and a thread-spawning failure thrown by the underlying operating system. More granular error matching and handling here could significantly improve dashboard diagnostics for the end user when registrations fail unexpectedly.

VDF API Processing — Part 2: Renewals & Task Management

Crate: kinetic-daemon Stage: 9 Reading time: 20 minutes Depends on: 04_api_vdf_1.md (VDF Generation), docs/learn/verify/01_overview.md, docs/learn/core/01_overview.md


What Is This?

This file completes our deep dive into the VDF API layer of the Kinetic Daemon by documenting the second half of kinetic-daemon/src/api/vdf.rs. While the first half focused exclusively on generating new identities and names from scratch, this half focuses on the lifecycle of existing names. Specifically, it covers the logic required to renew a Kinetic name, which involves reading old cryptographic proofs from the local storage database and linking them to new, freshly computed proofs.

Additionally, because VDF operations (both generation and renewal) are intentionally slow, blocking operations that utilize the CPU, they cannot be processed synchronously within a standard HTTP request-response cycle. If the daemon tried to compute a 10-minute VDF while the HTTP client waited, the network connection would time out and the HTTP worker thread would be stalled. Instead, the daemon uses an asynchronous background task architecture. The endpoints documented in this file include the mechanisms the daemon uses to expose the status of those background tasks to the outside world, allowing user interfaces to poll for progress, handle errors gracefully, and clean up memory when tasks are complete.


Why Kinetic Needs This

The Kinetic network is designed around the principle of continuous proof of work. In traditional centralized domain name systems (like DNS), you maintain ownership of a name by paying a central registrar a yearly fiat fee. In Kinetic, there is no central registrar, and there are no ongoing fiat or token fees for standard names. Instead, you pay with computational time. To maintain ownership of a standard name, you must periodically prove that you are still willing to dedicate CPU resources to it by generating a new Verifiable Delay Function (VDF) proof. If you fail to do this before your previous proof expires, the network considers the name abandoned, and anyone else can claim it.

The VDF utilized here (the Chia VDF Engine) guarantees that time has passed because it is sequential; it cannot be parallelized across multiple cores or GPUs. This levels the playing field so that massive data centers do not have an unfair advantage over standard consumer hardware when claiming and renewing names.

However, a critical architectural decision was made regarding how renewals work. If a user has owned a name for five years, they have built up a massive chain of VDF proofs over time. If they were forced to generate an new proof from scratch every time they renewed, the computational burden would be staggering and punishing to long-term participants. To solve this, Kinetic implements a renewal discount. When you renew a name, you reference your previous proof. Because the network can verify the link between the old proof and the new one, it grants an 80% discount on the required VDF iterations. You only have to do 20% of the work that a brand new registration would require.

To make this discount a reality, the daemon needs a specialized API endpoint (handle_vdf_renew). This endpoint must act as an orchestrator between the local storage database (where the old proof lives), the network (to fetch new randomness), the local CPU (to calculate the new 20% proof), and the DHT (to broadcast the linked proofs).

Furthermore, because these operations are dispatched to background threads, the daemon needs a robust way to communicate across thread boundaries. If a background thread encounters a network error while fetching Drand, the HTTP client needs to know immediately. If the thread successfully finishes the 20% computation, the client needs the final transaction hash. The task management endpoints (handle_vdf_status and handle_vdf_status_delete) provide the necessary visibility into the black box of Tokio background tasks without exposing the underlying memory structures.


How It Works

The back half of vdf.rs revolves around the handle_vdf_renew function and its supporting task management infrastructure. Let us break down the exact sequence of events when a renewal request hits the daemon.

Phase 1: Request Validation and Queue Management

When an HTTP POST request arrives at the renewal endpoint, it carries a JSON payload matching the NameRenewRequest struct. The daemon immediately performs several synchronous checks before dedicating any background resources to the request.

Authorization via Axum Extensions

-> See: kinetic-daemon/src/api/vdf.rs — Lines 378 to 397

The handler function signature makes heavy use of Axum extractors:

#![allow(unused)]
fn main() {
pub async fn handle_vdf_renew(
    Extension(role): Extension<Role>,
    State(state): State<ApiState>,
    Json(req): Json<NameRenewRequest>,
)
}

The Extension extractor pulls data that was inserted earlier in the request lifecycle by an authentication middleware. The daemon uses this to retrieve the Role of the API caller. If the caller does not possess the can_vdf privilege (usually reserved exclusively for the Node Admin), the request is instantly rejected with a 403 FORBIDDEN status code.

If authorized, the daemon extracts the target name from the JSON payload and passes it through kinetic_core::types::normalize_name. This ensures the name is converted to lowercase and stripped of any invalid or hidden characters. The daemon then verifies that the name is a valid apex name (e.g., alice, not alice.bob). If the format is invalid, a 400 BAD REQUEST is returned.

Iteration Bounds and Task Pruning

-> See: kinetic-daemon/src/api/vdf.rs — Lines 398 to 415

The user can optionally request a specific number of iterations (useful if they want to over-collateralize their name against faster CPUs in the future). To prevent denial-of-service attacks where a user requests an astronomically large number of iterations (which would tie up the node’s CPU indefinitely), the daemon enforces a hard limit of MAX_USER_ITERATIONS, set to 10,000,000.

Next, the daemon accesses the shared vdf_tasks state map. This map holds the status of all currently running or recently completed tasks. Before adding a new task, the daemon performs maintenance using the retain method:

#![allow(unused)]
fn main() {
tasks.retain(|_, t| t.progress < 100 && t.error.is_none());
}

This line iterates over the map and drops any task that has reached 100% progress or encountered an error. If, after this cleanup, there are still 50 or more tasks actively running, the daemon rejects the new request with a 429 TOO MANY REQUESTS error. This is a critical self-preservation mechanism ensuring the node does not exhaust its memory or CPU threads.

Task Initialization

-> See: kinetic-daemon/src/api/vdf.rs — Lines 417 to 425

If there is space in the queue, the daemon generates a unique UUID (Version 4) to identify the task. It creates a new VdfTaskStatus object, initialized with 0% progress and a status message of “Starting Renewal…”. It inserts this into the map, drops the Mutex lock to free up the shared state for other API requests, and returns the UUID to the HTTP client. Everything from this point forward happens asynchronously.

Phase 2: The Asynchronous Renewal Pipeline

The daemon uses tokio::spawn to launch a green thread that handles the heavy lifting. This thread moves sequentially through several stages, updating the task status map along the way.

Step 1: Retrieving the Previous Proof from Sled

-> See: kinetic-daemon/src/api/vdf.rs — Lines 434 to 470

To qualify for the renewal discount, the daemon must present the exact cryptographic state of the name as it currently exists on the network. It attempts to load this state from the local Sled database via the storage_clone. It constructs the database key by concatenating kinetic_core::constants::DB_PREFIX_REVEAL and the normalized FQDN. Sled operates on raw bytes (IVec). If the raw bytes are found, the daemon uses serde_json::from_slice to deserialize them into a strongly-typed NameRecord.

A critical architectural distinction is made here based on the enum variant of the NameRecord:

  • If the record is a Standard name, the daemon extracts the previous reveal data and proceeds.
  • If the record is a Premium name, the daemon halts and logs an error to the task status. Premium names are purchased with KYN tokens and are permanently exempt from VDF resquaring. Attempting to run a VDF renewal on a premium name is a logical error, as the network consensus rules do not require it.

Step 2: Fetching Fresh Randomness

-> See: kinetic-daemon/src/api/vdf.rs — Lines 472 to 481

The daemon instantiates a DrandClient and fetches the latest beacon from the Drand network. This ensures that the new VDF computation is seeded with cryptographic unpredictability that did not exist when the previous proof was generated. This prevents users from pre-computing their renewals years in advance. If the fetch fails, the task aborts and updates the error state.

Step 3: Crafting the Private Commitment

-> See: kinetic-daemon/src/api/vdf.rs — Lines 483 to 521

The daemon generates a new 32-byte secure random salt using the getrandom crate. It then constructs a SHA-256 hash that binds together:

  1. The FQDN of the name being renewed
  2. The newly generated salt
  3. The cryptographic randomness from the latest Drand beacon
  4. The node’s ML-DSA public identity key

This hash becomes the Commitment. Crucially, the daemon does not broadcast this commitment to the network yet. In early versions of Kinetic, commitments were broadcast before the VDF started. This led to a vulnerability where an attacker could observe a commitment on the DHT and start computing a parallel VDF to steal the name. By generating the commitment privately and keeping it hidden until the VDF is complete, the daemon protects the user’s computational effort.

Step 4: The Discounted VDF Evaluation (Blocking Thread)

-> See: kinetic-daemon/src/api/vdf.rs — Lines 523 to 568

The daemon consults the ConsensusParams to determine the baseline number of iterations required for the name. It then applies the 80% renewal discount:

#![allow(unused)]
fn main() {
let discounted_iters = (required_iters as f64 * 0.2) as u64;
}

The daemon will execute whichever is larger: the discounted minimum, or the iterations requested by the user.

Because VDF evaluation is purely CPU-bound and can take several minutes, it cannot be run on standard Tokio async threads, which are meant for fast, non-blocking I/O. If a VDF was run on a standard async thread, it would block the executor from processing other network events or HTTP requests, stalling the entire node. Instead, the daemon requests a permit from the vdf_semaphore (to ensure the node isn’t running too many VDFs simultaneously). Once granted, it uses tokio::task::spawn_blocking to move the Chia VDF engine execution to a dedicated OS thread pool specifically designed for heavy, long-running computational workloads.

Step 5: Broadcast and Mandatory Maturation Delay

-> See: kinetic-daemon/src/api/vdf.rs — Lines 570 to 603

Once the blocking thread returns the completed VDF proof, the daemon finally broadcasts the Commitment to the DHT.

However, the network consensus rules state that a Reveal is only valid if its corresponding Commitment has been visible on the network for a minimum amount of time (CONSENSUS_MINIMUM_COMMIT_AGE_KYNS). Therefore, the daemon must intentionally pause its workflow. It calculates the required wait time in seconds (usually spanning a few Drand epochs plus a small buffer) and calls tokio::time::sleep. This suspends the background task entirely, yielding the thread back to the Tokio executor to do other work, until the commitment has aged sufficiently.

Step 6: Assembling and Publishing the Reveal

-> See: kinetic-daemon/src/api/vdf.rs — Lines 605 to 665

After waking up from the sleep, the daemon constructs the final Reveal struct. It creates a PreviousProof struct containing the salt, drand signature, iterations, vdf proof, and ML-DSA signature from the old reveal. It attaches this PreviousProof to the new Reveal. This is what allows the network nodes to verify the chain of custody and authorize the 80% discount. It also ensures that any payload data (like DNS records) attached to the old name is preserved in the new Reveal.

Next, it applies the ML-DSA post-quantum signature. Because these signatures are quite large and computationally intensive, they are applied at the very end of the process to the signable_bytes of the Reveal. Finally, it serializes the Reveal to JSON, broadcasts it to the DHT via publish_redundant_payload (which ensures the payload reaches multiple distinct nodes on the Kademlia network), and saves the updated record to the local Sled database. The task progress is set to 100%, and the background thread terminates successfully.


Phase 3: Task Status and Visibility

While the background thread is grinding through the steps above, the HTTP client needs a way to check in. The API provides endpoints specifically for this purpose.

Status Updates via handle_vdf_status

-> See: kinetic-daemon/src/api/vdf.rs — Lines 700 to 718

This endpoint uses the Path<String> Axum extractor to grab the UUID from the request URL. It locks the Arc<Mutex<HashMap>> holding the tasks, looks up the UUID, and returns the VdfTaskStatus object. The status object contains a progress integer (0-100), a status string explaining the current step (e.g., “Computing Renewal VDF… (this may take a while)”), and an optional error string.

Notice the use of .unwrap_or_else(|e| e.into_inner()) when locking the Mutex:

#![allow(unused)]
fn main() {
let tasks = state.vdf_tasks.lock().unwrap_or_else(|e| e.into_inner());
}

This handles Mutex Poisoning. If another thread panics while holding the lock, the Mutex becomes “poisoned”, meaning subsequent lock attempts will return an error to prevent the use of potentially corrupted state. However, in this API, the state is just a HashMap of task statuses. If a thread panics, the map might be incomplete, but it is not dangerous to read. into_inner() allows the daemon to safely bypass the poison error and read the map anyway, preventing a single panic from taking down the entire API layer.

Memory Management via handle_vdf_status_delete

-> See: kinetic-daemon/src/api/vdf.rs — Lines 720 to 734

Although the daemon automatically prunes completed tasks when the queue hits 50, well-behaved clients should clean up after themselves. Once a UI client sees that a task has reached 100% progress or hit a fatal error, it should issue a DELETE request to this endpoint. The endpoint locks the map, calls .remove(&task_id), freeing up memory and keeping the queue lean.


Key Pieces

handle_vdf_renew

  • What it does: The primary API handler for initiating the renewal of an existing Kinetic name.
  • Where it lives: kinetic-daemon/src/api/vdf.rs — Lines 378 to 671
  • Why it matters: This function implements the vital 80% renewal discount logic. Without it, maintaining long-term identities on the Kinetic network would be computationally prohibitive for average users.

update_task_status & update_task_error

  • What they do: Internal utility functions used by the background threads to safely mutate the shared task map.
  • Where they live: kinetic-daemon/src/api/vdf.rs — Lines 673 to 698
  • Why they matter: They encapsulate the lock acquisition logic, ensuring that updating a progress bar does not introduce data races or complex locking boilerplate directly inside the main asynchronous flow.

handle_vdf_status

  • What it does: An HTTP GET endpoint that returns the current state of a running task.
  • Where it lives: kinetic-daemon/src/api/vdf.rs — Lines 700 to 718
  • Why it matters: It bridges the gap between long-running asynchronous cryptography and instantaneous HTTP requests, allowing user interfaces to remain responsive while waiting for VDF proofs.

handle_vdf_status_delete

  • What it does: An HTTP DELETE endpoint to manually remove a task from the node’s memory.
  • Where it lives: kinetic-daemon/src/api/vdf.rs — Lines 720 to 734
  • Why it matters: It provides a mechanism for proactive memory management, preventing the vdf_tasks HashMap from slowly leaking memory over the lifetime of the daemon process.

How This Connects to the Rest of Kinetic

  • Storage Subsystem: The renewal process relies on kinetic_storage (wrapped in the ApiState) to locate the previous Reveal data using the DB_PREFIX_REVEAL constant.
  • Core Types: The task relies on the Reveal, NameRecord, and PreviousProof structures defined in the core crate. (CROSS-CRATE: NameRecord — defined and explained in docs/learn/types/06_name_records.md).
  • Consensus Math: The core logic for calculating the baseline iterations and enforcing the delay lives in kinetic_core::consensus_math and kinetic_core::constants.
  • Network Layer: The final output of the VDF process is handed off to kinetic-network via the publish_redundant_payload method, pushing the new identity state into the global DHT across multiple peers.

Quick Reference

  • Renewal Discount: Renewals only require 20% of the baseline iterations (an 80% discount).
  • Premium Exemption: Premium names (purchased with KYN tokens) do not require VDF resquaring and will trigger an error if submitted to this endpoint.
  • Task Queue Limit: The daemon will reject new requests with a 429 status code if there are 50 or more active tasks in memory.
  • Commitment Privacy: Commitments are generated privately and only broadcast after the VDF proof is computed, mitigating front-running attacks.
  • Mandatory Sleep: The daemon will intentionally suspend execution for CONSENSUS_MINIMUM_COMMIT_AGE_KYNS epochs to satisfy network maturity rules before publishing the Reveal.
  • Mutex Poisoning: Handled gracefully on status reads using unwrap_or_else(|e| e.into_inner()) to prevent cascading failures.
  • Background Tasks: Handled asynchronously using tokio::spawn and tokio::task::spawn_blocking.

Open Questions / Things to Revisit

  • Hardcoded Discount: The 80% discount is currently hardcoded directly in the API handler as (required_iters as f64 * 0.2). From an architectural perspective, this specific modifier should probably live inside kinetic_core::consensus_math alongside the baseline calculations. If the network ever votes to change the discount to 75%, having it buried in the daemon API layer could lead to mismatched consensus rules between different implementations.
  • Passive Memory Leaks: The automatic pruning in handle_vdf_renew only triggers when a new request is received. If a user generates a few VDFs and then never calls the API again, those completed task records will sit in RAM indefinitely. A lightweight background ticker that sweeps the map every hour might provide more robust garbage collection.
  • Error Granularity: When a task fails, the error string is populated with a plain text message. Moving to a structured error enum for VDF tasks could allow frontend UIs to respond more intelligently (e.g., automatically retrying network timeouts vs. aborting on cryptographic failures).

API Publish Handlers (Part 1)

Crate: kinetic-daemon Stage: 9 Reading time: 60 minutes Depends on: docs/learn/core/04_types.md, docs/learn/network/03_dht.md, docs/learn/storage/01_overview.md


What Is This?

This document provides a detailed architectural breakdown of the first half of the kinetic-daemon/src/api/publish.rs file.

Specifically, it covers lines 1 through 331 of the source code.

This section of the codebase contains the HTTP REST API handlers that act as the entry point for publishing data.

In the Kinetic architecture, users do not interact with the raw P2P network directly.

Writing a client application that natively speaks Kademlia routing over QUIC is complex.

It requires managing persistent sockets.

It requires maintaining routing tables.

It requires handling binary serialization for network packets.

It requires a deep understanding of cryptographic primitives and decentralized state machines.

Instead, the Kinetic Daemon acts as an abstraction layer, or a “sidecar” process.

The daemon runs locally on the user’s machine.

It exposes a developer-friendly HTTP API.

Client applications (like the CLI or a web dashboard) send standard JSON payloads to this API.

The daemon then translates these HTTP requests into complex decentralized network operations.

This document focuses exclusively on the two most critical endpoints for the name registration lifecycle:

  1. The handle_commit endpoint: Used for locking in a cryptographic claim.

  2. The handle_publish endpoint: Used for finalizing that claim by revealing the actual data.

Together, these handlers form the absolute boundary between the untrusted outside world and the secure Kinetic ecosystem.


Why Kinetic Needs This

To understand the necessity of this code, we must explore the specific threat models of a decentralized system.

In a centralized system, such as traditional DNS, you ask a central server to register a name.

If it is available, the server gives it to you over TLS.

In a decentralized system like Kinetic, there is no central authority.

You must broadcast your intent to register a name to a public swarm of untrusted peer nodes.

This creates a massive vulnerability known as “Front-Running.”

The Front-Running Threat Model

Imagine you discover that a valuable name, such as banking.kyn, is currently unregistered.

You decide to register it immediately.

If the system used a single-step registration, you would broadcast a message saying: “Assign banking.kyn to me.”

Because the DHT is a public peer-to-peer network, your message must be routed through intermediate nodes.

Any of those intermediate nodes can inspect the unencrypted contents of your packet.

A malicious node operator could see your request.

They realize that banking.kyn is valuable.

They instantly drop your packet so it never reaches its destination.

Then, they generate their own registration request for banking.kyn and broadcast it.

Because they are already positioned deep in the network topology, their request propagates faster.

They successfully steal the name you discovered.

In decentralized finance, this is known as MEV (Miner Extractable Value) or a front-running attack.

The Commit-Reveal Solution

Kinetic neutralizes this threat by enforcing a cryptographic Commit-Reveal scheme.

This scheme is directly mirrored in the daemon’s API structure.

Phase 1: The Commitment

The user generates a random, cryptographically secure secret, known as a salt.

They locally concatenate the desired name (banking.kyn) and the salt.

They hash this combination together using a strong cryptographic hash function.

They send only this hash to the handle_commit endpoint.

The daemon broadcasts this opaque hash to the DHT.

The network timestamps and records that the user claimed this hash.

Crucially, because hash functions are one-way, intermediate nodes cannot reverse the hash.

They have no way to know that the hash corresponds to banking.kyn.

They cannot steal what they cannot see.

Phase 2: The Reveal

Sometime later, the user sends the plaintext name and the secret salt to the handle_publish endpoint.

The daemon broadcasts this ‘Reveal’ payload to the DHT.

The storage nodes receive the reveal.

They hash the plaintext name and salt together themselves.

They check if the resulting hash matches the one recorded during the commitment phase.

If it matches, the network grants the name to the user.

Their priority is retroactively backdated to the exact timestamp of the original commitment.

The handlers in publish.rs are the gatekeepers that enforce this secure choreography.


How It Works

The API is built using Axum, the standard asynchronous web framework in the Rust ecosystem.

Axum utilizes a concept called “Extractors.”

An extractor is a declarative way to pull data out of an HTTP request.

By specifying types in the function signature, Axum automatically handles parsing, deserialization, and error handling.

Let’s break down the handle_publish endpoint step by step.

1. Role Authorization and Access Control

-> See: kinetic-daemon/src/api/publish.rs — Lines 15 to 27

The function starts by extracting Extension<Role>.

This represents the authorization context of the incoming HTTP request.

The daemon API might be bound to a public IP or exposed on a local network.

We cannot allow unauthenticated scripts to trigger massive DHT operations.

The function calls role.can_publish().

If the client lacks the necessary administrative or publishing privileges, the request is terminated.

It returns a 403 FORBIDDEN HTTP status with a JSON error payload.

2. Name Normalization and Validation

-> See: kinetic-daemon/src/api/publish.rs — Lines 29 to 40

Before any cryptographic work begins, the daemon must standardize the requested name.

It passes the raw name string through kinetic_core::types::normalize_name.

This is an absolute necessity for the integrity of a DHT.

In a Distributed Hash Table, the storage location of a piece of data is determined by the hash of its key.

If a user registered Kinetic.kyn (with a capital K), it would hash to Value A.

If someone else searched for kinetic.kyn (lowercase), it would hash to Value B.

The network would store them on different nodes.

This would fracture the namespace and break resolution.

Normalization forces all inputs into a canonical format.

It converts everything to lowercase.

It trims whitespace.

It enforces specific character sets.

If the resulting string fails validation, it is rejected with a 400 BAD REQUEST.

3. VDF Staleness and Drand Integration

-> See: kinetic-daemon/src/api/publish.rs — Lines 46 to 112

This block handles the computational spam resistance mechanism.

Kinetic uses Verifiable Delay Functions (VDFs) for ‘Standard’ tier name registrations.

A VDF is a cryptographic function that requires a sequential, non-parallelizable sequence of operations.

It is designed to take a specific amount of real-world wall-clock time to compute.

However, a spammer could theoretically pre-compute millions of VDFs over a period of 5 years.

They could store them on a hard drive and then release them all at once to flood the network.

To defeat this, a VDF must be tied to a recent, unpredictable event.

Kinetic uses Drand (Distributed Randomness Beacon).

The user must use the latest Drand ‘kyn’ (epoch number) as the seed for their VDF.

The daemon enforces this ‘staleness’ check:

First, it instantiates a DrandClient.

It calls fetch_latest().await to execute an HTTP request to the external Drand network.

Offline-First Resilience:

If the daemon has no internet access, it gracefully falls back to load_cached_kyn().

This reads the last known epoch from the embedded sled database.

This prevents the daemon from entering a crash loop when disconnected.

The actual DHT network will still enforce the staleness check later.

The Staleness Math:

The daemon calculates the age of the VDF: current_kyn - drand_kyn.

If the VDF claims an epoch from the future (drand_kyn > current_kyn), it is rejected.

If the age exceeds RESQUARING_EPOCH_KYNS, the VDF is deemed too old.

The request is rejected, and the user must recompute a fresh proof.

4. Payload Serialization and Errors

-> See: kinetic-daemon/src/api/publish.rs — Lines 114 to 124

Before sending to the DHT, the struct must be serialized to bytes.

The daemon uses serde_json::to_vec(&domain_record) to convert the Rust struct into a JSON byte array.

If this fails, the handler catches the Err.

It immediately returns an HTTP 500 INTERNAL_SERVER_ERROR.

This prevents malformed data from ever entering the network queue.

5. Network Broadcasting

-> See: kinetic-daemon/src/api/publish.rs — Lines 125 to 135

Once validation passes, the NameRecord is ready.

The daemon calls state.network.publish_redundant_payload(&fqdn, payload_bytes).

This is where the API bridges into the P2P layer.

The network layer calculates the Kademlia XOR distance.

It compares the hash of the name to the Node IDs of all known peers.

It selects the 5 closest nodes.

It attempts to push the binary payload to them.

Why 5 nodes?

In a distributed system, nodes churn (go offline unexpectedly).

If we only stored the data on 1 node, it would be lost immediately.

Replicating it to 5 nodes guarantees high availability.

6. Daemon State Persistence and Heartbeats

-> See: kinetic-daemon/src/api/publish.rs — Lines 136 to 170

The daemon acts as the user’s automated agent.

Data on the Kademlia DHT naturally expires after a few hours.

To keep a name alive permanently, the owner must periodically send a ‘Heartbeat’.

The daemon automates this, but it must remember what names it is responsible for.

It locks a global Mutex OWNED_NAMES_LOCK.

It retrieves the DB_PREFIX_OWNED_NAMES list from the sled database.

It appends the new FQDN to this vector.

It enforces a maximum limit of 10,000 names to prevent unbounded memory growth.

It writes the vector back to the sled database.

Additionally, the daemon persists the full Reveal payload under DB_PREFIX_REVEAL.

This is a massive quality-of-life feature.

If the user wants to update their DNS records later, they need to re-sign the Reveal.

If the daemon didn’t save the Reveal locally, the user would have to recompute the 2-hour VDF proof.

By keeping the Reveal on disk, the daemon can simply modify the zone file and instantly re-publish.

7. Asynchronous Quorum Verification

-> See: kinetic-daemon/src/api/publish.rs — Lines 172 to 195

The API must return an HTTP response immediately so the client UI does not hang.

However, we need to know if the network actually accepted the payload.

The daemon uses tokio::spawn to launch a detached, background async task.

The task immediately calls tokio::time::sleep for 10 seconds.

This gives the Kademlia network time to negotiate connections and transfer the payload.

After waking up, the task calls network.verify_quorum().

It directly asks the target nodes if they have the data.

It checks if at least 3 out of the 5 nodes acknowledge holding the payload.

This 3/5 threshold is rooted in Byzantine Fault Tolerance principles.

If 3 nodes confirm, we have a statistical guarantee that the data is safely replicated.

The handle_commit Endpoint

-> See: kinetic-daemon/src/api/publish.rs — Lines 213 to 319

The handle_commit endpoint follows the exact same architectural flow.

It performs the same authorization.

It performs the same normalization.

It performs the same broadcasting.

It performs the same quorum verification.

The defining feature of this handler is the trivial commitment check.

The All-Zeros Hash Check

-> See: kinetic-daemon/src/api/publish.rs — Lines 243 to 255

The daemon checks if req.commitment.hash == [0u8; 32].

In cryptography, a hash of all zeros is considered a null or trivial value.

The security of the Commit-Reveal scheme relies on the hash strongly binding the user to their secret salt.

If an attacker submits an all-zeros hash, it breaks the entropy assumption.

If the downstream validation logic fails to enforce entropy, the attacker might bypass security.

By rejecting this at the API boundary, the daemon provides defense-in-depth.

It ensures that malformed or trivial cryptographic commitments never even reach the P2P layer.


Key Pieces

Functions

  • handle_publish (Line 15): The primary API endpoint for finalizing registrations.

  • It enforces VDF staleness.

  • It handles JSON serialization.

  • It routes Reveal payloads to the DHT.

  • It manages the daemon’s local sled state for heartbeats.

  • handle_commit (Line 219): The API endpoint for locking in priority over a name.

  • It enforces the first phase of the anti-front-running scheme.

  • It rejects trivial [0u8; 32] hashes.

Rust Concepts in Action

  • axum::extract::State: This is how Axum shares state across threads safely. Instead of relying on global singletons, Axum clones the state reference for each request. In Kinetic, ApiState contains the network client and storage handles.

  • tokio::spawn: This is Tokio’s way of executing futures concurrently. It takes a block of async move { ... } code and hands it to the runtime executor. The executor runs it on a background worker thread. This ensures the main HTTP handler thread can return a response immediately. It prevents the daemon from locking up during a 10 second sleep.

  • Result<Json<T>, (StatusCode, Json<E>)>: This is the idiomatic return type for Axum handlers. If the function returns Ok, Axum automatically sets a 200 OK header. It serializes the success structure into JSON. If it returns Err, Axum sets the HTTP status code specified in the tuple. It serializes the error structure into JSON.

  • std::sync::Mutex vs tokio::sync::Mutex: The code uses the standard library mutex for OWNED_NAMES_LOCK. The standard mutex blocks the OS thread until the lock is acquired. In a Tokio async context, blocking the thread is generally an anti-pattern. It is used here assuming the lock is held for short durations. However, it poses a performance risk if the lock is held during sled I/O. If sled blocks on a disk flush, the entire Tokio worker thread goes to sleep. This severely reduces the concurrent capacity of the API. Refactoring to a concurrent data structure would be superior.


How This Connects to the Rest of Kinetic

This file is the orchestration layer.

It contains very little core logic itself.

Instead, it coordinates actions across multiple other crates:

  • CROSS-CRATE: kinetic-core -> The API fundamentally relies on the types defined in the core crate.

  • It deserializes requests into kinetic_core::types::NameRecord and CommitRequest.

  • It uses kinetic_core::types::normalize_name to standardize inputs.

  • It relies on kinetic_core::drand::DrandClient to fetch external randomness.

  • CROSS-CRATE: kinetic-network -> The daemon API has no native Kademlia implementation.

  • It hands the validated byte arrays over to state.network.publish_redundant_payload().

  • The network crate takes over from there, finding peers and transmitting data.

  • CROSS-CRATE: kinetic-storage -> To persist daemon configuration across reboots, the API utilizes state.storage.put().

  • This interacts directly with the embedded sled database engine.

  • It manages the DB_PREFIX_OWNED_NAMES lists.

  • The storage module provides the fallback mechanism for Drand staleness caching.


Quick Reference

A rapid overview of the constraints enforced by these endpoints:

  • Commit Endpoint: POST /commit -> handle_commit
  • Publish Endpoint: POST /publish -> handle_publish
  • Trivial Commitments: Commitment hash cannot be an array of all 0x00 bytes.
  • VDF Staleness: current_kyn - drand_kyn must not exceed RESQUARING_EPOCH_KYNS.
  • Redundancy: Payloads are pushed to the 5 closest Kademlia DHT nodes.
  • Quorum Threshold: A minimum of 3 out of 5 nodes must confirm storage.
  • Daemon Capacity: The daemon will automatically heartbeat a maximum of 10,000 owned names.
  • Payload Limits: Payloads failing serialization will result in an HTTP 500 error.
  • Authorization Limits: Unauthorized requests will result in an HTTP 403 error.
  • State Handling: State errors correctly yield JSON formatting rather than raw strings.
  • Fallback Behaviors: Drand client uses embedded sled caching for robust offline operation.

Open Questions / Things to Revisit

There are several implementation details in this file that should be scrutinized for production deployment:

  1. The Synchronous Mutex Bottleneck:
  • On line 139, the code uses crate::api::OWNED_NAMES_LOCK.lock().unwrap().
  • This is a standard std::sync::Mutex.
  • In an asynchronous Rust environment like Tokio, holding a standard synchronous lock is risky.
  • Deserializing a 10,000 item list, appending to it, and serializing it back to disk takes time.
  • This can block the underlying OS worker thread.
  • If the daemon is hammered with concurrent publish requests, this could cause thread starvation.
  • It should be refactored to use tokio::sync::Mutex or a concurrent dashmap.
  1. The Fragile Quorum Sleep:
  • The background quorum verification task calls tokio::time::sleep for 10 seconds.
  • Hardcoding a 10-second delay is dangerous.
  • On a congested network, 10 seconds might not be enough time.
  • This results in a false-negative warning in the logs.
  • On a fast local testnet, waiting 10 seconds is wasteful.
  • The network layer should ideally provide an event-driven Future or Channel.
  • This would signal exactly when the publish operation has completed.
  1. Silent Eviction of Owned Names:
  • When a user publishes their 10,001st name, the daemon calculates a skip_count.
  • It silently drops the oldest names from the OWNED_NAMES list.
  • Because the daemon is no longer tracking these names, it will stop sending heartbeats.
  • Consequently, those names will eventually expire on the DHT and be lost.
  • There is currently no logging or API warning mechanism to alert the user.
  • This could lead to catastrophic, silent data loss for power users managing large domain portfolios.

API Publish Handlers: KIDs, Manifests, and Governance

Crate: kinetic-daemon Stage: 9 Reading time: 20 minutes Depends on: kinetic-kid (Stage 3), kinetic-core (Stage 7), kinetic-network (Stage 8)


What Is This?

This file documents the second half of the API publish handlers in kinetic-daemon/src/api/publish.rs (Lines 332-663).

While the first half of this file dealt with cryptographic commitments and reveals for the decentralized naming system (mapping human-readable names to public keys), this half is fundamentally about publishing higher-level network structures into the global state.

Specifically, this file contains the HTTP handlers that local clients, frontends, or command-line tools hit when they need to publish:

  • KIDs (Kinetic Identifiers): The core decentralized identity documents that define a user or an entity on the network.
  • Manifests (App Configurations): The application-specific connection routes and service definitions tied to a specific KID.
  • Governance Messages: System-wide administrative commands that update quorum rules, slash malicious actors, or migrate network parameters.

These endpoints act as the secure boundary between a local user’s requests and the broader kinetic peer-to-peer network.

They receive JSON payloads, cryptographically verify every single signature involved, ensure the user has the correct authorization, and then push that data out to the network.

They do this either by storing it redundantly in the Distributed Hash Table (DHT) or broadcasting it globally via the Gossipsub protocol.


Why Kinetic Needs This

The Kinetic daemon is the gateway.

The global peer-to-peer network has no central authority to validate data. If a node simply accepted any data stream and pushed it into the DHT, the network would collapse within minutes.

It would be trivial for a malicious actor to flood the DHT with fake identities, overwrite legitimate application manifests, or broadcast fake governance rules that disrupt network consensus.

These API handlers act as the first line of defense for the entire ecosystem. Before a daemon even asks the P2P network to store a piece of data, it rigorously validates the cryptography locally.

This architecture guarantees three critical invariants:

  1. Identity Integrity: You cannot publish an AuthorizedKid unless you prove you own the cryptographic keys associated with the registered human-readable Name attached to it.

  2. Application Integrity: You cannot publish a Manifest for an application unless you prove the signature on the manifest was generated by an authorized subkey listed inside the parent KID.

  3. Network Integrity: You cannot publish a Governance change unless the message is signed by a valid quorum key recognized by the current global governance state.

Without these specific handlers, local clients would have no secure, standardized HTTP interface to inject their identities, apps, and administrative actions into the kinetic global state.

The daemon handles the heavy lifting of P2P networking and complex cryptographic verification so that the client application doesn’t have to reinvent the wheel.


How It Works

This file contains three primary asynchronous handler functions.

Each follows a strict pipeline:

  • Role-Based Access Control (RBAC) check
  • Cryptographic Verification
  • Serialization
  • Network Broadcast.

We will dissect exactly how each step operates inside the Kinetic ecosystem.

1. Publishing a Kinetic Identifier (KID)

The handle_publish_kid function is the endpoint responsible for taking a user’s new or updated identity document and pushing it into the global DHT so that other peers can resolve it.

-> See: kinetic-daemon/src/api/publish.rs — Lines 326 to 430

Step 1: Role-Based Access Control (RBAC) The handler signature begins by extracting the Role from the Axum extension layers.

The API token that was used to authenticate this HTTP request must possess either the Publish or Admin role. If it does not, the handler immediately returns an Err containing a StatusCode::FORBIDDEN.

This ensures that read-only API keys cannot be abused to publish data.

Step 2: Inner Signature Verification The payload received from the client is an AuthorizedKid. This is an outer wrapper containing a KidDocument and a signature.

The very first cryptographic check is a call to: auth_kid.kid_doc.verify().

This verifies the mathematical consistency of the identity itself. Within a KID document, there is a primary identity key, and there may be several subkeys (for app signing, encryption, etc.).

This verify() call ensures that the primary identity key signed the document encompassing all of those subkeys. If this fails, the KID is inherently corrupt.

Step 3: Wrapper Signature Verification against Local State In Kinetic’s architecture, an AuthorizedKid is permanently linked to a registered human-readable Name (e.g., saif.kyn).

The network must ensure that the person trying to publish this KID actually owns the Name they are claiming. This is done by checking the “wrapper” signature (auth_kid.owner_signature).

-> See: kinetic-daemon/src/api/publish.rs — Lines 352 to 384

The node attempts to look up the NameRecord for the claimed name in its fast local key-value storage.

It constructs a lookup key by appending the name to: kinetic_core::constants::DB_PREFIX_REVEAL.

If it finds the record, it deserializes it into a NameRecord. Then, it performs a manual ML-DSA signature check.

It creates an ml_dsa::VerifyingKey from the public key slice in the record. It creates an ml_dsa::Signature object from the owner_signature slice in the request.

Crucially, it verifies this signature against: auth_kid.signable_bytes(NETWORK_ID).

Incorporating the NETWORK_ID into the signable bytes is a brilliant architectural decision to prevent cross-network replay attacks. A signature meant for the Kinetic Testnet cannot be intercepted and replayed on the Kinetic Mainnet, because the NETWORK_ID baked into the hashed bytes will differ.

Step 4: The Fallback “Allow” Decision What happens if the local node doesn’t have the NameRecord cached in its local database?

-> See: kinetic-daemon/src/api/publish.rs — Lines 385 to 389

If the node hasn’t seen the Reveal transaction for this name yet, it has no cryptographic way to verify the signature locally.

Instead of blocking the user, it logs a warning: "Could not find local reveal... Forwarding to DHT anyway".

It deliberately sets is_authorized = true and allows the request to proceed.

Why? Because in an eventually consistent P2P network, local state can lag behind global state. If an API node with a stale local cache blocked valid user activity, the user experience would degrade severely.

The architecture assumes that if the signature is actually invalid, the P2P network peers receiving the DHT PUT request will reject it.

Step 5: Serialization and DHT Publication Once authorized (or given the benefit of the doubt), the AuthorizedKid is serialized back into JSON bytes.

The node then calls: state.network.publish_redundant_payload(&fqdn, payload_bytes).

The DHT key used for this storage is the fully qualified DID string of the KID (e.g., did:kyn:12345...).

The redundant aspect means the network layer will attempt to store this on the Kademlia network’s K-closest peers to that hash, ensuring the data persists even if several peers go offline.

2. Publishing an Application Manifest

The handle_publish_manifest function processes application configurations.

While a KID defines who an entity is, a Manifest defines what services they offer, what APIs they expose, and what network addresses they can be reached at.

-> See: kinetic-daemon/src/api/publish.rs — Lines 438 to 576

Step 1: Local Wrapper Verification Exactly like the KID handler, the manifest handler first checks the owner_signature on the AuthorizedManifest wrapper against the local NameRecord to ensure the overarching name owner authorized this action.

Step 2: Network Resolution (The Blocking Pause) A Manifest cannot be verified in a vacuum.

A manifest asserts that it belongs to a specific KID, and it must be signed by an application key that is listed inside that parent KID document.

-> See: kinetic-daemon/src/api/publish.rs — Lines 504 to 514

To verify this linkage, the local node must fetch the current state of the KID from the global network.

It calls state.network.resolve_redundant_payload(did_str).await.

This means the API request physically pauses its execution thread, queries the Kademlia DHT, and waits for a response from remote peers.

If the network is offline, or if the KID has not yet propagated through the DHT, this lookup will fail, and the API request will terminate with a 404 NOT FOUND or 503 SERVICE UNAVAILABLE.

Step 3: Backwards-Compatible Deserialization -> See: kinetic-daemon/src/api/publish.rs — Lines 516 to 531

Once the byte payload for the KID is returned from the DHT, the node attempts to deserialize it.

It first attempts to parse it as an AuthorizedKid (the modern standard). However, to support network migrations and older testnet data, if that fails, it falls back to attempting to deserialize it as a legacy, raw KidDocument without the authorization wrapper.

Step 4: Manifest Cryptographic Verification With the parent KID document secured from the network, the code executes: auth_manifest.manifest.verify(&kid_doc).

This mathematical operation proves that the digital signature attached to the Manifest was produced by one of the valid subkeys listed in the KID document.

This prevents malicious actors from publishing fake manifests that redirect a legitimate application’s traffic to a rogue IP address.

Step 5: Derived DHT Key Generation and Publication -> See: kinetic-daemon/src/api/publish.rs — Lines 542 to 575

Where does a Manifest live on the Distributed Hash Table?

It cannot be stored under the exact same DHT key as the KID document itself, because doing so would cause a collision and overwrite the identity data.

Instead, the node takes the KID’s DID string, appends the string #manifest to it, and runs the combined string through a SHA-256 hash using the sha2::Digest trait.

The resulting raw bytes are hex-encoded to form a clean ASCII string. This derived hash becomes the new DHT key.

The manifest payload is then published redundantly across the network under this distinct derived key.

3. Publishing a Governance Message

The handle_publish_governance function is arguably the most sensitive endpoint in the file.

It is responsible for submitting global, system-wide state changes to the Kinetic network, such as slashing a malicious validator, updating block parameters, or triggering a hard fork migration.

-> See: kinetic-daemon/src/api/publish.rs — Lines 584 to 663

Step 1: In-Memory Global State Lock Governance changes affect the core operational rules of the node itself, not just user data.

-> See: kinetic-daemon/src/api/publish.rs — Lines 599 to 620

The code introduces a distinct block scope { ... }. Inside this scope, it acquires a lock on GLOBAL_GOVERNANCE_STATE. This is a global std::sync::Mutex that holds the node’s authoritative understanding of the current network rules.

The handler passes the message to: process_governance_message(&mut gov, &msg).

This function is complex; it validates the cryptographic signatures against the current recognized quorum keys, verifies sequence numbers to prevent replay attacks, and ensures the governance action is semantically valid.

Step 2: Synchronous Disk Persistence If the message is valid, the in-memory global state is mutated. Immediately after, while still holding the global Mutex lock, the code calls gov.save_to_disk(&path) to write the updated binary state to governance.bin.

This ensures that if the daemon crashes a millisecond later, the governance update is not lost.

Step 3: Mutex Block Scoping Architecture Notice that the Mutex lock is acquired and held inside the { ... } block, and the network broadcast happens after the block closes.

-> See: kinetic-daemon/src/api/publish.rs — Lines 640 to 662

This is a critical Rust architecture pattern when mixing synchronous Mutexes and asynchronous code.

The network broadcast uses .await, which is an asynchronous yield point.

If the code held the MutexGuard across that yield point, every single other thread or request in the daemon that needed to read or check governance rules would freeze until the network broadcast finished.

By wrapping the state update and disk I/O in a distinct lexical block, the MutexGuard is dropped gracefully and instantly before the async I/O begins, allowing the rest of the daemon to continue operating unimpeded.

Step 4: Gossipsub Broadcast (Flood Routing) Unlike KIDs and Manifests, governance messages are time-sensitive.

The network cannot wait for DHT propagation. Therefore, instead of storing the payload in the DHT, the node serializes the message and calls: state.network.broadcast_gossip(kinetic_core::constants::GOSSIP_TOPIC_GOVERNANCE, payload_bytes).await.

This uses the Gossipsub protocol. It immediately floods the message to all directly connected peers, who validate it and flood it to their connected peers. Within milliseconds, the governance message blankets the entire kinetic peer-to-peer network.


Key Pieces

  • handle_publish_kid

    • Where: kinetic-daemon/src/api/publish.rs:L326-L430
    • What: Accepts an AuthorizedKid JSON payload, validates inner identity signatures and outer name ownership signatures against the local database, and pushes it to the Kademlia DHT.
    • Why: This is the primary entry point for a user creating, rotating, or updating their decentralized identities in the Kinetic network.
  • handle_publish_manifest

    • Where: kinetic-daemon/src/api/publish.rs:L438-L576
    • What: Accepts an AuthorizedManifest, resolves the parent KID dynamically from the DHT, verifies the cryptographic linkage to ensure the app is authorized by the identity, hashes a derived key, and publishes the manifest to the DHT.
    • Why: This is how decentralized applications announce their connection details, APIs, and IP addresses to the broader ecosystem.
  • handle_publish_governance

    • Where: kinetic-daemon/src/api/publish.rs:L584-L663
    • What: Accepts a SignedGovernanceMessage, locks the global network state, validates the message against quorum rules, persists the changes locally to disk, and rapidly floods the message to all peers.
    • Why: Provides a secure, instantaneous pathway for network administrators to push real-time rule changes and network upgrades without requiring client restarts.

How This Connects to the Rest of Kinetic

  • CROSS-CRATE: AuthorizedKid, AuthorizedManifest, and KidDocument are defined and deeply explained in docs/learn/types/ and docs/learn/kid/.
  • CROSS-CRATE: The DHT publishing logic (publish_redundant_payload) and the Gossipsub flooding mechanism (broadcast_gossip) are core functionalities provided by kinetic-network (Stage 8).
  • CROSS-CRATE: The GLOBAL_GOVERNANCE_STATE Mutex and the process_governance_message business logic are central components of kinetic-core (Stage 7).
  • CROSS-CRATE: The manual ML-DSA signature verification and VerifyingKey instantiation directly leverage the cryptographic primitives outlined in kinetic-verify (Stage 2).

Quick Reference

| Action | API Endpoint Handler | P2P Mechanism | Storage Key / Topic | |—|—|—|—| | Publish KID | handle_publish_kid | Kademlia DHT Put | The KID’s exact DID string | | Publish Manifest | handle_publish_manifest | Kademlia DHT Put | SHA256 Hash of "{did}#manifest" | | Publish Governance | handle_publish_governance | Gossipsub Flooding | GOSSIP_TOPIC_GOVERNANCE |


Open Questions / Things to Revisit

  1. Local Reveal Fallback Security Profile: In both handle_publish_kid and handle_publish_manifest, if the local node doesn’t have the NameRecord cached, it assumes is_authorized = true and forwards the payload to the DHT anyway. While this smartly prevents local cache staleness from blocking legitimate users, it opens a potential vector for abuse. A malicious actor could spam an API node with KIDs for names the node hasn’t seen yet, forcing the node to perform computationally expensive DHT puts for garbage data. This is a potential DoS vector that requires architectural review. Should there be rate-limiting specifically for “uncached” publishes?

  2. Synchronous Disk I/O inside an API Handler: In handle_publish_governance, gov.save_to_disk(&path) is called synchronously inside the API handler while holding the global Mutex lock. If the underlying disk is under heavy load or experiences latency spikes, this could stall the entire API thread. More importantly, it will block all other governance reads across the daemon while waiting on disk I/O. Moving the disk persistence to a background worker channel via a message queue should be evaluated to keep the API layer responsive.

  3. Legacy Document Fallback Timeline: The handle_publish_manifest function gracefully attempts to deserialize raw KidDocument payloads from the DHT if the modern AuthorizedKid parsing fails. How long must the network support this fallback? Is there a coordinated deprecation strategy for raw documents, or will this legacy code live in the daemon indefinitely?

Local Certificate Authority (CA) and TLS Proxying

Crate: kinetic-daemon Stage: 9 Reading time: 30 minutes Depends on: kinetic-core (Constants like NETWORK_ID and TLD_SUFFIX)


What Is This?

The ca.rs file provides the cryptographic infrastructure for the Kinetic Daemon to act as a localized Certificate Authority (CA). It handles the generation of a root certificate, stores its sensitive private key, and injects that certificate into the operating system’s native trust store.

Beyond initialization, it provides the runtime machinery to dynamically generate “leaf” certificates on the fly. Whenever the user’s browser attempts to navigate to a .kin domain (or whichever custom TLD the network is configured to use), this module generates a valid, sound TLS certificate specifically for that domain, signed by the local root CA. It also provides an in-memory caching layer to ensure we do not waste CPU cycles re-generating these certificates for every single network request.

In the simplest terms, this module is the engine that allows the user’s web browser to show a secure, trusted green padlock when browsing the decentralized Kinetic network, without throwing aggressive security warnings that would ruin the user experience. It turns a chaotic, peer-to-peer network into something that feels as safe and standardized as traditional cloud computing to the end user.


Why Kinetic Needs This

To understand why this module is so critical to the Kinetic architecture, we must examine the strict security model of modern web browsers.

Modern web browsers (Chrome, Firefox, Safari) mandate HTTPS for almost all modern web APIs. If you attempt to serve a web application over plain HTTP, the browser will restrict its capabilities. Crucial features like:

  • Service Workers (required for offline support and caching)
  • The WebCrypto API
  • Secure Cookies
  • Modern clipboard access
  • Geolocation API will simply fail to execute. For Kinetic to provide a seamless, modern web experience where decentralized apps behave exactly like traditional cloud-hosted web apps, the Kinetic Daemon must serve its content over HTTPS.

However, the domains on the Kinetic network (e.g., app.kin) do not exist in the global ICANN DNS system. Because they are localized, virtual domains, traditional Certificate Authorities like Let’s Encrypt, DigiCert, or Cloudflare will never issue a TLS certificate for them. They cannot verify domain ownership over a domain that only exists inside Kinetic’s peer-to-peer overlay.

If the Kinetic Daemon attempted to serve standard self-signed certificates for every domain independently, the browser would present a massive, unskippable “Your connection is not private” error screen to the user for every single app. This is an unacceptable user experience, as it trains users to click through and ignore severe security warnings entirely.

The solution is to perform a benign, authorized Man-In-The-Middle (MITM) operation directly on the user’s own machine. The ca.rs module orchestrates this through a multi-step process:

  1. The Local Root: When the Kinetic Daemon starts for the very first time, it generates a brand new, privileged Root Certificate Authority. This CA exists locally on the user’s machine and its private key never leaves the hard drive.
  2. The Trust Injection: The daemon then prompts the user’s operating system to implicitly trust this new CA. Once the OS trusts it, the browser (which inherits trust from the OS) will automatically trust any certificate that bears the cryptographic signature of this specific Root CA.
  3. The Leaf Forgery: When the user types https://search.kin into their browser, the browser connects to the local Kinetic Daemon proxy. The daemon intercepts the TLS ClientHello, instantly uses the Root CA to forge a valid TLS “leaf” certificate specifically for search.kin, and hands it back to the browser.
  4. The Verification: The browser verifies the leaf certificate, traces its signature back to the trusted local Root CA, and establishes a secure TLS connection. The user sees a standard secure connection, and Kinetic can safely decrypt, route, and serve the decentralized content.

Without this module, the entire premise of using a standard web browser as a client for the Kinetic network collapses. It acts as the diplomatic bridge between Kinetic’s decentralized backend and the unforgiving, strict security model of the traditional web.

Furthermore, we cannot simply generate one giant wildcard certificate (like *.kin) because users might configure custom network IDs, complex nested subdomains, or arbitrary local routes. On-the-fly, dynamic generation is the only architecture that matches the browser’s expectations for any arbitrary request.


How It Works

The lifecycle of the CA system in Kinetic involves several complex, distinct mechanisms. It handles everything from preventing race conditions during parallel initialization to OS-specific security integrations and performance-critical caching.

1. Bootstrapping and Atomic File Locking

-> See: kinetic-daemon/src/ca.rs — Lines 44 to 71

When the daemon starts, it calls load_or_create_root_ca. Because a user might accidentally launch the daemon multiple times simultaneously, or a background OS service might race with a manual terminal launch, we must guarantee that two processes do not attempt to generate the Root CA at the same time. If they did, they would overwrite each other’s cryptographic keys, irrevocably breaking the trust chain and leaving the OS trusting a ghost certificate.

This race condition is solved using a primitive but effective file lock (.ca.lock). The code attempts to open this file with create_new(true). In Rust, create_new(true) translates directly to an atomic OS-level syscall (like O_CREAT | O_EXCL on Unix). This means the operating system itself guarantees that only one process can successfully create the file.

If we had instead written if !file.exists() { create_file() }, we would have introduced a classic TOCTOU (Time-Of-Check to Time-Of-Use) vulnerability. Two separate OS threads (or processes) could check exists() at the exact same nanosecond, both see false, and then both proceed to blindly create and overwrite the file. By using the atomic create_new(true), the OS kernel steps in at the lowest level and guarantees mutual exclusion. Only the absolute first thread will succeed, and the second will instantly fail.

If the syscall succeeds, the process holds the lock. If it fails with an AlreadyExists error, it enters a graceful retry loop, waiting 50 milliseconds between attempts. If it fails 100 times sequentially (indicating 5 seconds have passed), it makes an architectural assumption: the lock is “stale” (likely left behind by a previous daemon crash that failed to clean up). In this scenario, it forces a deletion of the lock file to recover. This ensures the daemon never permanently deadlocks on a subsequent startup.

2. The Immediately Invoked Closure Expression (IICE) Pattern

-> See: kinetic-daemon/src/ca.rs — Lines 72 to 202

Notice how the core logic of load_or_create_root_ca is wrapped inside an Immediately Invoked Closure Expression (IICE):

#![allow(unused)]
fn main() {
// An IICE used for cleanup emulation let result = (|| -> Result<(RootCa, bool), CaError> {
    // ... complex logic with many ? operators
})(); let _ = std::fs::remove_file(&lock_path); return result;
}

Why is this necessary? When holding a lock file, it is absolute paramount that the lock is deleted when the function exits. However, the internal logic uses the ? operator extensively for error handling. If we didn’t use a closure, an early return triggered by ? would bypass the lock deletion code at the end of the function, leaving a stale lock on disk.

Unlike languages like Go that have a defer keyword, Rust typically handles cleanup via the Drop trait on specific objects. However, creating a custom struct just to implement Drop for deleting a single file is overly verbose. By wrapping the logic in a closure, we can capture the Result of all the internal operations, unconditionally run std::fs::remove_file(&lock_path) afterward, and then return the captured result. This elegantly ensures clean state.

3. Root CA Generation and Basic Constraints

-> See: kinetic-daemon/src/ca.rs — Lines 107 to 132

If the lock is acquired and no existing CA is found on disk, the module leverages the rcgen crate to forge a brand new Root CA. This certificate is configured to expire in exactly 730 days (2 years).

A critical detail is how the CA is constrained. The code uses: IsCa::Ca(BasicConstraints::Constrained(0)) What does Constrained(0) mean? In X.509 terminology, this defines the maximum depth of the certificate chain. By setting it to 0, we are stating that this Root CA can sign leaf certificates, but it cannot sign other intermediate Certificate Authorities. If a hacker stole the key and tried to spin up a subordinate CA, browsers would reject it. If this were Constrained(1), the leaf could theoretically act as an intermediate CA itself, which is a massive security vulnerability we want to avoid.

Furthermore, the daemon injects NameConstraints into the root certificate. It restricts this CA so it is only allowed to sign certificates for the kin subtree and the specific TLD_SUFFIX defined in kinetic_core. By baking NameConstraints directly into the certificate’s X.509 extensions, modern browsers will outright reject any attempt to use this specific CA for standard web domains (like google.com). This limits the “blast radius” of a potential private key compromise exclusively to the Kinetic network itself.

It’s also worth noting why rcgen is used here instead of a traditional library like openssl. openssl requires complex C bindings and system dependencies (like libssl-dev), which makes cross-compiling the Kinetic daemon for Windows or macOS an absolute nightmare. rcgen relies on pure Rust/Assembly backends (like ring), meaning the daemon can be statically compiled and remains instantly portable across all architectures without users installing C libraries.

4. Private Key Storage and OS Keychain Integration

-> See: kinetic-daemon/src/ca.rs — Lines 137 to 184

The Root CA’s private key is the single most sensitive piece of cryptographic material the daemon holds. Storing it as raw plaintext on the filesystem is dangerous, as any script with read access could steal it.

The code attempts a progressive, fault-tolerant fallback strategy for storage: First, it uses the keyring crate to inject the private key directly into the Operating System’s native secure enclave (macOS Keychain, Windows Credential Manager, or Linux Secret Service via D-Bus). If this operation succeeds, the key is managed by the OS, encrypted at rest, and never written directly to the Kinetic configuration directory.

However, the OS keychain is frequently unavailable (especially on headless Linux servers or minimal CI environments). If the keychain injection fails, the daemon falls back to raw disk storage but locks down the file permissions. Using conditional compilation attributes (#[cfg(unix)]), the code imports std::os::unix::fs::OpenOptionsExt to enforce a file mode of 0o600. This means the file is readable and writable only by the user owner; group members and guests have zero access. On Windows (#[cfg(windows)]), Unix permissions do not exist. Therefore, it achieves the identical security posture by spawning a subprocess to invoke the icacls command-line utility. It strips all inherited permissions (/inheritance:r) and grants full control (/grant:r) solely to the current $USERNAME.

5. Injecting Trust into the Operating System

-> See: kinetic-daemon/src/ca.rs — Lines 210 to 265

Generating a perfect CA is useless if the host OS refuses to trust it. The trust_root_ca function contains platform-specific, conditionally compiled logic to force this trust.

  • Windows: It executes certutil -addstore -user root. certutil is a native Microsoft binary designed to manipulate the Windows certificate store.
  • macOS: It executes security add-trusted-cert wrapped inside an osascript (AppleScript) command. This AppleScript wrapper is vital because it guarantees the user is presented with the native macOS GUI prompt (requiring Touch ID or an administrator password) to authorize the modification of the restricted System Keychain.
  • Linux: It uses pkexec (Polkit) to escalate privileges, copies the certificate into /usr/local/share/ca-certificates/, and triggers the update-ca-certificates system utility to rebuild the OS trust bundle.

Because these commands fundamentally alter system security and require elevated privileges, they will trigger GUI prompts for the user. If the user clicks “Cancel”, denies the prompt, or if the environment is headless without Polkit, the subprocess will fail. The Rust code matches on the status and logs a warning but does not panic. The daemon will continue to run normally, but the user will face browser security warnings when they attempt to connect.

6. On-the-Fly Leaf Certificate Forgery

-> See: kinetic-daemon/src/ca.rs — Lines 270 to 315

When the daemon’s internal proxy receives an incoming TLS connection intended for app.kin, it must immediately present a valid certificate exclusively for app.kin. The generate_leaf_cert function handles this synchronous generation.

It initializes a new CertificateParams struct targeted precisely at the requested domain. This leaf certificate is configured to be valid for only 30 days. A deliberately short lifespan minimizes risk; if a leaf certificate is somehow cached or extracted, it expires rapidly, forcing a fresh rotation. It then signs this new leaf certificate using the Root CA’s private key.

The final hurdle is format conversion. The rcgen crate outputs certificates and keys in PEM format (Privacy Enhanced Mail, which is Base64 encoded text strings surrounded by -----BEGIN CERTIFICATE----- headers). However, the Kinetic Daemon uses rustls for its TLS termination, and rustls requires DER format (Distinguished Encoding Rules, which is a raw binary format). Rustls requires DER because it is a systems-level networking library optimized for extreme speed, avoiding slow text parsing in the critical path of the TLS handshake.

The function uses rustls_pemfile to parse the PEM strings back into binary CertificateDer arrays, builds the full certificate chain (Leaf -> Root), and constructs the final ServerConfig. Crucially, it calls .with_no_client_auth(). Client authentication (mTLS) is a feature where the server asks the browser to prove its identity using a certificate. Since standard web browsers do not have Kinetic identity certs installed in their client stores, asking for one would cause the browser to abort the connection. We disable it to prevent handshake failures.

7. The LRU Certificate Cache and Concurrency

-> See: kinetic-daemon/src/ca.rs — Lines 318 to 374

Generating RSA or ECDSA key pairs and executing cryptographic signatures requires measurable CPU time. Doing this synchronously for every single network request (every image, every script, every CSS file) requested by the browser would cripple the proxy’s throughput and cause massive latency spikes.

To mitigate this, the LeafCertCache struct implements a bounded LRU (Least Recently Used) caching mechanism. It maps string domains to a tuple containing (Arc<ServerConfig>, Instant).

When a request arrives, get_or_create queries the cache. If the domain exists and was generated less than 1 hour ago (3600 seconds, determined via Instant::now()), it instantly returns a clone of the Arc.

Why use Arc (Atomic Reference Counted pointer) here? The daemon proxy is concurrent, processing hundreds of async connections simultaneously. A ServerConfig contains the complex private key and the entire certificate chain. Cloning the whole structure for every single TCP connection would consume significant memory and CPU. By wrapping it in an Arc, multiple asynchronous tasks can safely share a pointer to the exact same immutable TLS configuration in memory, requiring only a nanosecond atomic integer increment to borrow it.

If the cache hits its maximum capacity (hardcoded to 256 entries), it performs a linear scan using .iter().min_by_key(...) to locate the absolute oldest entry based on its Instant creation timestamp. It evicts this oldest entry before inserting the new one. This ensures the daemon’s memory footprint remains bounded and predictable, even if a user browses thousands of unique .kin domains in a single session.

8. Fuzzing and Test Resiliency

-> See: kinetic-daemon/src/ca.rs — Lines 376 to 477

The module includes extensive #[cfg(test)] blocks to verify the stability of the CA infrastructure. It tests cache eviction, lock file recovery, and disk reads. More importantly, it uses the proptest crate for fuzzing the leaf certificate generation (test_fuzz_leaf_cert_generation).

Because the requested domain comes from the user’s browser, a malicious user might attempt to request a certificate for an absurdly long, malformed, or character-injected domain string (e.g., passing SQL or bash injections into the CommonName field). The proptest feeds chaotic strings (up to 255 characters) into the generate_leaf_cert function to ensure that even when rcgen fails to parse the invalid DNS name, the function returns a clean CaError::Rcgen rather than panicking and crashing the entire daemon. This is a vital security measure against Denial of Service (DoS) attacks on the proxy.


Key Pieces

CaError

-> See: kinetic-daemon/src/ca.rs — Lines 14 to 26 An enumeration capturing all possible failure states within the CA pipeline. It seamlessly wraps standard IO errors, rcgen cryptographic parsing/generation errors, and rustls configuration errors. It utilizes the thiserror crate macro #[from] to automatically derive the From trait, allowing the codebase to use the ? operator to bubble up vastly different error types into this single, unified CaError type.

RootCa

-> See: kinetic-daemon/src/ca.rs — Lines 28 to 36 A straightforward struct holding a tri-state representation of the Root Certificate Authority. It retains the raw PEM string (required for writing to disk and injecting into the OS), the rcgen::KeyPair (required for signing new leaf operations), and the parsed rcgen::Certificate (the public half of the identity). Grouping these prevents the costly need to constantly re-parse the PEM strings into cryptographic objects.

load_or_create_root_ca

-> See: kinetic-daemon/src/ca.rs — Lines 44 to 202 The massive orchestration function executed during the daemon’s boot sequence. It is responsible for managing the atomic .ca.lock file, reading existing certificates from the filesystem, attempting OS keychain retrieval, generating new keys if none exist, enforcing critical DNS name constraints, handling granular disk permission fallbacks, and triggering the OS trust injection subroutines. It returns the RootCa and a boolean flag indicating whether this was a fresh generation (true) or a successful load from disk (false).

trust_root_ca

-> See: kinetic-daemon/src/ca.rs — Lines 210 to 265 A platform-dependent function wrapped in #[cfg(...)] conditional compilation blocks. Note the #[cfg(not(test))] on line 210 — this intentionally disables the function during unit tests to prevent automated test runners from spamming the developer’s OS with permission GUI prompts. It shells out to certutil, security, or pkexec depending on the active compilation target.

generate_leaf_cert

-> See: kinetic-daemon/src/ca.rs — Lines 270 to 315 The core cryptographic engine for the dynamic proxy. It accepts an arbitrary string domain (like “search.kin”) and a reference to the RootCa. It returns a fully configured rustls::ServerConfig ready to be attached to a TCP listener. It handles the intricate, error-prone dance of converting rcgen PEM outputs into the strict, binary CertificateDer arrays required by the rustls ecosystem.

LeafCertCache

-> See: kinetic-daemon/src/ca.rs — Lines 318 to 374 A critical bounding mechanism designed to protect CPU and memory resources. By utilizing Instant::now() rather than system time, it guarantees that its 1-hour expiration logic is immune to NTP clock skews or the user manually changing their system timezone.


How This Connects to the Rest of Kinetic

  • kinetic-core: This entire module is anchored by constants imported from the core crate, specifically kinetic_core::constants::NETWORK_ID and TLD_SUFFIX. These foundational constants dictate the names of the files generated on disk ({network_id}.cert.pem), the organizational name embedded deep within the X.509 certificates, and most importantly, the NameConstraints that prevent the CA from being globally exploited. CROSS-CRATE: NETWORK_ID and TLD_SUFFIX — defined and explained in docs/learn/core/01_overview.md.
  • The Proxy Server (kinetic-daemon): The entire ca.rs file exists solely to serve the daemon’s HTTPS proxy module. The LeafCertCache is held continuously in memory by the proxy’s main routing loop, and get_or_create is invoked every single time a new TLS ClientHello handshake is initiated by the user’s browser.

Quick Reference

  • Root CA Lifespan: 730 days (2 years) from the exact moment of generation.
  • Leaf Cert Lifespan: 30 days.
  • Cache Eviction Policy: 1 hour (3600 seconds) based on monotonic Instant.
  • Maximum Cache Size: 256 unique domains in memory simultaneously.
  • Lock File Retry Logic: 100 attempts, with a 50ms sleep per attempt (totaling a 5-second wait before forced recovery).
  • Key Storage Hierarchy: OS Keychain (Primary) -> 0o600 restricted disk file (Unix Fallback) -> icacls restricted disk file (Windows Fallback).
  • Trust Injection Commands: certutil (Windows), security via osascript (macOS), pkexec (Linux).

Open Questions / Things to Revisit

  • Linux Trust Pathing Constraints: The trust_root_ca function hardcodes /usr/local/share/ca-certificates/ for Linux OS injection. While this works beautifully on Debian/Ubuntu derivatives, distributions like Arch Linux and Fedora use different paths for their system trust stores (/etc/ca-certificates/trust-source/anchors/ and /etc/pki/ca-trust/source/anchors/ respectively). This command will silently fail on those distributions, leaving users with persistent browser warnings despite a successful daemon launch.
  • Cache Eviction Algorithmic Complexity: When the LeafCertCache hits its 256 entry limit, it uses .iter().min_by_key(...) to find the oldest entry to evict. This performs an $O(N)$ linear scan over the entire HashMap. While $N=256$ is trivially small and executes in microseconds, implementing a proper doubly-linked LruCache struct would reduce this operation to true $O(1)$ constant time, which is architecturally cleaner for a high-throughput network proxy.
  • Lock File Edge Case Race Condition: The lock file retry loop removes the .ca.lock file if it hits 100 retries (5 seconds). If the user’s machine is under extreme IO load and a legitimate daemon startup takes longer than 5 seconds to generate the intensive RSA/ECDSA keys, a secondary daemon instance might assume the lock is stale and delete it out from under the first instance, causing a severe race condition during key disk writes.
  • Firefox NSS Store Limitation: Firefox natively maintains its own internal certificate store (NSS) and often ignores the OS-level trust store on macOS and Linux. The current trust_root_ca logic only injects into the OS store, meaning Firefox users may still see “Unknown Issuer” warnings even if the OS injection reports success. This is a known limitation of local proxy development and might require a separate certutil interaction targeted specifically at Firefox’s cert9.db in the future.
  • Keyring Fallback Silencing: The keyring crate might silently fail if the user is connected via SSH without active D-Bus session access, leading to a fallback to the disk storage method without the user ever realizing they lost the hardware-backed security of the OS keychain. We should probably log a more aggressive warning when the primary storage engine fails.
  • Hardcoded Cryptographic Algorithms: The rcgen crate defaults to whatever its backend (like ring or aws-lc-rs) considers the safest modern default, which is typically ED25519 or ECDSA P-256. While this is vastly superior in speed and security to legacy RSA-2048, older enterprise proxy software or legacy browsers might fail to parse elliptic curve root CAs. There is no configuration flag exposed yet in kinetic-daemon to force RSA generation for compatibility.

HTTP Proxying and Routing

Crate: kinetic-daemon Stage: 9 Reading time: 60 minutes Depends on: kinetic-types (Stage 1), kinetic-core (Stage 7), kinetic-network (Stage 8)

What Is This?

This file (http.rs) serves as the central traffic director and HTTP proxy interceptor for the Kinetic daemon. It acts as the critical bridge between traditional Web2 browsers (like Chrome, Firefox, or Safari) and the decentralized, P2P architecture of the Kinetic network.

When a user configures their operating system or web browser to use the Kinetic proxy (often facilitated via a PAC file), every single web request they make is routed through this daemon. The logic in this file determines exactly what to do with those HTTP and HTTPS requests.

This is not a general-purpose VPN or a generic web proxy designed to hide your IP address. It is a specialized, domain-aware routing engine that isolates traffic. Its singular purpose is to identify traffic destined for the .kin Top Level Domain (TLD), intercept it, resolve the decentralized routing paths via the Kinetic DHT (Distributed Hash Table), and forward the packets to the correct decentralized or web-bridged backend.

If a browser asks for google.com, this file will refuse to handle it, returning standard proxy error codes or simply dropping the traffic depending on the context. If a browser asks for saif.kin, this file takes complete ownership of the request lifecycle, ensuring that the traffic is routed over the Kinetic network.

This file is the literal entry point for all user-facing web traffic entering the Kinetic ecosystem.

Why Kinetic Needs This

Kinetic fundamentally changes how domains are registered, resolved, and hosted. Traditional DNS relies on ICANN, root servers, and centralized UDP-based queries. Kinetic relies on cryptographic keypairs, peer-to-peer hash tables, and decentralized storage.

However, web browsers are ignorant of Kinetic’s architecture. They speak HTTP, they expect standard DNS resolution via UDP port 53 to their ISP, and they definitely do not know what a libp2p PeerId is. We cannot ask users to write command-line scripts to download decentralized websites, nor can we realistically force the entire world to use a custom, bespoke “Kinetic Browser.” Widespread adoption requires friction-less integration with existing tools that users already have installed.

This file solves the integration problem. By running a local HTTP proxy on the user’s machine (for example, bound to 127.0.0.1:8080), Kinetic inserts itself into the browser’s standard execution flow. The browser asks the proxy to fetch a webpage, and the proxy (this file) translates that standard HTTP request into a series of decentralized operations.

Without this file, the entire system falls apart from a usability perspective:

  • Browsers would try to ask OS-level DNS for .kin records.
  • Because .kin is not an ICANN-recognized TLD, the ISP’s DNS servers would immediately return NXDOMAIN (domain does not exist).
  • Even if we somehow hijacked the OS DNS to return a decentralized identifier, standard browsers have no idea how to open a TCP connection to a libp2p PeerId or how to fetch data from an IPFS CID natively.
  • Users would not be able to load decentralized IPFS content directly from a .kin domain without manually typing out long, ugly IPFS gateway URLs.

Furthermore, because this proxy runs locally and has access to the user’s internal network, it introduces massive security risks if not handled perfectly. A malicious actor could register a .kin domain and point its DNS record to the user’s local router IP (192.168.1.1). If the user visited that domain, the proxy would obediently fetch the router’s admin page, potentially exposing the local network.

Therefore, this file is not just a router; it is Kinetic’s primary application- layer firewall. It implements strict Server-Side Request Forgery (SSRF) protections, aggressive header sanitization, and infinite proxy loop prevention. It protects the user’s local machine from the decentralized web, and it protects the decentralized web from leaking the user’s local secrets.

How It Works

The architecture of this file is divided into two distinct halves: a front-end receiver that handles the incoming browser protocol, and a backend router that performs the actual DHT resolution and forwarding.

Phase 1: Request Interception and Protocol Handling

The entry point for all proxy logic in this file is the handle_proxy_request asynchronous function.

-> See: crates/kinetic-daemon/src/proxy/http.rs — Lines 6 to 106

1. Loop Prevention via Custom Headers

Before doing any protocol analysis, the proxy inspects the incoming HTTP request headers for a custom Kinetic-specific header: x-kinetic-loop-protect. If this header is present, it checks if the value exactly matches the daemon’s own local libp2p PeerId. If it matches, the request is immediately aborted, returning an HTTP 508 Loop Detected status code to the client.

Why is this necessary? In a decentralized network where nodes can proxy traffic to other nodes, routing loops are a threat. Imagine this scenario:

  • Node A has a .kin domain that resolves to Node B’s IP address.
  • Node B’s DNS configuration for that same domain accidentally resolves back to Node A.
  • A user on Node A requests the domain. Node A forwards the request to Node B.
  • Node B receives the request, resolves the domain, and forwards it back to Node A.

Without loop protection, this request would bounce back and forth infinitely, consuming 100% of the network bandwidth and CPU on both daemons until they crash. By injecting the x-kinetic-loop-protect header on all outbound proxy requests, Kinetic ensures that a request can never bounce back to the node that originally sent it.

2. Handling HTTPS (The CONNECT Method)

When a browser wants to visit a secure HTTPS website, it does not send the actual HTTP GET request to the proxy. If it did, the proxy would be able to see the plaintext traffic, defeating the purpose of TLS encryption. Instead, the browser uses the HTTP CONNECT method. This is the browser asking the proxy to act as a blind TCP tunnel. The proxy just forwards encrypted bytes back and forth without looking at them.

-> See: crates/kinetic-daemon/src/proxy/http.rs — Lines 22 to 66

  • The proxy extracts the target hostname from the CONNECT URI.
  • It immediately performs TLD validation. It normalizes the host and checks if it ends with the configured TLD_SUFFIX (which is .kin). If it does not, the proxy returns an HTTP 403 Forbidden. This enforces that the Kinetic daemon is only used to access the Kinetic network, preventing it from being abused as an open relay for the standard web.
  • If the domain is a valid .kin domain, the proxy acknowledges the connection by returning an HTTP 200 OK.
  • Simultaneously, it uses tokio::spawn to spin up an asynchronous background task.
  • Inside this task, it calls hyper::upgrade::on(req). This is a crucial Rust networking concept. It takes the standard HTTP request-response cycle and “upgrades” the underlying socket into a raw, bidirectional byte stream.
  • This upgraded raw stream is then passed to the handle_connect function (which handles the actual TLS bridging and SNI routing, and is documented in its own topic file).

3. Handling Plain HTTP Fallback

If the request method is not CONNECT, it is a standard HTTP request (like a GET or POST over port 80).

-> See: crates/kinetic-daemon/src/proxy/http.rs — Lines 68 to 106

  • Because standard proxy requests include the full URL in the request line, the proxy manually extracts the HOST header and the URL path.
  • It performs the exact same TLD validation as the CONNECT block, returning an HTTP 502 Bad Gateway if the domain is not a .kin domain.
  • If valid, it passes the original request, the extracted hostname, the network client, and the daemon’s configuration into the massive forward_to_backend_direct function.

Phase 2: Domain Resolution and Routing

Once the proxy knows it needs to fetch a plain HTTP payload for a specific .kin domain, it enters the core routing logic inside forward_to_backend_direct.

-> See: crates/kinetic-daemon/src/proxy/http.rs — Lines 109 to 222

The Resolution Loop

Because DNS domains can use CNAME records to alias to other domains, the resolution process must happen in a loop. Kinetic enforces a strict maximum of 10 recursion steps. If a domain CNAMEs to another domain more than 10 times, the proxy aborts the request. This prevents malicious infinite CNAME chains designed to exhaust the daemon’s CPU.

Step-by-Step Resolution:

  1. Apex Extraction: The router takes the target domain (for example, api.saif.kin) and extracts the apex domain (saif.kin). This is vital because all DNS records in the Kinetic network are stored and signed at the apex level to ensure cryptographic integrity of the entire zone.
  2. DHT Lookup: The router calls network_client.resolve_redundant_payload(&apex_domain). Instead of asking a centralized DNS server over UDP, this asks the libp2p Kademlia DHT for the bytes associated with the apex domain.
    • CROSS-CRATE: The resolve_redundant_payload function and the DHT mechanics are explained deeply in the kinetic-network (Stage 8) documentation.
  3. JSON Deserialization Quirk: The bytes returned by the DHT are not raw DNS records. They are a JSON-serialized NameRecord. This record contains cryptographic signatures, timestamps, and an inner payload.
    • The code uses serde_json::from_slice to parse the NameRecord.
    • It then extracts the inner payload() field. This payload is the actual zone file data, which it passes to DnsZone::parse_payload to reconstruct the full zone structure in memory.
    • CROSS-CRATE: The NameRecord struct and DnsZone parsing logic are documented in kinetic-core (Stage 7).
  4. Subdomain Matching: The router must now find the specific record the user asked for. It calculates the requested subdomain. If the target was saif.kin, the subdomain is @ (the standard DNS symbol for the apex). If the target was api.saif.kin, it trims the apex and determines the subdomain is api.
  5. Record Selection & Precedence: It looks up the records for the calculated subdomain inside the parsed DnsZone. A single subdomain can have multiple records defined by the owner. The router iterates through them, attempting to find a routable target based on a specific match statement:
    • IP Addresses (A / AAAA): If found, the target is treated as a Web2 bridge. The loop breaks immediately.
    • P2P Identifiers (PeerId): If found, the target is treated as a native decentralized route. The loop breaks immediately.
    • Decentralized Storage (IPFS): If found, the target is formatted as an ipfs:// string and treated as a static content route. The loop breaks immediately.
    • Aliases (CNAME): If found, the loop updates the target domain and restarts the entire resolution process from Step 1 (up to the 10-hop limit). Crucially, the code checks if the CNAME target ends in .kin. External CNAMEs (e.g., pointing a .kin domain to aws.com) are rejected and fail the resolution.
    • Text (TXT): TXT records are skipped in this loop via a continue statement, as they contain verification data, not routing data.

Phase 3: The Three Routing Strategies

Once the resolution loop terminates with a concrete target string, the router branches into one of three distinct execution paths based on the format of the target.

Strategy 1: IPFS Gateway Proxying

-> See: crates/kinetic-daemon/src/proxy/http.rs — Lines 225 to 269

If the target string begins with the ipfs:// protocol prefix, the domain is hosting a static website on the InterPlanetary File System.

Since standard web browsers cannot natively speak the IPFS swarm protocol, the proxy must bridge the request through an HTTP IPFS Gateway (which can be a public gateway like ipfs.io, or a local IPFS node running on the user’s machine).

  • URL Construction: The proxy extracts the CID (Content Identifier) from the target string. It constructs a brand new URL by combining the configured ipfs_gateway setting from the daemon’s config, the CID, and the user’s originally requested HTTP path.
  • Example: A request for http://blog.kin/style.css pointing to ipfs://QmABC becomes a request to http://127.0.0.1:8080/ipfs/QmABC/style.css.
  • Request Forwarding: It uses the asynchronous reqwest HTTP client to forward the request to the gateway.
  • Header Stripping (Request): The proxy strips the original HOST header from the request before sending it to the gateway. If it left the HOST: blog.kin header intact, the IPFS gateway would try to serve content for blog.kin instead of parsing the URL path for the CID, and the request would fail with a 404.
  • Header Stripping (Response): When the IPFS gateway responds with the content, the proxy iterates through the response headers and strips out the Strict-Transport-Security (HSTS) header before passing the response back to the browser.
  • Why? Because if the browser caches an HSTS policy for a .kin domain, it will stubbornly refuse to load that domain over plain HTTP in the future. Because IPFS gateways often enforce HSTS by default, we must strip it to prevent breaking the local proxy experience for the user.

Strategy 2: IP Routing and SSRF Protection (Web2 Bridging)

-> See: crates/kinetic-daemon/src/proxy/http.rs — Lines 271 to 379

If the target string parses successfully as a standard IPv4 or IPv6 address, the .kin domain is bridging out to a traditional web server. Because this instructs the local daemon to make arbitrary TCP connections based on untrusted DNS data, this is the most dangerous path in the entire codebase.

  • The SSRF Threat: Server-Side Request Forgery (SSRF) occurs when an attacker tricks a server into making unauthorized internal network requests. If an attacker registered evil.kin and pointed its DNS A record to 127.0.0.1:8080, and the user visited that domain, the attacker could theoretically interact with local services running on the user’s machine, bypassing the user’s local firewall.
  • Port Protection (Case 199): To combat this, the proxy deeply inspects the requested port. If the domain points to a loopback address (127.0.0.1) OR an unspecified address (0.0.0.0), AND it attempts to hit one of the Kinetic Daemon’s own internal listening ports, the request is hard-blocked and rejected with a Proxy Error.
  • The blocked ports include: proxy_port, api_port, dns_port, backend_port, the network daemon_port, and the pac_port.
  • This prevents infinite proxy loops and protects the daemon’s internal, unauthenticated API from being accessed via a malicious domain. This check occurs even in Dev Mode. It cannot be bypassed.
  • Network Isolation: Next, it uses a helper function (is_ssrf_risk) to check if the IP resides in a private network block (e.g., 192.168.x.x, 10.x.x.x). Unless the user has enabled is_dev_mode in their configuration, the proxy blocks all requests to private networks entirely.
  • Dev Mode Bypass: Developers often need to point a .kin domain to a local development server running on their laptop (e.g., 192.168.1.5:3000). Enabling Dev Mode bypasses the private network block, allowing these requests through, though it still logs a stark warning.
  • HTTPS Auto-Upgrade: If the IP is a public, safe, routable IP, Kinetic assumes the target server is a modern Web2 server. To enforce security by default, it automatically upgrades the outbound connection scheme to HTTPS and defaults the port to 443 (unless a specific port was requested by the user).
  • Certificate Forgiveness: Because the target IP is hosting a .kin domain, it is unlikely to have a publicly trusted TLS certificate for that domain (since Certificate Authorities don’t recognize .kin). Therefore, the proxy configures the internal reqwest client with danger_accept_invalid_certs(true), allowing self-signed or invalid certificates for Web2 bridges.
  • Header Injection: It injects the x-kinetic-loop-protect header containing its own Peer ID before forwarding the request to the backend.
  • Just like the IPFS strategy, it sanitizes the response, stripping HSTS headers to prevent browser lock-out.

Strategy 3: Native P2P Routing

-> See: crates/kinetic-daemon/src/proxy/http.rs — Lines 380 to 468

If the target string parses successfully as a libp2p PeerId, the domain is hosted natively on another Kinetic node. The HTTP request must be serialized, wrapped, and sent over the decentralized swarm via libp2p.

  • Dynamic Resolution (HostRoutingRecord): Before routing, the proxy checks if the requested Peer ID is actually a static HostRoutingRecord. In the Kinetic ecosystem, users with dynamic IPs can register a static, permanent Host ID. They then dynamically update the DHT to point that Host ID to their current, ephemeral Peer ID whenever their IP changes.
  • The proxy transparently resolves this by asking the NetworkClient for the current ephemeral Peer ID associated with the static Host ID.
  • Aggressive Privacy Sanitization: This is a critical security and privacy feature. When routing standard HTTP traffic over a public P2P network, we cannot trust the intermediary nodes or the destination node. The proxy strips sensitive headers from the incoming browser request before serializing it:
    • authorization: Removes API tokens and Basic Auth credentials.
    • cookie: Removes session cookies. This prevents session hijacking. If a user had a cookie for a Web2 site, and accidentally visited a .kin site with the same path structure, their browser might send the cookie. The proxy stops this dead.
    • x-api-key: Removes custom authentication tokens.
    • proxy-authorization: Removes local proxy credentials.
  • Body Buffering and Memory Protection: Because P2P messages are handled as distinct byte packets in the libp2p request-response protocol, the proxy cannot easily stream infinite HTTP bodies as it could with a raw TCP socket. Instead, it uses http_body_util::BodyExt and the .frame().await method to incrementally read the incoming HTTP request body into memory.
  • The 5MB Limit: As it buffers the body, it constantly checks the size. It enforces a strict LIMITS_PROXY_MAX_BODY_BYTES limit (currently 5 Megabytes). If a user tries to POST a file larger than 5MB to a P2P node, the proxy immediately aborts the connection. This prevents a malicious user from executing a memory exhaustion (OOM) attack on the local daemon by uploading a 10GB file.
  • P2P Transport: The sanitized headers, the URL path, the HTTP method, and the buffered body bytes are packaged into a custom kinetic_network::ProxyRequest struct.
  • The proxy calls network_client.send_proxy_request. This function serializes the struct and transmits it over a dedicated libp2p request-response protocol to the target peer.
    • CROSS-CRATE: send_proxy_request and the underlying libp2p protocol are documented in kinetic-network (Stage 8).
  • When the remote peer processes the request and replies, the proxy reconstructs a standard HTTP response. It strips HSTS and public-key-pins headers, and streams the body back to the user’s local browser.

Key Pieces

handle_proxy_request

  • Location: http.rs:L6-L106
  • What it does: The primary entry point for all incoming HTTP traffic originating from the local web browser or OS.
  • Why it matters: It acts as the gatekeeper. It separates raw TCP CONNECT tunnels (used for secure HTTPS) from standard HTTP plaintext fallback requests. Crucially, it enforces the .kin TLD restriction, ensuring the daemon is not abused as an open relay proxy for the clearnet.

forward_to_backend_direct

  • Location: http.rs:L109-L475
  • What it does: The massive, monolithic routing engine that resolves a .kin domain to a physical target and executes the correct forwarding strategy.
  • Why it matters: This function contains the core logic for decentralized DNS resolution, CNAME handling, and SSRF security. It is the absolute heart of the Kinetic proxy feature.

SSRF Port Protection Block (Case 199)

  • Location: http.rs:L298-L312
  • What it does: Hard-blocks proxy requests that resolve to local IP addresses and attempt to hit the daemon’s own internal listening ports.
  • Why it matters: Prevents a malicious .kin domain from creating an infinite proxy loop or accessing the daemon’s private, unauthenticated management API. This protection cannot be bypassed, even when running in Dev Mode.

Header Sanitization Logic

  • Location: http.rs:L404-L419
  • What it does: Iterates through all HTTP headers and strips sensitive authentication and cookie headers from requests destined for P2P targets.
  • Why it matters: Ensures that a user’s local web browser does not accidentally leak sensitive Web2 session data over the public decentralized network to an untrusted peer.

How This Connects to the Rest of Kinetic

This file operates as a high-level orchestrator. It does not implement low-level networking protocols directly; instead, it glues together the local browser environment with the complex Kinetic core libraries.

  • Inputs from the User: It receives standard HTTP traffic directly from a web browser (e.g., Chrome, Safari) or from operating system utilities using PAC (Proxy Auto-Configuration) files.
  • Dependencies on kinetic-core: It relies on kinetic_core::types::DnsZone and kinetic_core::types::NameRecord to parse and understand the routing data retrieved from the network.
    • CROSS-CRATE: DnsZone and NameRecord concepts are explained thoroughly in Stage 7 (kinetic-core).
  • Dependencies on kinetic-network: It uses the NetworkClient to query the Distributed Hash Table via resolve_redundant_payload and to transmit HTTP requests to other peers via send_proxy_request.
    • CROSS-CRATE: DHT operations and the P2P proxy protocol are explained in Stage 8 (kinetic-network).
  • Outputs to Backends: It generates outbound HTTP traffic to IPFS gateways and traditional Web2 servers using the reqwest HTTP client, and outbound P2P traffic via the libp2p swarm.

Quick Reference

  • Supported Target TLD: .kin only. Requests for other TLDs are rejected.
  • Maximum CNAME Recursion: 10 hops before aborting with a NameNotFound error.
  • Loop Protection Header: x-kinetic-loop-protect prevents infinite A -> B -> A proxy loops.
  • Blocked Internal Ports (SSRF): Proxy Port, API Port, DNS Port, Backend Port, Daemon Port, PAC Port.
  • Stripped P2P Request Headers: authorization, cookie, x-api-key, proxy-authorization.
  • Stripped Response Headers: strict-transport-security (HSTS), public-key-pins.
  • Max P2P Body Size: 5MB. Requests larger than this are dropped to prevent memory exhaustion.
  • Dev Mode Impact: Allows routing to private IP addresses (e.g., 192.168.x.x), but still blocks access to internal daemon ports.

Open Questions / Things to Revisit

  • P2P Body Streaming Inefficiency: Currently, the entire HTTP request body is buffered into memory (up to the 5MB limit) before being packaged into a ProxyRequest and sent over the P2P network. While this effectively prevents memory exhaustion attacks, it makes uploading large files over the native P2P network practically impossible. We need to investigate if libp2p streams can be utilized to stream the HTTP body incrementally to the target peer, bypassing the need for a memory buffer entirely.

  • HSTS Stripping Policy: The proxy indiscriminately strips strict-transport-security from all backend responses. This is a pragmatic choice to prevent browsers from locking themselves into HTTPS for domains that might later change to plain HTTP or P2P hosts. However, for node operators that intentionally want to enforce strict HTTPS for their .kin bridges, this forcefully downgrades their security. A future enhancement could allow nodes to opt-in to HSTS via a specific DNS TXT record flag.

  • Hardcoded Connection Timeouts: The timeout for fetching data from an IPFS gateway is hardcoded to 30 seconds, and the timeout for Web2 IP bridges is 15 seconds. These values might be too aggressive for users on slow decentralized connections, leading to false failures. These values should eventually be extracted into the KineticConfig so they can be tuned by the node operator.

  • CONNECT Payload Inspection Blindspot: The handle_proxy_request function successfully intercepts the CONNECT method and spawns a tunnel, but this file does not inspect the contents of that tunnel whatsoever. If an attacker uses the proxy to tunnel non-TLS traffic (e.g., SSH or raw TCP attacks) to a .kin domain, the proxy will happily pass it through blindly. This is generally acceptable for a local proxy meant for web browsers, but we must ensure the downstream handle_connect logic provides sufficient safeguards against protocol abuse.

Deep Dive: The SSRF Threat Model

To truly understand why the forward_to_backend_direct function is so complex, we must deeply analyze the threat model it is defending against. A Server-Side Request Forgery (SSRF) attack on a local proxy is uniquely devastating because the proxy runs on the user’s trusted local network.

Consider three distinct attack vectors that this file actively mitigates:

Vector 1: The Localhost Admin Panel Many developers run local services on 127.0.0.1 (localhost), such as database admin panels (phpMyAdmin), local development servers, or even system management APIs. These services often lack authentication because they assume any traffic originating from 127.0.0.1 is the authorized local user. If an attacker registers evil.kin and points its DNS A record to 127.0.0.1:5432 (PostgreSQL), and tricks a Kinetic user into visiting http://evil.kin, the user’s browser sends the request to the Kinetic proxy. Without SSRF protection, the proxy would obediently connect to 127.0.0.1:5432 and forward the attacker’s HTTP payload into the local database, potentially executing arbitrary SQL commands. The is_ssrf_risk function in this file identifies 127.0.0.1 (loopback) and 0.0.0.0 (unspecified) as high-risk IPs and drops the connection.

Vector 2: The Home Router Attack Most consumer home networks use a private IP space, typically 192.168.1.0/24 or 10.0.0.0/8. The home router usually resides at 192.168.1.1 and often has an administrative web interface that is vulnerable to Cross-Site Request Forgery (CSRF) or uses default credentials. An attacker can point router.evil.kin to 192.168.1.1. When the user visits this domain, the proxy, acting on behalf of the user, connects to the router. Because the proxy is on the same local network as the router, the connection succeeds. The attacker can then use the proxy to reconfigure the user’s router, perhaps changing its DNS settings to a malicious server. This is why Kinetic blocks all private IPv4 (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16) and private IPv6 (Unique Local Addresses) by default.

Vector 3: The Internal Daemon Loop (Case 199) This is the most subtle vector. The Kinetic daemon itself listens on several local ports (e.g., 127.0.0.1:8080 for the proxy, 127.0.0.1:3000 for the management API). What happens if an attacker points a domain back at the proxy port itself? The request hits the proxy. The proxy resolves the domain. The domain points back to the proxy. The proxy opens a new connection to itself and forwards the request. This creates an infinite loop of recursive proxy connections, rapidly exhausting all available file descriptors and memory on the host system, resulting in a severe Denial of Service (DoS). Furthermore, if the domain pointed to the daemon’s internal management API (which requires no authentication because it assumes local trust), the attacker could instruct the daemon to delete files or broadcast malicious P2P messages. This is mitigated by the explicit port-checking block (Lines 298-312), which is enforced unconditionally, even if the user has enabled Dev Mode.

Deep Dive: Rust Asynchronous Networking

This file makes heavy use of advanced Rust networking concepts. Let’s break down the mechanics of the CONNECT tunnel establishment.

When the handle_proxy_request function encounters a CONNECT method, it executes the following critical block:

  • The function hyper::upgrade::on(req) takes ownership of the incoming HTTP request. In the HTTP/1.1 protocol, a client can send an Upgrade header (or in the case of CONNECT, it is implied) to signal that it wants to stop speaking HTTP and start speaking a custom protocol over the established TCP socket.
  • The hyper library handles the complex state machine required to flush the HTTP response headers and hand over the raw, underlying TcpStream (or TLS stream) back to the application.
  • Because waiting for the upgrade to complete and then pumping bytes back and forth is a long-running, blocking operation, Kinetic cannot perform it on the main proxy routing thread.
  • It uses tokio::spawn(async move { ... }) to detach this workload into a lightweight asynchronous Green Thread. The async move closure captures the necessary variables (like the root_ca and leaf_cache) and moves them into the background task.
  • The main thread immediately returns an HTTP 200 OK response to the browser, telling it “The tunnel is ready, start sending your encrypted bytes.”
  • In the background, the spawned task waits for hyper::upgrade::on to return the Upgraded object, which implements AsyncRead and AsyncWrite. This object is then passed to the specialized handle_connect TLS bridging logic.

Detailed Execution Walkthrough: P2P Routing

Let’s trace the exact lifecycle of a request destined for a decentralized peer.

  1. The Request Arrives: The user types http://decentralized.kin/app.js into Chrome. Chrome sends an HTTP GET request to the local Kinetic proxy.
  2. Entrypoint: handle_proxy_request receives a Request<Incoming> object from the hyper server.
  3. Loop Check: It extracts the x-kinetic-loop-protect header. It is absent (since Chrome didn’t send it). The check passes.
  4. Method Check: The method is GET, not CONNECT. It falls through to the plaintext handler.
  5. Host Extraction: It parses the Host: decentralized.kin header. It verifies the .kin suffix.
  6. Delegation: It calls forward_to_backend_direct, passing the request.
  7. DHT Resolution: The routing loop asks the P2P NetworkClient to resolve decentralized.kin.
  8. Payload Parsing: The DHT returns a JSON NameRecord. The code deserializes it, extracts the payload, and parses it into a DnsZone.
  9. Record Matching: It looks for the @ (apex) record in the zone. It iterates through the records and finds a DnsRecord::PeerId("12D3KooW...").
  10. Target Acquisition: The loop breaks. The target is a PeerId.
  11. Strategy Selection: The execution jumps to the P2P routing block (Strategy 3).
  12. Header Sanitization Loop: The code creates a new, empty Vec for headers. It iterates through the original request headers. If a header is named cookie, it is dropped. If it is named authorization, it is dropped. The Host header is overridden to ensure it exactly matches the resolved domain.
  13. Body Extraction: The code extracts the asynchronous body stream using .into_body(). It enters a while let Some(chunk) loop, polling the stream for data frames.
  14. Buffer Limit Enforcement: As each frame arrives, its bytes are appended to a Vec<u8>. If the vector’s length exceeds LIMITS_PROXY_MAX_BODY_BYTES (5MB), the loop aborts and returns an error.
  15. Serialization: The sanitized headers, the HTTP method string, the URL path, and the fully buffered body bytes are assembled into a ProxyRequest struct.
  16. Network Transmission: network_client.send_proxy_request is called. This uses libp2p’s request-response protocol to find the target peer in the swarm, open a substream, serialize the ProxyRequest via MessagePack or JSON, and transmit it.
  17. Response Handling: The target peer processes the request and sends back a ProxyResponse.
  18. Response Reconstruction: The proxy takes the ProxyResponse, uses axum::body::Body::from to convert the returned byte array back into an asynchronous body stream, applies the returned headers (while skipping strict-transport-security), and returns the final Response object to the hyper server.
  19. Delivery: The hyper server writes the HTTP response back down the TCP socket to Chrome. The user sees app.js load successfully.

Error Handling and Protocol Signalling

When a proxy fails to route a request, it is critical that it communicates the failure correctly to the upstream web browser. Browsers have specific hardcoded behaviors for different HTTP error codes when they are communicating with a proxy server. This file maps Kinetic’s internal routing failures to standard HTTP status codes.

  • HTTP 508 Loop Detected: If the x-kinetic-loop-protect header matches the daemon’s own Peer ID, the proxy returns a 508. This is a relatively obscure HTTP status code introduced by WebDAV, but it is semantically perfect here. It immediately tells the client (or the upstream proxy in a chain) that the network topology is broken.
  • HTTP 403 Forbidden: If a browser tries to establish a CONNECT tunnel to a non-.kin domain (like google.com:443), the proxy returns a 403. This is the standard proxy signal for “I understand your request, but I am administratively configured to refuse it.” This prevents the Kinetic daemon from being used as a generalized open relay, which would consume bandwidth and potentially expose the user’s IP address.
  • HTTP 502 Bad Gateway: If the proxy receives a plain HTTP request for a non-.kin domain, or if the forward_to_backend_direct function fails to resolve the domain in the DHT, it returns a 502. This tells the browser that the proxy itself is functioning correctly, but the upstream server (in this case, the decentralized network) failed to provide a valid response.

The overarching philosophy of error handling in this file is fail-closed. If a domain cannot be resolved, if an IP address is suspicious, or if a payload is too large, the proxy immediately aborts the connection rather than attempting a risky fallback.

Forward References: The CONNECT Tunnel and Certificates

While this file handles the interception of the CONNECT method, it does not actually perform the TLS man-in-the-middle attack necessary to bridge HTTPS traffic.

When hyper::upgrade::on yields the raw byte stream, it passes that stream, along with the root_ca and a leaf_cache, into the handle_connect function.

  • The root_ca is the Kinetic daemon’s dynamically generated local Certificate Authority.
  • The leaf_cache is a memory-safe, thread-safe hash map (Arc<Mutex<LeafCertCache>>) that stores temporarily generated certificates for specific .kin domains.

Because generating RSA or ECDSA keypairs for every single HTTPS request is computationally expensive, the leaf_cache is passed from the main proxy state down into the connection handler. This allows the connection handler to reuse the fake certificate for saif.kin across multiple concurrent HTTP requests, dramatically improving proxy latency. FORWARD DEPENDENCY: The exact mechanics of TLS certificate generation, SNI parsing, and stream bridging are documented in the subsequent topic file covering https.rs. For now, understand that http.rs sets up the tunnel, and https.rs executes the cryptography within it.

Architectural Analogy

Think of this file as the strict border control checkpoint of a sovereign nation (the Kinetic Network).

  • The browser is a foreigner arriving at the border. It speaks a different language (HTTP) and expects different infrastructure (ICANN DNS).
  • The handle_proxy_request function is the initial passport check. It ensures the traveler is actually trying to enter the correct country (the .kin TLD) and isn’t a known threat (loop protection).
  • The forward_to_backend_direct function is the immigration routing system. It looks up the traveler’s destination in the national database (the DHT), ensures they aren’t trying to access restricted military bases (SSRF private network protection), and assigns them an escort to safely reach their final destination (the P2P proxying or IPFS bridging).

API Routing, State, and Server Bootstrap

Crate: kinetic-daemon Stage: 9 Reading time: 60 minutes Depends on: Stage 7 (kinetic-core), Stage 8 (kinetic-network)


What Is This?

This file (kinetic-daemon/src/api/mod.rs) serves as the entry point, the structural core, and the ultimate security gatekeeper for the Kinetic Daemon’s HTTP API. It acts as the primary bridge between the node’s internal peer-to-peer (P2P) network capabilities and the external world. Specifically, it defines the shared state that every single API handler uses to interact with the node’s underlying services. It establishes the web server itself using the axum web framework, which is built on top of the Tokio asynchronous runtime. It configures Cross-Origin Resource Sharing (CORS) to prevent browser-based attacks from malicious websites. It enforces strict Role-Based Access Control (RBAC) via custom authentication middleware that intercepts every sensitive request. In the context of the Kinetic network, the daemon is the only practical way a user or external application can interact with a node. The P2P network speaks its own complex, binary gossip protocols (like Gossipsub) and custom RPC mechanisms over encrypted streams. These raw network protocols are not easily consumed by local bash scripts, Python utilities, or browser extensions. This HTTP API translates local, standardized web requests (like GET and POST) into complex node actions. Without this module, a Kinetic node would be an isolated, silent box sitting on a server or laptop. It would be able to synchronize with the network, validate blocks, and store records. However, it could not be commanded, queried, or monitored by the person running it. This file is what turns the node into a usable application rather than just a background service. It defines the exact pathways that data flows from the user’s CLI or UI, down into the daemon, and out to the global network. Crucially, it defines the pathways by which the daemon prevents unauthorized actors from doing the exact same thing. It is the translation layer between HTTP and the Kinetic protocol. It is the state manager that ensures the web server does not corrupt the node’s internal database. And it is the security layer that ensures your local node remains yours and yours alone. This highlights the importance of the API layer acting as a protective barrier and translation engine for the node. It cannot be overstated how critical this module is for the operational safety of a Kinetic deployment. The local environment is fundamentally dangerous because any malicious process or script running on the host machine could potentially access the API. Therefore, the daemon cannot assume that a request coming from localhost is inherently trustworthy or safe. It must rigorously authenticate and authorize every single action that modifies the state of the node. By providing a unified, secure interface, this module enables the rich ecosystem of Kinetic developer tools to exist without compromising the underlying node security. The design of this file is influenced by the principle of least privilege, ensuring that even if one component is compromised, the damage is contained. The mod.rs file effectively acts as a traffic controller, directing incoming HTTP requests to their appropriate handlers while blocking any unauthorized traffic. It is the heart of the node’s user-facing capabilities, transforming complex internal operations into simple, consumable RESTful endpoints. Understanding this file is essential for anyone looking to build applications on top of a local Kinetic node, as it defines the entire surface area available to developers. It is the definitive reference for what the node is capable of doing on behalf of the user.


Why Kinetic Needs This

The Kinetic core and network crates handle the actual heavy lifting of the protocol. They manage the gossip protocols, compute the Verifiable Delay Function (VDF) proofs, handle cryptographic name resolution, and manage distributed storage. However, those crates operate in the background. They do not have User Interfaces. They do not speak JSON. They do not understand HTTP headers. Kinetic needs a local HTTP server to expose these powerful capabilities to the user’s local environment. But a local server running on a user’s machine is a massive security risk if it is not designed with extreme paranoia. If a malicious website running in the user’s web browser can send unauthorized AJAX requests to http://localhost:<kinetic_port>, it could hijack the node. The malicious site could use the user’s funds, publish malicious records under the user’s identity, or exhaust the machine’s CPU by submitting thousands of fake VDF tasks. This file solves that exact problem by implementing an ironclad security and routing perimeter. It ensures that the convenience of a local HTTP API does not compromise the security of the node. It implements scoped authentication. Instead of a single password, it uses generated, single-purpose bearer tokens. It prevents timing attacks by using constant-time cryptographic string comparison, ensuring attackers cannot brute-force tokens by measuring response times. It restricts CORS aggressively. It blocks random web origins from accessing the API, while allowing local tools and known extension schemas. It manages state safely. It provides a thread-safe, unified view of the node’s internal state to all stateless HTTP handlers running across multiple Tokio threads. Without this constructed layer, the node would be vulnerable to a class of attacks known as Cross-Site Request Forgery (CSRF) and local privilege escalation. This module is what makes the node safe to run on a personal laptop while browsing the open web. The decisions made in this file—such as how to bind to a port, how to fail over to IPv6, and how to rotate tokens—are all driven by the need for operational resilience. A node must stay up, and it must stay secure, even when the local environment is hostile or misconfigured. This module guarantees that resilience. Every architectural choice in mod.rs was made to balance the need for developer ergonomics with the absolute necessity of node security. The local environment is often the most dangerous environment for a blockchain or P2P node. By assuming the local environment is hostile, this module protects the user from their own mistakes. The necessity of this module is driven by the reality that a node without an interface is useless, but an interface without security is dangerous. The careful balance struck here is the core value proposition of the daemon crate. If the API were exposed without these protections, any random script or browser tab could initiate a governance vote or overwrite critical zone data. By enforcing a strict boundary, Kinetic ensures that the user is always in control of their node, and that any automated interactions are authorized. This is particularly important for a decentralized system, where the user’s node is their sovereign agent on the network. A compromised node could be used to attack the rest of the network, making the security of this local API a matter of global network health. Furthermore, the clear separation of concerns provided by this module allows the core protocol engineers to focus on cryptography and networking, while the daemon engineers focus on user experience and API design. This modularity is essential for the long-term maintainability and evolution of the Kinetic codebase. Ultimately, without this security and routing layer, the system would collapse under the weight of local exploits.


How It Works

The mod.rs file orchestrates the entire API lifecycle, from the moment the daemon boots up to the processing of individual HTTP requests. The mechanisms here are complex because they bridge asynchronous Rust with web server paradigms, ensuring safety and performance.

1. Bootstrapping and Token Generation

Before the daemon ever opens a network port to listen for HTTP requests, it must secure itself. This boot sequence is handled by the ensure_api_tokens() and rotate_token_on_boot() functions. -> See: kinetic-daemon/src/api/mod.rs — Lines 247 to 306

Unlike legacy systems that use a single static “admin password” defined in a configuration file, Kinetic uses dynamic, scoped, single-purpose tokens. During the boot phase, the system goes through a rigorous generation process:

  • The system checks the local ~/.kinetic/api_tokens directory to ensure it exists.
  • It iterates through each distinct role the API supports: admin, publish, vdf, governance, and atlas.
  • For each role, it generates a new 32-byte cryptographically secure random token.
  • It uses the getrandom crate for this. This is not a pseudo-random number generator; it hooks directly into the operating system’s entropy pool (e.g., /dev/urandom on Linux).
  • It encodes these raw bytes into a hex string, which is standard for HTTP Bearer tokens.
  • It writes these hex strings to disk using strict Unix file permissions (0o600).
  • This 0o600 permission mask means that only the specific user account running the daemon can read or write the token files. Not even other users on the same physical machine can access them.
  • This prevents a scenario where a shared server environment compromises the node’s API tokens.
  • If the token files already exist from a previous run, they are overwritten. This is the concept of ephemeral tokens.

If the getrandom call fails (which can happen on deeply embedded systems that have not gathered enough entropy yet), the daemon is programmed to crash immediately. It refuses to start with predictable or weak tokens. This avoids a vulnerability where an attacker could guess the tokens due to low system entropy. This fail-fast behavior is a cornerstone of Kinetic’s security philosophy: it is better to be unavailable than to be insecure.

2. State Construction and Concurrency

Once the security tokens are generated, the daemon bundles everything the HTTP API needs into an ApiState struct. -> See: kinetic-daemon/src/api/mod.rs — Lines 105 to 129

Because an HTTP server is concurrent (it handles many requests at once, often across dozens of threads via the Tokio runtime), the state must be thread-safe. Rust’s compiler enforces this strictly, requiring all shared state to be Clone and Send.

  • The NetworkClient: This is passed in from the network module. It is already a cheap-to-clone handle designed for cross-thread usage.
  • The StorageEngine: This is wrapped in an Arc<dyn StorageEngine>. Arc (Atomic Reference Counted) allows multiple threads to hold a pointer to the storage engine without duplicating the database connection. The dyn keyword indicates it uses dynamic dispatch, allowing different storage backends.
  • VDF Tasks: Ongoing VDF tasks are stored in a HashMap. Because multiple endpoints might want to query or update a task’s status simultaneously, it is wrapped in an Arc<Mutex<HashMap<String, VdfTaskStatus>>>. The Mutex (Mutual Exclusion) ensures only one thread writes to the map at a time.
  • VDF Semaphore: A tokio::sync::Semaphore is included to throttle heavy VDF computations. If the API receives ten VDF requests at once, the semaphore limits how many can run in parallel so the daemon does not crash the host machine’s CPU.
  • Gossip Broadcast: A tokio::sync::broadcast::Sender is included so that HTTP requests can inject messages directly into the P2P gossip network, and subscribe to incoming gossip streams via Server-Sent Events (SSE).
  • Atlas TLDs: An Arc<RwLock<HashSet<String>>> tracks the top-level domains managed by the Atlas bridge, allowing concurrent reads but exclusive writes.
  • Bind IP: The IP address the daemon is bound to is stored so that handlers can reference it, particularly useful for constructing self-referential URLs or configuring CORS dynamically.

This rigorous state management ensures that no matter how many HTTP requests hit the API simultaneously, the node’s internal state remains consistent and free of race conditions. Rust’s borrow checker guarantees that the Mutex and RwLock primitives are used correctly at compile time.

3. Server Initialization and Fallback Binding

With the state fully constructed, the actual HTTP server is initialized in start_server(). -> See: kinetic-daemon/src/api/mod.rs — Lines 308 to 375

Kinetic attempts to bind to the user-specified IP and port (defaulting to 127.0.0.1 and 8080). However, local networking environments are notoriously finicky and unpredictable. If the primary bind fails, Kinetic implements a robust retry loop. It will try up to 10 times, pausing for 200 milliseconds between each attempt. This is crucial because sometimes the OS takes a few seconds to fully release a port from a previously crashed or terminated instance of the daemon (this is known as the TIME_WAIT TCP state). If all attempts to bind to the IPv4 address fail, the server initiates a fallback procedure. It attempts to bind to the IPv6 loopback address [::1]. This fallback ensures the daemon remains accessible even in strange networking environments, VPN configurations, or operating systems where IPv4 localhost networking is restricted or disabled entirely. Once successfully bound, it prints the local address to the console and hands the listener off to axum::serve(), which begins accepting incoming TCP connections and routing HTTP requests. This ensures a smooth developer experience; the daemon works hard to find a usable port configuration without requiring manual intervention from the user.

4. Routing Architecture and Submodules

The API is massive, so it is divided into a vast array of submodules. Each submodule manages a distinct part of the node’s functionality. -> See: kinetic-daemon/src/api/mod.rs — Lines 148 to 245

The atlas module: This module provides endpoints for the kinetic-atlas bridge. Atlas is a system for migrating traditional DNS Top-Level Domains (TLDs) into the Kinetic namespace. The endpoints here allow an external bridge service to notify the daemon of state changes, register new TLDs, or synchronize root zone data. Because modifying TLD mappings is sensitive and impacts global resolution, these routes are authenticated with the Atlas role token.

The config module: This module allows external clients to read and mutate the daemon’s runtime configuration dynamically. For example, a user might want to adjust their default gossip verbosity or change their primary seed nodes without restarting the daemon. The GET request is authenticated to prevent local scripts from scraping node configuration, while the POST request requires the Admin token.

The gossip module: Gossip is the heartbeat of the P2P network. This module allows local applications to interact directly with the pub/sub streams. The /gossip/publish/{topic} endpoint allows a client to inject a raw message into a specific network topic. The /gossip/subscribe/{topic} endpoint uses Server-Sent Events (SSE) to stream real-time network chatter back to the client. The subscription route is public because it only reads data, whereas publishing requires authentication to prevent local scripts from spamming the network under the node’s identity.

The kid module: KID (Kinetic Identity Document) is the cryptographic identity system. This module provides the tooling necessary to manage the user’s local keypairs. Endpoints allow for generating a new KID, listing all currently managed KIDs, fetching the details of a specific KID, and rotating the cryptographic keys associated with a given identity. Since the KID represents the user’s sovereign identity, any modification (like generation or rotation) is locked behind the Admin role.

The publish module: This is perhaps the most critical module. It handles the submission of data to the global network. It takes a fully formed NameRecord, signs it if necessary, commits it to local storage, and then broadcasts it to the P2P swarm via gossip. It includes specialized variants like /publish-kid for publishing identity documents, /publish-manifest for complex applications, and /publish-governance for voting. Because publishing consumes resources and commits the node cryptographically, it is protected by the Publish token.

The resolve module: The flip side of publishing is resolution. This module allows clients to query the network for the current state of a name. When a request hits /resolve/{name}, the daemon checks its local database, and if the record is missing or stale, it issues a request to its peers to find the latest data. It resolves standard Kinetic domains as well as KIDs. Since resolution is purely read-only and harmless, these routes are public.

The time module: The Kinetic network relies on a synchronized, decentralized clock to prevent replay attacks and ensure VDF proofs are valid in a specific window. The /time endpoint allows external local tools to ask the daemon what the current network time is, ensuring they construct records that will be accepted by the network. This is a public route.

The vdf module: Verifiable Delay Functions (VDFs) are computationally intensive proofs required to register names in Kinetic, preventing spam and domain squatting. This module provides endpoints to submit a registration or renewal request (/vdf/register). Because computing a VDF can take minutes or hours and maxes out the CPU, the request returns a task_id immediately while moving the computation to a background thread. The client can then poll /vdf/status/{task_id} to check on progress or delete the task to abort it.

The zone module: A zone in Kinetic is analogous to a DNS zone file. It contains the hierarchical mapping of subdomains and records. This module allows users to fetch their current zone layouts, submit modifications (like adding a new A record), and publish the updated zone to the network. Fetching a zone is public, but modifying or publishing it requires the Publish or Admin role.

The router is logically split into two distinct groupings:

  1. Public Routes: Endpoints exposed openly. They only query data and cannot modify the node’s state. Anyone on the local machine (or any authorized extension) can call them.
  2. Auth-Guarded Routes: sensitive endpoints. They are placed inside a nested router that is protected by the authentication middleware layer.

By organizing the routes in this manner, it guarantees that new sensitive endpoints cannot accidentally bypass the authentication layer, provided they are added to the correct router group.

5. CORS Enforcement

Cross-Origin Resource Sharing (CORS) is a critical defense-in-depth layer implemented at the router level. The app() function applies a restrictive CORS layer to the entire router using tower_http::cors::CorsLayer.

  • It intercepts the Origin header of every incoming HTTP request.
  • It evaluates a predicate function against the origin.
  • It only allows requests originating from exactly matching patterns: http://localhost, http://127.0.0.1, specific bound IPs, or browser extensions using specific protocols (chrome-extension://, moz-extension://).
  • If an external website (like https://evil.com) attempts to make an AJAX request to the local daemon, the user’s browser will execute a preflight OPTIONS request.
  • The daemon will refuse to return the necessary CORS allow headers, and the browser will actively block the actual request from ever reaching the node.
  • Furthermore, it allows only specific HTTP methods (GET, POST, OPTIONS) and specific headers (CONTENT_TYPE, AUTHORIZATION). This protects the user even if they accidentally browse a malicious site while their daemon is running in the background. CORS is notoriously difficult to get right, but Kinetic takes a zero-trust approach, enumerating the very few local origins that are permitted.

6. Authentication Middleware and Timing Attacks

The core security check for guarded routes happens inside the auth_middleware() asynchronous function. -> See: kinetic-daemon/src/api/mod.rs — Lines 377 to 435

When a request targets an authenticated route (like /publish), this middleware executes before the actual handler logic ever sees the request. First, it looks for the Authorization HTTP header. It expects the format Bearer <token>. If the header is missing, malformed, or does not start with Bearer , it rejects the request instantly with a 401 Unauthorized status code. If the token is present, it extracts the token string and prepares to compare it against the expected tokens stored in the ApiState.

The Critical Security Detail: It does not use standard string equality (==) to compare the tokens. It uses a specialized crate: subtle::ConstantTimeEq. Why is this necessary? If it used standard ==, Rust would check the characters one by one in a loop. It would return false the exact microsecond it found a mismatch. An attacker could send thousands of guesses. By measuring exactly how many microseconds the server took to reject each guess, the attacker could figure out if they got the first character right (because it would take slightly longer to reject). They could then guess the second character, and so on. This is known as a “timing attack,” and it breaks string-based passwords over time. By using ConstantTimeEq, the daemon forces the CPU to evaluate the entire string, taking the exact same amount of time to compare the strings regardless of where the mismatch occurs. This neutralizes timing attacks.

Once the token is verified, the middleware figures out which Role it corresponds to (e.g., Admin, Publish, Vdf). It then injects that Role into the request’s internal extensions map using req.extensions_mut().insert(r). This allows the downstream handler functions to know exactly what permissions the caller has, without having to re-parse the token themselves.

7. Deep Dive: Constant Time Authentication and Timing Attacks

The auth_middleware relies on subtle::ConstantTimeEq. This is not just a theoretical concern; timing attacks are practical on local networks or loopback interfaces where network jitter is minimal. When the auth_middleware receives a token, it converts both the expected token and the provided token into byte arrays. It then uses the ct_eq method from the subtle crate. -> See: kinetic-daemon/src/api/mod.rs — Lines 410 to 414 The ct_eq method performs a bitwise comparison of the entire byte array, accumulating any differences using bitwise OR operations. It does not use branching (if statements) to short-circuit the comparison when a mismatch is found. Because modern CPUs use branch prediction, a standard == check would cause the CPU to take a different execution path as soon as it found a wrong character. This branch misprediction is exactly what an attacker measures to perform a timing attack. By avoiding branches and always processing the full 32 bytes of the token, ConstantTimeEq ensures that an attacker cannot glean any information about which character in their guess was wrong. This is particularly important for the Admin token, as brute-forcing it would give an attacker total control over the node. The implementation in Kinetic is careful to only call ct_eq if the lengths of the tokens match. Checking lengths is generally safe from timing attacks because the length of the expected token (32 bytes hex-encoded, so 64 characters) is public knowledge by protocol design.

8. Deep Dive: The CorsLayer Configuration

The CorsLayer in Kinetic is exceptionally restrictive compared to standard web servers. -> See: kinetic-daemon/src/api/mod.rs — Lines 152 to 179 It is configured using tower_http::cors::CorsLayer. The most critical part is the allow_origin predicate. Instead of allowing * (any origin) or a fixed list of domains, it uses a dynamic closure. This closure checks if the incoming Origin header matches http://localhost, http://127.0.0.1, or http://[::1]. It also allows chrome-extension:// and moz-extension:// schemes. This specific inclusion is what allows the Kinetic browser extension (like a wallet or identity manager) to function without needing a separate native companion app. The browser extension can inject its requests directly into the daemon. However, because standard web pages are hosted on https:// or http:// with a specific domain, they will always fail the origin check. The CORS layer also restricts the allowed HTTP methods to GET, POST, and OPTIONS. OPTIONS is required for the browser’s preflight mechanism. Finally, it restricts the allowed headers to CONTENT_TYPE and AUTHORIZATION. If an attacker tries to pass custom headers to exploit a vulnerability in axum or hyper, the CORS layer will reject the request before it even reaches the route handlers.

9. Deep Dive: The Proptests and Fuzzing Strategy

At the very bottom of mod.rs, there is a test module utilizing the proptest crate. -> See: kinetic-daemon/src/api/mod.rs — Lines 440 to 464 proptest is a property testing framework for Rust. Unlike standard unit tests which check specific edge cases (e.g., assert_eq!(2 + 2, 4)), property tests generate random inputs to ensure the code maintains certain invariants. The test_fuzz_constant_time_eq_lengths test is a prime example of this. It generates random strings token_a and token_b with lengths anywhere from 0 to 128 characters. It then tests the ConstantTimeEq logic against these random fuzz inputs. The test asserts two properties:

  1. If the lengths of token_a and token_b are identical, the result of ct_eq must exactly match the result of standard string equality (==). This ensures the constant-time logic doesn’t introduce bugs where valid tokens are rejected or invalid tokens are accepted.
  2. If the lengths are different, it ensures that the middleware logic (which avoids calling ct_eq on different lengths) handles the discrepancy safely. By running this test with hundreds of random permutations, the daemon engineers can be confident that their critical authentication middleware is robust against unexpected token formats. Property testing is essential for security-critical pathways because human engineers often fail to imagine all the strange inputs an attacker might construct.

10. Deep Dive: axum Router Construction

The axum framework was chosen specifically because of its tight integration with the tokio asynchronous runtime and the tower middleware ecosystem. Unlike synchronous frameworks, axum handles each HTTP request in a separate lightweight async task, rather than a full OS thread. This allows the daemon to easily handle thousands of concurrent requests, such as hundreds of clients streaming gossip via SSE simultaneously. When the app() function constructs the router, it uses a pattern called “nesting” with the .nest("/api", ...) method. This takes all the previously defined routes (which were bound to bare paths like /health or /publish) and clones them so they are also available under the /api prefix. This dual-binding is a quality-of-life feature: local CLI tools often expect bare paths to minimize typing, while web UIs often proxy requests to a backend using an /api prefix to avoid routing collisions with frontend assets. By utilizing .merge() and .nest(), the router provides maximum flexibility without duplicating handler code.

11. Deep Dive: tokio Runtime Implications

Because axum runs on tokio, every single handler function must be an async fn. However, asynchronous programming introduces specific architectural constraints that mod.rs must manage. Most importantly, async tasks in Rust cannot easily borrow data with arbitrary lifetimes; they typically require 'static lifetimes or owned data. This is exactly why the ApiState relies on Arc (Atomic Reference Counted pointers). When an axum handler is invoked, axum extracts the state using the State extractor and passes it to the handler. Under the hood, axum is simply calling .clone() on the ApiState. Because all the heavy components (like the NetworkClient or the tokio::sync::Semaphore) are wrapped in Arc or are naturally cheap to clone, this .clone() operation is fast and allocates no new memory on the heap. It simply increments an atomic counter. This pattern ensures that the memory footprint of the HTTP server remains minimal, even under severe load.

12. Deep Dive: The getrandom Crate and System Entropy

The security of the entire API rests on the quality of the tokens generated during the bootstrap phase. The generate_and_write_token function uses the getrandom crate instead of a standard pseudo-random number generator (PRNG) like rand::thread_rng(). A PRNG starts with a seed and uses a mathematical formula to generate a sequence of numbers that appear random. If an attacker can guess the seed (which is often based on the system clock or PID), they can predict the entire sequence of tokens. The getrandom crate avoids this entirely. It directly asks the host operating system for cryptographic entropy. On Linux, it reads from /dev/urandom or uses the getrandom(2) syscall. On macOS, it uses getentropy. On Windows, it uses BCryptGenRandom. These OS-level systems gather entropy from unpredictable physical events: mouse movements, network packet arrival times, thermal noise on the CPU, and disk interrupt timings. By tying the token generation directly to true physical entropy, the daemon guarantees that the tokens are fundamentally unpredictable, even by a local attacker with full knowledge of the daemon’s internal state. The deliberate choice to unwrap() or return a fatal error if getrandom fails ensures the daemon “fails closed” (refuses to start) rather than “fails open” (starts with weak security).

13. Deep Dive: Handling IPv6 Fallback

The fallback mechanism in start_server() is a critical piece of operational engineering. Many modern operating systems, particularly containerized environments like Docker or specific Linux distributions, configure localhost networking differently. Sometimes 127.0.0.1 (IPv4 loopback) is disabled in favor of [::1] (IPv6 loopback). If the daemon required IPv4, it would simply crash on these systems, frustrating users. The start_server function mitigates this by looping over potential bind addresses. It first attempts the user’s explicit preference (usually IPv4). If the OS returns an EADDRINUSE (Address already in use) or a similar bind error, it waits 200ms and tries again. This handles the common scenario where a developer quickly restarts the daemon and the OS hasn’t garbage-collected the socket yet. If all 10 retries fail, it then tries to bind to the IPv6 loopback address [::1]. This dual-stack awareness is increasingly necessary as the world transitions to IPv6, ensuring the Kinetic daemon is future-proof and resilient across diverse deployments.


Key Pieces

struct ApiState

-> See: kinetic-daemon/src/api/mod.rs — Lines 105 to 129 The central hub and brain of the API layer. It holds references to everything the HTTP handlers might need. It contains the NetworkClient, the StorageEngine, the generated ApiTokens, the vdf_tasks map, the vdf_semaphore, the bind_ip, the gossip_tx channel, and the atlas_tlds set. Because the axum web framework requires state to be Clone (so it can be passed to many concurrent request handling threads), all internal components that are heavy or cannot be cheaply cloned are wrapped in thread-safe wrappers. This includes Arc (Atomic Reference Counted pointers) for shared ownership, Mutex (Mutual Exclusion) for synchronizing mutable access, and RwLock for optimized read-heavy concurrency. This is a classic, robust Rust pattern for sharing global state in an asynchronous web server. It ensures that handlers remain stateless and dependent on the injected state.

enum Role

-> See: kinetic-daemon/src/api/mod.rs — Lines 52 to 84 Defines the Role-Based Access Control (RBAC) levels for the daemon. Instead of a binary “you are admin or you are not” system, it provides fine-grained capabilities. It includes variants like Admin, Publish, Vdf, Governance, and Atlas. Crucially, it implements helper methods on the enum itself, such as can_publish(), can_vdf(), and can_govern(). This architecture allows a developer to write an automated script and grant it a token that can only submit VDF proofs. If that script is somehow compromised, the attacker cannot use its token to modify the node’s core configuration, publish unauthorized names, or vote in governance. This is the principle of least privilege in action. By separating these roles, the daemon ensures that a vulnerability in one area does not automatically grant full administrative access.

fn ensure_api_tokens()

-> See: kinetic-daemon/src/api/mod.rs — Lines 290 to 306 The function responsible for the node’s bootstrap security. It rotates all tokens every single time the daemon boots. This ensures that leaked tokens have a very short lifespan (only until the node restarts). It delegates the actual filesystem writing to the generate_and_write_token() helper function, which handles the getrandom invocation and the Unix permission setting. It is called before the HTTP server is bound to ensure that there is no race condition where the server accepts requests before the tokens are ready.

fn app(state: ApiState) -> Router

-> See: kinetic-daemon/src/api/mod.rs — Lines 148 to 245 The router builder. It maps HTTP methods (GET, POST, DELETE) and URL paths (like /zone/{name} or /health) to their corresponding asynchronous handler functions. It is responsible for applying the CORS middleware and correctly merging the public and authenticated routers into a single cohesive application tree. It also uses .nest("/api", ...) to expose all routes under an /api prefix for UI consumption, while keeping them available at bare paths for CLI convenience. This dual-routing structure provides maximum flexibility for both automated tools and browser-based frontends.

async fn auth_middleware(...)

-> See: kinetic-daemon/src/api/mod.rs — Lines 377 to 435 The ultimate gatekeeper for the daemon. It intercepts every request destined for a sensitive endpoint. It parses HTTP headers, executes the constant-time token comparison, and either aborts the request with a 401 Unauthorized HTTP status or forwards it to the intended handler. It uses axum::middleware::from_fn_with_state to gain access to the ApiState so it can check the incoming token against the freshly generated tokens. This middleware design pattern means that individual handlers never have to worry about authentication; if they are invoked, they are guaranteed to be authorized.

struct VdfTaskStatus

-> See: kinetic-daemon/src/api/mod.rs — Lines 39 to 50 A data structure representing the real-time status of an ongoing Verifiable Delay Function task. Because VDFs are complex, computationally expensive, and take a long time to run (often minutes or hours), they must be processed asynchronously in the background. This struct tracks whether the task is running, completed, or failed. It also tracks its precise progress (the number of iterations completed versus the number of iterations required). It derives Serialize and Deserialize so it can be automatically converted to JSON when requested by the /vdf/status/{task_id} HTTP endpoint. This allows the UI or CLI to display a progress bar while the daemon computes the proof.

struct PublishRequest

-> See: kinetic-daemon/src/api/mod.rs — Lines 131 to 136 The incoming JSON payload format for publishing a name record directly. It encapsulates a NameRecord (from kinetic_core) which contains the DID, the name, and the cryptographic signatures proving ownership. By accepting this directly in the API, the daemon allows external tools (like the CLI) to construct the complex cryptographic proofs offline and simply submit the finished payload for propagation to the network. This reduces the computational load on the daemon and allows for secure, offline signing of records.

struct PublishResponse

-> See: kinetic-daemon/src/api/mod.rs — Lines 138 to 145 The standard JSON response format for a publish action. It provides a high-level status string (usually ‘success’ or ‘error’) and a detailed message explaining the result of the publish attempt. This ensures that HTTP clients have a standardized way of parsing success or failure, rather than relying solely on HTTP status codes. This struct exemplifies the API’s focus on clear, structured communication with external tooling.


How This Connects to the Rest of Kinetic

This module is the grand synthesizer of the Kinetic ecosystem. It connects to almost everything below it in the architecture stack, pulling together discrete systems into a unified interface.

  • CROSS-CRATE: NetworkClient — This client handle is passed into the ApiState to allow the HTTP API to query the P2P network (e.g., resolving a name across peers) or broadcast gossip messages to connected nodes. It is defined and explained thoroughly in docs/learn/network/.
  • CROSS-CRATE: StorageEngine — This trait interface is passed into the ApiState to allow the API handlers to read and write records directly to the local database on disk. It abstracts away whether the node is using Sled, RocksDB, or another backend. It is defined and explained in docs/learn/storage/.
  • CROSS-CRATE: NameRecord — This struct is used inside the PublishRequest payload to represent the data being published to the network. It is the fundamental unit of data in Kinetic, representing a registered name and its associated data. It is defined and explained in docs/learn/types/.

Ultimately, mod.rs acts as the primary consumer of almost all lower-level Kinetic systems. It exposes their internal, Rust-native functionality as consumable, standardized HTTP REST endpoints that any programming language, CLI, or UI framework can interact with easily. It acts as the translator, taking HTTP requests and turning them into P2P gossip, database writes, or VDF computations.


Quick Reference

  • Token Storage Path: ~/.kinetic/api_tokens/*.token (Stores admin.token, publish.token, etc.)
  • Token Rotation Policy: Tokens are rotated automatically on every single daemon boot. No static passwords are used.
  • Port Fallback Logic: The server retries binding 10 times with a 200ms delay. It gracefully falls back to [::1] (IPv6 loopback) if all IPv4 attempts fail.
  • Constant Time Auth: It uses subtle::ConstantTimeEq to prevent cryptographic timing attacks during Bearer token verification.
  • CORS Allowed Origins: The API only responds to http://localhost, http://127.0.0.1, the specific bound IP, chrome-extension://, and moz-extension://.
  • Middleware Role Injection: Authorized roles are dynamically placed into the request extensions (req.extensions_mut()) for downstream consumption by handlers.
  • State Management Pattern: Uses axum::extract::State to pass the concurrent ApiState struct to all handler functions safely efficiently.
  • Property Testing: proptest is used to exhaustively fuzz the constant-time equality logic.
  • Background Tasks: VDFs and other heavy workloads are moved to background Tokio tasks to avoid blocking the HTTP threads.

Open Questions / Things to Revisit

  • Persistent Tokens vs Ephemeral Tokens: Currently, tokens rotate on every single boot. This provides excellent baseline security. However, it means that if a user sets up an external cron job or an automated bash script using a token, that script breaks the moment the daemon restarts. We may need to investigate durable tokens, or build a dedicated API endpoint that allows the user to request long-lived, named tokens for specific external integrations.
  • Granular CORS for Extensions: The current CORS policy allows all chrome-extension:// and moz-extension:// origins. This means literally any extension installed in the user’s browser can query the public routes of the daemon. While they cannot access the authenticated routes without a token, we might want to tighten this up to specifically allowlist known Kinetic extension IDs in the future, preventing random extensions from scraping node data or tracking node status.
  • HTTP Rate Limiting: There is currently no HTTP-level rate limiting implemented in the router (other than the CPU Semaphore specifically for VDF computations). A malicious local script, or a compromised browser extension, could spam the public API with thousands of requests per second and consume all node resources. Adding a basic rate limiting layer using a crate like tower_governor might be necessary for long-term stability and DoS protection.
  • IPv6 Dual Stack Handling: The fallback logic tries IPv4 then IPv6. In a modern networking environment, it might be preferable to bind to a dual-stack socket that listens on both IPv4 and IPv6 simultaneously, rather than treating IPv6 purely as a failure fallback option. This would ensure maximum compatibility out of the box.
  • Token Permissions Scoping Expansion: Right now, roles are fairly broad. As the API grows, we might need more fine-grained permissions attached to tokens, perhaps specifying exactly which zones or DIDs a specific token is permitted to modify, rather than granting blanket publish access.
  • WebSockets over SSE: Currently, gossip streams use Server-Sent Events (SSE). It might be worth investigating if bidirectional WebSockets would be more efficient or flexible for future API consumers, especially for complex real-time subscriptions.
  • Logging Verbosity: The current error logging during token mismatch or CORS rejection is minimal. Adding more detailed audit logs for failed API attempts could greatly assist in diagnosing misconfigured local scripts or detecting active local network attacks.
  • Configuration Hot-Reloading: Currently, the API server must be fully restarted to recognize changes in the bound IP or allowed CORS origins. Implementing a hot-reload mechanism that updates the axum router without dropping active connections would improve node uptime.
  • Test Coverage for Middleware: While the core functions are tested, ensuring the authentication middleware correctly rejects all invalid permutations (e.g., lowercase bearer, malformed tokens, expired signatures if they existed) requires expansive property-based testing.
  • Token Revocation: Since tokens are rotated on boot, there is no way to revoke a compromised token without restarting the entire node. A /token/revoke endpoint could be a critical security addition in future stages.
  • Performance Under Load: The current Arc<Mutex<HashMap>> for VDF tasks might become a bottleneck under extreme load since it locks the entire map for any update. Sharding the map or using a concurrent hash map could resolve this.
  • Structured Error Responses: While PublishResponse exists, many endpoints simply return a string or an HTTP status code on failure. Implementing a standardized JSON error format across all endpoints would vastly improve client developer experience.

14. Deep Dive: Tokio Semaphores for VDF Throttling

The vdf_semaphore inside the ApiState is a tokio::sync::Semaphore. A semaphore maintains a set of permits. In Kinetic, it is initialized with a specific number of permits (usually corresponding to the number of CPU cores available for VDF computations). When an HTTP request hits the /vdf/register endpoint, the handler attempts to acquire a permit from the semaphore before spawning the background task. If all permits are currently checked out (meaning the CPU is fully occupied with other VDF tasks), the semaphore will suspend the incoming asynchronous task. This is a critical form of backpressure. Without it, an attacker with a valid token could submit 10,000 VDF requests in a second. The Tokio runtime would blindly spawn 10,000 background threads, instantly exhausting the host machine’s memory and CPU, leading to an Out of Memory (OOM) kill by the operating system. The semaphore ensures that the daemon degrades gracefully under extreme load. The API request will simply wait until a permit is available. If it waits too long, the HTTP request will eventually time out, but the node itself will remain stable and responsive to other non-VDF requests.

15. Deep Dive: Mutex Contention and HashMap Scaling

The vdf_tasks field is a Arc<Mutex<HashMap<String, VdfTaskStatus>>>. While a Mutex is necessary for safely updating the status of tasks across multiple threads, it introduces a potential performance bottleneck known as lock contention. Every time a background VDF worker wants to increment its progress counter, it must acquire the Mutex lock on the entire HashMap. Similarly, every time a user polls /vdf/status/{task_id}, the HTTP handler must also acquire that exact same lock to read the status. If there are many active tasks and many clients polling them simultaneously, threads will spend most of their time waiting for the lock rather than doing useful work. In the current implementation, this is mitigated by the fact that VDF progress updates are generally batched or throttled (they don’t update on every single iteration). However, as the daemon scales, this data structure might need to be replaced with a lock-free concurrent map (like dashmap) to allow truly concurrent reads and granular writes without locking the entire task registry. This highlights the constant trade-off in Rust web services between the simplicity of standard library primitives and the extreme performance of specialized concurrent data structures.

16. Deep Dive: Broadcast Channels and SSE Subscriptions

The gossip_tx channel is a tokio::sync::broadcast::Sender. A broadcast channel is a multi-producer, multi-consumer channel where every sent value is seen by every active receiver. When a client subscribes via /gossip/subscribe/{topic}, the HTTP handler calls gossip_tx.subscribe() to obtain a new Receiver tied to that channel. The handler then converts this receiver stream into a Server-Sent Events (SSE) response using axum::response::sse. This means the HTTP connection remains held open indefinitely. As the core network stack processes incoming Gossipsub messages from the P2P swarm, it pushes them into the gossip_tx sender. The broadcast channel automatically duplicates the message to every currently connected HTTP client’s receiver. The beauty of this architecture is that it decouples the heavy network processing stack from the HTTP delivery stack. The core network doesn’t need to know how many web clients are connected, or if they are slow readers. If a web client reads too slowly, the broadcast channel will automatically drop older messages for that specific client (a feature known as lag), ensuring that a slow local client cannot back up the entire P2P node.

API Tests (Daemon API Testing Suite)

Crate: kinetic-daemon Stage: 9 Reading time: 30 minutes Depends on: docs/learn/network/01_overview.md, docs/learn/storage/01_overview.md, docs/learn/core/01_overview.md


What Is This?

This file (kinetic-daemon/src/api/api_tests.rs) contains the comprehensive internal testing suite for the Kinetic Daemon’s HTTP API.

It is designed to rigorously validate that all the public-facing HTTP endpoints exposed by the daemon behave correctly. These endpoints include critical paths such as:

  • /commit
  • /publish
  • /resolve
  • /vdf/register

These are isolated unit-level API tests. They do not require spinning up an entire local Kinetic node. They do not require connecting it to a live P2P network swarm. They do not require binding the HTTP server to a real localhost TCP port.

Instead, they construct the axum web router in memory. They inject mocked dependencies to isolate the API logic. These mocked dependencies include:

  • A dummy network client.
  • A temporary, ephemeral storage engine.

The tests then fire simulated HTTP requests directly at the router’s core logic.

The test suite thoroughly covers several distinct categories of API behavior:

  • Authentication Enforcement: Ensuring that protected endpoints demand valid Bearer tokens. Instantly rejecting unauthenticated requests with HTTP 401.
  • Payload Validation: Checking that malformed JSON bodies are caught. Validating that invalid domain names are rejected. Ensuring structurally incorrect cryptographic proofs are blocked. These checks prevent bad data from polluting the internal database.
  • Complex Business Logic Constraints: Validating advanced temporal rules. Validating cryptographic rules. Verifying that VDF proofs are cryptographically anchored to a recent Drand randomness round.
  • Concurrency Control: Ensuring multiple concurrent HTTP requests are handled safely. Validating that internal locks (Mutexes and Semaphores) successfully serialize the work. Preventing race conditions. Preventing duplicate CPU-heavy tasks.
  • Graceful Fallback Mechanisms: Verifying behavior when the primary network (the DHT) fails to find a record. Ensuring the daemon gracefully falls back to querying its own local database cache. Confirming this happens without crashing or surfacing confusing internal errors.

By mocking the external world, this suite allows the developers to instantly simulate extreme edge cases. These edge cases would be difficult, flaky, or nearly impossible to reliably trigger on a live, distributed network.


Why Kinetic Needs This

The daemon API is the absolute primary surface area for the Kinetic node. External applications interact with the node here. CLI tools interact with the node here. Frontends and end users interact with the node here.

It acts as the critical bridge between the chaotic, untrusted external world (the HTTP client) and the structured, cryptographic internal world of the Kinetic network. If this API fails, misbehaves, or contains logic vulnerabilities, the entire node is effectively compromised. It could even be rendered useless.

Kinetic requires this rigorous, isolated API testing suite for several non-negotiable reasons:

  1. Security and Access Control Guarantees: The API handles privileged operations. The API handles potentially destructive operations. Endpoints like /commit and /publish alter the local database. They broadcast messages to the global P2P network. We must be certain that endpoints requiring administrative access are secured. Endpoints requiring specific capabilities (governance, VDF registration) cannot be accessed by unauthorized HTTP requests. This test suite guarantees that the authentication middleware acts as an impenetrable shield.

  2. Deterministic Edge-Case Simulation: Testing advanced cryptographic business logic on a live testnet is difficult. It is flaky and time-consuming. Testing “stale Drand randomness” normally requires manually manipulating a live Drand feed. Or, it requires literally waiting hours for real time to pass just to test an expiry threshold. By mocking the database state in these unit tests, we bypass this. We instantly and deterministically simulate a scenario where a user submits a proof based on an ancient Drand round. We verify that the API immediately detects it and rejects it.

  3. Concurrency and Race Condition Prevention: The daemon utilizes asynchronous tasks. It relies on shared memory locks (like the vdf_tasks concurrent map). When two users attempt to register the exact same identity at the exact same millisecond, the system must hold up. The system must not crash. The system must not panic. The system must not create duplicate, CPU-heavy VDF tasks that would starve the machine. These in-memory API tests allow us to programmatically fire concurrent requests. We fire overlapping requests and prove that our concurrency primitives work. Our tokio::sync::Mutex and tokio::sync::Semaphore implementations must hold up under maximum, coordinated pressure.

  4. Instant Developer Feedback Loop: Standing up a full Kinetic node is slow. Establishing a DHT swarm takes time. Syncing blocks takes time. Booting a storage backend takes time. Doing all this just to see if /commit correctly returns a 400 Bad Request is inefficient. It disrupts the developer flow. These in-memory API tests run in mere milliseconds. They bypass the actual TCP networking stack. They bypass the actual libp2p network. They provide instant, reliable feedback during the software development process.

  5. API Contract Adherence: The HTTP status codes returned by the API constitute a binding contract. The error messages returned by the API are also part of this contract. Downstream clients (like the CLI or web wallets) rely on this contract. If a domain name is invalid, the client expects exactly a 400 Bad Request. They expect a specific error string along with it. If a cryptographic signature is invalid, it expects exactly a 422 Unprocessable Entity. These tests ensure that the API contract remains stable. It guarantees the contract does not accidentally regress when deep internal core logic is refactored.

Without this comprehensive suite, any minor change could silently break the daemon’s external interface. A change to the core validation logic could break it. An accidental misconfiguration of the HTTP routing tree could break it. This would leave users unable to interact with the broader network.


How It Works

The architectural magic and speed of this test suite relies on two core software engineering patterns. These patterns are Dependency Injection and Headless HTTP Testing. By combining these two techniques, the tests achieve total environmental isolation.

1. Dependency Injection for Isolated State Setup

When the actual compiled Kinetic daemon runs, it initializes a heavy environment. It initializes a real, persistent Sled database on the physical disk. It connects to real libp2p peers over TCP sockets. It spawns real gossipsub networking tasks. In these tests, we must bypass all of that heavy machinery. This is necessary to maintain extreme speed and determinism.

Every single test in this file begins by executing the setup_test_app() asynchronous helper function. This function constructs a isolated, fake universe for the API to live within. It creates a disposable universe.

The Ephemeral Storage Engine: It utilizes the powerful tempfile::tempdir() function from the Rust ecosystem. This creates a temporary directory directly on the operating system’s filesystem. It then initializes a real SledStorage engine inside this temporary directory. When the test finishes execution, the directory variable goes out of scope. At this moment, the directory is immediately and automatically deleted by Rust’s Drop implementation. This guarantees zero state contamination between different tests.

The Mock Network Client: It initializes the network interface using NetworkClient::new_mock(cmd_tx). It does not attempt to dial out to a real P2P network. This mocked network client simply takes internal network requests (like resolving a name). It sends them as serialized enums into a local MPSC (Multi-Producer, Single-Consumer) channel. The test itself can then safely await on the receiver end of this channel. This allows the test to inspect exactly what commands the API attempted to send. It even allows the test to manually inject fake responses back into the API. This simulates complex network behavior.

2. Anatomy of the Mocked ApiState

The Axum router requires an ApiState object to function. In production, this state is built dynamically during daemon startup. In testing, setup_test_app() constructs a specific dummy version:

  • network: Passed the NetworkClient::new_mock().
  • gossip_tx: Given a fresh tokio::sync::broadcast::channel. Since we don’t care about testing the gossip broadcast listener here, we just give it a channel that drops broadcast messages into the void.
  • storage: Passed the Arc<SledStorage> pointing to the ephemeral tempdir.
  • tokens: Hardcoded with strings like "test-token-123" and "publish-token". This allows tests to hardcode the Authorization: Bearer test-token-123 header easily. It prevents the tests from needing to dynamically read a config file.
  • vdf_tasks: Initialized as an empty Arc<Mutex<HashMap>>. This is the memory space what the concurrency test will intentionally pound against.
  • vdf_semaphore: Initialized with Arc::new(tokio::sync::Semaphore::new(1)). This restricts the mock state to only allowing one single VDF task at a time. It mimics a constrained CPU environment.
  • atlas_tlds: Initialized as an empty Arc<RwLock<HashSet>>.

This meticulously controlled state injection is what makes the tests fast, deterministic, and impenetrable. It removes all outside environmental flakiness.

3. Headless HTTP Testing via Tower Service Ext

The Kinetic API is constructed using the axum web framework. Axum is fundamentally built on top of the robust tower middleware ecosystem. In a normal production deployment, you bind an axum::Router to a real TCP port (like 127.0.0.1:8080). The operating system then routes incoming network packets to it.

In these specific tests, we never bind to a port. We bypass the OS network stack entirely. We utilize the tower::ServiceExt::oneshot method. Because an axum::Router implements the tower::Service trait, it can be treated uniquely. It can be conceptually treated as a simple, asynchronous Rust function. This function takes an http::Request object and returns an http::Response object. We manually construct these request objects in memory using a builder pattern. We pass them directly into the core router using the oneshot method. We simply await the HTTP response without ever touching a socket.

4. Reading the Response Body

Axum HTTP responses are asynchronous streams of bytes. The test must actively consume the stream body to read the response error message. It does this using the http_body_util::BodyExt extension trait. It calls .into_body().collect().await.unwrap().to_bytes(). This takes the asynchronous stream of HTTP body chunks. It collects them into a single contiguous byte array in memory. It allows the test to convert it into a UTF-8 string for exact string-matching assertions.

5. The Strict Execution Flow of a Standard Test

Every test in this suite follows a rigorous, three-step methodology. This is the Arrange-Act-Assert pattern:

  1. Arrange: The test calls setup_test_app(). It obtains the router, the network command receiver, and a pointer to the storage database. It intentionally prepopulates the mock storage database with fake data. For example, it might inject a specific historical Drand round. It might inject a fake Reveal record. This ensures the API inherently believes it is in a specific edge-case state.

  2. Act: The test constructs an http::Request manually. It uses the HTTP builder pattern. It serializes a JSON payload into the request body using serde_json::json!. It sets required headers. The most important header is usually the Authorization: Bearer token. It sets the URI and HTTP method. It then fires this request into the router using .oneshot(request).await.

  3. Assert: The test inspects the returned http::Response. It first verifies that the HTTP StatusCode exactly matches expectations. For example, it might assert StatusCode::BAD_REQUEST. It then extracts the raw bytes of the response body. It converts them to a UTF-8 string. It rigorously asserts that the string contains the precise error message. It checks the payload expected by the client application.


Key Pieces

The file contains numerous specific, tightly scoped tests. Here is a detailed breakdown of every critical piece. We explore what it tests, how it executes, and why it deeply matters to the daemon’s stability.

The Foundation: The setup_test_app Helper

-> See: crates/kinetic-daemon/src/api/api_tests.rs — Lines 21 to 50

This internal function is the bedrock of the entire testing suite. It takes zero arguments and returns a tuple containing three vital pieces of infrastructure:

  1. The axum::Router, which is the fully constructed API, loaded with the mocked state.
  2. An mpsc::Receiver<Command>, which is the listener for the mock network. The test can await on this receiver to see exactly what commands the API attempted to send to the P2P network.
  3. An Arc<SledStorage>, which is a direct, thread-safe pointer to the temporary database. The test uses this pointer to directly inject data into the database behind the API’s back. This sets up complex internal state scenarios without needing to run real network sync mechanisms.

The Security Check: Authorization Enforcement

-> See: crates/kinetic-daemon/src/api/api_tests.rs — Lines 52 to 64

This test is mechanically simple but architecturally critical. It constructs a raw POST request aimed at the /commit endpoint. It intentionally omits the Authorization: Bearer <token> HTTP header. It then verifies that the server rejects the request. It checks that it responds with a strict 401 UNAUTHORIZED status code. This definitively proves that the require_auth middleware is successfully wrapping the protected routes. It ensures no malicious external actor can bypass the token checks and interact with the node.

The First Line of Defense: Input Structure Validation

-> See: crates/kinetic-daemon/src/api/api_tests.rs — Lines 66 to 116

These tests ensure that the API does not blindly accept well-formatted JSON. It checks if the actual cryptographic contents logically violate network rules. For example, a commitment hash cannot logically be all zeros, as that represents an unhashed state. If a user submits a payload with hash: vec![0; 32], the API must deeply inspect the contents. It must reject it with a 400 BAD_REQUEST. Similarly, if a user attempts to commit a name like sub.saifmukhtar.kin, it fails. This is technically a subdomain, not a root apex domain, which violates current kinetic rules. The API instantly rejects it. These tests conclusively prove that the HTTP layer correctly invokes the core domain validators. These validators live inside kinetic-core. They are invoked before proceeding with any local state mutations.

The Domain Integrity Check: Structural Validation

-> See: crates/kinetic-daemon/src/api/api_tests.rs — Lines 154 to 189

When a user attempts to publish an identity, they must provide a massive, complex JSON payload. This payload contains cryptographic proofs, public keys, Ed25519 signatures, and protocol versions. If they provide an invalid protocol version, the API must reject it. For instance, purposefully providing 0 instead of the expected version 2 must fail. The test verifies that the body of the HTTP response mentions “Invalid Reveal”. This proves that the core structural validation logic is propagating its specific error messages correctly. It proves they travel correctly up through the HTTP boundary to the end user.

The Temporal Security Check: Drand Staleness Verification

-> See: crates/kinetic-daemon/src/api/api_tests.rs — Lines 191 to 249

This is arguably one of the most sophisticated and vital tests in the suite. It tests the temporal security mechanisms of the Kinetic network. When you publish a name, your expensive VDF proof must be firmly anchored to a recent Drand randomness round. If you utilize an ancient round, it implies you had too much time to precompute the proof. This is a massive security violation of the protocol designed to prevent front-running. The test purposefully mocks the storage database to logically believe the current Drand round (kyn) is 10_000_000. It then constructs and submits a publish HTTP request where the drand_kyn utilized for the proof is 100. This is functionally millions of rounds in the past. The API successfully detects this massive temporal gap. It calculates that it vastly exceeds the RESQUARING_EPOCH_KYNS safety threshold. It rejects the request with a 400 BAD_REQUEST. It notes “VDF kyn” in the error string to guide the user.

The System Redundancy Check: Network Fallback Simulation

-> See: crates/kinetic-daemon/src/api/api_tests.rs — Lines 251 to 306

When a user asks the daemon to resolve an identity name via the API, the daemon first attempts to query the global DHT (the P2P network). If the DHT fails to find the record (perhaps due to network partition), the daemon must recover. It is programmed to automatically fall back and check its own local storage database cache. This test beautifully simulates this complex, asynchronous two-step process:

  1. It manually inserts a valid mock_reveal into the temporary Sled storage.
  2. It spawns an async background Tokio task to actively listen on the mock network command receiver. When the API asks the network to resolve the name, this background task intercepts the command. It intentionally replies over a oneshot channel with an Error::ResolutionError::NotFound. This simulates a total network failure.
  3. The API, receiving this error from the mock network, silently and gracefully falls back. It queries the local SledStorage. It finds the manually injected record and successfully returns a 200 OK to the HTTP client containing the record. This test conclusively proves that the fallback mechanism is robust. It proves it is fully operational. It proves it is transparent to the end API user.

The Missing Dependency Check: Zone Publishing Rules

-> See: crates/kinetic-daemon/src/api/api_tests.rs — Lines 308 to 321

Kinetic enforces a strict structural hierarchy for zones. You cannot publish DNS zone records for a name unless that identity name has already been registered. It must be committed to the local database first. This test systematically verifies this strict constraint. It constructs an HTTP POST request targeting /zone/validname.kin/publish. The underlying mock storage database is empty. We did not manually inject a registration record for validname.kin. Because of this, the daemon’s internal lookup fundamentally fails. The API layer correctly intercepts this internal core failure. It translates it and returns a 404 NOT_FOUND HTTP status code. This proves that the API will block dangling or orphaned zone records from entering the network layer.

The Operational Race Condition Check: Concurrency Lock Verification

-> See: crates/kinetic-daemon/src/api/api_tests.rs — Lines 323 to 360

In Kinetic, initializing and running a VDF task is expensive in terms of sustained CPU cycles. If a user maliciously or accidentally spams the “register” button on a frontend client, the daemon could panic. It might inadvertently spawn two identical VDF tasks for the same name. This would burn unnecessary CPU resources and potentially crash the node through starvation. To prevent this, the daemon utilizes an in-memory concurrent lock map called vdf_tasks. This test conclusively proves that the lock works under heavy load. It utilizes the powerful tokio::join! macro to fire two identical HTTP requests at the exact same microsecond. Because they run concurrently on the Tokio async runtime, they will both hit the route handler simultaneously. The test verifies that the locking mechanism successfully serializes them. Exactly one request definitively wins the race and gets a 200 OK. The other request hits the locked Mutex and is rejected. It recognizes the task is already actively running for that specific name. It correctly returns a 409 CONFLICT. This is a mission-critical test for overall daemon stability.

The Identity Forgery Check: Cryptographic Signature Verification

-> See: crates/kinetic-daemon/src/api/api_tests.rs — Lines 362 to 385

This test guarantees that the /publish-kid endpoint enforces cryptographic signature checks before processing. It submits a JSON payload containing a purposefully fake public key. It also contains an intentionally invalid, garbage signature string. Kinetic fundamentally relies on Ed25519 signatures to cryptographically prove ownership of decentralized identities (KIDs). Therefore, the daemon must catch this blatant forgery instantly. The test confirms that the API immediately detects the invalid cryptographic structure. It returns a 422 UNPROCESSABLE_ENTITY. This permanently blocks the forged identity from ever touching the internal network queues. It prevents it from polluting the local persistent storage.


How This Connects to the Rest of Kinetic

This file acts as the ultimate integration testing point for almost all other core Kinetic crates. Because the API must touch every sub-system to serve a user request, the API tests inherently validate the inter-crate contracts:

  • CROSS-CRATE: SledStorage Defined and explained in docs/learn/storage/01_overview.md. The tests rely on instantiating an isolated Sled database. They use this to simulate the daemon’s local memory and inject complex, adversarial test states.
  • CROSS-CRATE: NetworkClient Defined and explained in docs/learn/network/01_overview.md. The tests utilize NetworkClient::new_mock(). They use this to intercept outgoing DHT requests. They use it to actively inject fake P2P responses back into the API logic.
  • CROSS-CRATE: Core Types Defined and explained in docs/learn/core/01_overview.md. The tests manually construct core structures. They construct Reveal, RawKyn, and VdfProof objects. They feed these into the mock API to trigger specific, deep validation execution paths.

The API routes act as thin wrappers around the deep core business logic. Therefore, these tests implicitly validate that the API layer is correctly translating standard HTTP concepts (headers, JSON bodies, status codes) into internal kinetic-core concepts. They also validate that it seamlessly translates deep kinetic-network errors back into understandable, standard HTTP status codes.


Quick Reference

When reviewing, debugging, or extending the API test suite, keep these core Rust mechanics in mind:

  • The Test Framework: Uses the standard #[tokio::test] attribute macro to run async tests on a local executor.
  • Headless HTTP Routing: Uses tower::ServiceExt::oneshot(request) to bypass the OS TCP network stack. This allows testing the axum router directly in memory.
  • Disposable Mock Storage: Uses tempfile::tempdir() to create a disposable storage instance. This ensures zero-contamination SledStorage instances for each individual test run.
  • Intercepted Mock Network: Intercepts outgoing network commands (like Command::ResolveRedundant) via an MPSC receiver channel. This is used to manually inject fake DHT responses.
  • Aggressive Concurrency Testing: Uses tokio::join! to fire parallel HTTP requests simultaneously. This verifies Mutex locking and prevents state race conditions.
  • Payload Construction: Uses the serde_json::json! macro to easily and construct complex request bodies. This handles nested JSON directly inline within the test functions.
  • Body Extraction: Uses BodyExt::collect().await.unwrap().to_bytes() to convert the asynchronous Axum response stream. It converts it into a readable string for assertions.

Open Questions / Things to Revisit

  • Missing Token Scope Tests: Right now, the authorization test just checks if any token is required. It does not thoroughly verify token scoping logic. For example, it doesn’t currently verify that a user holding only the vdf token is correctly denied access to the admin token routes. This is a critical gap in the API test coverage that needs to be addressed to ensure strict least-privilege enforcement.

  • Mock Network Completeness: Currently, the mock network client only actively handles a few specific commands (primarily ResolveRedundant). As the daemon API continues to expand in feature scope, the mock client will need to simulate far more complex P2P interactions. This includes Gossipsub message publishing and direct DHT put operations. If the mock client does not evolve alongside the API, future advanced tests will be impossible to write.

  • Database State Instantiation Speed: Right now, every single test spins up a brand new SledStorage instance in a newly created temporary directory on the physical disk. While this guarantees test isolation, it might become slow. If the test suite grows to hundreds of tests, it will introduce major disk I/O bottlenecks. We might eventually need to engineer a way to quickly wipe and reset a single in-memory database. This would be much faster than requesting a new filesystem directory from the OS for every single test execution.

  • Complete Error Code Coverage: While the critical happy paths and primary rejection paths are tested (commits, publishes, validation), there are blind spots. There may be deeply nested edge cases around error propagation. For example, what exactly happens if the database panics or corrupts during a read request? These complex failure modes are not currently simulated or caught by this specific suite.

API KID Management

Crate: kinetic-daemon Stage: 9 Reading time: 15 minutes Depends on: kinetic-kid (Stage 3), kinetic-core (Stage 7), kinetic-network (Stage 8)


What Is This?

This file (src/api/kid.rs) provides the REST API endpoints that external clients use to manage Kinetic Identity Documents (KIDs). It acts as the critical bridge between standard HTTP JSON requests and the complex cryptographic and network operations required to participate in the Kinetic identity system.

Instead of requiring every client application (like a CLI tool, a web dashboard, or an automated service) to understand ML-DSA cryptography, they simply talk to this API over HTTP. External clients do not need to generate proper Decentralized Identifiers (DIDs) locally. External clients do not need to construct raw cryptographic signatures. External clients do not need to communicate with the peer-to-peer network directly. When a user wants to create a new identity on the network, they simply send a structured JSON POST request with their desired domain name to the daemon. The daemon handles all of the heavy lifting on their behalf. It generates the quantum-resistant keys internally. It constructs the standard-compliant KID document according to the strict schema. It double-signs the payload to prove authorization and data integrity. It propagates the final document across the Distributed Hash Table (DHT) so other nodes can discover it.

This file essentially turns the daemon into an Identity Provider (IdP) agent for the local node. It is the entry point for all identity creation and management on a given Kinetic node.

An Analogy for the Daemon’s API

Think of the Kinetic network as a secure corporate building. You cannot just walk in and claim you are the new security guard. You need a cryptographic badge, issued by the central HR department. In this analogy, the external client application is the new employee. The Kinetic daemon is the HR department. This API is the front desk of the HR department. The employee walks up to the desk (POST /kids) and says “My name is admin.saif.kin”. The HR department generates the secure cryptographic badge (KidDocument). The HR department signs it with their master stamp to authorize it (AuthorizedKid). Finally, the HR department logs this new badge into the global corporate directory (the DHT). The employee never had to know how to print the badge or use the master stamp themselves.


Why Kinetic Needs This

Kinetic is fundamentally an identity-driven network. Every action, message, and piece of data in the system is cryptographically tied to a KID. However, the operations required to create and manage a KID are specialized and computationally delicate.

If this API module did not exist, any application wanting to interact with Kinetic would be forced to reimplement a massive amount of complex logic. They would have to bundle the post-quantum cryptography libraries, specifically the ml-dsa crate. They would have to implement the exact SHA-256 hashing logic required to derive a valid KineticDid from a raw public key. They would have to understand the strict, evolving schema of the KidDocument and the AuthorizedKid wrapper. They would have to maintain a persistent, raw connection to the P2P swarm to publish documents to the DHT reliably. Most dangerously, they would have to store and manage their own private key material on the filesystem.

By centralizing KID management in the daemon’s API, Kinetic ensures that cryptographic operations are performed in a secure, uniform environment. The daemon acts as the secure vault that holds the private keys safely in its configuration directory. The daemon acts as the reliable, always-on network node that guarantees data propagation to the DHT. The daemon also ensures that all identity operations are guarded by its internal Role-Based Access Control (RBAC) system. This RBAC integration guarantees that only authorized users or processes can create or rotate identities on the node. Without this centralized API, rogue scripts or compromised client applications could easily spam the network with invalid or unauthorized identities.


How It Works

The KID API lifecycle involves local filesystem storage, quantum-resistant key generation, dual-layered cryptographic signing, and asynchronous network propagation. This module uses the axum web framework to expose these workflows as HTTP endpoints. Here is the step-by-step breakdown of how these mechanisms operate under the hood.

1. Local Storage Management

All KIDs are stored locally on the filesystem within a dedicated kids directory. This directory is located inside the daemon’s base configuration path, which is resolved dynamically at runtime. -> See: kinetic-daemon/src/api/kid.rs — Lines 23 to 25 (get_kids_dir)

For every KID managed by the daemon, exactly two files are stored side-by-side:

  • {fqdn}.json: The public KID document, serialized as human-readable JSON. This can be safely shared.
  • {fqdn}.key: The private ML-DSA key seed, stored as raw, unencrypted bytes. This must be protected.

When listing or retrieving KIDs (handle_list_kids, handle_get_kid), the daemon simply reads these JSON files directly from the disk. It acts like a local, file-based database for identity documents, avoiding the overhead of a full SQL database for simple identity storage.

2. The Generation Flow

Generating a new KID is a multi-step orchestration handled by the handle_generate_kid function. This process ensures the identity is cryptographically sound, bound to the correct domain, and network-ready.

Step A: Authorization and Normalization The daemon first intercepts the request and checks if the caller has the Publish role. If the caller lacks permission, it immediately rejects the request with an HTTP 403 Forbidden. If authorized, it normalizes the requested domain name. It combines the base name and an optional sub-name into a single Fully Qualified Domain Name (FQDN). -> See: kinetic-daemon/src/api/kid.rs — Lines 81 to 93

Step B: Key Generation and DID Derivation The daemon invokes the ml-dsa library to generate a new quantum-resistant keypair. Specifically, it uses the MlDsa65 parameter set. It extracts the public verifying key from this pair. It encodes this public key in URL-safe Base64 without padding. It then hashes the raw public key bytes using the SHA-256 algorithm. This hashing step creates a deterministic fingerprint of the key. This hash is then converted to a hexadecimal string. It is prefixed with the standard DID prefix (did:kin:) to create a globally unique KineticDid. -> See: kinetic-daemon/src/api/kid.rs — Lines 98 to 113

Step C: Document Construction and Inner Signature A KidDocument struct is constructed in memory. The newly generated DID is set as the document’s canonical kid. The URL-safe Base64 public key is added as the primary ControllerKey. The daemon then signs this entire document struct using the newly generated ML-DSA private signing key. This inner signature is critical: it proves that the document was created by the actual holder of the controller key, preventing tampering. -> See: kinetic-daemon/src/api/kid.rs — Lines 114 to 134

Step D: Filesystem Persistence The daemon serializes the signed KidDocument to pretty-printed JSON and writes it to disk. It simultaneously writes the raw private key bytes to the corresponding .key file in the same directory. -> See: kinetic-daemon/src/api/kid.rs — Lines 135 to 141

Step E: The Outer Signature (Authorization Wrap) This is a core architectural concept in Kinetic’s identity system. Just because a KID document has a valid internal signature doesn’t mean it has the right to claim a specific domain name (like admin.saif.kin). To prove authority over the domain name, the signed KidDocument is wrapped in an AuthorizedKid struct. The daemon loads its root identity key (identity.key) from the base configuration directory. It signs the AuthorizedKid wrapper using this root identity key. This outer signature acts as an attestation. It tells the network: “The owner of this daemon authorizes this specific KID document to operate under this specific domain name.” -> See: kinetic-daemon/src/api/kid.rs — Lines 143 to 157

Step F: Network Publication Finally, the double-signed AuthorizedKid struct is serialized into a raw byte vector. It is pushed to the DHT using the network layer’s publish_redundant_payload function. This makes the identity discoverable by any other node in the Kinetic network. -> See: kinetic-daemon/src/api/kid.rs — Lines 158 to 160

3. The Rotation Flow

Keys can be compromised, or they may simply age out of a security policy, requiring rotation. The rotation flow (handle_rotate_kid) follows similar steps to generation, but with one vital cryptographic difference: the chain of trust.

When updating the KidDocument with a new controller key, the updated document MUST be signed by the OLD private key. The daemon reads the old key file from disk, assuming it contains a 32-byte seed. It reconstitutes the old signing key from this seed. It generates a new ML-DSA-65 keypair and replaces the controller key in the document. It then signs this rotation update with the OLD key. This proves to the entire network that the entity rotating the key is the legitimate owner of the previous key. It maintains a continuous chain of cryptographically verifiable authority. After signing, the new key replaces the old key on disk, the document is re-wrapped in an AuthorizedKid, re-signed by the root identity, and published to the DHT. -> See: kinetic-daemon/src/api/kid.rs — Lines 210 to 225

4. Axum Web Framework Integration

This module is built on top of axum, a modern web framework for Rust. It relies on Axum’s extractor pattern to parse requests and manage state safely.

  • Path<String>: Used in handle_get_kid and handle_rotate_kid to safely extract the target domain name directly from the URL path.
  • Json<T>: Used to automatically deserialize incoming HTTP bodies into Rust structs (like GenerateKidRequest), and to serialize outgoing responses directly from raw JSON values.
  • State<ApiState>: Provides safe, concurrent access to the daemon’s global state, primarily the network handle needed for DHT publication.
  • Extension<Role>: Injects the authenticated user’s role into the handler, enabling the RBAC checks that protect generation and rotation.

Key Pieces

GenerateKidRequest

-> See: kinetic-daemon/src/api/kid.rs — Lines 15 to 21 This is the incoming JSON payload structure used when a client wants to create an identity. It derives serde::Deserialize to automatically parse HTTP request bodies. It splits the desired identity into a mandatory base_name (e.g., saif.kin) and an optional sub_name (e.g., admin). The API handler normalizes and combines these strings into a single canonical FQDN.

get_kids_dir

-> See: kinetic-daemon/src/api/kid.rs — Lines 23 to 25 A small but crucial helper function that resolves the exact filesystem path where KIDs are stored. It queries the global configuration crate for the active base directory. The base directory dynamically changes depending on whether the daemon is running in production, testing, or a custom specified path. It then appends the kids subdirectory to this base path. This guarantees that all KID-related filesystem operations remain isolated from other daemon data like logs or databases.

handle_list_kids

-> See: kinetic-daemon/src/api/kid.rs — Lines 28 to 51 This function handles GET requests to list all locally managed KIDs. It iterates over the kids directory on the filesystem using std::fs::read_dir. It reads every .json file and attempts to parse it as a KidDocument using serde_json::from_str. It collects all valid, parseable documents into a vector. Instead of returning strongly typed Rust structs, it returns a generic serde_json::Value. This provides flexibility but means the compiler cannot guarantee the final JSON schema structure matches the API specification perfectly. It returns them to the caller wrapped in an axum::Json response. It safely ignores invalid files, directories, or the raw .key files.

handle_get_kid

-> See: kinetic-daemon/src/api/kid.rs — Lines 54 to 73 This handler retrieves a specific KID document by its requested domain name. It extracts the name using the axum::extract::Path extractor. It normalizes the requested name to prevent directory traversal attacks or mismatch issues. It constructs the expected filesystem path. It reads the file and serves the parsed JSON if it exists and is valid. It returns a standard HTTP 404 Not Found error tuple if the local daemon does not possess that KID.

handle_generate_kid

-> See: kinetic-daemon/src/api/kid.rs — Lines 76 to 168 This is the most complex and orchestrated handler in the module. It manages the entire creation lifecycle of new identities on the node. It uses several Axum extractors to gather context, including Extension<Role>, State<ApiState>, and Json<GenerateKidRequest>. The function performs the following strict sequence of operations:

  1. Validates that the caller holds at least a Publish role.
  2. Normalizes the incoming base_name and sub_name into a single FQDN.
  3. Generates a new ML-DSA-65 signing keypair.
  4. Encodes the public key using base64 URL-safe, no-padding encoding.
  5. Hashes the raw public key with SHA-256 to derive the unique KineticDid.
  6. Constructs the in-memory KidDocument setting the DID and controller keys.
  7. Signs the document using the newly generated key.
  8. Writes both the JSON document and the raw key seed to the local filesystem.
  9. Wraps the signed document into an AuthorizedKid struct.
  10. Loads the daemon’s root identity.key from disk.
  11. Signs the AuthorizedKid wrapper with the daemon’s root identity to prove ownership.
  12. Publishes the double-signed payload asynchronously to the DHT.
  13. Returns the final document as a JSON response to the client.

handle_rotate_kid

-> See: kinetic-daemon/src/api/kid.rs — Lines 171 to 256 This handler manages the delicate and sensitive process of key rotation. Key rotation is critical in case of suspected compromise or routine security policy enforcement. It performs the following sequence of operations:

  1. Validates the caller’s RBAC permissions (Publish or Admin).
  2. Normalizes the target FQDN from the URL path.
  3. Verifies that both the local .json document and the .key file exist on disk.
  4. Reads and parses the existing KidDocument from disk.
  5. Generates an new ML-DSA-65 keypair.
  6. Replaces the primary controller key in the document with the new public key.
  7. Reads the OLD key seed from disk.
  8. Validates that the old key seed is exactly 32 bytes long.
  9. Reconstitutes the OLD signing key from the 32-byte seed.
  10. Signs the updated document using the OLD key to prove cryptographic continuity.
  11. Overwrites the local .json and .key files with the new data.
  12. Wraps the updated document in an AuthorizedKid.
  13. Re-signs the authorization wrapper with the daemon’s root identity key.
  14. Publishes the newly rotated identity to the DHT to inform the rest of the network.

How This Connects to the Rest of Kinetic

This API module acts as the integration glue tying together several lower-level Kinetic crates into a unified HTTP interface:

  • CROSS-CRATE: kinetic-kid — Provides the foundational cryptographic data structures. Specifically, this module uses KidDocument, KineticDid, and ControllerKey. This API instantiates, populates, signs, and manipulates these core structures.
  • CROSS-CRATE: kinetic-core — Supplies the critical AuthorizedKid wrapper necessary for proving domain authority. It also provides the name normalization utility (kinetic_core::types::normalize_name) and global configuration path resolution (kinetic_core::config::get_base_dir).
  • CROSS-CRATE: kinetic-network — The API utilizes the network state via state.network.publish_redundant_payload to push the finalized, double-signed identity documents out to the broader peer-to-peer network via the DHT, making them globally discoverable.

Quick Reference

  • Storage Location: <base_dir>/kids/{fqdn}.json (public JSON document) and <base_dir>/kids/{fqdn}.key (private ML-DSA seed bytes).
  • GET /kids: Lists all locally stored KIDs. Handled exclusively by handle_list_kids.
  • GET /kids/:name: Retrieves a specific local KID document by name. Handled by handle_get_kid.
  • POST /kids: Generates a new KID, double-signs it, saves it locally to disk, and publishes it to the DHT network. Requires Publish permissions. Handled by handle_generate_kid.
  • POST /kids/:name/rotate: Generates a new key for an existing KID, signs the update with the old key, saves the new files, and republishes. Requires Publish permissions. Handled by handle_rotate_kid.
  • Cryptography Standard: Exclusively uses ML-DSA-65 for all KID controller keys.
  • Inner Signature: The KidDocument is signed by its own newly generated KID key to prevent tampering.
  • Outer Signature: The AuthorizedKid wrapper is signed by the daemon’s root identity.key to cryptographically prove domain ownership and authorization.

Open Questions / Things to Revisit

  • Plaintext Secret Storage: Currently, the ML-DSA private key seeds ({fqdn}.key) are written directly to disk in raw plaintext format. If the host machine’s filesystem is compromised, all hosted identities are instantly compromised. Should these keys be encrypted at rest using a daemon-level master password or integrated into OS-level secure keyrings (like Secret Service or Keychain)?
  • Hardcoded Key Length Assumptions: In handle_rotate_kid, the code checks if old_key_data.len() != 32. This operates on the hard assumption that ML-DSA seeds will always be exactly 32 bytes long. If the cryptographic backend changes or supports multiple algorithm variants with different seed sizes in the future, this hardcoded check will fail and block key rotation.
  • Network Publication Reliability: The publish_redundant_payload call happens asynchronously, and its return Result is ignored using let _ = .... If the daemon is temporarily disconnected from the DHT during a generation or rotation event, the identity is updated locally but the network never learns about it. The API still returns a success response to the client, leading to a split-brain state. There should likely be a background sync job, a retry queue, or better error reporting if DHT publication fails.
  • Error Types in API: The error handling directly passes formatted strings inside untyped JSON structs (Json(serde_json::json!({"error": format!(...)}))). A structured error enum (e.g., implementing axum::response::IntoResponse) might be safer, less repetitive, and significantly easier for typed API clients to consume reliably than parsing arbitrary strings.

API: Zone Management

Crate: kinetic-daemon Stage: 9 Reading time: 15 minutes Depends on: 01_overview.md, 11_api_identity.md, 12_api_names.md, docs/learn/types/05_dns_zones.md


What Is This?

This file (kinetic-daemon/src/api/zone.rs) provides the HTTP REST API endpoints that allow users, frontends, or automation scripts to manage their DNS zones. In the Kinetic ecosystem, a “zone” is the complete collection of DNS records (like A, AAAA, TXT, CNAME) that define what a specific domain name actually resolves to. However, because Kinetic operates a decentralized DHT rather than a traditional DNS registry, publishing a zone is not just a simple database update. It is a complex cryptographic operation. It requires taking the raw zone data, cryptographically signing it with the identity key that originally registered the name, and then broadcasting that proven payload to the peer-to-peer network. This module acts as the crucial bridge between user-friendly JSON payloads and the complex, cryptographic DHT network layer. It provides three primary operations: retrieving the current local draft of a zone, saving edits to that local draft, and finalizing those edits by signing and publishing them to the global network.


Why Kinetic Needs This

To truly understand why this module exists in its current form, we have to contrast the Kinetic architecture with traditional DNS management platforms. If you buy a domain on Namecheap or Amazon Route53, you log into a web dashboard, add an A record for your IP address, and click “Save”. Behind the scenes, the provider simply updates their centralized PostgreSQL database. That database then pushes changes out to their authoritative nameservers. You don’t have to cryptographically sign anything yourself, because you are logged into their centralized system, and they trust their own database implicitly. Kinetic has no centralized server and no trusted database. The “authoritative nameservers” are the thousands of independent nodes running the Kinetic DHT. If you want to tell the world that saif.kyn points to 192.168.1.100, you cannot just send that IP address to a central server and ask it to remember it.

Instead, you must manually perform the following sequence:

  1. Construct a formal DnsZone data structure in memory.
  2. Serialize that entire data structure into a precise byte array.
  3. Retrieve your original cryptographic registration record (NameRecord) for the domain saif.kyn.
  4. Embed the newly serialized zone bytes into the payload section of that record.
  5. Use your local ML-DSA identity key to generate a post-quantum signature over the updated record.
  6. Package the signed record and flood it into the DHT so peers can verify your signature against the public key and cache your new IP.

If Saif (or any regular user) had to do this manually using command-line tools, building byte arrays by hand, and managing ML-DSA signatures directly, Kinetic would be unusable for daily operations. This API module exists to hide that entire 6-step cryptographic pipeline behind a simple, standard REST interface. A frontend web application can simply send a JSON list of DNS records to the POST /zone/saif.kyn endpoint, and then subsequently call the POST /zone/saif.kyn/publish endpoint. The daemon handles the heavy lifting of the cryptography, the local storage lookups, and the network flooding automatically behind the scenes. Furthermore, by deliberately splitting the process into a two-step “Save” and “Publish” workflow, this module protects the health of the entire Kinetic network. If every minor typo fix or sequential record addition immediately triggered a DHT broadcast, the network would be flooded with redundant signature verifications and propagation traffic. By allowing users to save locally as much as they want, and only publishing when the entire zone is finalized, we conserve massive amounts of network bandwidth and node CPU cycles. In legacy systems, DNS propagation relies on TTLs (Time to Live) and hierarchical caching, meaning you wait hours for caches to expire. Kinetic’s DHT architecture behaves differently. When a signed record is flooded into the network, peers actively receive the update, verify the signature in real-time, and overwrite their local caches because they trust the new signature over the old one. The network achieves near-instantaneous global consistency. But that only works if the cryptographic payload is perfect. This module guarantees that perfection, acting as a strict cryptographic firewall that prevents malformed or improperly serialized payloads from ever leaving the local daemon.


How It Works

This file implements three distinct HTTP handlers using the Axum web framework. Let’s break down the execution flow of each handler, as they represent the complete lifecycle of a domain zone in Kinetic.

1. Retrieving the Local Zone Draft (handle_get_zone)

-> See: kinetic-daemon/src/api/zone.rs — Lines 15 to 43

When a client or frontend wants to see their current DNS records, they send a GET request to the path /zone/{name}. The execution flow is straightforward and local:

  • The daemon uses Axum’s Path extractor to pull the {name} variable from the URL.
  • It normalizes the name (converting it to lowercase and handling trailing dots) to ensure exact, predictable matching.
  • It validates that the requested name is a proper apex name using kinetic_core::types::is_valid_apex_name.
  • A critical architectural note here: you cannot manage a sub-zone (like sub.saif.kyn) directly through this endpoint; you must manage the entire apex zone for saif.kyn.
  • It constructs a filesystem path pointing to the daemon’s local configuration directory: get_zones_dir() / {fqdn}.json.
  • It attempts to read the file.
  • If the file is missing, it returns a 404 Not Found.
  • If it exists, it parses it as JSON.
  • If the JSON is corrupted, it returns a 422 Unprocessable Entity.
  • If everything succeeds, it returns the parsed JSON to the client.

Notice that this endpoint does not query the DHT. It only looks at the local filesystem. This is because the local daemon is the authoritative source of truth for its own domains before they are published. The DHT is just where we publish to, not where we read our drafts from.

2. Saving a Local Draft (handle_post_zone)

-> See: kinetic-daemon/src/api/zone.rs — Lines 50 to 90

When a user adds, modifies, or removes a DNS record in the UI, they send a POST request with the new DnsZone JSON payload to /zone/{name}. This endpoint functions exclusively as a “Save Draft” feature.

  • Authentication Check: The very first operation is an authorization check.
  • The Extension(role): Extension<Role> parameter is an Axum extractor.
  • In Axum, HTTP requests flow through a chain of middleware before hitting this handler.
  • Earlier in the request lifecycle, an authentication middleware inspected the API token, validated it, and injected a Role enum into the request’s internal map.
  • When Axum invokes handle_post_zone, the Extension extractor reaches into that map and pulls out the Role.
  • Only API tokens with the Publish or Admin privileges can modify zone files; others receive a 403 Forbidden.
  • Validation Extractors: The Json(zone): Json<kinetic_core::types::DnsZone> parameter is another vital Axum extractor.
  • When the POST request arrives, the HTTP body is just raw bytes.
  • Axum intercepts these bytes and pipes them through the serde_json deserializer to construct a valid DnsZone Rust struct.
  • If the JSON is missing required fields (like a TTL), Axum catches the error and automatically returns a 400 or 422 error to the client.
  • This means the code inside handle_post_zone is immune to malformed JSON; zone is guaranteed to be semantically valid.
  • Local Persistence: It ensures the local zones/ directory exists.
  • Then, it uses serde_json::to_string_pretty to serialize the DnsZone struct back into a human-readable JSON string.
  • This choice to use a pretty-printed JSON string ensures that sysadmins can manually edit the zone files directly in ~/.kinetic/zones/ if they prefer.
  • Finally, it overwrites the local .json file on the disk with this new content.

Crucially, the execution of this function stops here. It does not interact with the DHT. It does not sign any payloads. It simply writes a text file to the local disk. This architectural choice is vital for enabling complex frontend workflows, allowing users to “Discard Changes” or build up massive, intricate zone files across multiple API calls over hours or days before finally committing them to the permanent network record.

3. Finalizing, Signing, and Publishing (handle_publish_zone)

-> See: kinetic-daemon/src/api/zone.rs — Lines 98 to 245

This is the cryptographic heavy lifter of the module. Once the local JSON file is crafted, the user triggers a publish. This is the exact moment where local, mutable data is transformed into immutable, verifiable network truth.

Step A: Verification and Draft Loading The daemon first verifies the user’s role. Then it attempts to read the .json zone file from the disk. If you haven’t saved a draft zone yet via the POST endpoint, the publish process immediately aborts. It parses the JSON back into a DnsZone struct to ensure it hasn’t been externally corrupted on disk.

Step B: Retrieving the Local Registration Record To publish an update to a domain name, you must cryptographically prove you own it. When you originally registered the name on this daemon, the system saved a NameRecord (often referred to as a “Reveal” transaction) in its local key-value storage engine. The handler fetches this historical record from state.storage using the prefix DB_PREFIX_REVEAL. This specific record contains your original public key and your previous signature.

Step C: Loading the Daemon Identity Key The daemon then loads its core identity.key file from the disk. This file contains the ML-DSA keypair that serves as the identity for the entire daemon. It extracts the public key bytes from this loaded keypair and compares them to the public key stored inside the historical NameRecord. This is a critical, non-negotiable security boundary: You cannot publish a zone for a name if your current daemon identity does not match the identity that originally registered the name. If you manually copied a .json zone file from another node, you cannot trick the Kinetic network into accepting it because you do not possess the corresponding private key required to sign the update.

Step D: The Cryptographic Update Process This is where the actual transformation occurs (Lines 188-210):

  • The DnsZone struct is serialized into a raw byte vector using serde_json::to_vec. This byte array is the new payload.
  • The code matches on the NameRecord enum to determine its type.
  • If it is a standard name (NameRecord::Standard), it replaces the old payload field with the newly generated bytes.
  • It then invokes signable_bytes().
  • This function structures the record in a specific way, combining it with the NETWORK_ID constant.
  • By including the NETWORK_ID in the signed data, the signature is bound to the current Kinetic network (for example, binding it to the testnet vs the mainnet).
  • This cryptographically prevents replay attacks where someone might copy your testnet zone and fraudulently publish it to the mainnet.
  • It utilizes the ML-DSA private key to generate a brand new, post-quantum secure signature over these signable bytes.
  • It overwrites the old signature in the NameRecord with this newly generated one.

Step E: Deep Dive - Network Broadcast Now that the daemon has constructed a freshly signed NameRecord containing the updated DNS zone payload:

  • It saves this updated NameRecord back into its local state.storage.
  • This local caching ensures that if the daemon immediately restarts, the very next publish operation will correctly build upon this latest state rather than reverting to an older sequence.
  • Finally, it serializes the entire signed NameRecord into bytes and hands it off to state.network.publish_redundant_payload().
  • When this network function is called, the daemon calculates the SHA-256 hash of the domain name (saif.kyn).
  • This hash determines the “address” of the data on the DHT.
  • The daemon looks up the closest peers to that hash in its routing table and opens direct connections to them.
  • It transmits the serialized, signed NameRecord bytes to them.
  • Those nodes receive the bytes, extract the public key and signature, and run the ML-DSA verification algorithm.
  • If it passes, they store the record and gossip it to their closest peers, creating a viral cascade.
  • Within seconds, any node looking up saif.kyn will be routed to a node holding this fresh, proven zone data.

Deep Dive: The Cryptographic Security Model

To appreciate the design of this API, we must understand the threat model it defends against. Traditional REST APIs rely on transport layer security (TLS) and bearer tokens. If an attacker compromises the central database of a traditional provider, they can change any DNS record. In Kinetic, the API daemon is merely a convenient interface; it is not the ultimate authority. The ultimate authority is the mathematics of the ML-DSA post-quantum signature scheme. When handle_publish_zone executes, it performs a strict validation of the identity key. Why is this necessary? Imagine an attacker compromises your local zones/ directory and modifies saif.kyn.json. They add a malicious A record pointing to a phishing server. If they then hit the POST /zone/saif.kyn/publish endpoint, the daemon will dutifully read the modified file. However, if the daemon does not possess the correct identity.key that originally registered saif.kyn, the publish will fail. The DHT peers do not care what the API daemon says; they only care about the signature attached to the payload. When the payload is serialized via serde_json::to_vec, it is converted into a deterministic stream of bytes. This exact stream of bytes is what the ML-DSA private key signs. If even a single bit in the JSON changes (for example, an IP address changes from .100 to .101), the entire byte stream changes. Because the byte stream changes, the old signature becomes invalid. Therefore, the daemon must generate a new signature every single time a zone is published. Furthermore, the signable_bytes function embeds the NETWORK_ID into the signed data payload. This is a defense against replay attacks across different Kinetic environments. Without the NETWORK_ID, an attacker could monitor the testnet, copy your signed testnet zone update, and broadcast it to the mainnet DHT. Because the signature would be valid for the payload, the mainnet nodes might accept it. By forcing the signature to cover both the payload AND the specific network identifier, this cross-network pollution becomes impossible.


Technical Concept: Axum Extractors in Practice

Throughout this API, you will see function parameters like Path(name), Extension(role), and Json(zone). These are known in the Rust Axum framework as “Extractors”. They are a powerful, declarative way to pull data out of an incoming HTTP request. Instead of writing boilerplate code to read the request body, parse JSON, and handle errors manually, you simply declare what you want. If you declare Path(name): Path<String>, Axum automatically parses the URL, extracts the segment matching the route variable {name}, and provides it as a Rust String. If you declare Json(zone): Json<DnsZone>, Axum automatically reads the request body stream. It pipes that stream directly into the serde_json deserializer. If the JSON is malformed, or if it doesn’t match the DnsZone struct schema, Axum intercepts the error. It prevents the function from ever executing. It automatically generates an appropriate HTTP 400 or 422 response and sends it back to the client. This means that by the time the first line of your function runs, you have absolute mathematical certainty that zone is a fully valid, correctly shaped Rust data structure. The Extension extractor works similarly but for server-side state. Earlier in the request lifecycle, authentication middleware validates the user’s API token. It creates a Role enum and stashes it in a type-mapped dictionary attached to the request. When your function asks for Extension(role): Extension<Role>, Axum reaches into that dictionary and pulls it out. This eliminates the need for global variables or passing context objects down a long chain of functions.


Key Pieces

handle_get_zone

  • What it does: Reads and returns a domain’s local JSON zone file draft from the daemon’s disk.
  • Where it lives: kinetic-daemon/src/api/zone.rs — Lines 15 to 43
  • Why it matters: This endpoint allows the frontend dashboard to accurately display the current state of a domain’s DNS records so the user can see exactly what they are editing before they publish.

handle_post_zone

  • What it does: Accepts a JSON DnsZone payload over HTTP and saves it directly to the local filesystem without interacting with the network.
  • Where it lives: kinetic-daemon/src/api/zone.rs — Lines 50 to 90
  • Why it matters: Acts as a vital staging area for DNS changes. It prevents the peer-to-peer network from being spammed with incomplete, intermediate, or broken updates while a user is in the middle of configuring multiple complex records.

handle_publish_zone

  • What it does: Reads the staged local zone file, embeds the payload into a NameRecord, cryptographically signs it with the daemon’s ML-DSA identity key, updates local storage, and broadcasts the finalized record to the DHT.
  • Where it lives: kinetic-daemon/src/api/zone.rs — Lines 98 to 245
  • Why it matters: This is the singular, core mechanism for actually updating the global Kinetic state. It bridges the gap between local, mutable configuration drafts and immutable, proven network consensus.

The ML-DSA Signing Block

  • What it does: Generates the cryptographic proof of ownership for the new DNS zone payload.
  • Where it lives: kinetic-daemon/src/api/zone.rs — Lines 202 to 205
  • Why it matters: Without this explicit signing step, every single peer on the DHT would immediately reject the updated payload. They would have no mathematical proof that the update actually originated from the true, verified owner of the domain.

How This Connects to the Rest of Kinetic

  • CROSS-CRATE: kinetic_core::types::DnsZone — Defined in the kinetic-core crate, this struct dictates the exact, required schema of the JSON payload.
  • CROSS-CRATE: kinetic_core::types::NameRecord — Defined in the kinetic-core crate, this is the wrapper struct that holds the signature, the public key, and the actual zone payload bytes.
  • CROSS-CRATE: kinetic_core::config — Provides the exact directory paths for where the zones/ folder and the core identity.key are physically stored on the local disk.
  • CROSS-CRATE: kinetic-network — The publish_redundant_payload function, which is called at the very end of the publish handler, belongs to the network crate. That crate is responsible for correctly routing the data to the appropriate DHT peers based on the domain’s hash.

Quick Reference

  • GET /zone/{name}: Fetch the currently saved local draft of a zone. This operation is purely local and does not touch the network.
  • POST /zone/{name}: Save a new draft of the zone to the local disk. Requires the Publish role. Validates the JSON schema against the Rust structs but does not touch the network or sign anything.
  • POST /zone/{name}/publish: Read the local draft, serialize it, cryptographically sign it with the daemon’s ML-DSA identity key, update the local key-value database, and broadcast the signed record to the DHT network.
  • File Storage Path: ~/.kinetic/zones/{fqdn}.json
  • DB Storage Prefix: reveal_ (used to lookup original registration data).
  • Authentication: All modifying endpoints require Role::Admin or Role::Publish.

Open Questions / Things to Revisit

  • Premium Name Branching: Inside handle_publish_zone, there is an explicit branch handling for NameRecord::Premium (Line 208). It updates the raw payload but it does not generate a new signature. Is this intentional behavior? Do Premium names use an different mechanism for update validation on the DHT, or is this a missing signature generation step that will eventually cause DHT peers to reject any Premium name updates?
  • Concurrency Race Conditions: If an automated script or a fast-clicking admin triggers POST /zone/{name}/publish twice in rapid succession, is it possible for the file read operation and the database read operation to interleave? This could cause a dangerous race condition where an older zone gets signed with a newer sequence number, corrupting the timeline. A simple file-level or name-level mutex around the publish operation might be necessary in high-throughput environments.
  • Semantic Record Validation: The POST endpoint verifies that the incoming data matches the DnsZone Rust struct shape, but it does not appear to perform deep semantic validation. For example, it does not verify that an A record actually contains a valid, well-formed IPv4 address string, rather than just returning any random string. Should this semantic validation happen directly here in the API daemon before allowing the save, or is that solely the responsibility of the frontend dashboard?
  • Error Verbosity: The API currently returns detailed internal error messages directly to the client (e.g., "File write failed: {e}"). While useful for debugging, in a production environment, exposing raw filesystem or DHT networking errors to an API client might be a minor information disclosure risk. We may want to sanitize these errors before returning them over HTTP.

Background Network Services

Crate: kinetic-daemon Stage: 9 Reading time: 15 minutes Depends on: kinetic-network, kinetic-core


What Is This?

This topic covers the background networking workers that run persistently as long as the Kinetic daemon is alive. Specifically, it documents two critical autonomous loops inside kinetic-daemon/src/services/network.rs. The first is the Proof of Work (PoW) Sybil resistance miner. The second is the Distributed Hash Table (DHT) name republisher.

Inside the Kinetic network, your node cannot simply connect to peers and sit idle. The network imposes strict, time-based rules to prevent spam (Sybil attacks). It also has rules to keep the global routing table clean of dead or obsolete data. These background services act as the life support systems of your node. They run in the background, out of sight of the user. They ensure that your node remains a valid, legally participating citizen of the Kinetic network. They are automatically spawned when the daemon starts and run until the daemon process is killed.

Without these background workers, your node would inevitably degrade in the network’s eyes. First, it would get blacklisted by peers as its cryptographic identity expires. Second, the network would slowly forget your claimed names as your DHT records rot away. This file explains how the daemon prevents both scenarios automatically.


Why Kinetic Needs This

The Kinetic daemon must solve two fundamental problems related to time and decay in decentralized networks:

1. Identity Expiration and Sybil Resistance

The Kinetic network uses Proof of Work (PoW) to prevent malicious actors from spinning up thousands of fake nodes. If a node could mine a valid PoW once and use it forever, an attacker could slowly build up a massive botnet over months. To prevent this, Kinetic ties the validity of a node’s PoW to the current “epoch”. This epoch shifts regularly based on Drand randomness.

When an epoch changes, your node’s PeerId and its associated PoW are no longer valid. If you do nothing, other nodes will reject your messages. They will drop active connections with you because your identity is cryptographically stale. Therefore, the daemon needs a mechanism to detect when its identity is expiring. It must mine a new identity before it gets disconnected. Crucially, it must transition to this new identity without interrupting the user’s experience or dropping pending requests. This transition must happen behind the scenes, abstracting the complexity away from the user.

2. DHT Record Rot

The Distributed Hash Table (DHT) is not a permanent, reliable database. It is a volatile memory space shared across thousands of transient nodes. Nodes go offline, drop records to save space, or simply forget things after a timeout. When you register a name in Kinetic, you publish a Commitment and a Reveal to this DHT.

If you just publish these records once and walk away, the network will eventually forget you own that name. The daemon must act as a heartbeat for your data. It must constantly wake up to remind the network, “I am still here, and I still own this name.” Without this constant refreshing, names would silently expire and become claimable by others.


How It Works

The PoW Miner Loop: Seamless Hot-Swapping

The start_pow_miner_loop function is responsible for keeping the node’s identity valid.

1. Listening to Drand: The loop is powered by a tokio::sync::watch::Receiver. This channel receives the latest Drand “kyn” (round number) as soon as it is fetched by the Drand service. Because Drand kyns are the heartbeat clock of the Kinetic network, every new kyn triggers an evaluation. For every new kyn, the loop evaluates the current state of the world to see if action is needed. -> See: kinetic-daemon/src/services/network.rs — Lines 28 to 36

2. Evaluating the Epoch: It calculates the “staggered epoch” for the current PeerId. The staggered epoch ensures that not all nodes expire at the exact same time. If everyone expired simultaneously, it would cause massive network-wide disconnect events. After calculating the epoch, it checks if the PoW for this identity is still valid. -> See: kinetic-daemon/src/services/network.rs — Lines 39 to 57

3. Offloaded Mining: If the epoch has shifted and the PoW is no longer valid, the daemon must mine a new one. Because mining is a CPU-intensive hashing operation, it cannot run directly in the async loop. If it did, it would freeze the entire tokio runtime, stopping all other networking and the REST API. To solve this, the daemon uses tokio::task::spawn_blocking. This sends the mining job to a dedicated OS thread pool specifically designed for heavy computation. -> See: kinetic-daemon/src/services/network.rs — Lines 61 to 68

4. The Hot-Swap Maneuver: Once the new keypair is mined, the daemon performs a dangerous but necessary maneuver: the hot-swap.

  • First, it forcefully aborts the currently running NetworkEventLoop (the active libp2p backend).
  • Second, it loops and creates a new NetworkEventLoop.
  • It uses the newly mined keypair for this new loop.
  • It injects the exact same shared states: storage, configuration, and drand receivers.
  • It contains a retry loop with exponential backoff (up to 10 times) in case the OS hasn’t released the network port yet.
  • Third, it calls hc_client.update_backend(...).
  • This tells the frontend NetworkClient (which the REST API is actively holding) to point its internal channels to the new backend.
  • The rest of the daemon has no idea the underlying identity just changed.
  • The frontend steering wheel stayed the same, but the engine was swapped while driving. -> See: kinetic-daemon/src/services/network.rs — Lines 71 to 108

The DHT Republisher: Preserving Ownership

The start_republisher function ensures your names don’t fade from the network’s memory.

1. The 12-Hour Heartbeat Timer: It sets up a tokio::time::interval timer. This timer wakes the task up every 12 hours. The 12-hour value is defined by the TIMEOUTS_HEARTBEAT_AGE_WARNING_SECONDS constant. -> See: kinetic-daemon/src/services/network.rs — Lines 124 to 126

2. Reading from Local Storage: When the timer ticks, it reads the list of all owned names from the local storage database. It uses the DB_PREFIX_OWNED_NAMES prefix to find this array. For each name found in the array, it fetches the corresponding Reveal payload from storage. -> See: kinetic-daemon/src/services/network.rs — Lines 129 to 139

3. Anti-Spam Staggering: The republisher spawns a new async Tokio task for each individual name it owns. To prevent flooding the local libp2p node and overwhelming the network with hundreds of concurrent DHT requests, it staggers these tasks. It multiplies the loop index i of the name by 100 milliseconds and sleeps for that duration before starting the republish sequence. This ensures a smooth, staggered broadcast instead of a massive sudden spike. -> See: kinetic-daemon/src/services/network.rs — Lines 144 to 148

4. Reconstructing the Commitment on the Fly: The daemon does not store the Commitment directly in the database. Because a Commitment is just a SHA-256 hash of the Reveal components, the daemon reconstructs it. It creates a new sha2::Sha256 hasher and updates it in a very specific order. It hashes the name as bytes, then the salt, then the decoded Drand signature, and finally the public key. This specific order must exactly match the original commitment logic used during name registration. This saves storage space and ensures the commitment always matches the reveal data. -> See: kinetic-daemon/src/services/network.rs — Lines 150 to 159

5. The Two-Step Publication and the Maturity Gate: First, it publishes the reconstructed Commitment to the DHT. -> See: kinetic-daemon/src/services/network.rs — Lines 167 to 169 Then, it hits a critical, hardcoded delay: it sleeps for exactly 36 seconds. -> See: kinetic-daemon/src/services/network.rs — Lines 172 to 173 Why 36 seconds? Because Kinetic has a “maturity gate.” A reveal is only considered valid by the network if its corresponding commitment was published at least 10 Drand rounds prior. 10 Drand rounds is exactly 30 seconds. The 36-second sleep provides a 6-second safety buffer. If the daemon published both instantly, new nodes on the network would reject the reveal. They would reject it because they haven’t seen the commitment age properly. After the 36-second wait, it safely publishes the actual Reveal to the DHT. -> See: kinetic-daemon/src/services/network.rs — Lines 175 to 185

Analogy for Republishing: Think of the DHT as a massive bulletin board in a windy town square. People routinely tear down old posters to make room for new ones. If you claim a name, you pin your poster to the board. Because the town square forgets over time, you have to hire a worker (this background task). The worker comes back every 12 hours. They pin a fresh copy of your initial poster (the commitment). They wait exactly 36 seconds for the glue to dry and for people to notice it (the maturity gate). Finally, they pin the final document (the reveal) right next to it.


Key Pieces

  • start_pow_miner_loop

    • Where it lives: kinetic-daemon/src/services/network.rs — Line 8
    • What it does: The autonomous worker that monitors Drand rounds. It detects epoch expiration, mines new libp2p identities, and hot-swaps them into the running application.
    • Why it matters: It keeps the node alive on the network without requiring restarts from the user. It prevents Sybil decay.
  • tokio::task::spawn_blocking

    • Where it lives: kinetic-daemon/src/services/network.rs — Line 49 and Line 61
    • What it does: Sends CPU-heavy functions like is_valid_sybil_pow and mine_sybil_keypair to a separate operating system thread.
    • Why it matters: It prevents the async event loop from freezing. If it ran on the main thread, all incoming network requests would timeout while the CPU churns through hashes.
  • hc_client.update_backend(...)

    • Where it lives: kinetic-daemon/src/services/network.rs — Line 104
    • What it does: The critical mechanism that connects the old frontend client handles to the newly spawned libp2p backend loop.
    • Why it matters: It allows the REST API and other external services to maintain their reference to the NetworkClient even after the underlying P2P node has been destroyed and recreated.
  • start_republisher

    • Where it lives: kinetic-daemon/src/services/network.rs — Line 119
    • What it does: Periodically iterates over owned names in local storage and re-broadcasts them to the DHT in two steps.
    • Why it matters: It prevents name ownership from silently expiring in the volatile DHT memory space. It keeps the node’s claimed assets alive.

How This Connects to the Rest of Kinetic

  • CROSS-CRATE: The concept of staggered epochs, Drand kyns, and PoW Sybil resistance is fundamentally defined and implemented in kinetic-network (Stage 8).
  • CROSS-CRATE: The Commitment and Reveal flow, including the hashing logic and the 10-round maturity requirement, is part of the naming system defined in kinetic-core (Stage 7).
  • This file acts as the operational glue. It takes the theoretical rules defined in core and network and actually executes them persistently in a live daemon.
  • It leverages the StorageEngine trait from kinetic-core to read the locally saved names that need republishing.

Quick Reference

  • Drand Trigger: The miner loop evaluates identity validity on every new Drand round received over the watch channel.
  • Validation: Uses spawn_blocking to check is_valid_sybil_pow before deciding to mine.
  • Mining Strategy: Offloaded to a blocking thread pool to preserve async responsiveness using mine_sybil_keypair.
  • Backend Hot-Swap: When an epoch expires, the old libp2p loop is aborted via its JoinHandle.
  • Port Retries: The hot-swap includes up to 10 retries with exponential backoff if the port is in use.
  • Republish Interval: Runs unconditionally every 12 hours.
  • Staggering: Names are delayed by 100ms per index before being republished.
  • On-the-fly Hashing: Commitments are rebuilt from Reveals rather than stored raw.
  • Maturity Wait: Sleeps 36 seconds (12 Drand rounds) between publishing a commitment and publishing the corresponding reveal to satisfy network maturity rules.

Open Questions / Things to Revisit

  • Hot-Swap Port Contention: The hot-swap logic uses an exponential backoff retry loop (up to 10 retries) if the network port is still in use when spinning up the new backend. If this retry loop takes several seconds to complete, could incoming proxy requests from the REST API be dropped during the transition window?
  • Reconstructed Commitments Consistency: The republisher reconstructs the commitment from the Reveal struct (name, salt, signature, pubkey). If a future update changes how commitments are hashed, the daemon will need to ensure this background loop mirrors the original registration hashing logic, or else the republisher will accidentally publish invalid commitments.
  • Memory Growth in Republisher: The republisher spawns a new async task for every single name it owns, staggered by 100ms. If a node owns thousands of names, this could spawn thousands of overlapping sleep-and-publish tasks every 12 hours. Is there a risk of hitting resource limits here?
  • Missing Error Handling: If hc_client.update_backend fails or the network loop crashes immediately after being spawned, the network_loop_handle might be pointing to a dead task, leaving the daemon disconnected from the network without recovering.

Name and KID Resolution API

Crate: kinetic-daemon Stage: 9 Reading time: 15 minutes Depends on: docs/learn/core/01_overview.md, docs/learn/network/01_overview.md


What Is This?

This module (kinetic-daemon/src/api/resolve.rs) defines the REST API endpoints that the Kinetic daemon uses to resolve human-readable .kin names and Kinetic Identifiers (KIDs). It acts as the HTTP bridge between standard web clients and the underlying Kinetic peer-to-peer network.

Instead of forcing a web application or mobile app to implement the complex Kinetic DHT protocol, this module allows them to simply make standard HTTP GET requests to a running Kinetic daemon. The daemon takes the query, translates it into a network lookup, waits for the peer responses, and returns clean JSON.

The primary function of this module is data translation and network abstraction. It hides the complexity of:

  • Cryptographic hash routing.
  • Peer discovery and connection management.
  • Protocol timeouts and retry logic.
  • Data validation and authorization checks.

To the outside world, querying a name like saif.kin looks exactly like querying a traditional centralized web API. But under the hood, this module is orchestrating a decentralized search across the DHT and applying specialized fallback logic. Without this API, any interaction with the Kinetic network would require raw socket programming and binary packet parsing. This file abstracts away all of those networking primitives, providing a seamless HTTP layer on top of the P2P infrastructure.


Why Kinetic Needs This

The Kinetic network operates on cryptographic proofs, byte slices, and Distributed Hash Table (DHT) routing. Standard web browsers, frontend React applications, and command-line tools like curl do not speak “Kinetic Network Protocol”—they speak HTTP and expect JSON data.

If this module did not exist, the developer experience for building on Kinetic would be miserable. Every application that wanted to resolve saif.kin or look up a user’s KID would need to:

  • Bundle the entire Kinetic networking stack.
  • Manage long-lived connection pools to DHT peers.
  • Implement complex asynchronous polling mechanisms.
  • Parse raw binary payloads into structured data.

When building a modern web application, frontend developers are used to calling straightforward endpoints and getting back a JSON blob. They do not want to negotiate connections with random peers across the globe or compute cryptographic hashes just to see who owns a name.

By providing this REST abstraction, the Kinetic daemon allows the network to be easily integrated into:

  • Traditional web frontends (React, Vue, Svelte) via standard fetch() calls.
  • Backend services written in other languages (Node.js, Python, Go) that need to query identity data.
  • Simple shell scripts using curl for automation or monitoring.

Furthermore, this module acts as a critical safety net for data retention. The DHT is a distributed system. Data is stored on volatile peers who can go offline at any time. Records can occasionally drop or propagate slowly due to network partitions.

The resolution API implements a crucial local fallback mechanism. If the network says a name doesn’t exist, but the daemon has a local backup cache of that name on its disk, it serves the local copy. This rescues users who might have momentarily lost connectivity or whose DHT records expired before their daemon could successfully republish them. Without this specific file, temporary DHT instability would result in hard application failures.


How It Works

This file exposes two primary Axum route handlers. They both operate asynchronously and interact closely with the network and storage components provided via the ApiState.

1. Resolving a .kin Name

When a user requests a name resolution, the handle_resolve_name function is triggered. This is the entry point for turning a string like saif.kin into a full NameRecord JSON object. -> See: kinetic-daemon/src/api/resolve.rs — Lines 19 to 83

Step 1: Normalization The requested name is passed through kinetic_core::types::normalize_name. This ensures that capitalization differences (e.g., SAIF.kin vs saif.kin) do not cause hash mismatches on the DHT.

Step 2: The DHT Lookup The daemon asks the network layer to resolve the fully qualified domain name (FQDN) using state.network.resolve_redundant_payload(&fqdn). This function sends lookup queries out to connected peers, hunting for the node closest to the hash of the requested name. This is an asynchronous operation that blocks the HTTP request until the network responds.

Step 3: Payload Parsing If the network peers return the requested bytes, the daemon attempts to parse the payload as a kinetic_core::types::NameRecord using serde_json::from_slice.

  • If parsing succeeds, the record is wrapped in an Axum Json response and returned to the caller with an HTTP 200 OK status.
  • If parsing fails, it means the network returned corrupted data, and an HTTP 500 error is thrown.

Step 4: The Local Fallback Rescue If the network layer returns a ResolutionError::NotFound, the daemon does not immediately return a 404 error to the HTTP client. Instead, it checks its local state.storage database.

  • It constructs a lookup key using DB_PREFIX_REVEAL + fqdn.
  • If a cached record is found locally, it means the user previously registered or revealed this name using this specific daemon instance.
  • The daemon serves the local backup and logs a message: Recovered {} from local daemon storage backup!.
  • This local fallback prevents total failure if the DHT drops the record.

Step 5: Error Mapping If the user’s network is disconnected, the function intercepts ResolutionError::Offline and maps it to a 503 Service Unavailable response, letting the frontend know to try again when the connection is restored.

Step 6: Constructing the Final HTTP Response Once all operations are complete, whether they succeed or fail, this handler converts the internal Kinetic network outcome into a clean HTTP response.

  • Successes return an Axum Json response, which serializes the Rust struct into a standard JSON string.
  • Errors are mapped to specific status codes (e.g., 404 Not Found or 500 Internal Server Error) and return a JSON payload containing an {"error": ...} message.
  • This ensures that frontend developers consuming this API do not need to understand Rust Result types; they simply parse standard REST errors.

2. Resolving a KID and its Manifest

When an application needs to look up the public keys or capabilities associated with a Kinetic Identifier (KID), the handle_resolve_kid function is triggered. -> See: kinetic-daemon/src/api/resolve.rs — Lines 90 to 150

Step 1: Fetching the KID Payload Similar to name resolution, the daemon queries the DHT for the raw payload associated with the did (the KID string). It applies the same error mapping for NotFound and Offline scenarios.

Step 2: Legacy Support Parsing The daemon attempts to parse the returned payload as an AuthorizedKid (the modern network standard, which wraps the document in cryptographic signatures).

  • If this fails, the code includes a fallback mechanism: it tries to parse it as a raw KidDocument.
  • This allows backward compatibility with older identity data structures that might still exist on the network.

Step 3: Calculating the Manifest Key A KID often has an associated “Manifest” that defines its capabilities, service endpoints, or linked data. This manifest is stored separately on the DHT to keep the core KID document lightweight.

  • The daemon calculates the DHT key for this manifest by taking the SHA256 hash of the string "{did}#manifest".
  • It encodes this hash into a hex string to query the network.

Step 4: Stitching the Response The daemon then makes a second, independent DHT lookup for the manifest key.

  • If the manifest is found, the daemon seamlessly stitches it into the final JSON response under the manifest_document key.
  • If the manifest is not found, it simply omits it, returning only the kid_document without throwing any HTTP error. The frontend receives a unified JSON object containing everything the network knows about that identity.

Key Pieces

handle_resolve_name

  • Location: api/resolve.rs — Lines 19-83
  • Role: The Axum HTTP handler for resolving human-readable names.
  • Why it matters: It implements the vital DHT-to-local-storage fallback logic. Without this function, naming on Kinetic would be unstable during network churn, and lightweight web clients would not be able to interact with .kin names easily.

handle_resolve_kid

  • Location: api/resolve.rs — Lines 90-150
  • Role: The Axum HTTP handler for resolving a user’s decentralized identifier (KID).
  • Why it matters: It abstracts away the complexity of resolving a KID and its associated capability manifest. It executes multiple DHT lookups and aggregates the results into a single clean JSON response, reducing the workload for application developers.

Axum Extractor: State<ApiState>

  • Location: Function signatures (e.g., Line 20)
  • Role: Safely pulls the shared daemon context into the isolated request handler.
  • Why it matters: This is the idiomatic way Axum handles concurrency and dependency injection. It allows the HTTP handlers to remain stateless themselves, while still accessing the persistent daemon infrastructure (like references to the network pool and the local database). It guarantees thread-safe access without manual locking in the handler.

Axum Extractor: Path<String>

  • Location: Function signatures (e.g., Line 21)
  • Role: Automatically extracts the dynamic portion of the URL directly into a local Rust variable.
  • Why it matters: If a user queries /name/saif.kin, this extractor automatically pulls "saif.kin" into the name variable. It prevents the need for manual URL parsing or regex matching, making the routing code exceptionally clean and error-free.

The Fallback Key Formatting

  • Location: api/resolve.rs — Line 38
  • Role: format!("{}{}", kinetic_core::constants::DB_PREFIX_REVEAL, fqdn)
  • Why it matters: This precise string formatting determines where the daemon looks for emergency backups. It relies on the local storage system maintaining a mirrored cache of data the user has revealed to the network.

How This Connects to the Rest of Kinetic

  • CROSS-CRATE: kinetic_core::types::NameRecord — Defined in the core crate, this is the exact data structure the API expects to retrieve from the DHT and serialize into the final HTTP JSON response.
  • CROSS-CRATE: kinetic_core::error::ResolutionError — Used by this module to determine if a failure was due to the network being Offline or if the record was simply NotFound. The handler translates these internal Rust errors into standardized HTTP status codes.
  • CROSS-CRATE: kinetic_kid::KidDocument and CapabilityManifest — Defined in the kid crate, these define the identity schema. This module is responsible for fetching these raw bytes from the network and parsing them into these structured types.
  • CROSS-CRATE: ApiState — The shared state structure that holds state.network (for DHT interactions) and state.storage (for local database fallbacks).

Quick Reference

  • Name Lookup Flow:
    1. Query the DHT via resolve_redundant_payload.
    2. If successful, parse and return the NameRecord.
    3. If not found, check the local daemon database using DB_PREFIX_REVEAL + name.
    4. Return the local backup if it exists, otherwise return a 404.
  • KID Lookup Flow:
    1. Resolve the core identity document first.
    2. Independently hash did#manifest using SHA256.
    3. Attempt to resolve and append the manifest to the response JSON.
  • Response Format: Returns standard Axum Json<T> which automatically serializes the Rust structs and sets the correct Content-Type: application/json headers for the browser.
  • HTTP Error Mapping:
    • 404 Not Found: The requested name or KID does not exist on the DHT and is not cached locally.
    • 503 Service Unavailable: The daemon is fully disconnected from the Kinetic peer-to-peer network.
    • 500 Internal Server Error: The DHT returned data, but the byte payload was corrupted or failed to parse into the expected JSON structure.

Open Questions / Things to Revisit

  • Manifest Lookup Performance: The handle_resolve_kid function blocks on a second network lookup if it tries to resolve the capability manifest. Since these two items (the KID and the Manifest) are separate records on the DHT, could these two network queries be spawned concurrently using tokio::join! to reduce the overall API latency?
  • Legacy Structure Fallbacks: The code handles falling back to parse raw KidDocument and CapabilityManifest byte payloads if the modern AuthorizedKid parsing fails. At some point in the network’s lifecycle, this backwards compatibility might need to be removed to enforce strict cryptographic authorization wrappers on all records.
  • Storage Redundancy: The local fallback relies on DB_PREFIX_REVEAL. If the local storage is cleared, or if the daemon is running in a fresh state without historical caches, the fallback cannot rescue a failed DHT lookup. Is there a need for a secondary fallback mechanism?

P2P Proxy Inbound Requests

Crate: kinetic-daemon Stage: 9 Reading time: 15 minutes Depends on: 15_proxy_http.md (or HTTP proxy concepts), kinetic-network (Stage 8)


What Is This?

This file implements the inbound side of the Kinetic proxy system.

When another node in the P2P network wants to access a service hosted on your local machine, it sends a ProxyRequest through the libp2p network. This module is responsible for listening to those incoming requests.

Once a request is received, it acts as a gatekeeper and a translator. It validates the request to ensure it isn’t malicious—blocking things like path traversal attacks or excessively large payloads that could crash the daemon. After the request passes these stringent security checks, the module translates the P2P message into a standard HTTP request.

It then acts as an HTTP client, forwarding that request to a standard HTTP server running on your local machine. This local backend could be anything: a local web application, a REST API, or a media server.

Finally, it receives the HTTP response from your local backend, packages it back into a ProxyResponse, and sends it over the P2P network to the remote peer.

In short: it is the secure bridge that exposes your local web services to the Kinetic P2P network, ensuring that the network remains isolated from arbitrary filesystem or system access. It makes sure that interacting with the decentralized network feels exactly like interacting with standard web infrastructure for the local application.


Why Kinetic Needs This

For the Kinetic network to function as a truly distributed, decentralized application platform, nodes must have a robust mechanism to serve content, applications, or APIs to each other. They need to do this without relying on public IP addresses, centralized DNS, or traditional web hosting infrastructure. This is the core promise of a P2P network: direct, peer-to-peer interaction.

However, providing remote peers with raw socket access to our local machine is not an option. That would introduce massive security vulnerabilities, allowing any node on the network to probe our local ports, exploit internal services, or worse. Kinetic requires a secure, controlled choke-point between the untrusted P2P network and the trusted local execution environment.

This module provides exactly that secure choke-point. It ensures several vital protections:

  • Path traversal attacks are thwarted: Requests attempting to use ../ (like ../../../etc/passwd) are blocked before they ever touch the local filesystem or the backend web server.
  • Memory exhaustion is prevented: The daemon enforces strict payload size limits on both incoming requests and outgoing responses. This protects the Kinetic node from Out-Of-Memory (OOM) crashes caused by malicious or broken peers sending gigantic payloads.
  • Protocol integrity is maintained: Arbitrary or malformed HTTP methods are rejected, ensuring the local backend only sees well-formed requests.
  • Server spoofing is mitigated: The Host header provided by the remote peer is deliberately ignored and rewritten. This ensures the local server accurately sees the request as coming from the proxy interface, preventing virtual-host confusion attacks.

Without this module, safely exposing a local service to the P2P network would be impossible, and the entire peer-to-peer proxy architecture would collapse under the weight of security risks.


How It Works

The core of this module is built around a single asynchronous loop that listens for incoming ProxyRequest messages from the libp2p network and handles them concurrently.

Here is the step-by-step lifecycle of an inbound P2P proxy request as it flows through this system:

  1. Listening for Incoming P2P Requests: The handle_incoming_proxy_requests function is the entry point. It takes a tokio::sync::mpsc::Receiver. This receiver channel is fed by the underlying kinetic-network layer whenever a peer sends a proxy request over the libp2p request-response protocol. The function loops indefinitely, waiting for messages to arrive on this channel. -> See: kinetic-daemon/src/proxy/p2p.rs — Lines 21 to 22

  2. Spawning Concurrent Tasks: For every incoming request, a new asynchronous task is spawned using tokio::spawn. This is a crucial architectural decision. It ensures that if a local server is slow to respond, it does not block the daemon from processing requests from other peers. Each request is handled in its own isolated execution context. -> See: kinetic-daemon/src/proxy/p2p.rs — Lines 26 to 27

  3. Security Validation — Path: Before anything is forwarded, the path is rigorously checked. It must not contain .. (which indicates a path traversal attempt) and it must start with a /. If it fails these checks, the peer receives a 400 Bad Request immediately, and processing stops. -> See: kinetic-daemon/src/proxy/p2p.rs — Lines 28 to 44

  4. Security Validation — Payload Size: To prevent memory exhaustion attacks, the daemon checks the size of the incoming request body against a hardcoded limit. This limit is defined by LIMITS_PROXY_MAX_BODY_BYTES (typically 5MB). Oversized requests are immediately rejected with a 413 Payload Too Large response. -> See: kinetic-daemon/src/proxy/p2p.rs — Lines 45 to 63

  5. Security Validation — HTTP Method: The HTTP method string is parsed to ensure it represents a valid HTTP verb (GET, POST, etc.). If the method string is garbage data, it returns a 400 Bad Request. -> See: kinetic-daemon/src/proxy/p2p.rs — Lines 66 to 82

  6. Constructing the Local HTTP Request: If the request is deemed safe, a standard HTTP client (reqwest) builds a new request directed at the local backend address (e.g., 127.0.0.1:8080). All headers from the peer are copied over, except the Host header. The Host header is overwritten to match the local binding, ensuring the backend server behaves correctly. -> See: kinetic-daemon/src/proxy/p2p.rs — Lines 84 to 94

  7. Forwarding and Streaming the Backend Response: The constructed local request is sent to the backend. The response body is read chunk by chunk using an asynchronous stream (res.bytes_stream()). Crucially, the payload size limit is enforced again on this response body. This prevents a misconfigured or malicious local server from flooding the daemon with a massive file. -> See: kinetic-daemon/src/proxy/p2p.rs — Lines 104 to 118

  8. Returning the Final Response to the Peer: Finally, the HTTP response status, headers, and buffered body are bundled back into a ProxyResponse struct. This struct is sent back to the original peer via the libp2p ResponseChannel provided by the network layer. -> See: kinetic-daemon/src/proxy/p2p.rs — Lines 140 to 142


Key Pieces

  • handle_incoming_proxy_requests This is the primary driver of the inbound proxy system. It loops endlessly, pulling requests from the MPSC channel and delegating them to spawned worker tasks to ensure high concurrency and non-blocking I/O operations. -> Lives at: kinetic-daemon/src/proxy/p2p.rs — Lines 6 to 14

  • tokio::sync::mpsc::Receiver The asynchronous channel through which kinetic-network delivers requests. It effectively decouples the raw P2P swarm implementation from the higher-level HTTP processing and validation logic found in the daemon. -> Lives at: kinetic-daemon/src/proxy/p2p.rs — Lines 8 to 11

  • Path Traversal Protection Logic A mandatory security boundary that blocks .. sequences in URL paths. This prevents remote peers from tricking the local web server into navigating outside its designated root directory and serving sensitive files from the host operating system. -> Lives at: kinetic-daemon/src/proxy/p2p.rs — Lines 28 to 44

  • reqwest::Client The asynchronous HTTP client used to make the actual local request. It is instantiated once and cloned for each request. Because it uses connection pooling under the hood, cloning it is very efficient and prevents exhausting local sockets. -> Lives at: kinetic-daemon/src/proxy/p2p.rs — Lines 15 and 84

  • Streaming Body Reader (res.bytes_stream()) Uses futures_util::StreamExt to read the local server’s response incrementally. By processing chunks, the daemon can keep a running total of the bytes received and abort early if the payload exceeds the strict 5MB safety limit, protecting daemon memory. -> Lives at: kinetic-daemon/src/proxy/p2p.rs — Lines 104 to 118


How This Connects to the Rest of Kinetic

  • CROSS-CRATE: kinetic-network This module relies on the network crate to handle the complex, low-level libp2p request-response protocol. The NetworkClient and ResponseChannel types are passed in directly from that crate, treating the network as an abstract transport layer.

  • CROSS-CRATE: kinetic-core The maximum payload size (LIMITS_PROXY_MAX_BODY_BYTES) is pulled directly from the core constants crate. This ensures that size limits are unified and consistent across the entire platform, preventing fragmented rules.

  • Internal Integration: This file is the exact mirror image of kinetic-daemon/src/proxy/http.rs. While the HTTP proxy converts local browser requests into P2P messages, this P2P proxy takes those P2P messages and converts them back into HTTP requests for a local server. Together, they form the complete proxy circuit.


Quick Reference

  • Core Function: handle_incoming_proxy_requests
  • Trigger Condition: A peer sends a libp2p ProxyRequest targeted at this node.
  • Security Check 1: Path cannot contain ...
  • Security Check 2: Path must begin with /.
  • Security Check 3: The HTTP Method must parse correctly.
  • Security Check 4: The inbound request body must be <= 5MB.
  • Security Check 5: The outbound response body must be <= 5MB.
  • Forwarding Target: http://<bind_ip>:<local_port><path>.
  • Header Modification: Host header from peer is stripped and rewritten to match the local binding address.

Open Questions / Things to Revisit

  • Streaming vs. Buffering in Memory: Currently, the entire response body from the local server is buffered into a memory vector (Vec::new()) before being sent back over the P2P network. While the 5MB limit prevents OOM crashes, buffering still wastes memory under heavy load. We should investigate streaming the response directly into the libp2p response channel if the protocol allows it.

  • Timeout Handling for reqwest: There does not appear to be an explicit timeout configured on the reqwest client when calling the local backend server. If the local server hangs indefinitely, the spawned tokio::spawn task will live forever, leading to task leaks over time. We should add a strict timeout configuration to the reqwest::ClientBuilder to ensure task completion.

  • Status Code Ambiguity (413 vs 502): When the local server returns a response larger than 5MB, the code currently overrides the status to 502 Bad Gateway (with a comment // Or 413). This semantic distinction might confuse remote clients; returning 502 implies the backend is dead or invalid, while 413 implies a specific size violation occurred. We should standardize on the most accurate HTTP status code to improve client debugging.

Proxy Base, Tunneling, and Security

Crate: kinetic-daemon Stage: 9 Reading time: 30 minutes Depends on: 16_proxy_http.md


What Is This?

This file covers the foundational architecture of the local HTTP/HTTPS proxy server inside the kinetic-daemon crate. Specifically, it documents the proxy entry point (mod.rs), the TLS termination and upgrading mechanism (tunnel.rs), and the network security perimeters designed to prevent abuse (security.rs and proxy_tests.rs).

Inside the Kinetic network architecture, the daemon acts as the critical gateway between the user’s standard web browser (like Google Chrome, Mozilla Firefox, or Safari) and the decentralized .kin peer-to-peer network.

To achieve this seamless integration without requiring users to install custom browsers, the daemon runs a local HTTP proxy server that intercepts outgoing traffic directly from the browser’s network stack.

This documentation explains exactly how that server boots up, how it handles the deeply complex process of decrypting HTTPS traffic via Man-In-The-Middle (MITM) TLS termination, and how it filters out dangerous local network requests to prevent Server-Side Request Forgery (SSRF) attacks.

This is the system that takes raw TCP streams from a web browser, decrypts the TLS layer cryptographically on the fly, and prepares the inner HTTP requests for decentralized P2P routing.

It is the very edge of the Kinetic network on the local machine. It represents the boundary where legacy web protocols (HTTP/TCP) are translated into Kinetic’s modern cryptographic peer-to-peer overlay.


Why Kinetic Needs This

The local proxy is the mandatory bridge between legacy internet protocols and the Kinetic network. Without these modules, the browser would have no way to talk to .kin websites, because standard web browsers do not natively understand P2P networking protocols, Distributed Hash Tables (DHTs), or peer ID routing.

Here is why each individual component is necessary for the daemon to function:

The Server Loop (mod.rs)

We need a continuous listener bound to a local port (for example, 127.0.0.1:8080) that the user’s browser is configured to use as its proxy. This listener must be resilient. It must gracefully handle operating system quirks (like IPv6-only environments where IPv4 bindings silently fail, which is common in modern containerized deployments). It must also be concurrent, capable of handling thousands of parallel connections asynchronously using Tokio’s event loop without blocking the main thread. If this server loop crashes, the user loses all access to the .kin network.

TLS Termination (tunnel.rs)

When a browser navigates to a secure site like https://something.kin, it expects to negotiate a secure TLS connection. If the daemon simply routed those encrypted bytes blindly over the P2P network, the target node wouldn’t know what to do with them, because the target node expects Kinetic’s internal P2P encryption, not a browser’s TLS encryption. More importantly, the daemon itself wouldn’t be able to inspect the HTTP headers to determine the destination peer ID. The routing information is inside the HTTP Host header, which is locked inside the TLS payload. The daemon must decrypt the traffic locally. It does this by acting as a local Man-In-The-Middle (MITM). It dynamically generates a forged TLS certificate for something.kin, performs a full cryptographic TLS handshake with the browser, and extracts the plain HTTP request from the decrypted tunnel.

SSRF Protection (security.rs)

The daemon is a powerful agent. It operates with unrestricted network access privileges on the user’s local machine. If a malicious .kin website, or a compromised P2P peer, could trick the daemon into making requests to the user’s home router (e.g., 192.168.1.1) or local cloud metadata APIs (169.254.169.254), the attacker could steal sensitive data, read AWS/GCP administrative credentials, or pivot into the internal network. This is a classic Server-Side Request Forgery (SSRF) vulnerability. Strict SSRF rules ensure the daemon never routes traffic to private or restricted IP spaces, sandboxing the daemon’s network access to the public internet and the P2P overlay.


How It Works

This system operates in distinct, sequential phases: bootstrapping the server, intercepting the HTTP connection, upgrading to a TCP tunnel, terminating the TLS encryption, and verifying strict security boundaries.

1. Bootstrapping the Proxy Server (mod.rs)

The entry point for this entire subsystem is the start_proxy_server function. Its primary job is to bind a TCP listener to the host network and spawn asynchronous Tokio tasks for every incoming connection.

-> See: crates/kinetic-daemon/src/proxy/mod.rs — Lines 46 to 74

The Binding Loop and IPv6 Fallback: The function attempts to bind a tokio::net::TcpListener to the IP address specified in the Kinetic configuration (usually 127.0.0.1). However, network stacks can be temperamental. In some Docker environments or specific OS configurations (referred to in the code comments as “Case 198: IPv6 Only Network Support”), IPv4 loopback binding outright fails and throws an error.

To mitigate this race condition or stack limitation, the code uses a for loop that tries up to 10 times, with a 200ms delay between attempts. If binding to the configured IPv4 IP fails repeatedly, it immediately attempts to bind to [::1] (the standard IPv6 loopback address) on the same port. This intelligent fallback ensures the daemon can boot successfully even on strict IPv6-only machines without requiring manual user intervention or complex configuration.

Handling Connections with Tokio and Hyper: Once the TcpListener is active, it enters an infinite loop, passively waiting for a client connection via listener.accept().await. When a browser connects, it produces a raw TCP stream. This stream is wrapped in a TokioIo::new(stream) struct. This wrapper is necessary because Hyper 1.x (the HTTP library Kinetic uses) is runtime-agnostic. It requires IO streams to implement specific generic traits, and TokioIo bridges Tokio’s asynchronous IO traits with Hyper’s strict generic expectations.

A new Tokio task is spawned using tokio::task::spawn for every single connection. Inside this asynchronous task, http1::Builder::new().serve_connection(...) takes control. It uses a service_fn to pass the raw HTTP request directly to handle_proxy_request. Crucially, it chains .with_upgrades(). This function call signals to Hyper that this standard HTTP connection might ask to be upgraded to a raw TCP tunnel — which is exactly what happens when the browser sends a CONNECT request for HTTPS proxying.

2. TLS Termination and The Nested Server (tunnel.rs)

When the browser wants to talk HTTPS, it doesn’t immediately start sending TLS bytes. First, it sends an HTTP CONNECT request to the proxy, asking for a direct pipe to the target domain (e.g., CONNECT domain.kin:443 HTTP/1.1). Once the proxy agrees (sending a 200 OK), the connection is formally “upgraded”. It ceases to be an HTTP connection and becomes a raw, bidirectional byte pipe.

-> See: crates/kinetic-daemon/src/proxy/tunnel.rs — Lines 20 to 57

The handle_connect function manages this delicate state transition.

Step A: Minting the Certificate Before the daemon can talk TLS to the browser over this raw pipe, it needs an X.509 certificate for the requested domain. It locks the LeafCertCache and calls get_or_create(&raw_host, &root_ca). This dynamically generates a certificate signed by the daemon’s local Root CA.

Architectural Detail: Notice that the cache.lock().await call happens inside an explicit synchronous block { ... }.

#![allow(unused)]
fn main() {
let server_config = {
    let mut cache = leaf_cache.lock().await;
    cache.get_or_create(&raw_host, &root_ca)?
}; // Lock released here
}

This is a critical Rust concurrency pattern. Although Kinetic uses tokio::sync::Mutex (which technically can be held across an .await point without compiler errors), holding a lock across a slow network operation like a TLS handshake is a massive anti-pattern. If the lock was held during acceptor.accept(...).await, then no other browser connection could generate a certificate until this specific client finished its cryptographic handshake. It would stall the entire proxy server. By encapsulating the lock in a block, the lock is dropped immediately after the certificate is retrieved and before the asynchronous handshake yields.

Step B: The Cryptographic TLS Handshake With the server_config (which contains the newly forged certificate), the code creates a tokio_rustls::TlsAcceptor. It takes the raw upgraded TCP stream, wraps it again in TokioIo, and awaits acceptor.accept(...). This performs the actual cryptographic handshake with the browser, negotiating cipher suites, verifying the Root CA, and establishing the encrypted tunnel.

Step C: The Nested Hyper Server Once acceptor.accept resolves successfully, we have a tls_stream. This stream contains the decrypted, plaintext HTTP traffic. But how do we parse it? We don’t write a custom HTTP parser, which would be error-prone and dangerous. Instead, we literally spin up a second Hyper HTTP server inside the connection!

#![allow(unused)]
fn main() {
http1::Builder::new()
    .serve_connection(TokioIo::new(tls_stream), service)
    .await?;
}

This nested HTTP server uses its own inner service_fn to take the decrypted Request<Incoming> and pass it to forward_to_backend_direct. If the backend routing fails, it guarantees a response by returning a 502 Bad Gateway. Notice the use of std::convert::Infallible in the return type of the closure. This tells Rust’s type system that this specific closure will never panic or return an unhandled error; it will always successfully yield an HTTP Response, even if that response represents a hard failure.

3. Network Security and SSRF Protection (security.rs & proxy_tests.rs)

Because the daemon executes network requests on behalf of the user, it must rigorously verify that the destination IP address is safe to query.

-> See: crates/kinetic-daemon/src/proxy/security.rs — Lines 3 to 5

The is_ssrf_risk function simply delegates to kinetic_core::net::is_ssrf_safe and negates the result. This gatekeeper ensures that traffic is never routed to restricted internal network ranges.

Property-Based Testing with proptest! To guarantee the safety of the SSRF filter, security.rs utilizes property-based testing via the proptest! macro. Unlike a standard unit test that checks a single hardcoded IP (e.g., 127.0.0.1), a property test generates thousands of random inputs that conform to specific rules.

For example, to test that all loopback addresses are rejected, the test defines the generation ranges:

#![allow(unused)]
fn main() {
a in 127u8..=127, b in 0u8..=255, c in 0u8..=255, d in 0u8..=255
}

The fuzzer continuously generates random IP addresses matching the 127.*.*.* pattern and asserts that is_ssrf_risk returns true every single time. It repeats this logic for the 10.*.*.* internal range, and similarly ensures that public IPs (e.g., ranges starting with 1 to 9) are always allowed. This provides a much higher degree of confidence than standard unit tests, effectively fuzzing the security boundaries.

Static Boundary Testing In proxy_tests.rs, specific edge cases and boundaries are tested explicitly. -> See: crates/kinetic-daemon/src/proxy/proxy_tests.rs — Lines 21 to 30

This file verifies that Carrier-Grade NAT (CGNAT) addresses (100.64.0.1) are blocked. Why is this important? Because cloud providers like AWS and GCP often use CGNAT spaces to host sensitive instance metadata APIs. If a cloud-hosted daemon was subjected to an SSRF attack, the attacker could extract AWS IAM credentials by querying the metadata API. It also blocks Link-Local addresses (169.254.169.254 and fe80::1), ensuring complete coverage against standard local network attacks.


Key Pieces

  • start_proxy_server (in mod.rs) The primary boot sequence for the local proxy. It handles IP binding, IPv6 fallback loops, and spawns the primary asynchronous connection handlers using Tokio. It is the backbone of the local interceptor.

  • ProxyError (in mod.rs) A centralized thiserror enum that standardizes failure states across the proxy module. It covers DNS failures (NameNotFound), Hyper library errors, CA generation failures, and generic IO faults, providing a clean abstraction for error propagation.

  • handle_connect (in tunnel.rs) The core Man-In-The-Middle logic. It takes an upgraded TCP stream from a CONNECT request, mints a TLS certificate on the fly using the leaf cache, completes the cryptographic handshake, and spawns a nested Hyper server to handle the decrypted internal traffic.

  • is_ssrf_risk (in security.rs) A boolean check that validates IP addresses against known private, local, and restricted subnets to prevent malicious network pivoting and unauthorized internal data access.

  • proptest! fuzzers (in security.rs) Property-based tests that exhaustively generate thousands of IP combinations to prove the SSRF filters have no logical gaps or edge-case failures.


How This Connects to the Rest of Kinetic

  • CROSS-CRATE: kinetic_core::config::KineticConfig — Determines the IP and port the proxy server attempts to bind to during initialization.

  • CROSS-CRATE: kinetic_core::net::is_ssrf_safe — Provides the low-level logic for IP address categorization used by security.rs.

  • CROSS-CRATE: kinetic_network::NetworkClient — Passed through the proxy and tunneling layers so that decrypted HTTP requests can eventually be routed over the P2P network to the correct peer.

  • Same-Crate Reference: LeafCertCache and RootCa (from the ca module) These are used directly by tunnel.rs to dynamically forge the TLS certificates required for HTTPS interception without triggering browser security warnings.


Quick Reference

  • IPv6 Fallback: If binding to the configured IPv4 address fails during boot, the proxy loops and automatically falls back to [::1] (IPv6 loopback).

  • TokioIo Wrapper: Hyper 1.x requires IO streams to implement specific traits. TokioIo wraps standard Tokio streams to make them compatible with Hyper’s engine.

  • Nested Servers: HTTPS interception works by upgrading the HTTP connection to a raw TCP stream, doing a TLS handshake, and running a second Hyper HTTP server over the decrypted bytes.

  • Mutex Lock Scope: When fetching certificates in async contexts, the Mutex lock is constrained to a synchronous block { ... } so it is dropped before the .await on the TLS handshake, preventing deadlocks.

  • SSRF Ranges Blocked: 127.0.0.0/8, 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, 100.64.0.0/10 (CGNAT/Metadata), and 169.254.0.0/16 (Link-Local).


Open Questions / Things to Revisit

  • Performance of Nested Servers: Spawning a new, second Hyper serve_connection for every single HTTPS request is functionally brilliant and separates concerns, but it might incur minor memory and CPU overhead. Profiling under high load (thousands of concurrent .kin requests) would determine if this architecture is too heavy for low-end mobile devices.

  • HTTP/2 & HTTP/3 Support: The nested server in tunnel.rs uses http1::Builder. As the Kinetic network grows, upgrading the decrypted inner streams to multiplexed HTTP/2 might yield significant performance benefits for complex .kin websites that load many assets simultaneously.

  • Binding Retry Loop: The 10-iteration loop for port binding (with 200ms sleeps) in mod.rs is functional but somewhat arbitrary. A more robust state machine or explicit network interface query might be a cleaner architectural choice in the future, rather than relying on a blind retry mechanism.

4. Deep Dive: Why Mutexes and Awaits Don’t Mix

You might look at the lock scoping in tunnel.rs and wonder why it was written with such explicit blocks. This is a fundamental Rust concurrency concept.

In a normal synchronous program, you lock a mutex, do your work, and unlock it. In Tokio, tasks are multiplexed over a pool of operating system threads. When you call .await on a future (like a network request or a TLS handshake), Tokio might decide to park your current task and run something else on that thread. Later, when the network request finishes, Tokio might wake your task up on a different thread.

The tokio::sync::MutexGuard technically can be held across an .await point, unlike std::sync::MutexGuard. However, just because you can doesn’t mean you should.

If you write code like this:

#![allow(unused)]
fn main() {
let mut cache = leaf_cache.lock().await; let cert = cache.get_or_create(&raw_host, &root_ca)?; // ... keeping the lock alive ... let tls_stream = acceptor.accept(io).await?; // AWAIT POINT
}

The proxy will compile, but it will suffer performance degradation. The .await on the TLS handshake can take hundreds of milliseconds if the network is slow. If the lock on the global LeafCertCache is held during this time, every single other incoming HTTPS connection will be blocked waiting for the lock to be released.

To fix this, the developer must ensure the lock is dropped before the .await. This is achieved by wrapping the lock acquisition in its own lexical scope:

#![allow(unused)]
fn main() {
let server_config = {
    let mut cache = leaf_cache.lock().await;
    cache.get_or_create(&raw_host, &root_ca)?
}; // Lock is guaranteed to be dropped right here. // Now it's safe to await. let tls_stream = acceptor.accept(io).await?;
}

This pattern is utilized in tunnel.rs to allow the concurrent proxy to mint certificates without blocking the entire async executor pool or angering the borrow checker.

Background Services: Heartbeats and Gossip

Crate: kinetic-daemon Stage: 9 Reading time: 20 minutes Depends on: docs/learn/network/01_overview.md, docs/learn/core/01_overview.md


What Is This?

In the kinetic-daemon crate, the src/services/ module contains the continuous background loops that keep a node alive, synchronized, and active within the broader Kinetic network. While the REST API handles direct, synchronous user commands (like “register this name”) and the P2P swarm handles low-level socket connections, these background services act as the autonomous nervous system of the daemon. They run endlessly, driven by timers and event streams, independent of user interaction.

Specifically, we are looking at two critical architectural loops:

  1. The Heartbeat & Drand Loop (heartbeat.rs): A timed, active loop that serves dual purposes. First, it constantly fetches the latest network time (Drand kyns). Second, it uses that time to continually broadcast cryptographic proofs of ownership to the Kademlia DHT, ensuring the names registered by this node do not expire.
  2. The Gossip Processor (gossip.rs): A reactive, listening loop that waits for network-wide pub-sub (publish-subscribe) broadcasts. It monitors channels for critical governance votes and new Drand kyns, validates them cryptographically, seamlessly applies those updates to the daemon’s local memory and disk state.

The mod.rs file simply groups these loops (along with network.rs, which handles mining loops) into a unified services module. These services are what elevate the daemon from a simple passive database into a participating citizen of the decentralized network.


Why Kinetic Needs This

Kinetic is fundamentally designed as a decentralized, living network. It rejects the model of a central database where you can register a name and walk away forever. To prevent the network from accumulating dead data, hijacked names, or stale state, Kinetic requires continuous action from its participants.

Why the Heartbeat Loop is Constitutionally Mandatory: When you register a name in the Kinetic ecosystem, you are not buying it; you are leasing it. To maintain that lease and prove you are a legitimate, active participant, your daemon must repeatedly announce to the Kademlia DHT that you are still online and still in control of the private key for that specific name. You achieve this by signing the current Drand clock tick (the “kyn”). If the daemon did not have an automatic background loop doing this every 30 seconds, your names would rapidly expire, disappearing from the network, and someone else could immediately claim them. Furthermore, the heartbeat loop is the engine responsible for fetching those Drand kyns in the first place. Without Drand, the daemon has no concept of absolute network time. Without time, it cannot sign valid heartbeats, nor can it verify the heartbeats or proofs of other peers. The entire cryptographic validation pipeline would stall.

Why the Gossip Processor is Constitutionally Mandatory: Kinetic uses a pub-sub (publish-subscribe) model for network-wide announcements, utilizing the libp2p::gossipsub protocol. This is different from the Kademlia DHT. The DHT is for point-to-point data retrieval (like looking up an IP address). Gossipsub is for shouting critical information to the entire network simultaneously. When the governance council successfully completes a vote to grant a premium name, or when a peer discovers a new Drand kyn, they shout this payload over the gossip network. If your daemon lacked the gossip.rs background loop, you would be deaf to these shouts. Your local node would never realize a premium name was granted, leading it to reject valid transactions simply because its local state was out of date. The gossip processor is the mechanism that ensures your node’s localized worldview stays synchronized with the global network consensus.


How It Works

Because these are two distinct sub-systems, we will break down their mechanics and code paths separately.

Understanding the Tokio Channels

The background services do not operate in a vacuum; they must communicate their findings to the rest of the daemon. They do this using two very specific types of Tokio channels.

1. The Watch Channel (tokio::sync::watch::Sender<u64>) Used for broadcasting Drand kyns. A watch channel is a single-producer, multi-consumer channel where only the most recent value is kept. If the heartbeat loop discovers kyn 100, then quickly discovers kyn 101 before anyone reads 100, kyn 100 is silently dropped and replaced by 101. This is perfect for timekeeping. When the REST API or the mining loop needs to know the time, they only care about the absolute latest time. They do not want a backlog of old times. Both heartbeat.rs and gossip.rs hold a Sender to this channel, pushing new kyns into it, while the rest of the daemon holds a Receiver that can instantly read the most current value without waiting. If a receiver of a watch channel is too slow, it never receives an error—it just skips to the newest value next time it looks.

2. The Broadcast Channel (tokio::sync::broadcast::Receiver) Used for Gossipsub messages. A broadcast channel is a multi-producer, multi-consumer channel where every consumer sees every message. If 10 governance votes arrive, the channel stores all 10 in a queue. If the gossip.rs loop falls behind (known as “lagging”), it might miss messages. In this case, Tokio intervenes to prevent memory exhaustion by dropping the oldest unread messages and throwing a RecvError::Lagged(_) error to warn the receiver that it missed data. Unlike the watch channel, we cannot simply drop older governance votes. Every vote must be processed in sequence. The broadcast channel ensures that as long as the processor doesn’t fall too far behind, every network shout is systematically ingested.

Part 1: The Heartbeat and Drand Loop (heartbeat.rs)

When the daemon initializes, it spawns start_heartbeat_loop using tokio::spawn. This places the loop onto the Tokio async runtime, allowing it to execute concurrently with the HTTP server and the P2P swarm.

Step 1: The Three-Second Tick and State Tracking The loop relies on an atomic integer to track time across threads safely. It initializes an Arc<AtomicU64> called last_known_live_kyn. -> See: kinetic-daemon/src/services/heartbeat.rs — Lines 20 to 21. It then enters an infinite loop {}. The pace of this loop is controlled by a Tokio interval set to wake up every 3 seconds (tokio::time::interval(Duration::from_secs(3))). Every time interval.tick().await completes, the loop evaluates the state of the network time.

Step 2: Securing the Clock (The Drand Fallback Mechanism) Kinetic’s reliance on Drand means time synchronization is a critical vulnerability. The user can configure the daemon to run in p2p_only mode, which instructs the daemon to avoid centralized HTTP requests and only acquire Drand kyns by listening to other peers via the gossip network. However, heartbeat.rs contains a crucial, hardcoded safety valve. It calculates what the current Drand kyn should be based on your computer’s local wall-clock time (expected_kyn). If the latest kyn in your local cache is more than 5 kyns behind the expected time (a drift of approximately 15 seconds), it triggers a fallback warning. It temporarily overrides the p2p_only restriction and forces an HTTP fetch from a Drand relay. -> See: kinetic-daemon/src/services/heartbeat.rs — Lines 30 to 49. This logic prevents a failure scenario where your node gets disconnected from the gossip mesh, loses track of time, and starts having all its names expire because it cannot sign current heartbeats.

Step 3: Broadcasting Time to the Swarm If the daemon is permitted to fetch (either because p2p_only is false, or the fallback triggered), it calls hb_drand.fetch_latest().await. If successful, and the kyn is new, it immediately broadcasts this kyn payload to the rest of the network over the GOSSIP_TOPIC_DRAND topic. This altruistic behavior is how the P2P mesh helps keep firewalled or p2p_only nodes synchronized. It also updates the rest of the local daemon using the watch channel (drand_kyn_tx_hb). -> See: kinetic-daemon/src/services/heartbeat.rs — Lines 51 to 72.

Step 4: The 30-Second Name Renewal Cycle While the loop ticks every 3 seconds to check the Drand clock, it only performs the heavy lifting of name renewals every 30 seconds. It tracks this using tokio::time::Instant. Before doing any renewals, it checks !kyn.is_usable_for_heartbeat(current_live). If the kyn is technically valid but functionally useless (e.g., it’s a fallback kyn from hours ago), the loop simply skips this cycle via continue. It will not attempt to renew names with an invalid or outdated timestamp, preventing accidental spam. -> See: kinetic-daemon/src/services/heartbeat.rs — Line 88. When the 30-second mark is hit, it queries the local Sled database for the list of names you own under the key DB_PREFIX_OWNED_NAMES. For each name retrieved, it constructs a fresh Heartbeat struct containing the name and the absolute latest Drand kyn. -> See: kinetic-daemon/src/services/heartbeat.rs — Lines 92 to 105.

Step 5: Offloading Cryptography (Avoiding Executor Blocking) To sign the heartbeat, the daemon uses the ML-DSA post-quantum signature scheme. Cryptographic signing is a CPU-intensive, heavy operation. If we ran this math directly inside the async loop, it would “block the executor”—meaning the Tokio thread would freeze, unable to handle incoming network requests until the math finished. To prevent this, the daemon uses tokio::task::spawn_blocking. This macro takes the CPU-heavy closure and moves it to a dedicated background thread pool specifically designed for synchronous blocking work. Once the thread pool returns the generated signature, it is packed into the heartbeat payload. The loop then spawns a tiny, lightweight async task to call hb_network.publish_heartbeat, sending the signed heartbeat directly into the Kademlia DHT for peers to discover. -> See: kinetic-daemon/src/services/heartbeat.rs — Lines 107 to 128.

Part 2: The Gossip Processor (gossip.rs)

Unlike the heartbeat loop, which operates on a rigid timer, the gossip processor is purely reactive. It is a listener.

Step 1: Message Ingestion from the Void The loop blocks on gossip_rx.recv().await. When a message arrives from the network swarm, it is unpacked into four components: the topic, the raw byte payload, a unique message_id, and the propagation_source (the PeerId of the node that handed us the message). -> See: kinetic-daemon/src/services/gossip.rs — Lines 19 to 23. If the channel buffer fills up because the loop is processing too slowly, it receives a Lagged error and simply continues to the next available message.

Step 2: Processing Governance Edicts If the incoming topic matches GOSSIP_TOPIC_GOVERNANCE, the daemon recognizes this as a high-stakes consensus message. It attempts to deserialize the payload into a SignedGovernanceMessage. It then locks the GLOBAL_GOVERNANCE_STATE—a system-wide Mutex holding the current network council votes—and feeds the message into the core process_governance_message function. -> See: kinetic-daemon/src/services/gossip.rs — Lines 24 to 38.

Step 3: Direct Database Injection for Premium Names If the governance message is cryptographically valid and results in a state change—specifically, GovernanceEffect::PremiumNameGranted—the background loop takes immediate, direct action. It manually crafts a NameRecord::Premium struct. It sets the granted_at time to the current UNIX epoch, leaves the signature arrays empty (since governance dictates the validity, not a user signature), formats the database key to match DB_PREFIX_REVEAL + name, and shoves the serialized bytes directly into the local Sled storage engine using storage.put(). -> See: kinetic-daemon/src/services/gossip.rs — Lines 52 to 74. This is a critical architectural shortcut. It bypasses the standard network registration flow because premium names are granted by network-wide governance consensus, not by individual user proof-of-work. By writing the record straight to the Sled database, the premium name becomes instantaneously usable and recognized by the local node. This design decision highlights a core philosophical point: governance is absolute. When the network votes on a state change, the node does not question it or route it through pending transaction queues. It treats it as an immutable fact and writes it to disk. Likewise, if the effect is GovernanceEffect::PremiumNameRevoked, it immediately issues a storage.delete() command to rip the name out of local memory.

Step 4: Processing Drand Gossip If the topic is GOSSIP_TOPIC_DRAND, the loop deserializes the payload into a RawKyn. Before trusting it, it must verify the BLS signature on the Drand kyn. Similar to the heartbeat loop’s signing process, it uses tokio::task::spawn_blocking to push the CPU-heavy verification math onto a background thread pool. If the verification passes, and the kyn is greater than the one currently held in the local cache, it saves the new kyn and alerts the rest of the daemon via the drand_kyn_tx_gossip channel. -> See: kinetic-daemon/src/services/gossip.rs — Lines 102 to 121.

Step 5: Network Feedback and Peer Scoring At the absolute end of processing any message (whether it was Governance or Drand), the loop makes a vital call to network_client.report_gossip_validation(...). This is a strict requirement imposed by the underlying libp2p networking stack. Libp2p maintains a complex trust-scoring system for all connected peers. If a peer sends us invalid governance votes, malformed JSON, or fake Drand kyns, we must report is_valid = false. Libp2p will then lower that peer’s reputation score, eventually disconnecting and banning them if they continue to spam. This feedback loop is how the network organically protects itself from malicious actors. -> See: kinetic-daemon/src/services/gossip.rs — Lines 101 and 122.


Key Pieces

start_heartbeat_loop

  • Location: kinetic-daemon/src/services/heartbeat.rs — Lines 11 to 134
  • What it does: The primary clock driver and survival mechanism. It ensures Drand is fresh and continuously renews the user’s names on the DHT.
  • Why it matters: If this function crashed or stalled, all names owned by the daemon would expire and be swept from the network within minutes, resulting in total data loss for the user.

The last_known_live_kyn Atomic Tracker

  • Location: kinetic-daemon/src/services/heartbeat.rs — Line 20
  • What it does: Uses Arc<AtomicU64> to track the highest valid Drand kyn seen so far.
  • Why it matters: An atomic integer allows thread-safe, lock-free read and write access. This is significantly faster and eliminates the risk of deadlocks that would come from wrapping a simple number in a Mutex. By using Ordering::Relaxed, it checks the time with near-zero overhead. When an updated kyn arrives, it uses lklr.store(kyn.kyn, Ordering::Relaxed) to overwrite the old value. This allows the inner asynchronous spawned tasks inside the loop to reference the exact same state without needing to pass heavy locks back and forth across thread boundaries. It guarantees the loop always references a monotonically increasing time source.

The P2P Drand Fallback Threshold

  • Location: kinetic-daemon/src/services/heartbeat.rs — Lines 39 to 45
  • What it does: If the daemon is in p2p_only mode but observes that its internal clock has fallen 5 kyns (about 15 seconds) behind expected real-world time, it panics and falls back to HTTP polling.
  • Why it matters: Gossip networks can fragment, causing partitions where nodes stop hearing updates. This safety hatch ensures the node doesn’t become paralyzed if it loses its connection to the Drand gossip mesh.

start_gossip_processor

  • Location: kinetic-daemon/src/services/gossip.rs — Lines 4 to 125
  • What it does: The ever-listening ear for network-wide broadcasts. It acts as a filter, applying valid state changes and discarding junk.
  • Why it matters: This is the exclusive pathway for a local daemon to stay up to date with global governance decisions. Without it, the node operates in a permanent blind spot.

tokio::task::spawn_blocking Integration

  • Location: kinetic-daemon/src/services/heartbeat.rs (Line 112) & gossip.rs (Line 108)
  • What it does: Bridges the asynchronous Tokio runtime with synchronous, CPU-bound cryptographic operations.
  • Why it matters: If you attempt to verify a BLS signature directly inside an async task, you block the executor. No other async tasks (like handling incoming HTTP API requests) can run until the math finishes. spawn_blocking solves this by ejecting the work to a separate thread pool.

How This Connects to the Rest of Kinetic

  • CROSS-CRATE: kinetic-core — Both loops are dependent on the core cryptographic and state definitions found in Stage 7. Specifically, they rely on kinetic_core::drand::DrandClient, kinetic_core::types::Heartbeat, and the GLOBAL_GOVERNANCE_STATE.
  • CROSS-CRATE: kinetic-network — The NetworkClient (covered in Stage 8) is injected into these loops. The heartbeat loop relies on it to execute .publish_heartbeat(), and the gossip loop relies on it to execute .report_gossip_validation() and .broadcast_gossip().
  • Storage Subsystem: These background tasks are intimately, perhaps dangerously, tied to the local Sled database. They must read DB_PREFIX_OWNED_NAMES to know what to renew, and the gossip loop directly writes to DB_PREFIX_REVEAL when governance dictates a state change.

Quick Reference

  • Heartbeat Tick Frequency: Wakes up every 3 seconds to ensure Drand is current.
  • DHT Renewal Interval: Actually constructs and publishes name renewals to the DHT every 30 seconds.
  • P2P Fallback Trigger: 5 kyns (approximately 15 seconds) behind expected wall-clock time.
  • Gossip Topics Handled: Listens to kinetic_drand_v1 and kinetic_gov_v1.
  • Watch Channel (watch::Sender): Keeps only the newest value, dropping older ones. Used for Drand time updates.
  • Broadcast Channel (broadcast::Receiver): Keeps all values in a queue. Used for ingesting network chatter.
  • Crypto Threading: Both loops mandate the use of spawn_blocking to ensure ML-DSA signing and BLS verification do not freeze the background runtime.
  • Libp2p Requirement: All processed gossip messages MUST be validated and reported back to the network client to maintain peer scoring and prevent spam.

Open Questions / Things to Revisit

  • Direct Storage Coupling: In gossip.rs, the loop directly formats a raw database key string (DB_PREFIX_REVEAL + name) and shoves it straight into the Sled storage engine. This works, but it breaks encapsulation. If the storage layout or prefix architecture ever changes in kinetic-storage, this background gossip service will break silently. This injection logic ideally belongs inside an abstracted method in the storage layer, not the daemon layer.
  • Hardcoded Fallback Thundering Herd: The 5-kyn threshold in heartbeat.rs is hardcoded across all nodes. In a rare scenario where the Drand network itself goes offline or stutters globally, all daemons on the network might simultaneously decide they are “behind” and launch massive DDoS-style HTTP requests to the Drand relay servers at the exact same second. Adding jitter or a randomized backoff here would be safer.
  • Heartbeat Congestion and Task Explosion: If a user registers and owns 10,000 names, the loop will attempt to sign and publish 10,000 individual heartbeats every 30 seconds. The loop currently spawns a brand new Tokio task for each individual publish operation. This could easily lead to a massive task explosion, exhausting the node’s resources and flooding the Kademlia DHT client. Batching heartbeats into a single payload or pacing the network requests might become necessary as node portfolios grow.

Miscellaneous API Endpoints & Daemon Root

Crate: kinetic-daemon Stage: 9 Reading time: 20 minutes Depends on: 18_api_server.md, 17_auth_middleware.md


What Is This?

This document covers the kinetic-daemon library root structure. It also covers a collection of miscellaneous but critical HTTP API endpoints. While earlier documents covered the core API lifecycle (routing, middleware, and domain resolution), the daemon API is broader. It also exposes several administrative endpoints. It exposes deep integration endpoints. These endpoints include:

  • Configuration management
  • Node health observation
  • Cross-network bridging configuration
  • Network time delivery
  • Real-time P2P gossip streaming

Specifically, we are examining four distinct API modules and the library root:

1. The Config Module (config.rs) This module contains endpoints for managing the node’s configuration state. It allows checking operational health. It allows retrieving the active network status. It allows querying the names owned by the node’s identity.

2. The Gossip Module (gossip.rs) This module contains endpoints for broadcasting arbitrary JSON payloads to the network. It provides a mechanism to subscribe to live topics. It uses Server-Sent Events (SSE) for this streaming.

3. The Atlas Module (atlas.rs) This is a webhook endpoint. It is designed specifically for the kinetic-atlas bridge. It synchronizes traditional ICANN TLDs and Web3 TLDs dynamically.

4. The Time Module (time.rs) This is an endpoint that serves verified Kinetic network time. The time is derived cryptographically from Drand. It avoids relying on local NTP clocks.

5. The Library Root (lib.rs) This is the structural entry point of the crate. It binds the HTTP API together. It binds the local proxy server together. It binds CA management and background services together into a single cohesive library.

These endpoints form the integration layer of the daemon. They are what allow external tools (like the Kinetic CLI) to function. They allow local frontend applications to interact with the node. They allow external bridging services to integrate with the underlying Kademlia network. They allow querying local storage databases. All of this is possible without having to speak raw P2P protocols or manage raw socket connections.


Why Kinetic Needs This

A decentralized node cannot function purely as an isolated black box. It requires observation. It requires dynamic configuration. It requires extensive integration points for external tooling. Each of these miscellaneous modules solves a specific operational problem for the Kinetic network architecture.

The Necessity of Configuration and Observation (config.rs) Saif, as an operator, you need visibility into your node. You need to know if your node is actually connected to the DHT. You need to know how many peers it currently sees. You need to know whether its underlying storage engine is corrupted or responding normally. The health and network status endpoints provide this exact telemetry. Furthermore, the Kinetic CLI needs a way to query what names the node owns. It needs to dynamically change its network mode. For example, switching from a client to a bootstrap node. Providing HTTP endpoints for these actions ensures that the CLI does not need to directly mutate the YAML configuration files. It ensures the CLI does not need to lock the sled databases. Directly touching files or databases from the CLI would cause contention and race conditions. The API acts as the safe, synchronized gatekeeper for node state.

The Necessity of Real-time Web Integration (gossip.rs) Kinetic relies on Gossipsub for real-time publish/subscribe message propagation. However, standard web browsers cannot natively speak libp2p Gossipsub. Local frontend applications cannot easily compile libp2p stacks. Basic external scripts cannot speak the complex libp2p Gossipsub protocol over TCP/QUIC. To bridge this significant gap, the daemon exposes an HTTP publish endpoint. It also exposes an SSE (Server-Sent Events) subscribe endpoint. This allows a standard, unmodified web application to stream live Kinetic gossip events. It does this using nothing but a standard unidirectional HTTP connection. If this abstraction did not exist, building live, reactive applications on Kinetic would be virtually impossible. You would have to compile massive WASM libp2p stacks directly into the browser.

The Necessity of The ICANN Bridge Configuration (atlas.rs) Kinetic intercepts local DNS traffic to resolve .kin names. But it should only intercept .kin and other registered Web3 TLDs. It needs to dynamically know which TLDs it should handle natively. It needs to know which TLDs it should blindly forward to standard ICANN DNS servers (like 1.1.1.1). The kinetic-atlas bridge is a separate, specialized service. It monitors a smart contract (or remote registry) of supported foreign TLDs. It uses this webhook endpoint to push the latest list of TLDs into the daemon’s runtime memory. Without this dynamic synchronization, the daemon would not know how to dynamically adapt its DNS routing. When new decentralized TLDs are registered on the network, the node must adapt. Failing to adapt would lead to broken DNS resolution for users.

The Necessity of Cryptographically Verified Time (time.rs) Decentralized, distributed systems cannot trust local system clocks. If a user’s local clock is drastically wrong (due to a dead CMOS battery or a malicious NTP spoofing attack), everything breaks. Cryptographic operations like verifying signatures will fail. Validating certificates will fail. Checking block freshness will fail. The time.rs endpoint queries the daemon’s synchronized Drand state. It calculates the exact KineticTime. This gives local applications a trusted, cryptographically verifiable source of time. This time does not rely on NTP. It ensures that the local ecosystem remains synchronized with the global network consensus.

The Necessity of A Unified Library Architecture (lib.rs) While the Kinetic daemon is distributed as a massive, long-running binary executable, structurally it is built as a modular library. By exposing api, ca, proxy, and services as public modules in lib.rs, Kinetic is flexible. It allows other Rust crates (like integration test harnesses) to embed the daemon programmatically. It allows simulators to spawn multiple daemons in a single test process. It allows custom GUI wrappers to bundle the daemon. This prevents the daemon from being locked into a standalone executable format.


How It Works

The Library Structure and Modularity

If you examine kinetic-daemon/src/lib.rs, you will notice it simply exposes four top-level modules. These are api, ca, proxy, and services.

-> See: crates/kinetic-daemon/src/lib.rs — Lines 5 to 13

This intentional modularity ensures that the HTTP server (api) does not tightly couple with the TLS certificate generation logic (ca). It does not tightly couple with the P2P networking loops (services). Instead of passing direct references to each other, they communicate safely. They communicate by passing shared Arc state structures (like ApiState). They communicate using message-passing channels across module boundaries. This separation of concerns is critical for preventing spaghetti code in a concurrent environment.

Managing Configuration and Health Checks

The config.rs module handles several distinct operational queries and mutations:

Active Health Checks When a user or load balancer queries the /health endpoint, the daemon actively tests its internal components. It does not just blindly return a static “OK”. It asks the network channel for its status. It attempts to read a known key (DB_PREFIX_LAST_DRAND) from the sled storage engine. If either of these operations times out or returns an error, it reports that specific subsystem as unresponsive in the JSON payload.

-> See: crates/kinetic-daemon/src/api/config.rs — Lines 88 to 111

Retrieving Owned Names The handle_owned_names endpoint reads the DB_PREFIX_OWNED_NAMES key directly from storage. Because storage interactions can fail, it must be robust. It can yield corrupted byte arrays. Therefore, it uses serde_json::from_slice. Crucially, if the bytes are corrupted, it logs a KIN-IMPL-003 tracing error. This alerts you to the corruption. It then gracefully falls back to returning an empty list rather than panicking the entire API server.

-> See: crates/kinetic-daemon/src/api/config.rs — Lines 33 to 43

Configuration Mutation When a CLI user updates the configuration, the handle_set_config endpoint activates. It loads the current KineticConfig from the disk. It mutates the network mode field. It saves it back to the disk. Notice that it does not attempt to restart the node process itself. It simply returns a JSON message instructing the user to restart the daemon. This maintains absolute simplicity in the API layer and avoids dangerous self-termination routines.

The Server-Sent Events (SSE) Gossip Stream

The handle_gossip_subscribe function is arguably one of the most complex HTTP handlers in the entire API. It utilizes Server-Sent Events (SSE) combined with an asynchronous stream generator macro. When a client hits this endpoint, the Axum framework keeps the HTTP connection open indefinitely. This overrides the standard request-response lifecycle.

Inside the handler, the daemon subscribes to its internal tokio::sync::broadcast channel. This is done via state.gossip_tx.subscribe(). This channel acts as the firehose receiving all Gossipsub messages from the Kademlia network.

-> See: crates/kinetic-daemon/src/api/gossip.rs — Lines 20 to 42

The handler uses the async_stream::stream! macro to create a Rust generator. It enters an infinite loop. It asynchronously awaits messages from the broadcast channel. If the message’s topic matches the requested topic in the URL path, it processes it. It attempts to parse the payload as a UTF-8 string. It then yields it as an SSE Event.

Critically, it handles the RecvError::Lagged(skipped) error variant. Broadcast channels in tokio are bounded. They will forcefully drop messages if the receiver is too slow to process them. If a web client has a slow connection, the daemon will drop their missed messages. It logs a warning indicating how many messages were skipped. It then gracefully continues streaming new messages. This mechanism prevents a single slow HTTP client from causing the entire daemon to run out of memory.

The Atlas Webhook Synchronization

The handle_atlas_sync endpoint receives a JSON AtlasSyncPayload. This contains a raw list of TLD strings from the Atlas bridge. It iterates over these TLDs and sanitizes them. It trims whitespace. It conditionally removes leading dots. It normalizes them to lowercase. It then inserts them into a fresh, allocated HashSet.

-> See: crates/kinetic-daemon/src/api/atlas.rs — Lines 28 to 40

Once this clean set is fully built in memory, the handler attempts to acquire a write lock on state.atlas_tlds. This is an Arc<RwLock<HashSet<String>>>. It replaces the entire old set with the newly generated set in one atomic assignment operation. This pattern is important. The proxy server is constantly acquiring read locks on atlas_tlds for every single DNS request it intercepts. By building the new set outside of the lock, we optimize performance. By only acquiring the write lock to perform the instantaneous swap, the lock is held for mere nanoseconds. This ensures that DNS resolution is never blocked during an Atlas synchronization event.

Cryptographic Time Delivery

The handle_get_time endpoint initializes a fresh DrandClient instance. It passes in a clone of the daemon’s storage layer. It fetches the latest verified drand round (referred to as a kyn). It then utilizes the KineticTime::from_kyn function. It passes in the globally defined KINETIC_GENESIS_DRAND_KYN constant. This calculates the exact network time mathematically.

-> See: crates/kinetic-daemon/src/api/time.rs — Lines 14 to 28

If the node is offline and cannot fetch a kyn from the network or storage, it must fail safely. The daemon chooses to return an INTERNAL_SERVER_ERROR (500). It does this rather than falling back to an unverified mathematical time estimation. As noted in the inline comments, returning a hard error ensures safety. It ensures that consumers (like the CLI or local apps) are aware that the node is unsynchronized. It ensures they know it cannot provide trustworthy time data.


Key Pieces

handle_config / handle_set_config

What it does:

  • Retrieves or mutates the node’s persistent KineticConfig.
  • Specifically targets properties like the network mode.

Where it lives:

  • kinetic-daemon/src/api/config.rs

Why it matters:

  • It provides the CLI with the critical ability to observe the daemon’s underlying configuration state.
  • It provides the ability to modify this state.
  • It does this without needing raw file system access.
  • It enforces API-level JWT access controls.
  • It prevents data races on the YAML files.

handle_health

What it does:

  • Actively verifies the Kademlia network loop is responsive.
  • Actively verifies the sled storage engine is responsive.
  • Returns a comprehensive JSON health report.

Where it lives:

  • kinetic-daemon/src/api/config.rs

Why it matters:

  • This serves as the primary liveness probe.
  • If the node runs in a Docker container, this dictates restart policies.
  • If the node runs in a Kubernetes pod, this endpoint dictates whether the orchestrator should forcefully restart the node process.

handle_gossip_subscribe

What it does:

  • Upgrades an HTTP connection into an SSE stream.
  • Continuously yields live Gossipsub messages for a specific topic.
  • Handles slow consumers gracefully by dropping lagged messages.

Where it lives:

  • kinetic-daemon/src/api/gossip.rs

Why it matters:

  • It acts as the vital bridge between complex P2P network chatter and standard web frontends.
  • It relies on tokio_stream and yield generators.
  • It pushes events continuously over a single, multiplexed HTTP connection.

AtlasSyncPayload and handle_atlas_sync

What it does:

  • Defines the JSON schema for incoming TLD updates.
  • Processes the webhook payload pushed from the external atlas bridge service.
  • Atomically updates the in-memory HashSet used by the DNS resolver.

Where it lives:

  • kinetic-daemon/src/api/atlas.rs

Why it matters:

  • It is the singular mechanism by which the daemon dynamically learns which domain spaces it is responsible for intercepting.
  • The atomic RwLock swap pattern guarantees that DNS resolution remains blazingly fast.
  • It ensures DNS is never blocked during an update.

handle_get_time

What it does:

  • Uses the synchronized Drand state from storage.
  • Cryptographically calculates the KineticTime.
  • Returns the time as a JSON response.

Where it lives:

  • kinetic-daemon/src/api/time.rs

Why it matters:

  • It provides a trustless, cryptographically verified time source for local applications.
  • It allows the Kinetic ecosystem to bypass unreliable local system clocks.
  • It prevents attacks based on spoofed NTP data.

How This Connects to the Rest of Kinetic

Storage Layer Integration

  • Both the config.rs and time.rs endpoints rely on the storage layer.
  • They read owned names and the latest Drand kyn.
  • CROSS-CRATE: Sled Storage — defined and explained in docs/learn/storage/01_overview.md

Network Gossip Facade

  • The gossip.rs module acts as an HTTP facade directly over the Kademlia network’s libp2p publish/subscribe capabilities.
  • It bridges libp2p logic directly to HTTP.
  • CROSS-CRATE: Gossipsub — defined and explained in docs/learn/network/04_gossip.md

Drand Synchronization

  • The time endpoint relies directly on the Drand verification client.
  • It fetches cryptographic proofs of time from this client.
  • CROSS-CRATE: DrandClient — defined and explained in docs/learn/core/04_drand.md

DNS and Proxy Interaction

  • The atlas.rs webhook directly mutates the shared, in-memory state.
  • This state is exactly what the PAC server and DNS resolver use.
  • They use it to deterministically decide if a network request should be intercepted or forwarded to the open internet.

Quick Reference

Config Endpoints:

  • /config (GET/POST): Retrieves or sets node configuration.
  • /health: Checks internal daemon health (storage and network).
  • /status: Gets network metrics.
  • /owned-names: Retrieves domains owned by this node.
  • /peer-id: Returns the node’s Kademlia identity.
  • These generally require the Admin or Publish roles where appropriate, authenticated via JWT.

Gossip Endpoints:

  • /gossip/subscribe/:topic (GET): Establishes an SSE stream.
  • /gossip/publish/:topic (POST): Broadcasts JSON to the Kademlia network.

Atlas Endpoint:

  • /atlas/sync (POST): Synchronizes ICANN TLDs.
  • requires the Atlas or Admin JWT role to prevent unauthorized routing table poisoning.

Time Endpoint:

  • /time (GET): Returns verified network time.
  • Requires a verified, up-to-date Drand state to be present in local storage.
  • Otherwise returns a 500 error.

State Injection:

  • All of these Axum handlers rely on axum::extract::State<ApiState>.
  • They use it to access safely shared resources like broadcast network channels and database connections.

Concurrency Protection:

  • State updates (like the Atlas TLD synchronization) use RwLock.
  • They prioritize fast, concurrent reads.
  • They safely allow infrequent, atomic writes.

Open Questions / Things to Revisit

Configuration Hot-Reloading

  • Currently, /config updates the configuration YAML file on disk.
  • However, it just returns a static message telling the user to manually restart the daemon.
  • In the future, implementing true hot-reloading of the configuration without restarting the process would greatly improve node uptime and user experience.

Gossip Lag Handling

  • In gossip.rs, if an HTTP client lags due to poor network conditions, we log a warning.
  • We then irrevocably skip messages.
  • There is currently no mechanism to retroactively fetch missed messages from the stream.
  • This means SSE clients that experience brief network drops will silently lose data.
  • We may need to implement an ephemeral buffer or cache of recent gossip events to allow for seamless, lossless reconnections.

Time Endpoint Fallback Behavior

  • The time.rs endpoint currently returns a hard 500 error if it cannot fetch the latest Drand kyn.
  • While this accurately reflects that the node is unsynchronized, it might cause brittle behavior.
  • Local frontend apps simply need a roughly accurate timestamp when the node is temporarily partitioned from the network.
  • We should seriously consider returning an estimated mathematical time alongside an is_synced: false boolean flag instead of failing the request entirely.

Crate: kinetic-dns

Stage: 10

Reading Time: 120 mins

Depends On: kinetic-core, kinetic-daemon (running)


What Is This?

kinetic-dns is the bridge that makes .kin domains work inside a standard web browser or any application that performs DNS lookups. It runs as a background service on the local machine, binding to a UDP socket and acting as the local DNS resolver.

When the user’s OS asks “what is the IP address of saif.kin?”, this process answers by querying the running Kinetic daemon’s HTTP API, which triggers a full DHT lookup through the Kinetic peer-to-peer network.

For all standard internet domains (.com, .org, .io), it transparently passes the query to the operating system’s configured DNS server or falls back to Cloudflare 1.1.1.1 if the system configuration is unavailable.


Key Pieces

  1. bin/kinetic-dns.rs: The binary entrypoint. Contains the CLI (install, uninstall, start, stop, run), the OS DNS configuration injection, and the server boot loop.
  2. lib.rs and KineticDnsHandler: The central struct that holds all shared state — the HTTP client, the Moka cache, the upstream resolvers, and the Atlas TLD set.
  3. handler.rs: The request router. Checks if the query suffix is .kin and dispatches to either resolve_kinetic() or resolve_upstream().
  4. kinetic_records.rs: The heavy-lifting resolution pipeline. Handles cache misses, daemon API calls, record signature verification, KID end-to-end authentication, SSRF filtering, and DNS packet construction.
  5. upstream.rs: Creates and manages the standard upstream resolver (system DNS or Cloudflare DoH fallback) and the kinetic-atlas resolver.
  6. cache.rs: Asymmetric TTL Moka cache. Positive hits survive 5 minutes, NXDOMAIN hits expire in 30 seconds.

How to Read This Stage

  • Start with 02_bin_main.md to understand how the service installs and boots.
  • Read 04_handler_upstream_cache.md to understand the router that dispatches queries.
  • Finish with 03_kinetic_records.md to understand the deepest part — how a .kin domain name becomes a real IP address via the DHT.

Binary Entrypoint: Service Management and Boot Loop

File: kinetic-dns/src/bin/kinetic-dns.rs Crate: kinetic-dns | Stage: 10 Reading Time: 30 minutes


1. What Is This?

This is the actual executable binary of kinetic-dns. When a user runs kinetic-dns-server run or when the OS service manager starts the daemon, this file is what gets executed.

It does two completely separate jobs:

  1. Service management: Installing, uninstalling, starting, and stopping the DNS server as a persistent background system service (via systemd on Linux, launchd on macOS, or SCM on Windows).
  2. Server execution: The actual server boot loop — binding to port 53, registering sockets with the hickory server, dropping OS privileges, and then running until a shutdown signal arrives.

It also contains the OS-level DNS configuration injection, which is the critical step that tells the operating system “for .kin domains, use this DNS server instead of your normal one.”


2. Why Kinetic Needs This

A standard OS won’t send .kin queries anywhere useful. By default, if the user’s browser queries for saif.kin, the OS sends it to the configured system DNS resolver (e.g., 8.8.8.8). That resolver has no idea what .kin is and returns NXDOMAIN.

To intercept these queries before they ever leave the machine, the DNS server must be injected as a domain-specific override into the OS resolver configuration. This binary does exactly that — it hooks into systemd-resolved on Linux, /etc/resolver/ on macOS, or the NRPT policy engine on Windows to redirect only .kin queries to itself.

Important

Without this OS-level hook, the entire DNS resolution pipeline is unreachable.


3. How It Works

The CLI structure

The binary uses clap to parse command-line arguments. The Cli struct accepts:

  • --api-url: URL to the running kinetic-daemon (default: http://127.0.0.1:16000).
  • --dns-port: UDP port to bind (default: 53).
  • A Commands subcommand: one of install, uninstall, start, stop, or run. -> See: kinetic-dns/src/bin/kinetic-dns.rs — Lines 21-50

Install flow

When the user runs kinetic-dns-server install:

  1. It calls install_service() which reads the KineticConfig to get the configured dns_port.
  2. It reads the current executable path using env::current_exe().
  3. It creates a ServiceLabel like kinetic-dns using the NETWORK_ID constant from kinetic-core.
  4. It calls <dyn ServiceManager>::native() to automatically detect the host OS’s service manager (systemd, launchd, SCM).
  5. It issues a manager.install(ServiceInstallCtx { ... }) call. This registers the binary with the OS service manager so it auto-starts on reboot.
  6. Immediately after installation, configure_os_dns() is called to inject the DNS override rules into the OS. -> See: kinetic-dns/src/bin/kinetic-dns.rs — Lines 146-178

OS DNS configuration injection

The configure_os_dns() function inspects std::env::consts::OS at runtime to determine the platform and injects the configuration differently for each:

Linux (systemd-resolved):

  • Creates a file at /etc/systemd/resolved.conf.d/<NETWORK_ID>.conf.
  • The content is:
    [Resolve]
    DNS=127.0.0.2:53
    Domains=~kin
    
  • The ~kin prefix is the key. It tells systemd-resolved to only send .kin queries to this specific DNS server, leaving all other traffic untouched.
  • It then restarts systemd-resolved via systemctl restart systemd-resolved. -> See: kinetic-dns/src/bin/kinetic-dns.rs — Lines 80-93

macOS (/etc/resolver/):

  • macOS uses a per-TLD resolver directory. Kinetic writes /etc/resolver/kin with the nameserver IP and port.
  • The macOS network stack reads this directory and automatically routes .kin queries to the specified server.
  • Additionally, because macOS’s loopback interface only listens on 127.0.0.1 by default, the binary calls setup_macos_alias() to create a loopback alias IP (ifconfig lo0 alias 127.0.0.2 ...), ensuring the DNS server can bind without conflicting with other services. -> See: kinetic-dns/src/bin/kinetic-dns.rs — Lines 52-103

Windows (NRPT):

  • Uses PowerShell to call Add-DnsClientNrptRule -Namespace '.kin' -NameServers '127.0.0.2'.
  • NRPT (Name Resolution Policy Table) is a Windows feature specifically designed for domain-specific resolver overrides, making it the correct tool for this job. -> See: kinetic-dns/src/bin/kinetic-dns.rs — Lines 104-115

Uninstall flow

uninstall_service() reverses the install:

  • Calls manager.uninstall() to deregister from the OS service manager.
  • Calls remove_os_dns() which deletes the config files and on Linux, restarts systemd-resolved to flush the override.
  • On macOS, teardown_macos_alias() removes the loopback alias by calling ifconfig lo0 -alias <ip>. -> See: kinetic-dns/src/bin/kinetic-dns.rs — Lines 119-144

The actual server run loop (run_server)

When the command is run (or no subcommand is given), the binary enters run_server():

Step 1: Logging setup It initializes the tracing subscriber with a logging level pulled from the RUST_LOG environment variable, falling back to info.

Step 2: Handler creation Creates a KineticDnsHandler instance (defined in lib.rs), passing it:

  • The daemon API URL (e.g., http://127.0.0.1:16000)
  • A shared Arc<RwLock<HashSet<String>>> for Atlas TLDs (initially empty)
  • Port 5354 for the Atlas resolver -> See: kinetic-dns/src/bin/kinetic-dns.rs — Lines 221-225

Step 3: Socket binding (with fallback) The server attempts to bind a tokio::net::UdpSocket to <LOCAL_BIND_IP>:53.

  • If it succeeds, it also tries to bind [::1]:53 for IPv6 and registers both sockets with hickory’s ServerFuture.
  • If binding to port 53 fails (because the process isn’t running as root), it falls back to port 5353 (or dns_port + 1000), printing a warning to use sudo for native interception.
  • Both the primary and fallback sockets register on IPv4 and IPv6. -> See: kinetic-dns/src/bin/kinetic-dns.rs — Lines 233-337

Step 4: Privilege dropping After successfully binding to the privileged port 53 (which requires root), the process immediately drops its root privileges using the privdrop crate:

#![allow(unused)]
fn main() {
privdrop::PrivDrop::default().user("nobody").group("nogroup").apply()
}

Important

This is standard security practice: acquire the privileged resource (the port), then surrender the privileges. If the process is subsequently exploited, the attacker only gains a nobody shell, not root. -> See: kinetic-dns/src/bin/kinetic-dns.rs — Lines 244-257

Step 5: Run and shutdown server.block_until_done() runs the Hickory server. The code uses tokio::select! to simultaneously watch for either:

  • The server completing (error or normal exit).
  • A shutdown signal from kinetic_core::shutdown::shutdown_signal().

Whichever arrives first stops the other branch. On macOS, teardown_macos_alias() is called after the loop ends to clean up the loopback alias. -> See: kinetic-dns/src/bin/kinetic-dns.rs — Lines 259-271


4. Key Pieces

configure_os_dns(dns_port: u16) -> Result<()>

The most important function for getting .kin to actually work on the host machine. Does nothing to the internet DNS. Injects a narrow, domain-specific override targeted exclusively at .kin. Returns an error if writing to system directories fails (e.g., lack of root permission). -> See: kinetic-dns/src/bin/kinetic-dns.rs — Lines 74-117

setup_macos_alias(ip: &str) and teardown_macos_alias(ip: &str)

macOS-only functions wrapped in #[cfg(target_os = "macos")] so they are completely stripped from Linux and Windows builds. They shell out to ifconfig lo0 alias <ip> and ifconfig lo0 -alias <ip> respectively. Required because the /etc/resolver/ mechanism needs to reach the DNS server on a specific IP, which must exist on the local network interface. -> See: kinetic-dns/src/bin/kinetic-dns.rs — Lines 52-72

install_service() / uninstall_service()

Wrappers around the service_manager crate that abstract the differences between systemd unit files, launchd plist files, and Windows SCM entries behind a common API. -> See: kinetic-dns/src/bin/kinetic-dns.rs — Lines 146-208

run_server(api_url: String, dns_port: u16) -> Result<()>

The actual async loop that keeps the DNS server alive. This is the function called by both start (background) and run (foreground) commands. -> See: kinetic-dns/src/bin/kinetic-dns.rs — Lines 210-341


5. Cross-Crate Connections

  • kinetic_core::constants::TLD: The registered Kinetic TLD (.kin). Used when writing DNS override configs so the correct domain suffix is always registered.
  • kinetic_core::constants::LOCAL_BIND_IP: The IP the server binds to (typically 127.0.0.2). Shared constant to ensure consistency across all Kinetic tools.
  • kinetic_core::constants::NETWORK_ID: Used to construct the ServiceLabel (e.g., kinetic-dns) and file paths (e.g., kinetic.conf).
  • kinetic_core::config::KineticConfig::load(): Loads the user’s local TOML config file to get the configured dns_port.
  • kinetic_core::shutdown::shutdown_signal(): A shared future that resolves when SIGINT or SIGTERM is received by the process.
  • kinetic_dns::KineticDnsHandler: The struct defined in lib.rs that implements the actual DNS resolution logic.

6. Quick Reference

CommandWhat Happens
kinetic-dns-server installRegisters service + injects OS DNS rules for .kin
kinetic-dns-server startStarts the registered background service
kinetic-dns-server stopStops the background service
kinetic-dns-server uninstallRemoves service + removes OS DNS override rules
kinetic-dns-server runRuns the server in the foreground (no service)
No argumentSame as run
PlatformDNS Override Mechanism
Linux/etc/systemd/resolved.conf.d/kinetic.conf with Domains=~kin
macOS/etc/resolver/kin + lo0 loopback alias
WindowsNRPT policy via PowerShell Add-DnsClientNrptRule

Bind IP: kinetic_core::constants::LOCAL_BIND_IP (typically 127.0.0.2) Default Port: 53 with automatic fallback to 5353 Privilege drop: Immediately after binding port 53, drops to nobody:nogroup


7. Open Questions

  • Windows non-root binding: On Windows, binding to port 53 also requires elevated privileges but there is no equivalent of privdrop. The current implementation doesn’t explicitly handle this case.
  • Reload on config change: The service doesn’t hot-reload when the user changes the port in KineticConfig. It must be reinstalled.
  • IPv6 loopback alias on macOS: Only IPv4 loopback alias is created. If a user has IPv6-only networking, this may not work.
  • Atlas TLD hot-injection: The Atlas TLD set is initialized empty at startup. The mechanism for the daemon to push new TLDs to the running DNS server is not documented here and likely uses an HTTP endpoint or IPC.

8. Rust Concepts

  • <dyn ServiceManager>::native(): Calls a trait object’s associated function using Fully Qualified Syntax. Returns a boxed platform-specific ServiceManager (systemd/launchd/SCM) without the caller knowing the concrete type.
  • privdrop crate and POSIX privilege dropping: A critical security pattern. Bind to a privileged port as root, then immediately drop to an unprivileged user so that any runtime exploit can’t escalate.
  • #[cfg(target_os = "macos")]: Strips macOS-only code from Linux and Windows binaries at compile time, not at runtime.
  • tokio::select! with shutdown signal: Two async futures race — the server loop and the OS signal watcher. Whichever wins cancels the other, ensuring graceful shutdown without zombie threads.

The Kinetic Resolution Pipeline: From DNS Query to DHT Record

File: kinetic-dns/src/kinetic_records.rs Crate: kinetic-dns | Stage: 10 Reading Time: 40 minutes


1. What Is This?

This file contains the complete .kin resolution pipeline — the code that turns a raw DNS query packet for saif.kin into a real DNS response containing an A record, CNAME, or TXT record.

It is a single asynchronous function resolve_kinetic() that does six distinct things in strict sequence:

  1. Intercepts reserved/public names before touching the network.
  2. Checks the local Moka cache for a previously resolved apex domain.
  3. If the cache misses, hits the kinetic-daemon REST API (/api/resolve/<name>).
  4. Validates the signature of the returned NameRecord.
  5. Performs E2E KID authentication if the domain specifies a KID record.
  6. Filters SSRF-dangerous IPs, converts Kinetic DNS records into hickory wire-format records, and sends the response.

2. Why Kinetic Needs This

The Hickory DNS library will call the KineticDnsHandler::handle_request() method for every incoming DNS packet. That method routes .kin queries here. At this point, the code has a raw domain name string and must produce a valid DNS wire response.

Important

This pipeline cannot trust the data it receives from the daemon. The daemon is fetching data from a decentralized DHT where anyone can potentially inject malicious records. Therefore, every resolved record must be:

  • Cryptographically verified (signature check via verify_signature()).
  • Optionally E2E authenticated (KID check).
  • SSRF-filtered (no A/AAAA records pointing to 127.x.x.x, 10.x.x.x, etc.).

Warning

Without this pipeline, a malicious actor could publish a DNS zone pointing saif.kin to 127.0.0.1 and cause the browser to attack the user’s own machine.


3. How It Works — Step by Step

Step 1: Reserved Name Interception

-> See: kinetic-dns/src/kinetic_records.rs — Lines 29-80

Before any network call, the code checks if the apex domain (the root part without subdomains) is in kinetic_core::types::PUBLIC_NAMES.

PUBLIC_NAMES is a list of names that are permanently reserved in the Kinetic namespace — things like localhost, internal bootstrap names, and other special identifiers that should never be routable.

localhost handling: If someone queries localhost.kin, the code doesn’t reject it. Instead it returns a hardcoded 127.0.0.1 A record (or ::1 AAAA record) directly, without touching the network.

All other PUBLIC_NAMES: They immediately return NXDOMAIN. No cache lookup. No daemon call. No chance of being redirected to attacker-controlled records.

Step 2: Cache Lookup (Moka try_get_with)

-> See: kinetic-dns/src/kinetic_records.rs — Lines 82-125

The code calls cache.try_get_with(apex_domain, async move { ... }) on the Moka cache.

The key insight here is try_get_with: This method is cache-stampede-safe. If 100 simultaneous DNS queries come in for saif.kin at the same time during a cache miss, only ONE of them will actually fire the API request. The other 99 will wait for that single request to complete and then share its result. This prevents the daemon from being flooded.

The cache stores raw response bytes (Vec<u8>), keyed by apex domain string.

  • Cache Hit (positive): Returns Ok(Some(Vec<u8>)) immediately — no network call.
  • Cache Hit (negative / NXDOMAIN): Returns Ok(None) immediately — the previous lookup found no record, and this answer is cached for 30 seconds.
  • Cache Miss: Fires the API request inside the closure.

Step 3: Daemon API Call

-> See: kinetic-dns/src/kinetic_records.rs — Lines 92-125

On a cache miss, the code constructs the URL <api_url>/api/resolve/<apex_domain> and calls it using the reqwest::Client.

While reading the response, it enforces a strict 100KB payload size limit. The response is read in chunks using resp.chunk().await. If the cumulative size exceeds 100KB, the chunk loop exits immediately with Ok(None), treating it as if the domain doesn’t exist. This prevents a malicious node from sending megabyte payloads to exhaust the DNS server’s memory.

If the daemon returns 404 Not Found, the code returns Ok(None) — the NXDOMAIN path. This negative result is then cached for 30 seconds.

If the daemon returns another error, it returns Err(...) which the cache does NOT store, ensuring the error is not permanently cached.

Step 4: Signature Verification

-> See: kinetic-dns/src/kinetic_records.rs — Lines 131-146

The raw bytes from the cache are deserialized into a kinetic_core::types::NameRecord via serde_json::from_slice().

Immediately after deserialization, before ANY other processing:

#![allow(unused)]
fn main() {
domain_record.verify_signature(kinetic_core::constants::NETWORK_ID).is_err()
}

If the signature fails, the code returns ServFail and logs a warning. The record is completely ignored, even if it contains perfectly formed DNS data.

Important

This is the core anti-tampering check. If a malicious DHT node modifies the payload bytes in transit, the signature fails here.

Step 5: Payload Parsing and KID E2E Authentication

-> See: kinetic-dns/src/kinetic_records.rs — Lines 148-224

After the signature check, the payload is parsed into a DnsZone struct via DnsZone::parse_payload(domain_record.payload()).

The code then scans the apex domain’s @ records for any DnsRecord::KID(did) entries.

A KID record means: “This domain only accepts traffic signed by the specified Kinetic Identity Document.”

If a KID record is found:

  1. It constructs the URL <api_url>/api/resolve-kid/<did> and sends it to the daemon.
  2. The daemon returns the KID document, which contains a list of controller_keys.
  3. The code base64-encodes the NameRecord’s public key and checks if it matches any key in the KID’s controller_keys.
  4. If NO match is found, the DNS query is rejected with ServFail.
  5. If a match is found, E2E authentication passes and resolution continues.

Note

This guarantees that even if an attacker has obtained a valid signature for saif.kin (perhaps via a compromised key), the domain’s KID lock will block them unless their key is also listed as a controller in the KID document.

Step 6: Subdomain Mapping and Record Construction

-> See: kinetic-dns/src/kinetic_records.rs — Lines 226-395

After authentication, the code determines which subdomain is being queried:

  • If domain_name == apex_domain, it uses "@" (the apex record).
  • Otherwise, it strips the apex suffix to get the subdomain label (e.g., "www" from "www.saif.kin").
  • If the exact subdomain is missing from the zone, it falls back to "*" (wildcard records).

For each matching DnsRecord in the zone:

A records: SSRF-checked via kinetic_core::net::is_ssrf_safe(IpAddr::V4(*ip)). Private ranges (127.x, 10.x, 192.168.x, 169.254.x) are blocked with a warning. Safe IPs are added as hickory A records with a 60-second TTL.

AAAA records: Same SSRF filter applied. IPv6 private ranges are blocked.

CNAME records: Checked for local domain targets (localhost, *.local). CNAME targets that parse as IP addresses are SSRF-checked. Valid external CNAMEs are added regardless of query type (per DNS RFC — the resolver will follow CNAMEs automatically).

TXT, PeerId, KID, IPFS records: Returned as TXT records. PeerId is formatted as peerid=<id>, KID as kid=<did>, IPFS as ipfs=<cid>. These are useful for discovery but don’t route traffic.

If response records were constructed, they are sent with NOERROR. If no records matched, the code falls through to the final NXDOMAIN return.


4. Key Pieces

resolve_kinetic<R: ResponseHandler>(...) -> ResponseInfo

The sole exported function. It is generic over R — any type implementing ResponseHandler — so it can work with Hickory’s internal response channel. -> See: kinetic-dns/src/kinetic_records.rs — Lines 15-26

cache.try_get_with(key, async move { ... })

The stampede-safe cache fetch. The closure only fires on cache misses and only once per concurrent group of misses. All concurrent waiters share the result. -> See: kinetic-dns/src/kinetic_records.rs — Lines 86-125

SSRF filter: kinetic_core::net::is_ssrf_safe(ip)

Reused from kinetic-core. Returns false for any private, loopback, link-local, or reserved IP range. Called for every A and AAAA record before it is included in the response. -> See: kinetic-dns/src/kinetic_records.rs — Lines 270-296

KID E2E Authentication block

The nested API call to /api/resolve-kid/<did>. Fetches the KID document and cross-checks the record’s public key against the KID’s controller keys. -> See: kinetic-dns/src/kinetic_records.rs — Lines 152-222


5. Cross-Crate Connections

  • kinetic_core::types::PUBLIC_NAMES: The set of reserved names that bypass resolution.
  • kinetic_core::types::NameRecord::verify_signature(): The cryptographic signature verification for all DHT-fetched records.
  • kinetic_core::types::DnsZone::parse_payload(): Parses the opaque payload bytes into a typed zone with typed records.
  • kinetic_core::types::DnsRecord: The enum of supported record types (A, AAAA, CNAME, TXT, KID, PeerId, IPFS).
  • kinetic_core::net::is_ssrf_safe(): The shared SSRF protection filter applied consistently across the daemon proxy and this DNS layer.
  • kinetic_core::constants::NETWORK_ID: Passed into verify_signature() to tie the signature to this specific Kinetic network deployment.

6. Quick Reference

Query ResultResponse CodeCache Duration
Record found, signature valid, SSRF safeNOERROR5 minutes
Domain not registeredNXDOMAIN30 seconds
Domain in PUBLIC_NAMES (non-localhost)NXDOMAINNot cached
Signature invalidServFailNot cached
KID auth failedServFailNot cached
Daemon API errorServFailNot cached
Payload exceeds 100KBNXDOMAIN5 minutes

Resolution order:

  1. Check PUBLIC_NAMES (instant local decision).
  2. Check Moka cache (try_get_with, stampede-safe).
  3. Query daemon /api/resolve/<apex>.
  4. Verify NameRecord signature.
  5. If KID record exists, verify E2E auth via /api/resolve-kid/<did>.
  6. Map subdomain, filter SSRF, construct hickory records.

7. Open Questions

  • CNAME recursion: When the DNS server returns a CNAME, it expects the client OS resolver to follow the CNAME chain. For .kin CNAME chains that point to other .kin domains, this may cause infinite loops or missing records if the OS resolver doesn’t loop back.
  • TXT-only queries: A client querying only for TXT records on a domain that also has A records will receive the TXT plus any matched KID/PeerId/IPFS records. It does not get the A record unless it also makes an A query.
  • Cache invalidation from daemon: The daemon calls KineticDnsHandler::invalidate_cache() after successful writes, but this only works if both the DNS server and the daemon share the same process (or communicate via IPC). In the standard setup they are separate processes, so this may not actually invalidate across process boundaries.

8. Rust Concepts

  • moka::future::Cache::try_get_with(): The async, concurrent, stampede-safe cache fetch. On a cache miss with N concurrent waiters, exactly one fires the async closure and all N waiters receive the same result.
  • serde_json::from_slice(&bytes): Deserializes raw bytes directly into a typed Rust struct. Used here instead of parsing a String first to avoid an extra allocation.
  • Generic <R: ResponseHandler>: Makes the function work with any type that implements Hickory’s ResponseHandler trait, including test mocks, without dynamic dispatch overhead.
  • Arc<anyhow::Error> inside moka: Moka requires error types to be Clone. anyhow::Error is not Clone, so it is wrapped in Arc (which is cheap to clone via reference-counting) to satisfy the trait bound.

DNS Request Routing: Handler, Upstream Resolver, and Cache

Files: handler.rs, upstream.rs, cache.rs, lib.rs Crate: kinetic-dns | Stage: 10 Reading Time: 25 minutes


1. What Is This?

These four files form the structural backbone of the DNS server. Together they define the KineticDnsHandler struct, the packet dispatcher, the upstream forwarding, and the in-memory cache.

  • lib.rs: Defines KineticDnsHandler — the central struct holding all shared state. It also spawns a background task to hot-reload the OS DNS config every 5 minutes.
  • handler.rs: Implements RequestHandler for KineticDnsHandler. This is the single function Hickory calls for every DNS packet. It classifies the query and routes it either to resolve_kinetic() or resolve_upstream().
  • upstream.rs: Creates the upstream resolver (system DNS or Cloudflare DoH fallback) and executes upstream queries, translating results back into Hickory wire format.
  • cache.rs: Configures the Moka cache with an asymmetric TTL policy — 5 minutes for positive hits, 30 seconds for NXDOMAIN.

2. Why Kinetic Needs This

Important

The critical design constraint is: the DNS server must handle .kin queries specially, but must pass everything else to the normal internet DNS infrastructure completely transparently.

This is non-negotiable. If the Kinetic DNS server drops or mishandles any .com or .org query, the user’s entire internet connectivity breaks. The dispatcher in handler.rs is the gatekeeper that enforces this boundary.

The cache is equally critical for performance. DNS is called for every single network request a browser makes — loading a webpage can trigger dozens of DNS queries. Without caching, every one of those would hit the daemon’s HTTP API and then the P2P network. The Moka cache absorbs repeated queries and makes .kin resolution feel as fast as regular DNS.


3. How It Works

The KineticDnsHandler struct (lib.rs)

-> See: kinetic-dns/src/lib.rs — Lines 37-50

The struct holds:

  • api_url: String: The daemon’s HTTP API URL. Stored as a simple string — cheap to pass.
  • http_client: reqwest::Client: A connection-pooled HTTP client. reqwest::Client is internally an Arc-wrapped pool, so .clone() is cheap and shares connections.
  • resolver: Arc<RwLock<TokioAsyncResolver>>: The upstream DNS resolver, wrapped in a read-write lock so the background hot-reload task can atomically swap it out without dropping active queries.
  • cache: Cache<String, Option<Vec<u8>>>: The Moka async cache. Keyed by apex domain string, values are raw payload bytes (or None for NXDOMAIN).
  • atlas_tlds: Arc<RwLock<HashSet<String>>>: The set of foreign TLDs that kinetic-atlas has registered. Used in the router to know when to use the Atlas resolver instead of the internet resolver.
  • atlas_resolver: Arc<RwLock<TokioAsyncResolver>>: A second upstream resolver that points specifically at the local kinetic-atlas bridge process.

The struct derives Clone — this is required by Hickory because it clones the handler for each request handling task.

Background hot-reload task (lib.rs)

-> See: kinetic-dns/src/lib.rs — Lines 73-85

KineticDnsHandler::new() spawns a tokio::spawn background task that runs every 5 minutes using tokio::time::interval.

Each iteration calls upstream::create_resolver() to read fresh OS DNS configuration and then acquires a write lock on resolver_clone to swap in the new resolver. This handles the case where the user’s network changes (e.g., switching from home Wi-Fi to a corporate VPN that uses a different DNS server).

The write lock ensures that no concurrent read query sees the resolver mid-swap. Once the lock is released, all subsequent reads see the new resolver atomically.

The packet dispatcher (handler.rs)

-> See: kinetic-dns/src/handler.rs — Lines 11-91

Every DNS UDP packet flows through handle_request(). The dispatch logic:

Step 1: Strip the trailing dot from the query name (DNS wire format always includes a trailing dot; comparing against string suffixes requires removing it).

Step 2: Check if the name ends with kinetic_core::constants::TLD_SUFFIX (i.e., .kin.). If yes, dispatch to resolve_kinetic().

Step 3: If not a .kin query, check if the apex TLD is in the Atlas TLD set (atlas_tlds). Atlas TLDs are domains from the traditional internet (e.g., .eth, .bit) that kinetic-atlas has bridged into the Kinetic namespace. These go to the atlas_resolver (pointing at the local bridge).

Step 4: Everything else goes to the main resolve_upstream() with the standard internet resolver.

The read lock on atlas_tlds is acquired briefly for the iteration. Even under heavy load, read locks don’t block each other, so this check is safe for concurrent requests.

Upstream resolution (upstream.rs)

create_resolver(): -> See: kinetic-dns/src/upstream.rs — Lines 17-26

Calls hickory_resolver::system_conf::read_system_conf() to parse /etc/resolv.conf (Linux/macOS) or the Windows Registry DNS configuration. If this fails, it falls back to ResolverConfig::cloudflare_https() — Cloudflare’s 1.1.1.1 DNS-over-HTTPS endpoint.

create_atlas_resolver(port: u16): -> See: kinetic-dns/src/upstream.rs — Lines 29-46

Builds a resolver that points specifically to 127.0.0.1:<port> on both UDP and TCP. This is the port where the kinetic-atlas bridge daemon is listening.

resolve_upstream(): -> See: kinetic-dns/src/upstream.rs — Lines 51-100

Calls resolver.lookup(name, query_type) on the Hickory async resolver. On success, collects the returned Record iterator into a Vec<Record> and builds a Hickory response. On error, maps NoRecordsFound to NXDOMAIN and everything else to ServFail.

The asymmetric TTL cache (cache.rs)

-> See: kinetic-dns/src/cache.rs — Lines 1-60

The cache uses a custom KineticExpiry struct implementing Moka’s Expiry trait. The key method is expire_after_create():

  • If the value is Some(bytes) (positive hit), return a 5-minute duration.
  • If the value is None (NXDOMAIN), return a 30-second duration.

This asymmetry is important: NXDOMAIN results must expire quickly because a new .kin domain might be registered at any moment. Positive results can be cached longer since registered domains change rarely.

The cache is configured with:

  • Max capacity: 10MB total memory.
  • Weigher function: Each entry weighs its byte length (or 1 for None entries). This prevents a flood of tiny entries from exhausting the 10MB budget while also preventing one giant entry from evicting everything else.

The expire_after_read() method returns duration_until_expiry unchanged — TTL is not extended on reads. A record’s lifetime starts the moment it was fetched from the daemon, not the moment it was last accessed.


4. Key Pieces

impl RequestHandler for KineticDnsHandler

The Hickory trait implementation. This is the single entry point for all DNS packets. The function signature is:

#![allow(unused)]
fn main() {
async fn handle_request<R: ResponseHandler>(&self, request: &Request, response_handle: R) -> ResponseInfo
}

-> See: kinetic-dns/src/handler.rs — Lines 11-91

struct KineticExpiry

Implements moka::Expiry to control cache TTL per value type, not per key. The same cache can hold entries with completely different lifetimes based on whether they represent a found record or a NXDOMAIN result. -> See: kinetic-dns/src/cache.rs — Lines 9-49

pub fn create_cache() -> Cache<String, Option<Vec<u8>>>

Builds the Moka cache with the custom expiry policy, 10MB memory ceiling, and a byte-accurate weigher function. -> See: kinetic-dns/src/cache.rs — Lines 52-60

pub fn create_resolver() -> TokioAsyncResolver

The OS-aware resolver factory. Reads system DNS config first, falls back to Cloudflare if unavailable. -> See: kinetic-dns/src/upstream.rs — Lines 17-26


5. Cross-Crate Connections

  • kinetic_core::constants::TLD_SUFFIX: The suffix string (e.g., .kin) used to classify incoming queries.
  • kinetic_core::types::normalize_name(): Called in handler.rs before passing the name to resolve_kinetic(). Strips trailing dots and normalizes formatting.
  • kinetic_core::types::extract_apex_name(): Extracts the root domain from a potentially subdomain-prefixed name (e.g., www.saif.kinsaif.kin).
  • Hickory DNS (hickory-server, hickory-resolver, hickory-proto): The underlying DNS library that handles UDP packet parsing, the RequestHandler trait, and resolver machinery.
  • Moka (moka::future::Cache): The async-safe, concurrent, TTL-based in-memory cache.

6. Quick Reference

ComponentRole
KineticDnsHandlerCentral state holder, Clone-able for concurrent requests
handle_request()Routes every packet to the correct resolver
resolve_kinetic()Handles .kin queries via DHT (in kinetic_records.rs)
resolve_upstream()Handles standard queries via OS/Cloudflare DNS
create_resolver()Reads OS DNS config or falls back to Cloudflare DoH
create_atlas_resolver()Points to local kinetic-atlas bridge
KineticExpiry5-min TTL for positive hits, 30-sec for NXDOMAIN
Background reload taskRe-reads OS DNS config every 5 minutes

7. Open Questions

  • Atlas TLD registration: The atlas_tlds set is initialized empty. The mechanism by which the kinetic-atlas bridge registers new TLDs at runtime is not shown in these files. Likely uses an HTTP endpoint that calls atlas_tlds.write().
  • Cache invalidation across processes: invalidate_cache() clears the in-process Moka cache, but the DNS server and daemon are separate processes. Cross-process invalidation would need IPC.
  • Concurrent resolver swap: The background hot-reload acquires a write lock on resolver. If many queries are in flight and holding read locks, the write lock will block. Under heavy load, a 5-minute reload interval might cause brief resolution delays.

8. Rust Concepts

  • RwLock<T> (Read-Write Lock): Allows unlimited concurrent readers but only one exclusive writer. Used on resolver and atlas_tlds so hot-reload swaps don’t block incoming queries except during the instant of the swap itself.
  • tokio::time::interval(Duration): Creates a recurring timer that fires at fixed intervals. Unlike a sleep loop, it accounts for the time spent executing each iteration to maintain consistent intervals.
  • moka::Expiry trait: A custom hook into Moka’s eviction policy. By implementing expire_after_create(), the cache can assign different TTLs to different values at insertion time.
  • reqwest::Client cloneability: reqwest::Client wraps an internal Arc-ed connection pool. Cloning it is O(1) and all clones share the same pool, preventing connection exhaustion.

DNS Test Suite: Mock Daemon, Integration Tests, and Fuzzing

File: kinetic-dns/src/tests.rs Crate: kinetic-dns | Stage: 10 Reading Time: 30 minutes


1. What Is This?

This file contains the complete integration test suite for kinetic-dns. Unlike unit tests that call individual functions in isolation, these tests run the entire request handling pipeline end-to-end — from constructing a raw DNS query packet to asserting the correct DNS response code.

It uses a mock HTTP server (built with Axum) that simulates the kinetic-daemon’s REST API, and a MockResponseHandler that captures the DNS responses produced by KineticDnsHandler without needing an actual OS socket.

There are also property-based fuzz tests that throw random bytes and strings at the resolution pipeline to ensure it never panics.


2. Why Kinetic Needs This

Important

The DNS resolution pipeline is deeply stateful and security-critical. It must:

  • Never panic on malformed input from the network (the internet is adversarial).
  • Correctly map daemon API errors to the appropriate DNS response codes.
  • Correctly handle casing normalization (DNS is case-insensitive).
  • Correctly handle subdomain lookups, wildcard fallbacks, and wrong record type queries.

Integration tests are the only way to verify that the full pipeline behaves correctly end-to-end. Unit tests for individual helper functions would miss bugs that only appear when components interact.

The fuzz tests are essential because the daemon can receive data from untrusted P2P peers. A single panic in the DNS handler would kill the server and break all name resolution for the user.


3. How The Test Infrastructure Works

The Mock Daemon (start_mock_daemon())

-> See: kinetic-dns/src/tests.rs — Lines 71-108

The tests cannot depend on a real running kinetic-daemon. Instead, start_mock_daemon() builds a minimal Axum router with one route: GET /api/resolve/:domain.

The mock returns different responses based on the requested domain:

  • test1.kin: Returns a fully valid, signed Reveal containing a DnsZone with an A record pointing to 1.2.3.4. This is the “happy path” test fixture.
  • invalid-payload.kin: Returns raw garbage bytes ([0, 1, 2, 3]) as the HTTP body. Tests that the JSON parser fails gracefully.
  • invalid-zone.kin: Returns a valid signed Reveal, but the inner payload is not a valid DnsZone JSON. Tests that DnsZone::parse_payload() fails gracefully.
  • 500.kin: Returns HTTP 500. Tests that server errors map to ServFail.
  • Anything else: Returns HTTP 404. Tests that missing domains map to NXDOMAIN.

The mock server binds to 127.0.0.1:0 (OS-assigned random port) to avoid port conflicts. It returns the bound address so the test can point KineticDnsHandler at it.

The tokio::time::sleep(50ms) after spawning gives the Axum server time to become ready before the first test query fires.

The mock_reveal() Helper

-> See: kinetic-dns/src/tests.rs — Lines 43-69

This function constructs a fully valid, cryptographically signed Reveal (which contains the DNS payload).

It generates a real ML-DSA-65 keypair on every call, signs the signable_bytes() of the reveal, and includes the public key. This means the signature verification step in resolve_kinetic() will actually pass for test1.kin and invalid-zone.kin.

This is important because it tests the real verification code, not a mocked bypass.

The MockResponseHandler

-> See: kinetic-dns/src/tests.rs — Lines 16-41

Hickory’s RequestHandler returns a ResponseInfo struct and calls response_handle.send_response() to deliver the DNS response to the client. In tests, there is no real client socket to send to.

MockResponseHandler implements ResponseHandler by capturing the Message (DNS response packet) into a Vec protected by a tokio::sync::Mutex. Tests can then inspect what responses were generated.

The build_request() Helper

-> See: kinetic-dns/src/tests.rs — Lines 110-123

Creates a raw Hickory Request (what the DNS handler receives) from a domain name string and record type. The process:

  1. Constructs a DNS Message with one query.
  2. Serializes it to wire bytes using BinEncodable.
  3. Deserializes it back into a MessageRequest to simulate what Hickory would produce from a real UDP packet.
  4. Wraps it in a Request with a fake source address of 127.0.0.1:12345.

4. Test Coverage — What Each Test Verifies

Test 1: test_resolve_standard_domain

-> See: kinetic-dns/src/tests.rs — Lines 126-146

Sends a query for google.com. (not .kin). The handler must route this to resolve_upstream() using the internet DNS resolver.

The assertion allows either NoError (resolved successfully) or ServFail (no internet access in CI). This prevents test failures in sandboxed CI environments where outbound DNS is blocked.

Test 2: test_resolve_kin_success

-> See: kinetic-dns/src/tests.rs — Lines 149-173

The core happy path. Sends test1.kin. query for A record. The mock daemon returns a valid signed record with 1.2.3.4.

The entire pipeline runs:

  1. Cache miss → daemon API call.
  2. NameRecord deserialization.
  3. Signature verification (using the freshly generated ML-DSA key).
  4. DnsZone parsing.
  5. Subdomain mapping to @.
  6. SSRF check on 1.2.3.4 (passes — it’s a public IP).
  7. hickory A record construction and response.

Asserts NoError.

Test 3: test_resolve_kin_api_404_nxdomain

-> See: kinetic-dns/src/tests.rs — Lines 176-196

Sends missing.kin. which the mock daemon returns 404 for. Asserts the response code is NXDomain.

This verifies the HTTP 404 → DNS NXDOMAIN mapping in the daemon API call path inside resolve_kinetic().

Test 4: test_resolve_kin_api_500_servfail

-> See: kinetic-dns/src/tests.rs — Lines 199-219

Sends 500.kin. which the mock daemon returns HTTP 500 for. Asserts the response code is ServFail.

This verifies that unrecoverable daemon errors (database crashes, internal errors) correctly map to ServFail rather than incorrectly returning NXDOMAIN.

Test 5: test_resolve_kin_invalid_payload

-> See: kinetic-dns/src/tests.rs — Lines 222-242

Sends invalid-payload.kin.. The mock daemon returns raw bytes [0,1,2,3] that cannot be deserialized as JSON or as a NameRecord.

Asserts NXDomain. This tests the serde_json::from_slice::<NameRecord>() failure path — the code logs a warning and falls through to NXDOMAIN rather than panicking.

Test 6: test_resolve_kin_invalid_zone

-> See: kinetic-dns/src/tests.rs — Lines 245-265

Sends invalid-zone.kin.. The mock daemon returns a valid signed Reveal, but the inner payload is [1,2,3,4] which is not a valid DnsZone JSON.

This test verifies that signature verification passes (the Reveal is signed correctly) but DnsZone::parse_payload() fails gracefully and logs a warning rather than panicking.

Test 7: test_resolve_kin_subdomain_fallback

-> See: kinetic-dns/src/tests.rs — Lines 268-289

Sends www.test1.kin.. The test1.kin zone only has a @ (apex) record, no www record and no * wildcard.

Asserts NXDomain. This verifies that subdomain lookup falls through correctly when neither the exact subdomain nor a wildcard is present in the zone.

Test 8: test_resolve_kin_uppercase

-> See: kinetic-dns/src/tests.rs — Lines 292-313

Sends TEST1.KIN. in uppercase. DNS is case-insensitive by RFC, so this must resolve to the same record as test1.kin..

This tests the clean_name.to_lowercase() normalization in handler.rs.

Test 9: test_resolve_kin_wrong_record_type

-> See: kinetic-dns/src/tests.rs — Lines 316-336

Sends test1.kin. but queries for AAAA (IPv6). The zone only has an A (IPv4) record.

Asserts NXDomain. This verifies that the record type matching in the zone loop correctly skips A records when the client asked for AAAA.

Test 10: test_cache_invalidation

-> See: kinetic-dns/src/tests.rs — Lines 339-366

Tests that invalidate_cache() works correctly:

  1. Calls invalidate_cache("test1.kin") on an empty cache (no-op, should not panic).
  2. Queries test1.kin. — cache miss → API call → NoError.
  3. Calls invalidate_cache("test1.kin") again. The next query would hit the API again.

5. Fuzz Tests

doesnt_crash_on_random_payload_parsing

-> See: kinetic-dns/src/tests.rs — Lines 375-381

Uses proptest to generate completely random Vec<u8> and passes them directly to serde_json::from_slice::<Reveal>().

The DNS server receives raw bytes from the daemon which in turn receives them from the P2P network. A panic here would kill the server. The fuzz test runs hundreds of iterations to prove that no byte sequence can trigger a panic.

doesnt_crash_on_random_reveal_strings

-> See: kinetic-dns/src/tests.rs — Lines 383-390

Generates random valid UTF-8 strings (using proptest’s regex ".*") and passes them to DnsZone::parse_payload(). Similar goal: no UTF-8 string should be able to panic the zone parser.

doesnt_crash_on_random_domain_normalization

-> See: kinetic-dns/src/tests.rs — Lines 392-398

Generates random UTF-8 strings and passes them to normalize_name() and extract_apex_name(). Domain names in DNS can be malformed or contain non-ASCII bytes. These functions must never panic regardless of input.


6. Key Pieces

struct MockResponseHandler

Implements Hickory’s ResponseHandler trait to capture DNS responses in memory. The responses field holds Arc<Mutex<Vec<Message>>> — an atomically shared, async-locked vector. -> See: kinetic-dns/src/tests.rs — Lines 16-41

fn mock_reveal(name: &str, payload: Vec<u8>) -> Reveal

Generates a real cryptographic signature using the ML-DSA-65 algorithm. The key is freshly generated per call — different key per test run — ensuring signature verification tests are not hardcoded. -> See: kinetic-dns/src/tests.rs — Lines 43-69

async fn start_mock_daemon() -> String

Runs a real Axum HTTP server on a random port in a background Tokio task. Returns the base URL for KineticDnsHandler to use instead of the real daemon. -> See: kinetic-dns/src/tests.rs — Lines 71-108

async fn build_request(name: &str, rtype: RecordType) -> Request

Converts a domain name and query type into a real Hickory Request by round-tripping through DNS binary wire format. -> See: kinetic-dns/src/tests.rs — Lines 110-123


7. Cross-Crate Connections

  • kinetic_core::types::Reveal: The full signed record type that the daemon API returns. mock_reveal() constructs one with a real ML-DSA signature.
  • kinetic_core::types::DnsZone: The zone record embedded inside the reveal. start_mock_daemon() creates one with a simple A record.
  • kinetic_core::types::normalize_name() and extract_apex_name(): Fuzz tested directly.
  • Hickory DNS (hickory_proto, hickory_server): Used for request/response construction, BinEncodable, and the RequestHandler trait.
  • proptest: Property-based test framework used in the fuzzing submodule.

8. Quick Reference

TestWhat it verifies
test_resolve_standard_domainNon-.kin queries pass through to upstream
test_resolve_kin_successFull pipeline: cache miss → API → verify → SSRF → NoError
test_resolve_kin_api_404_nxdomainHTTP 404 maps to DNS NXDomain
test_resolve_kin_api_500_servfailHTTP 500 maps to DNS ServFail
test_resolve_kin_invalid_payloadGarbage bytes don’t panic, return NXDomain
test_resolve_kin_invalid_zoneInvalid zone JSON doesn’t panic, return NXDomain
test_resolve_kin_subdomain_fallbackMissing subdomain + no wildcard → NXDomain
test_resolve_kin_uppercaseUppercase domain names normalize correctly
test_resolve_kin_wrong_record_typeQuerying AAAA when only A exists → NXDomain
test_cache_invalidationinvalidate_cache() works without panic
Fuzz: doesnt_crash_on_random_payload_parsingRandom bytes never panic serde_json
Fuzz: doesnt_crash_on_random_reveal_stringsRandom UTF-8 never panics DnsZone::parse_payload
Fuzz: doesnt_crash_on_random_domain_normalizationRandom domains never panic normalization

9. Rust Concepts

  • #[tokio::test]: Marks an async function as a test case and wraps it in a single-threaded Tokio runtime. Required because the DNS handler and mock server use async I/O.
  • proptest! macro: Generates hundreds of random inputs bounded by a strategy (like any::<Vec<u8>>()). If any input panics, proptest reports the minimal failing case.
  • Arc<Mutex<Vec<Message>>> in test infrastructure: Shares the response collector between the handler (which calls send_response) and the test assertion (which reads responses). The Mutex is tokio::sync::Mutex (async) because send_response is async.
  • TcpListener::bind("127.0.0.1:0"): Binding to port 0 lets the OS choose a free port, avoiding hardcoded port conflicts between concurrent test runs.
  • BinEncodable / BinDecodable: Hickory’s DNS wire format serialization traits. Round-tripping through wire format is the only way to construct a MessageRequest, because the type has no public constructor.

Resolution Pipeline & Record Conversion

Crate: kinetic-dns Stage: DNS (Daemon Extension) Reading time: 25 minutes Depends on: Types, Core, Daemon


What Is This?

This file contains the resolve_kinetic function, which serves as the central nervous system and the absolute core of the Kinetic DNS server. It is a massive, asynchronous pipeline that handles the end-to-end resolution of any .kin domain query. To understand what this file is, you must first understand the architectural impedance mismatch between legacy operating systems and decentralized networks. When an operating system (like Linux, macOS, or Windows) or a browser (like Chrome or Firefox) attempts to connect to a domain, it relies on the Domain Name System (DNS) to figure out which IP address to talk to. Standard DNS uses centralized servers, communicates over UDP/TCP on Port 53, and expects responses formatted according to legacy RFC standards established in the 1980s. Kinetic, on the other hand, uses a decentralized Distributed Hash Table (DHT), communicates over peer-to-peer libp2p multiplexed streams, and stores data as cryptographic JSON payloads signed with ed25519 keys. Because operating systems do not understand what a DHT is, and they definitely do not understand Kinetic’s custom cryptographic payloads, this file exists to act as an active, intelligent middleware layer. It receives a raw, legacy UDP/TCP DNS query directly from the operating system. It intercepts that query, fetches the corresponding decentralized data from the local Kinetic Daemon via an HTTP API, verifies all cryptographic signatures locally, enforces strict network security firewalls, and then repackages the decentralized data into a standard legacy DNS packet. Essentially, it allows legacy software to seamlessly interact with a next-generation decentralized network without realizing that anything has changed, bridging the gap between the old internet and the new.


Why Kinetic Needs This

To understand why this file is so large and complex, you have to understand the fundamental difference in trust models between legacy DNS and Kinetic.

In traditional DNS, trust is hierarchical and centralized. When your operating system resolver asks a server for the IP address of saif.kin, the server replies with 192.168.1.5. The operating system trusts this answer blindly because it trusts the server it asked (usually provided by your ISP or Cloudflare). There is no built-in cryptography in standard DNS (excluding DNSSEC, which is complex and centralized). In Kinetic, the network is entirely trustless. Anyone can claim to own any domain if they just broadcast it to the DHT.

Therefore, Kinetic domains are stored as a NameRecord. A NameRecord contains a DnsZone (the actual routing rules) and a cryptographic signature proving ownership.

Caution

If Kinetic simply passed the IP address from the DHT directly to the operating system, the system would be vulnerable to a massive array of catastrophic attacks. A malicious node anywhere in the world could intercept the DHT query and hand us a fake IP address. The OS would blindly accept it, and the user would be instantly phished or exploited.

This file is required because Kinetic needs a rigorous “Trust Boundary” on the local machine before any data is handed to the naive operating system. The resolve_kinetic pipeline acts as that exact boundary. It is a cryptographic bouncer for your network stack. It enforces strict, uncompromising rules before it ever allows the operating system to see a single IP address:

  1. Signature Integrity: It mathematically verifies that the domain data was actually signed by the domain owner.
  2. Identity Authentication: It links the domain’s signing key to a decentralized Kinetic Identity (KID), ensuring the owner is actively authorized to control the domain.
  3. Network Safety (SSRF): It filters out dangerous IP addresses to prevent Server-Side Request Forgery attacks against the user’s local network hardware.

Without this exact process, the Kinetic network would be fundamentally insecure and entirely unusable by standard web browsers.


The Threat Model & The Firewall

Before diving into the mechanical steps of the pipeline, it is crucial to understand the three specific threats this script is designed to neutralize. Every block of code in this file exists to counter one of these vectors.

Threat 1: Malicious DHT Nodes (Memory Exhaustion)

Caution

When this script asks the Daemon for the domain data, the Daemon fetches it from untrusted peers on the DHT. A malicious peer could respond to the DHT query with a 5-Gigabyte text file masquerading as a DNS record. If this script blindly parsed the response into memory, the DNS server would instantly crash from Out-Of-Memory (OOM) errors, taking the user’s internet connection down with it. This file mitigates this by streaming the Daemon API response in chunks and enforcing a strict 100-Kilobyte hard limit.

Threat 2: Domain Hijacking & Key Revocation

Warning

If someone steals your private key, they can sign fake DNS records for your domain. Kinetic solves this by linking domains to Decentralized Identities (KIDs). But the DHT itself does not enforce this link; this script does. By fetching the KID document from the Daemon and verifying that the key used to sign the domain is still actively listed in the controller_keys array, this script ensures that revoked keys cannot be used to hijack domains.

Threat 3: Server-Side Request Forgery (SSRF)

Caution

Because anyone can register a .kin domain, an attacker could register evil.kin and configure its A record to point to 192.168.1.1 (the default IP for most home routers). When you visit evil.kin in your browser, the browser thinks it is talking to a public website, but it is actually sending HTTP requests to your local router’s admin panel. The attacker could use JavaScript to change your router settings. This script completely eliminates this vector by silently dropping any DNS record that points to a local, loopback, or private IP address before the browser ever sees it.


The Lifecycle of a .kin Query

To see how this fits into the broader Kinetic architecture, here is the exact, chronological sequence of events when a user types saif.kin into their browser:

  1. Browser Request: The browser asks the OS networking stack for the IP address of saif.kin.
  2. OS Forwarding: The OS checks its DNS configuration (which the Kinetic app modifies to point to 127.0.0.1:53). It sends a standard UDP DNS query to the local Kinetic DNS server.
  3. Hickory Reception: The Hickory DNS server framework receives the raw UDP packet on port 53 and passes it directly to the resolve_kinetic function.
  4. Cache Lookup: The script checks the in-memory cache to see if we recently resolved this exact domain.
  5. Daemon API Request: If the cache misses, the script acts as an HTTP client and makes a GET request to the local Kinetic Daemon API (/api/resolve/saif.kin).
  6. Daemon DHT Resolution: The Daemon searches the P2P network, finds the cryptographically signed NameRecord, and returns the raw bytes to this script.
  7. Local Verification: The script deserializes the bytes and checks the ed25519 signature on the NameRecord against the current network ID.
  8. Identity Check: If the domain is configured to require E2E Identity, the script makes a second HTTP request to the Daemon (/api/resolve-kid) to verify the identity keys.
  9. Zone Extraction: The script extracts the inner DnsZone structure and looks up the specific requested subdomain (e.g., www or @).
  10. SSRF Firewall Execution: The script passes every returned IP address through the is_ssrf_safe filter.
  11. Packet Assembly: The script bundles the safe, converted records into a legacy Hickory DNS response packet.
  12. OS Delivery: The script sends the final packet back to the OS networking stack via UDP.
  13. Browser Connection: The browser receives the IP address from the OS and establishes a TCP connection to the destination.

This entire multi-layered pipeline happens in milliseconds, completely invisibly.


How It Works: Step-by-Step Mechanics

The resolve_kinetic function is essentially a massive state machine that processes the DNS query through multiple distinct phases of verification, fetching, and translation. Here is exactly how it executes.

Phase 1: Hickory Integration and Reserved Name Interception

-> See: kinetic-dns/src/kinetic_records.rs — Lines 15 to 80

The function signature takes a large number of arguments because it must interface deeply with the hickory_server framework. It receives the raw Request, a ResponseHandler to actually dispatch the final packet, a MessageResponseBuilder to construct the packet headers, and state variables like the Moka cache and the Daemon HTTP client.

The very first action the script takes is checking the requested apex_domain against a hardcoded list of PUBLIC_NAMES. Kinetic reserves certain names that should never hit the DHT network under any circumstances. The most critical of these is localhost. If the user queries localhost.kin or any nested subdomain like api.localhost.kin, the script short-circuits the entire resolution pipeline. It bypasses the cache, bypasses the Daemon API, and immediately constructs an A record pointing to 127.0.0.1 (or a AAAA record pointing to ::1). It then returns this immediately with a NOERROR response code. For any other reserved public names that aren’t localhost, it instantly returns an NXDOMAIN (Not Found) response code. This is a critical security mechanic: it ensures that system-level testing domains cannot be hijacked, spoofed, or impersonated by malicious nodes on the public DHT. It provides a guaranteed, hardcoded fallback for local application development.

Phase 2: Thundering Herd Mitigation via Moka

-> See: kinetic-dns/src/kinetic_records.rs — Lines 86 to 93

If the domain is a standard .kin name, the script leverages a moka::future::Cache. Crucially, it does not use the cache to store the final, parsed DNS records. It caches the raw bytes of the NameRecord payload. It wraps the Daemon API call inside cache.try_get_with(). This specific method is incredibly important for concurrency. If a web browser opens a page that requests 50 different assets from saif.kin simultaneously, the browser will fire 50 parallel DNS queries to the OS. If the script didn’t use try_get_with, it would fire 50 simultaneous HTTP requests to the Daemon, which would fire 50 simultaneous DHT lookups, instantly congesting the local node. By using try_get_with, the cache ensures that only the very first request actually executes the closure to hit the Daemon API. The other 49 requests instantly await the result of the first request, completely eliminating the “thundering herd” problem.

Phase 3: The API Stream and Memory Safety

-> See: kinetic-dns/src/kinetic_records.rs — Lines 94 to 126

When a cache miss occurs, the script acts as a standard HTTP client and sends a GET request to the Daemon API: /api/resolve/{apex_domain}. Notice that the script does not simply call .json() or load the entire response body into memory at once. Instead, it streams the response chunks asynchronously: while let Ok(Some(chunk)) = resp.chunk().await. As it reads the chunks from the stream, it keeps a running count of the total payload bytes. If the payload size ever exceeds 100 Kilobytes (100 * 1024 bytes), it immediately sets a limit_exceeded flag, breaks the loop, and aborts the connection. This prevents the memory exhaustion attacks detailed in the Threat Model section.

Phase 4: Cryptographic Deserialization and Integrity

-> See: kinetic-dns/src/kinetic_records.rs — Lines 131 to 146

Once the raw payload bytes are successfully fetched and the size is verified, they are deserialized back into a kinetic_core::types::NameRecord. At this exact moment, the script has a record in memory, but it does not trust it. It executes: domain_record.verify_signature(kinetic_core::constants::NETWORK_ID). This single line performs an ed25519 signature verification against the public key embedded inside the record. It proves mathematically that the data was signed by the true owner of the domain. It also ensures the signature is valid specifically for the current NETWORK_ID, which prevents replay attacks where an attacker takes a valid record from a testnet and broadcasts it on the mainnet. If the signature fails this cryptographic check, the script actively rejects it and returns a SERVFAIL (Server Failure) response code to the OS. The OS assumes the DNS server is temporarily broken, perfectly shielding the user from the tampered data.

Phase 5: The End-to-End Identity (KID) Authentication

-> See: kinetic-dns/src/kinetic_records.rs — Lines 150 to 223

This is where Kinetic’s decentralized identity system merges with standard DNS. The script parses the verified payload into a DnsZone. It then explicitly checks the apex domain (@) for a special Kinetic-only record called KID(did). If this record exists, it represents a strict rule: “This domain requires End-to-End authorization. Only the cryptographic keys actively listed in this specific KID document are legally allowed to sign this domain.”

To enforce this rule, the script suspends resolution and fires a second HTTP request to the Daemon: /api/resolve-kid/{did}. It downloads the decentralized identity document for that DID in JSON format. It then extracts the base64-encoded public_key from the NameRecord (the exact key that was just used to pass the signature check in Phase 4). It iterates through every key listed in the controller_keys array inside the KID JSON document. If the signing key is found in the array, the domain is fully authenticated. If the key is NOT found in the array, it means the identity has revoked the key, or an attacker has hijacked the domain using an obsolete key. The script immediately logs an E2E Auth Failure and aborts the resolution entirely, returning SERVFAIL.

Phase 6: Subdomain Mathematics and Routing

-> See: kinetic-dns/src/kinetic_records.rs — Lines 226 to 246

DNS queries are highly specific. The browser rarely asks for just the apex saif.kin; it frequently asks for nested subdomains like api.production.saif.kin. The script needs to figure out which string to look up in the internal DnsZone hash map. If the queried domain matches the apex domain exactly, the target subdomain is @. Otherwise, it performs exact string subtraction. It takes api.production.saif.kin, and trims .saif.kin off the end of it, leaving exactly api.production. It then checks the DnsZone map for the api.production key. If api.production does not exist in the zone, the script gracefully falls back and checks if a wildcard * record exists. This fallback allows domain owners to route all unspecified, random subdomains to a default server IP address.

Phase 7: The SSRF Firewall Execution

-> See: kinetic-dns/src/kinetic_records.rs — Lines 265 to 316

Once the script locates the correct list of records for the requested subdomain, it begins looping through them to construct the final response packet. This is where the Server-Side Request Forgery (SSRF) firewall is actively enforced. For every single A (IPv4) and AAAA (IPv6) record, the script extracts the IP address and passes it through kinetic_core::net::is_ssrf_safe. If the IP address is classified as local, loopback, link-local, or part of a private intranet subnet, the script silently drops the record entirely and logs a warning. It applies the exact same rigorous check for CNAME records. It ensures the CNAME target string does not equal localhost, does not end with .local, and does not resolve to a private IP address string. This guarantees, at an architectural level, that .kin domains can only ever route the user to public internet infrastructure.

Phase 8: Record Downgrade and Packet Assembly

-> See: kinetic-dns/src/kinetic_records.rs — Lines 326 to 391

Finally, the script must map Kinetic’s rich, custom decentralized record types into the legacy, primitive types that Hickory and the OS network stack can actually understand. Standard A, AAAA, and CNAME records are passed through directly (assuming they survived the SSRF firewall). However, Kinetic supports custom metadata records like PeerId, KID, and IPFS. Since standard DNS has absolutely no concept of a “PeerId record”, the script dynamically downgrades them into legacy TXT records. A PeerId("12345") record is converted into a standard TXT record containing the literal string "peerid=12345". An IPFS("QmHash") record is converted into a TXT record containing "ipfs=QmHash". This is a brilliant architectural bridge because it allows legacy applications (like standard web crawlers or terminal scripts) to extract Kinetic-specific decentralized metadata simply by performing a standard, ubiquitous TXT lookup on the domain.

Once all records are processed and converted, the MessageResponseBuilder compiles them into a valid byte-aligned DNS packet, sets the response code header to NOERROR, and dispatches it back to the operating system via UDP.


Anatomy of a Record Conversion

Because this file acts as a translator, it is important to understand exactly how the kinetic_core::types::DnsRecord enum variants map to Hickory’s hickory_proto::rr::Record types.

  • DnsRecord::A(ip) -> Maps to RData::A. Requires the query type to be RecordType::A. Passes through the SSRF filter.
  • DnsRecord::AAAA(ip) -> Maps to RData::AAAA. Requires the query type to be RecordType::AAAA. Passes through the SSRF filter.
  • DnsRecord::CNAME(target) -> Maps to RData::CNAME. Interestingly, CNAMEs are returned regardless of what the user asked for (A, AAAA, TXT). The OS resolver receives the CNAME and is responsible for recursively following it.
  • DnsRecord::TXT(txt) -> Maps to RData::TXT. Returned for both TXT and ANY queries.
  • DnsRecord::PeerId(pid) -> Custom Kinetic type. Maps to RData::TXT with the format peerid={pid}.
  • DnsRecord::KID(kid) -> Custom Kinetic type. Maps to RData::TXT with the format kid={kid}.
  • DnsRecord::IPFS(cid) -> Custom Kinetic type. Maps to RData::TXT with the format ipfs={cid}.

Key Pieces

Here is a structured breakdown of the most critical moving parts in this process file.

resolve_kinetic

  • What it is: The massive asynchronous function that handles the entire lifecycle of a single .kin DNS query.
  • Where it lives: kinetic-dns/src/kinetic_records.rs, Lines 15-418
  • Why it matters: It is the primary entry point for all resolution. Every single translation, security check, and network request originates here. Without it, the DNS server is just an empty shell.

The Chunking API Stream Loop

  • What it is: The while let Ok(Some(chunk)) = resp.chunk().await loop that enforces the 100KB payload limit.
  • Where it lives: kinetic-dns/src/kinetic_records.rs, Lines 99-105
  • Why it matters: It prevents network-based memory exhaustion attacks, ensuring the DNS server remains stable regardless of what malicious data the DHT serves.

The Signature Verification Block

  • What it is: The strict cryptographic enforcement call: domain_record.verify_signature(NETWORK_ID).
  • Where it lives: kinetic-dns/src/kinetic_records.rs, Lines 133-146
  • Why it matters: This enforces the fundamental security promise of the entire Kinetic network. The OS does not know how to verify the DHT; this script acts as the cryptographic bodyguard on behalf of the OS.

The KID E2E Matcher

  • What it is: The authorization logic that matches the domain’s signing key against the identity’s controller_keys array.
  • Where it lives: kinetic-dns/src/kinetic_records.rs, Lines 165-177
  • Why it matters: This is how we prove that the person who mathematically signed the domain is actually legally authorized by the KID document they are claiming to represent. It ties infrastructure directly to identity and prevents hijacking via revoked keys.

The Hickory MessageResponseBuilder

  • What it is: The struct used to construct the final outgoing DNS packet (builder.build(...)).
  • Where it lives: kinetic-dns/src/kinetic_records.rs, Lines 382-389
  • Why it matters: It formats the disparate data back into a strict byte structure that complies with decades-old internet RFCs, allowing the operating system to parse it natively.

How This Connects to the Rest of Kinetic

This file sits at the absolute edge of the Kinetic architecture, serving as the bridge to the outside world.

  • Receives from OS: It receives raw DNS queries via the external hickory_server framework.
  • Calls to Daemon: It acts as an HTTP client, making synchronous requests to the local Kinetic Daemon (/api/resolve and /api/resolve-kid). It never speaks to the P2P network directly.
  • CROSS-CRATE: NameRecord and DnsZone — defined and explained in the kinetic-core crate.
  • CROSS-CRATE: is_ssrf_safe — defined and explained in the kinetic-core/net module.

The final output of this script is completely consumed by the operating system’s DNS resolver, which then hands the translated IP addresses up to web browsers, terminal applications like curl, or any other legacy network software.


Quick Reference

When you need to remember exactly how the .kin resolution pipeline operates, here is the fast summary table:

  • Localhost Override: localhost.kin always resolves locally to 127.0.0.1. It never hits the network or the daemon API.
  • Data Source: Fetches from /api/resolve/{domain} on the local daemon. The Moka cache is checked first to prevent redundant API calls via try_get_with.
  • Size Limit: API payloads are strictly capped at 100KB to prevent memory exhaustion and buffer bloat.
  • Validation 1 (Crypto): The NameRecord signature must be mathematically valid for the current NETWORK_ID.
  • Validation 2 (Identity): If a KID record exists at the apex, the signing pubkey must exactly match one of the keys in the KID document’s controller_keys array.
  • Validation 3 (SSRF): A, AAAA, and CNAME records must not point to local, private, or loopback IP addresses.
  • Custom Records: PeerId, KID, and IPFS records are dynamically downgraded into standard TXT records so legacy applications can still read them.
  • Failures: Invalid cryptography yields SERVFAIL. Blocked SSRF attempts are silently dropped. No matching records yields NXDOMAIN.

Open Questions / Things to Revisit

  • CNAME Recursive Lookups: Currently, if a .kin domain has a CNAME record, this script returns the literal CNAME string back to the OS. The OS resolver is then responsible for doing a second lookup to resolve that CNAME into an IP address. We should verify if legacy OS resolvers correctly follow CNAMEs that point to other decentralized .kin domains, or if they drop them because they are not standard global TLDs.
  • Subdomain Edge Cases: The string manipulation used to extract subdomains (domain_name.trim_end_matches) works perfectly for standard queries, but might behave unpredictably if a malicious user queries an extremely malformed string with trailing dots, duplicate apexes, or illegal characters. We should consider using a more robust DNS label parser.
  • Cache Invalidation: The Moka cache currently holds the resolved payloads for a set duration based on time. If a user updates their domain on the DHT, the DNS server will serve stale data until that cache naturally expires. We may want the daemon to actively push cache invalidation events to the DNS server via a webhook or unix socket, rather than relying on passive time-to-live expiration.
  • API Error Handling Granularity: If the Daemon API is down or returns a 500 Internal Server Error, the script currently returns a generic SERVFAIL to the OS. We might want to differentiate between “The DHT could not find this record” and “The Daemon HTTP server crashed” in our local logging for easier debugging during local development.

Project: kinetic-atlas

Stage: 16 — The Multi-TLD Proxy Daemon

Reading Time: 25 mins


What Is This?

kinetic-atlas is a standalone daemon that acts as a universal HTTP proxy bridge. While the standard kinetic-daemon only knows about the .kin network, Atlas dynamically routes traffic to any Kinetic network fork (e.g., .alt, .game, .local) based on a central registry.

It allows ISPs or homelab users to run a single daemon that provides resolution and P2P proxying for the entire Kinetic ecosystem.


Architecture

  1. GitHub-backed Registry: Atlas configurations for different TLDs are distributed as .json files hosted in a GitHub repository (kinetic-atlas).
  2. Dynamic Swarm Management: It spawns full libp2p nodes dynamically on-demand for whatever TLD is requested.
  3. HTTP Proxy: Intercepts standard web traffic, determines the target TLD, and routes the P2P proxy request through the correct libp2p swarm.
  4. Auto-Updater: Periodically syncs the registry from GitHub, adding new TLDs without a daemon restart.

Files

  • 02_registry_swarms.md — The TldRegistry loader and on-demand SwarmManager.
  • 03_proxy_updater.md — The HTTP proxy interceptor and the background auto-updater.

Registry Loading and Dynamic Swarms

#![allow(unused)]
fn main() {
**Files:** `registry.rs`, `swarm_manager.rs`
**Crate:** kinetic-atlas | **Stage:** 16
}

registry.rs — The TLD Mapping

TldRegistry loads .json configuration files from the local networks/ directory. Each JSON defines a Kinetic fork: its TLD (e.g. .alt), its network_id (used for isolating libp2p and signatures), its bootstrap nodes, and an optional seed domain.

Filtering:

Applies the global whitelist and blacklist arrays from atlas.json. If a network is blacklisted, it is skipped entirely.

PAC Integration:

Note

When a TLD is loaded, Atlas creates a JSON file in the OS local app data directory (e.g., ~/.local/share/kinetic_pac/proxies/atlas_<tld>.json). The kinetic-pac daemon watches this directory and automatically configures system-wide proxy routing so that the OS knows to send traffic for that TLD to the Atlas port.


swarm_manager.rs — On-Demand Networking

Atlas does not start libp2p swarms for every TLD on startup. If you have 500 TLDs in the registry, starting 500 swarms would crash the machine.

The On-Demand Pattern:

  • get_or_spawn_swarm(tld): If a swarm for the TLD is already running, it returns the existing communication channel (mpsc::Sender<NetworkCommand>).
  • If not, it spawns a new KineticAtlasNode in a background Tokio task.
  • Bootstrapping: If the registry entry has a seed_domain (e.g., seed.alt.network), it resolves the Kintree Merkle DNS tree to dynamically find P2P bootstrap IPs before starting the node.

Garbage Collection:

  • start_garbage_collector() runs a background loop every 60 seconds.
  • It iterates through the active swarms and checks their last_used timestamp.
  • Any swarm that has not routed traffic for 10 minutes is gracefully shut down.

Proxy Server and Auto-Updater

#![allow(unused)]
fn main() {
**Files:** `proxy.rs`, `updater.rs`
**Crate:** kinetic-atlas | **Stage:** 16
}

proxy.rs — The Universal Bridge

Runs an Axum HTTP server that intercepts traffic sent by the OS (configured via PAC or manual proxy settings).

Request Flow:

  1. Extraction: Parses the Host header to determine the target domain (e.g., saif.alt).
  2. TLD Lookup: Extracts the TLD (.alt) and asks the TldRegistry if it’s supported. If not, returns 403.
  3. Swarm Retrieval: Asks SwarmManager to get_or_spawn_swarm(".alt"). This guarantees a live libp2p node for the specific network fork.
  4. DHT Resolution: Sends a NetworkCommand::GetRecord to the swarm to look up saif.alt on the DHT.

Important

5. Signature Verification: Crucially, it validates the DHT record’s cryptographic signature using the specific network_id configured for .alt in the registry. A record signed with the default .kin network ID is rejected.

  1. Proxying / IPFS Routing: If the record contains a P2P peer address, it sends a NetworkCommand::SendProxyRequest. If the record is a DnsRecord::IPFS(cid), it automatically routes the request to an IPFS HTTP gateway.
  2. Response Streaming: Receives the remote peer or IPFS gateway’s response and streams it back to the local browser via standard HTTP.

updater.rs — Zero-Downtime Sync

Kinetic forks are created by the community. To ensure users can access new forks immediately, Atlas implements an auto-updater.

  1. start_auto_updater() spawns a background loop running every 24 hours (with a 1-hour exponential backoff interval during failures). It silently disables itself if registry_url or updater_public_key are missing from the configuration.
  2. It hits the GitHub API (configured via registry_url in atlas.json) to list all .json files in the central repository.
  3. It downloads any new or updated configuration files, as well as their corresponding .sig files.

Important

4. It cryptographically verifies the Ed25519 signature of each configuration file against the updater_public_key. Unsigned or invalid files are rejected.

  1. Valid files are written to the local networks/ folder, and registry.load_from_dir() is called to refresh the internal map.
  2. Because swarms are spawned dynamically on-demand by the SwarmManager, the daemon never needs to restart to support a new TLD. As soon as the updater writes the JSON, the TLD is immediately resolvable.

Crate: kinetic-host

Stage: 13

Reading Time: 45 mins

Depends On: kinetic-core, kinetic-network, kinetic-storage


What Is This?

Note

kinetic-host is the binary for domain owners who want to publicly serve content through the Kinetic network. Unlike the infrastructure node (kinetic-node) which is a passive DHT relay, the host is an active content server — it receives real HTTP traffic from Kinetic users and forwards it to a local web backend.


How It Differs From the Other Binaries

Featurekinetic-nodekinetic-hostkinetic-daemon
RoleDHT backboneContent serverUser client
P2P identityStatic Ed25519Dual: static host key + ephemeral PoWEphemeral PoW
Serves HTTPNoYes — reverse proxies to local backendNo (proxy client only)
User-facing API/health, /peer_id/health, /peer_idFull registration API
DNS resolverNoNoYes
mDNSDisabledConfig-drivenConfig-driven
API port160031600416000

What Makes It Uniquely Complex

  1. Dual-key architecture: Has both a static long-lived host key (host.key) and an ephemeral epoch-bound PoW keypair. The host key is the permanent identity visible to clients. The PoW key is the active libp2p peer identity for Sybil resistance.
  2. Hot-swap network loop: When the Drand epoch advances and the current PoW key expires, the host mines a new keypair and restarts the entire NetworkEventLoop without downtime — traffic continues flowing during the swap.
  3. HostRoutingRecord publishing: Every 30 seconds, publishes a signed record to the DHT mapping its permanent host key → current ephemeral Peer ID. Clients look up the host key and use this record to find the current P2P address.
  4. Reverse proxy: Receives ProxyRequest messages over libp2p, forwards them to a local HTTP server, and returns the response over the same P2P channel.

Files

  • 02_main.md — Boot sequence with focus on dual identity + hot-swap wiring
  • 03_heartbeat.md — PoW hot-swap and HostRoutingRecord publisher (most unique code)
  • 04_proxy.md — P2P reverse proxy: path validation, body size limits, backend forwarding
  • 05_gossip_service_api.md — Governance gossip (simpler than node), service manager, health API

Boot Sequence: Dual Identity and Hot-Swap Wiring

File: kinetic-host/src/main.rs Crate: kinetic-host | Stage: 13


What’s Unique Here (Skip What’s Already Documented)

Service management, governance key validation, Sled storage init, Drand client init — all identical to kinetic-node (docs/learn/node/02_main.md). Skip those. What’s unique:


Dual-Key Identity — The Core Difference

Important

The infrastructure node has one identity (static). The host has two.

Step 1: Load the static host key

-> See: kinetic-host/src/main.rs — Lines 140–143

#![allow(unused)]
fn main() {
let key_path = get_base_dir().join("host.key");
let host_key = identity::load_or_generate_host_key(&key_path);
let host_peer_id = PeerId::from_public_key(&host_key.public());
}

host.key is the permanent identity. It is registered in the DNS zone record by the domain owner (host_id field in HostRoutingRecord). It never changes. Clients look up a .kin domain, find the PeerId in the zone, then use that PeerId to look up the current HostRoutingRecord to find the actual live address. This indirection is what allows the ephemeral key to rotate without breaking connectivity.


Step 2: Mine the ephemeral PoW keypair

-> See: kinetic-host/src/main.rs — Lines 145–159

#![allow(unused)]
fn main() {
let local_key = tokio::task::spawn_blocking(move || {
    kinetic_network::pow::mine_sybil_keypair(initial_drand_kyn, POW_DIFFICULTY_BITS)
}).await?;
}

mine_sybil_keypair() is CPU-intensive (mining PoW). It is run in spawn_blocking to avoid blocking the Tokio async executor. The resulting keypair is the active libp2p identity used in the NetworkEventLoop. Its Peer ID will change every Drand epoch.

Note

Unlike the node, the host’s local_key is always freshly mined at startup — never loaded from disk. This is by design: the PoW keypair is only valid for the current epoch anyway.


P2P Port from Environment Variable

-> See: kinetic-host/src/main.rs — Lines 161–164

#![allow(unused)]
fn main() {
let p2p_port = std::env::var(ENV_HOST_P2P_PORT)
    .unwrap_or_else(|_| config.network.host_port.to_string())
    .parse::<u16>()
    .unwrap_or(config.network.host_port);
}

The host uses ENV_HOST_P2P_PORT env var as a first override for its port, then falls back to config. This allows running multiple hosts on the same machine (for multi-domain hosting) by setting different port env vars without editing the config file.


Incoming Proxy Channel

-> See: kinetic-host/src/main.rs — Lines 217, 249–254

#![allow(unused)]
fn main() {
let (incoming_tx, incoming_rx) = tokio::sync::mpsc::channel(32);
}

An mpsc channel with capacity 32 sits between the network event loop and the proxy handler. When the network loop receives a ProxyRequest from a remote client (via libp2p RequestResponse), it sends the request + response channel into incoming_tx. The proxy handler receives from incoming_rx, forwards to the local backend, and sends the response back over the libp2p channel.

The capacity of 32 is intentional — it provides backpressure. If the local backend is slow and 32 requests pile up, the network loop blocks sending into the channel, applying TCP backpressure upstream to the requesting P2P peer.


Backend Configuration From Environment

-> See: kinetic-host/src/main.rs — Lines 242–248

#![allow(unused)]
fn main() {
let backend_port = std::env::var(ENV_HOST_BACKEND_PORT)
    .unwrap_or_else(|_| "80".to_string())
    .parse::<u16>()
    .unwrap_or(80);
let backend_host = std::env::var(ENV_HOST_BACKEND_HOST)
    .unwrap_or_else(|_| config.daemon.bind_ip.clone());
}

Two env vars control where the proxy forwards traffic:

  • ENV_HOST_BACKEND_PORT — the port of the local web server (default: 80).
  • ENV_HOST_BACKEND_HOST — the IP of the local web server (default: configured bind_ip).

This means the “backend” can be on a different machine in the same LAN, not just localhost.


Hot-Swap Wiring — The Arc<Mutex<JoinHandle>>

-> See: kinetic-host/src/main.rs — Lines 232–234

#![allow(unused)]
fn main() {
let network_loop_handle = Arc::new(tokio::sync::Mutex::new(tokio::spawn(async move {
    network_loop.run().await;
})));
}

The JoinHandle of the network loop is wrapped in Arc<tokio::sync::Mutex<...>> and passed to the start_drand_heartbeat task. When the epoch expires, the heartbeat acquires the mutex, calls handle.abort() to kill the old loop, mines a new keypair, and replaces the handle with a new spawned loop. The Arc allows the handle to be shared between main and the heartbeat task.

Warning

The tokio::sync::Mutex (not std::sync::Mutex) is used because handle.abort() and the new tokio::spawn happen inside an async context. Using a blocking std::sync::Mutex inside an async context risks deadlocking the executor.


Spawned Tasks Summary

TaskStarted InPurpose
network_loop.run()mainCore libp2p event loop
gossip::start_gossip_listener()mainGovernance gossip → disk
proxy::handle_incoming_proxy_requests()mainP2P → local HTTP backend
heartbeat::start_dynamic_routing_publisher()mainPublishes HostRoutingRecord every 30s
heartbeat::start_drand_heartbeat()mainMonitors epoch, hot-swaps PoW key

Cross-Crate Connections

  • kinetic_network::pow::mine_sybil_keypair(kyn, bits): CPU-intensive PoW mining that produces a keypair whose Peer ID satisfies the Sybil resistance constraint for the given Drand epoch.
  • kinetic_network::NetworkMode::FullNode: Binds to 0.0.0.0 publicly, identical to kinetic-node.
  • kinetic_core::constants::ENV_HOST_P2P_PORT: Env var key for the P2P port override.
  • kinetic_core::constants::ENV_HOST_BACKEND_PORT / ENV_HOST_BACKEND_HOST: Env vars for the reverse proxy backend.

PoW Epoch Hot-Swap and HostRoutingRecord Publisher

File: kinetic-host/src/heartbeat.rs Crate: kinetic-host | Stage: 13


What Is This?

The most unique code in kinetic-host. Two background tasks run here:

  1. start_dynamic_routing_publisher() — Every 30 seconds, signs and publishes a HostRoutingRecord to the DHT, telling clients “I am still alive and my current ephemeral Peer ID is X.”
  2. start_drand_heartbeat() — Every 3 seconds, fetches the latest Drand beacon, checks if the current PoW key is still valid for this epoch, and if not: aborts the old network loop, mines a new keypair, and restarts the loop with zero downtime.

There is nothing equivalent to this in kinetic-daemon or kinetic-node. This is the host’s core Sybil-resistance mechanism.


How start_dynamic_routing_publisher Works

-> See: kinetic-host/src/heartbeat.rs — Lines 10–53

Why this is needed

The host’s active P2P identity (its libp2p Peer ID) changes every Drand epoch. Clients looking up saif.kin find a PeerId in the DNS zone record — but that Peer ID is the static host key, not the ephemeral one. Without a routing record, clients have no way to find the current ephemeral peer to connect to.

HostRoutingRecord bridges this gap: it maps (static host PeerId) → (current ephemeral PeerId, drand_kyn, signature).


Key conversion chain (Ed25519 format bridging)

-> See: kinetic-host/src/heartbeat.rs — Lines 18–26

libp2p uses its own Keypair type. ed25519_dalek is needed to produce the actual signature. The conversion chain:

  1. publisher_host_key.try_into_ed25519() → libp2p’s internal ed25519::Keypair.
  2. .to_bytes() → raw 64-byte representation.
  3. ed25519_dalek::SigningKey::try_from(&ed_bytes[0..32]) → dalek key using only the 32-byte private key portion.

The dalek signing key is constructed once outside the loop and reused on every interval tick — no redundant key parsing overhead.


The record and signature

-> See: kinetic-host/src/heartbeat.rs — Lines 33–51

#![allow(unused)]
fn main() {
let mut record = HostRoutingRecord {
    host_id: host_peer_id_str.clone(),  // permanent static Peer ID
    current_peer_id: local_peer_id_str.read()...clone(), // current ephemeral Peer ID
    drand_kyn,          // current beacon round number
    signature: vec![],  // filled after signing
};
let signature = dalek_kp.sign(&record.signable_bytes(NETWORK_ID));
record.signature = signature.to_bytes().to_vec();
}

The current_peer_id is read from the Arc<RwLock<String>> shared with the hot-swap heartbeat. After each hot-swap, the heartbeat writes the new Peer ID into this shared string. The next tick of the publisher will automatically pick up the updated ID.


How start_drand_heartbeat Works (The Hot-Swap)

-> See: kinetic-host/src/heartbeat.rs — Lines 61–162

Tick interval and data flow

Runs every 3 seconds. Calls hb_drand.fetch_latest(). If the result is a fresh (non-cached, non-unavailable) kyn, sends it through drand_kyn_tx (the watch channel that all tasks share).


Staggered epoch check

-> See: kinetic-host/src/heartbeat.rs — Lines 86–93

#![allow(unused)]
fn main() {
let current_epoch = kinetic_network::pow::get_staggered_epoch(
    &hb_local_peer_id.to_bytes(),
    kyn.kyn,
);
let needs_validation = match last_verified_epoch {
    Some(epoch) => epoch != current_epoch,
    None => true,
};
}

get_staggered_epoch() computes a per-peer-id staggered epoch from the kyn. “Staggered” means different peers rotate their keys at different kyn offsets — not all simultaneously. This prevents a network-wide reconnection storm when a Drand epoch boundary passes.

last_verified_epoch caches the last epoch we validated against. On epoch change, needs_validation becomes true and we re-check PoW validity.


PoW validity check

-> See: kinetic-host/src/heartbeat.rs — Lines 97–108

#![allow(unused)]
fn main() {
let pow_valid = tokio::task::spawn_blocking(move || {
    kinetic_network::pow::is_valid_sybil_pow(
        &peer_id_clone, kyn_round, POW_DIFFICULTY_BITS,
    )
}).await.unwrap_or(false);
}

PoW validation is also CPU-bound. Offloaded to spawn_blocking. Returns false if the current Peer ID no longer satisfies the PoW constraint for the new epoch.


The hot-swap sequence (zero-downtime restart)

-> See: kinetic-host/src/heartbeat.rs — Lines 109–154

When pow_valid is false:

1. Mine a new keypair (offloaded, CPU-bound):

#![allow(unused)]
fn main() {
let current_local_key = tokio::task::spawn_blocking(move || {
    kinetic_network::pow::mine_sybil_keypair(kyn_round, POW_DIFFICULTY_BITS)
}).await.unwrap_or_else(|_| Keypair::generate_ed25519());
}

If mining panics (extremely unlikely), falls back to a random Ed25519 key so the process keeps running.

2. Update shared Peer ID:

#![allow(unused)]
fn main() {
if let Ok(mut lock) = shared_peer_id.write() {
    *lock = hb_local_peer_id.to_string();
}
}

The routing publisher will pick up the new ID on its next 30-second tick.

3. Abort the old network loop and replace it:

#![allow(unused)]
fn main() {
let mut handle = loop_handle_ref.lock().await;
handle.abort();  // Kill the old libp2p swarm
// Create new NetworkEventLoop with the new key
if let Ok((new_client, new_loop)) = NetworkEventLoop::new(..., current_local_key, ...) {
    hc_client.update_backend(new_client.get_sender(), new_client.stream_control());
    *handle = tokio::spawn(async move { new_loop.run().await; });
}
}

Important

hc_client.update_backend() is the critical step: it replaces the internal sender and stream control inside the NetworkClient that all other tasks (proxy handler, routing publisher) hold references to. This means those tasks don’t need to be restarted — they continue using the same NetworkClient struct, which now routes through the new swarm.


Quick Reference

FunctionIntervalWhat It Does
start_dynamic_routing_publisher30 secondsSigns + publishes HostRoutingRecord to DHT
start_drand_heartbeat3 secondsFetches Drand kyn, checks PoW validity, hot-swaps if expired

Note

Key concept: The hot-swap is zero-downtime because NetworkClient::update_backend() swaps the internal channel sender in-place. No other task needs to be told about the change.

Tip

Staggered epochs: Peers rotate at different kyn offsets (derived from their Peer ID bytes) to avoid a network-wide reconnection storm.

P2P Reverse Proxy

File: kinetic-host/src/proxy.rs Crate: kinetic-host | Stage: 13


What Is This?

The reverse proxy is what makes the host actually serve content. When a Kinetic user’s daemon makes a P2P ProxyRequest to saif.kin, that request travels over libp2p to this host’s running process. proxy.rs receives it, translates it into a standard HTTP request to the local web server, and routes the HTTP response back over P2P.

This is the “server side” of the proxy pair — kinetic-daemon/src/proxy/ is the “client side” that sends requests.


How It Works

handle_incoming_proxy_requests — the main loop

-> See: kinetic-host/src/proxy.rs — Lines 83–149

Receives (ProxyRequest, ResponseChannel<ProxyResponse>) tuples from the incoming_rx mpsc channel. For each request, spawns a new Tokio task so multiple requests are handled concurrently (not sequentially).

Path validation (before any HTTP call):

#![allow(unused)]
fn main() {
let decoded_path = percent_encoding::percent_decode_str(&req.path)
    .decode_utf8()
    .unwrap_or(...);
if decoded_path.contains("..") || !decoded_path.starts_with('/') {
    // Return 400 Bad Request
}
}

Warning

Percent-decodes the path first (to catch %2E%2E as ..), then checks for path traversal attempts. Paths not starting with / are also rejected.

Body size check:

#![allow(unused)]
fn main() {
if req.body.len() > LIMITS_PROXY_MAX_BODY_BYTES {
    // Return 413 Payload Too Large
}
}

Important

Checked before calling forward_request. Prevents the local backend from receiving oversized P2P payloads.


forward_request — HTTP translation

-> See: kinetic-host/src/proxy.rs — Lines 10–76

Constructs a reqwest HTTP request from the ProxyRequest:

  • Parses req.method as an HTTP method, defaults to GET if unrecognized.
  • Copies all headers from the P2P request except Host (stripped and replaced with the local backend’s host:port). Without stripping, the backend would see Host: saif.kin and reject the request.
  • Sets req.body as the HTTP body.

Response body streaming with size cap:

#![allow(unused)]
fn main() {
let mut stream = res.bytes_stream();
while let Some(chunk_res) = stream.next().await {
    body.extend_from_slice(&chunk);
    if body.len() > LIMITS_PROXY_MAX_BODY_BYTES {
        // Truncate, set status = 502
        break;
    }
}
}

The backend response is streamed in chunks. If the total exceeds LIMITS_PROXY_MAX_BODY_BYTES (5MB), the body is discarded and a 502 is returned. This prevents the host from relaying gigabyte responses over P2P.

Error → 502: If reqwest fails to connect to the backend at all (backend not running), returns 502 Bad Gateway with an error message including the failed port number.


reqwest::Client configuration

#![allow(unused)]
fn main() {
reqwest::Client::builder().no_proxy().build().unwrap_or_default()
}

Note

.no_proxy() prevents the HTTP client from accidentally routing through the system proxy (which on a host machine might be Kinetic’s own PAC). Without this, you’d get infinite loops.


Tests

proxy_handles_chaotic_requests_gracefully (proptest):

-> See: kinetic-host/src/proxy.rs — Lines 157–179

Generates random HTTP methods, paths, and bodies and calls forward_request() pointing at port 65534 (guaranteed dead). Asserts:

  • Never panics.
  • Always returns 502 (since the backend is dead).
  • Response body contains "Bad Gateway".

Full integration tests are in proxy_tests.rs (a separate file, 427 lines) — those spin up a real Axum backend server, send real P2P-format requests through forward_request, and assert the correct HTTP response is mirrored back.


Quick Reference

CheckWhereResponse on Fail
Path traversal (..)Before HTTP call400 Bad Request
Path doesn’t start with /Before HTTP call400 Bad Request
Request body too largeBefore HTTP call413 Payload Too Large
Backend unreachableIn forward_request502 Bad Gateway
Backend response too largeDuring streaming502 Payload Too Large
Host header from P2PIn forward_requestStripped + replaced

Governance Gossip, Service Manager, and Health API

Files: gossip.rs, service.rs, api.rs (+ bin/browser_gateway.rs, bin/ping_proxy.rs) Crate: kinetic-host | Stage: 13


gossip.rs — Simpler Than the Node Version

The host’s governance gossip handler is a stripped-down version of the node’s (docs/learn/node/04_gossip.md).

Key differences from kinetic-node/src/gossip.rs:

  • No storage writes. The node writes NameRecord::Premium to Sled on PremiumNameGranted. The host does not — it has no DHT authority to inject records.
  • No spawn_blocking for disk saves. The host’s save_to_disk is called directly inside the gossip loop (still holding the GLOBAL_GOVERNANCE_STATE lock). This is simpler but means the disk write is synchronous within the async loop.
  • No lagged message handling. The node explicitly handles RecvError::Lagged. The host uses while let Ok(...) = gossip_rx.recv().await — it simply exits the loop if the channel closes or on any error.
  • Filter is the same: Only GOSSIP_TOPIC_GOVERNANCE is processed. Other gossip topics (Drand, etc.) are ignored. -> See: kinetic-host/src/gossip.rs — Lines 10–40

Fuzz test: doesnt_panic_on_garbage_gossip — passes random bytes directly to serde_json::from_slice::<SignedGovernanceMessage>. Proves parsing never panics. -> See: kinetic-host/src/gossip.rs — Lines 42–55


service.rs — Standard Service Manager

Identical pattern to kinetic-node/src/service.rs and the daemon’s service management. Uses <dyn ServiceManager>::native() to register as kinetic-host with the OS service manager (systemd/launchd/SCM).

One host-specific detail: during install, it passes username: std::env::var("SUDO_USER").ok().or_else(|| Some("nobody".to_string())) — so if you run sudo kinetic-host install, it installs the service to run as the invoking user rather than root. -> See: kinetic-host/src/service.rs — Lines 1–90


api.rs — Health API at Port 16004

Two routes, same as kinetic-node but for the host:

  • GET /health"OK"
  • GET /peer_id → the static host Peer ID (not the ephemeral PoW one)

Port: 16004 (node uses 16003, daemon uses 16000).

start_health_api(host_peer_id, bind_ip) binds the Axum server and runs with with_graceful_shutdown(shutdown_signal()).

The peer_id endpoint returns the static host key’s Peer ID — the permanent identity. Monitoring tools use this to verify which host they’re checking, and it’s the same Peer ID that clients look up in the DNS zone record.


bin/browser_gateway.rs — Browser WebSocket Gateway (184 lines)

A standalone binary (not the main host binary). Provides a WebSocket-to-HTTP bridge so web browsers can make requests into the Kinetic P2P network without needing a local daemon installed.

Key behavior:

  • Binds a WebSocket server locally.
  • When a browser connects and sends a request, it forwards it through the P2P network via NetworkClient::send_proxy_request().
  • Replies to the browser with the ProxyResponse received back over P2P.

This is an experimental entrypoint for browser-native Kinetic access. It is not started by the main kinetic-host binary.


bin/ping_proxy.rs — P2P Proxy Latency Tester (139 lines)

Another standalone utility binary. Sends test ProxyRequest messages through the P2P network to a target host and measures round-trip latency. Used for diagnosing connectivity and measuring proxy performance.

Not started by the main binary. Run manually: kinetic-ping-proxy <target_host_peer_id>.


Quick Reference

FileLinesRole
gossip.rs56Governance gossip → disk (no Sled writes unlike node)
service.rs90OS service manager wrapper
api.rs~30/health + /peer_id at port 16004
bin/browser_gateway.rs184Experimental: WebSocket → P2P bridge
bin/ping_proxy.rs139Utility: P2P proxy latency tester

Crate: kinetic-pac

Stage: 11

Reading Time: 60 mins

Depends On: kinetic-core, kinetic-daemon (running at port 16000)


What Is This?

kinetic-pac is the Proxy Auto-Configuration server for the Kinetic network. It serves a dynamically generated JavaScript PAC file at http://127.0.0.1:16001/proxy.pac, and injects this URL into the OS-level proxy settings so all browsers and network-aware applications automatically route .kin traffic through the local Kinetic HTTP proxy.

A PAC (Proxy Auto-Config) file is a JavaScript function that browsers and OSes call for every HTTP request: “given this URL and hostname, should I go direct or use a proxy?” Kinetic generates this function dynamically based on whatever TLD proxies are currently registered.


Key Pieces

  1. SavedState + lockfile: Captures the original OS proxy settings before modification and writes them atomically to proxy_active.lock. If the daemon crashes, the next launch restores the old settings before applying new ones.
  2. ProxyConfigurator trait + OS implementations: Platform-specific code (KdeConfigurator, GnomeConfigurator, MacosConfigurator, WindowsConfigurator) that shells out to gsettings, kwriteconfig5, networksetup, or PowerShell to inject the PAC URL into the OS network settings.
  3. PacManager: The lifecycle controller. Calls save_previous_state → atomic lockfile write → install. Reverses on uninstall.
  4. The PAC HTTP server: An Axum server at http://127.0.0.1:16001 with a single /proxy.pac route. On each request, it scans the proxies/ directory for registered JSON proxy descriptors and generates a fresh JavaScript PAC function.
  5. Graceful shutdown: A tokio::spawn background task listens for SIGTERM (Unix) or Ctrl+C and calls pac_manager.uninstall() before std::process::exit(0).

How to Read This Stage

  • Start with 02_pac_main_1.md to understand SavedState, ProxyConfigurator, PacManager, and crash recovery.
  • Read 03_pac_main_2.md to see the PAC file generation loop and the shutdown signal handling.
  • Finish with 04_pac_os.md to understand each platform’s concrete proxy injection commands.

PAC Server Part 1: State Management, PacManager, and Boot

File: kinetic-pac/src/main.rs — Lines 1–280 Crate: kinetic-pac | Stage: 11 Reading Time: 25 minutes


1. What Is This?

The first half of main.rs defines everything that makes OS-level proxy injection safe and reversible. It defines the core types and the PacManager — the struct responsible for atomically installing and cleanly removing the Kinetic PAC URL as the system-wide proxy autoconfiguration.

Three things are defined here that underpin the entire crate:

  1. SavedState: A snapshot of the OS proxy settings before Kinetic modified them. Serialized to disk so it survives crashes.
  2. ProxyConfigurator trait: The abstract interface that each platform (Linux, macOS, Windows) implements independently.
  3. PacManager: The lifecycle controller. It calls the configurator, manages the lockfile, and ensures that if the process is killed mid-run, the previous proxy state can always be recovered.

2. Why Kinetic Needs This

Warning

When the PAC server runs, it must modify the OS-wide network proxy settings. These are global, persistent system settings — not local to the application. If the PAC server crashes or is killed with SIGKILL, the OS will be left with Kinetic’s PAC URL permanently set. The user’s internet traffic would then route through a dead local proxy, breaking all network access.

Note

SavedState + the lockfile pattern solves this completely. Before overwriting the OS settings, the original settings are captured and written to proxy_active.lock on disk. The next time the process starts, it reads that lockfile, restores the old settings, then applies the new ones. This means a crashed daemon automatically cleans up its own mess on the next launch.

The ProxyConfigurator trait is needed because there is no cross-platform API for “set the system proxy”. Each OS does it differently — Linux via gsettings or kwriteconfig5, macOS via networksetup, Windows via PowerShell registry edits.


3. How It Works

SavedState — the crash recovery contract

#![allow(unused)]
fn main() {
-> See: `kinetic-pac/src/main.rs` — Lines 31–39
}

SavedState is a simple struct that gets serialized to JSON and written to disk. It holds:

  • previous_pac_url: The PAC URL that was configured before Kinetic changed it (if any).
  • proxy_type: For GNOME/KDE, the mode string (e.g., 'none', 'auto', "2").
  • macos_services: A HashMap<String, String> mapping each macOS network service name to its previously configured PAC URL. macOS has multiple network services (Wi-Fi, Ethernet, VPN) and each must be independently saved and restored.

ProxyConfigurator trait — the platform interface

#![allow(unused)]
fn main() {
-> See: `kinetic-pac/src/main.rs` — Lines 45–69
}

Four methods:

  • install(&self, pac_url: &str): Sets the OS proxy to the Kinetic PAC URL.
  • uninstall(&self): Removes the Kinetic PAC URL from OS settings.
  • save_previous_state(&self): Reads the current OS proxy config and returns it as a SavedState.
  • restore_state(&self, state: &SavedState): Takes a previously saved SavedState and writes it back to the OS.

The trait requires Send + Sync because the PacManager holding the boxed configurator must be shareable across the Tokio shutdown task.

FallbackConfigurator — the safe default

#![allow(unused)]
fn main() {
-> See: `kinetic-pac/src/main.rs` — Lines 75–99
}

When running on an unrecognized OS (or in a headless server environment with no desktop), FallbackConfigurator is used. Its install() simply prints a warning message telling the user to configure their proxy manually. All other methods are no-ops.

This prevents the PAC server from crashing just because it can’t automatically inject proxy settings — it degrades gracefully.

detect_configurator() — runtime platform dispatch

#![allow(unused)]
fn main() {
-> See: `kinetic-pac/src/main.rs` — Lines 103–120
}

Inspects std::env::consts::OS at runtime and returns the correct boxed ProxyConfigurator:

  • "linux" → calls detect_linux_configurator() (in os/linux.rs), which further inspects XDG_CURRENT_DESKTOP.
  • "macos" → returns Box::new(MacosConfigurator) (inside #[cfg(target_os = "macos")] guard).
  • "windows" → returns Box::new(WindowsConfigurator) (inside #[cfg(target_os = "windows")] guard).
  • Everything else → Box::new(FallbackConfigurator).

The #[cfg] guards inside this function ensure that even if the OS string says "macos" at runtime on a cross-compiled binary, the macOS struct isn’t compiled in for non-macOS targets.

PacManager — the lifecycle controller

#![allow(unused)]
fn main() {
-> See: `kinetic-pac/src/main.rs` — Lines 123–180
}

PacManager holds a Box<dyn ProxyConfigurator> and the path to the lockfile (proxy_active.lock).

PacManager::install(pac_url) — the exact sequence:

  1. Check for existing lockfile. If proxy_active.lock exists, a previous run may have left the OS in a modified state. The code reads the lockfile using serde_json::from_reader and immediately calls restore_state() to clean up. This is the crash recovery in action — even if the previous run was killed, this restores the original settings before overwriting them again.
  2. Save current OS state. Calls configurator.save_previous_state() to capture whatever the OS currently has configured.
  3. Atomically write the lockfile. The saved state is written to a .tmp file first, then std::fs::rename() moves it to proxy_active.lock. rename() is atomic on POSIX systems — the file either exists completely or not at all. There is no window where a half-written lockfile can be read by a concurrent process.
  4. Apply the new PAC URL. Calls configurator.install(pac_url) to write the Kinetic PAC URL to the OS network settings.
#![allow(unused)]
fn main() {
-> See: `kinetic-pac/src/main.rs` — Lines 141–158
}

PacManager::uninstall() — the exact sequence:

  1. Check if lockfile exists.
  2. If yes, read SavedState from the lockfile and call restore_state() to write the original user settings back to the OS.
  3. If the lockfile is missing or unreadable, fall back to calling configurator.uninstall() which sets a safe default (no proxy).
  4. Delete the lockfile.
#![allow(unused)]
fn main() {
-> See: `kinetic-pac/src/main.rs` — Lines 164–179
}

Service management (install/uninstall/start/stop)

#![allow(unused)]
fn main() {
-> See: `kinetic-pac/src/main.rs` — Lines 221–279
}

Identical in pattern to kinetic-dns. Uses <dyn ServiceManager>::native() to register the binary with the OS service manager:

  • install_service(): Registers the binary with autostart: true.
  • uninstall_service(): Deregisters it, then also explicitly calls pac_manager.uninstall() to strip the OS proxy setting — so uninstalling the service also cleans up the proxy config.

4. Key Pieces

struct SavedState

The serializable snapshot of OS proxy state. Written to disk before any modification. The key to making this entire system crash-recoverable.

#![allow(unused)]
fn main() {
-> See: `kinetic-pac/src/main.rs` — Lines 31–39
}

trait ProxyConfigurator

The platform-agnostic interface for OS proxy management. The Send + Sync bounds allow the concrete implementor to be stored in the PacManager and shared across threads.

#![allow(unused)]
fn main() {
-> See: `kinetic-pac/src/main.rs` — Lines 45–69
}

struct PacManager

The high-level controller. Manages the install/uninstall lifecycle with crash recovery via the atomic lockfile.

#![allow(unused)]
fn main() {
-> See: `kinetic-pac/src/main.rs` — Lines 123–180
}

fn detect_configurator() -> Box<dyn ProxyConfigurator>

The runtime OS detector. Returns the correct platform-specific implementor, wrapped in Box<dyn ProxyConfigurator> so the PacManager doesn’t need to know the concrete type.

#![allow(unused)]
fn main() {
-> See: `kinetic-pac/src/main.rs` — Lines 103–120
}

5. Cross-Crate Connections

  • kinetic_core::constants::NETWORK_ID: Used to form the ServiceLabel (e.g., kinetic-pac) and the directory name for the proxies/ registry folder.
  • os/linux.rs: Contains detect_linux_configurator(), KdeConfigurator, GnomeConfigurator.
  • os/macos.rs: Contains MacosConfigurator (compiled only on target_os = "macos").
  • os/windows.rs: Contains WindowsConfigurator (compiled only on target_os = "windows").
  • error.rs: Defines PacError — the custom error type returned by all ProxyConfigurator methods.

6. Quick Reference

TypePurpose
SavedStateSerialized original OS proxy state, written to lockfile
ProxyConfiguratorTrait abstracting OS-specific proxy commands
FallbackConfiguratorNo-op for unsupported environments
PacManagerLifecycle controller with atomic lockfile
detect_configurator()Returns correct ProxyConfigurator for current OS

Lockfile path: <data_local_dir>/kinetic_global/proxy_active.lock Atomic write: .tmprename().lock Crash recovery: On next install() call, reads lockfile and restores old state first


7. Open Questions

Important

  • SIGKILL race on first install: Between save_previous_state() and writing the lockfile, a SIGKILL leaves no lockfile and no way to restore automatically. Only mitigated by never having a gap where the OS is modified but the lockfile isn’t written — the current code writes the lockfile before calling install().

Note

  • Multi-user environments: The lockfile path is in data_local_dir() which is per-user. If the daemon runs as a system service (not per-user), the lockfile location may not be accessible during restore.

Warning

  • Headless Linux servers: XDG_CURRENT_DESKTOP is empty. The FallbackConfigurator is used. No automatic proxy injection. The user must configure their proxy manually.

8. Rust Concepts

ConceptDescription
Box<dyn Trait>A heap-allocated trait object. Used here so PacManager can hold any ProxyConfigurator implementor without knowing its concrete type. The dynamic dispatch overhead is negligible — proxy installation happens once per boot.
std::fs::rename() for atomic writesOn POSIX systems, rename() is guaranteed to be atomic. The old file path is atomically replaced by the new one. This prevents partial writes to the lockfile from corrupting the crash recovery data.
serde_json::from_readerReads and deserializes JSON directly from a File handle without loading the entire file into memory as a String first. More efficient for structured config files.
Send + Sync on trait boundsRequired to store Box<dyn ProxyConfigurator> in a struct that will be moved into a Tokio async task. Without these bounds, the compiler would reject the code at the point where the PacManager is moved into the shutdown handler.

The PAC HTTP Server and Service Lifecycle

Crate: kinetic-pac Stage: N/A (Supplemental to core Kinetic) Reading time: 20 minutes Depends on: 01_overview.md, 02_pac_main_1.md


What Is This?

This file covers the second half of the kinetic-pac daemon implementation. It specifically covers lines 231 through 461 in kinetic-pac/src/main.rs. The primary focus of this file is the dynamic server loop. This loop stays alive in the background to serve the Proxy Auto-Configuration (PAC) file. It serves this file directly to the host operating system.

It also thoroughly covers the daemon’s service management lifecycle commands. These commands dictate how kinetic-pac registers itself as a native background service. They explain how it gracefully handles uninstalls. Crucially, they explain how it manages termination events. This ensures the user’s internet connection does not break when the daemon stops.

Rather than relying on a static .pac file stored somewhere on the user’s hard drive, kinetic-pac launches an active HTTP web server. This server is bound to 127.0.0.1:16001. This server generates the PAC file on-the-fly. It does this every single time the operating system’s network stack requests it.

This is a fundamental architectural decision for the Kinetic ecosystem. It allows the system to respond instantly to changes in the peer-to-peer network. This includes new nodes coming online. This includes nodes disconnecting. This includes new Top Level Domains (TLDs) being registered. It does all this without ever needing to forcibly reload a static file on the OS side. Every time a browser resolves a new domain, it fetches the most recent set of proxy rules directly from this HTTP server.


Why Kinetic Needs This

If the Kinetic network only operated by writing a static proxy.pac file to your hard drive, it would be severely handicapped. This is because of how operating systems handle proxy configurations. Both Windows and macOS aggressively cache PAC files. If you point the OS to a static file file:///C:/proxy.pac, the OS will read it once. It will load those rules into memory. It will then refuse to read the file again unless the networking adapter is restarted. Alternatively, the user would have to manually toggle their proxy settings off and back on.

When a new peer joins the Kinetic network, it might start hosting a new decentralized domain. The static PAC file would remain entirely unaware of this. The operating system would be completely blind to those real-time network changes. The user would not be able to resolve the new .kinetic domains until they rebooted their network connection.

Kinetic needs a reliable mechanism to push real-time routing changes. These changes must be pushed to the operating system’s proxy resolver seamlessly. They must be pushed transparently.

By running a very lightweight background HTTP server, kinetic-pac sidesteps the OS caching problem entirely. By instructing the OS to fetch the PAC rules from an HTTP URL (http://127.0.0.1:16001/proxy.pac), the OS recognizes that the configuration is dynamic. Whenever the OS needs to make a network request, it pings the local HTTP server. It asks for the latest routing instructions.

The run_server loop intercepts that request. It reads the current state of the Kinetic network from the local filesystem. It instantly compiles a Javascript PAC script. This script is tailored exactly to that split-second snapshot of the network.

Warning

Furthermore, this daemon directly manipulates operating system-level internet settings. Because of this, stability is a paramount concern. If the kinetic-pac daemon were to crash, it would cause major issues. If it were forcefully closed, it would leave the OS proxy settings pointing to a dead local port. The user’s entire internet connection would be broken. Their browser would try to route traffic to 127.0.0.1:16001. It would find nothing there. It would drop the connection.

This specific section of the codebase provides the crucial lifecycle hooks. This includes Unix signal handlers. This includes background service management routines. These hooks guarantee that when the Kinetic node shuts down, the OS proxy settings are restored to their default state. This saves the user from a catastrophic network failure.


How It Works

The lifecycle of the kinetic-pac server can be broken down into five major phases. These execute sequentially when the daemon starts in the background and runs its continuous loop.

1. Directory Initialization and Setup

#![allow(unused)]
fn main() {
-> See: crates/kinetic-pac/src/main.rs — Lines 285 to 293
}

Before the server can begin serving any PAC files, it needs a source of truth. It needs to establish a central source of truth for the network’s state. It identifies the user’s local application data directory. It does this using the dirs::data_local_dir() function. Depending on the operating system, this could be AppData/Local on Windows. It could be ~/Library/Application Support on macOS.

It then appends kinetic_global. It creates this base directory if it doesn’t already exist. Inside that folder, it creates a proxies subdirectory.

This folder serves as the central communication bridge. It bridges the core Kinetic network daemons and the PAC server. When other parts of Kinetic successfully establish a tunnel, they act. When they discover a new decentralized peer, they act. They write a simple .json file into this proxies folder. The PAC server, in turn, will read these files. It uses them to understand the current topology of the network.

2. OS Settings Injection

#![allow(unused)]
fn main() {
-> See: crates/kinetic-pac/src/main.rs — Lines 294 to 297
}

Once the directories are prepared, the server initializes the PacManager struct. This manager handles OS-specific registry editing and system configuration.

The daemon then defines the pac_url. It defines it as http://127.0.0.1:16001/proxy.pac. It immediately calls pac_manager.install(pac_url). This is the critical moment where control of the network is handed over. This function call injects the local HTTP URL directly into the Windows Registry. Or, it injects it into the macOS SystemConfiguration framework. From this exact line onward, the host operating system is dependent on this background daemon. It depends on it for all internet routing decisions.

3. The Axum HTTP Router and Security Filtering

#![allow(unused)]
fn main() {
-> See: crates/kinetic-pac/src/main.rs — Lines 299 to 308
}

To handle the incoming HTTP requests from the OS proxy resolver, Kinetic uses axum. axum is a highly performant asynchronous web framework built by the Tokio team.

It defines a single HTTP GET route. This route listens at /proxy.pac.

Important

Because this server binds to a port on the user’s machine, security is a major concern. We absolutely do not want other devices on the same local Wi-Fi network attempting to query the PAC server. We do not want them discovering the internal proxy configurations.

To prevent this, the route handler acts immediately. The very first thing it does is extract the HTTP Host header. It extracts this from the incoming request. It parses the header. It splits off any port numbers. It checks the raw hostname. If the request does not originate from localhost, it fails. If it does not originate from 127.0.0.1, it fails. If it does not originate from the IPv6 loopback [::1], it fails. The server immediately terminates the request. It returns a 403 Forbidden error. This guarantees that only the host operating system running the daemon can read the PAC rules.

4. Dynamic PAC Script Generation

#![allow(unused)]
fn main() {
-> See: crates/kinetic-pac/src/main.rs — Lines 309 to 396
}

If the incoming request passes the security check, the server begins work. It begins constructing a Javascript string in memory. The standard PAC script specification dictates a specific format. The file must contain a specific Javascript function named FindProxyForURL(url, host). The operating system executes this Javascript function for every single network request.

The server begins the script generation by scanning. It scans the kinetic_global/proxies directory. It looks for any files ending in .json.

For each JSON file it finds, it opens it. It reads the contents as a string. It deserializes it into a RegisteredProxy struct using serde_json. It validates the data. It ensures that the proxy_ip field is a valid IP address. If it encounters corrupted or malformed data, it logs a warning. It simply skips the file. This prevents a single bad file from breaking the entire proxy script.

The code then categorizes the proxy. It identifies whether the proxy is a “Native” Kinetic proxy (a direct peer). Or, it identifies if it is an “Atlas” proxy (a specialized relay). It does this by checking the JSON filename. It checks if it starts with the prefix atlas_.

The proxies are grouped together in a HashMap. The key is the Top Level Domain (TLD), such as .kinetic. This hash map uses a tuple. The tuple holds (Option<NativeProxy>, Option<AtlasProxy>). This ensures that for any given domain, the system knows exactly which primary and backup relays are available.

Once the hash map is populated, the code iterates over every discovered TLD. It begins writing the actual Javascript proxy instructions. For each TLD, it constructs a complex proxy fallback string. The fallback chain is constructed as follows:

  • First attempt: PROXY <native_ipv4>:<port>
  • Second attempt: PROXY <native_ipv6>:<port>
  • Third attempt: PROXY <atlas_ipv4>:<port>
  • Fourth attempt: PROXY <atlas_ipv6>:<port>
  • Final fallback: DIRECT

This specific string format is crucial. It tells the operating system’s browser what to do. It says: “Try the Native proxy first.” “If it fails, try the Atlas proxy.” “If both of them fail, bypass the proxy entirely.” “Try connecting directly.”

This string is then injected into a Javascript conditional statement. It uses the PAC shExpMatch function. It writes two identical rules for each domain. This handles variations in trailing dots:

  • Rule 1: if (shExpMatch(host, "*.tld")) return "PROXY_STRING";
  • Rule 2: if (shExpMatch(host, "*.tld.")) return "PROXY_STRING";

Finally, after all TLD rules are written into the script, it adds a final catch-all statement. It places this at the very end of the function: return "DIRECT"; This ensures that all standard internet traffic completely bypasses the Kinetic network. Traffic like google.com or github.com routes normally.

The server sets the HTTP Content-Type header. It sets it to application/x-ns-proxy-autoconfig. It returns the generated Javascript to the OS.

5. Graceful Shutdown via Signal Handlers

#![allow(unused)]
fn main() {
-> See: crates/kinetic-pac/src/main.rs — Lines 414 to 432
}

Because kinetic-pac actively altered the host operating system’s network settings in Step 2, it cannot simply exit when closed. If it just abruptly terminated, the OS would keep trying to reach the PAC file at port 16001. It would fail. It would subsequently drop all internet traffic.

To prevent this, the server spawns a dedicated background Tokio task. Its sole purpose is to listen for operating system interrupt signals.

On Unix systems (like macOS or Linux), it registers a listener for SIGTERM. Across all platforms, it registers a listener for standard Ctrl+C interrupts. It uses the tokio::select! macro to wait for any of these signals concurrently.

When a termination signal is received, the background task acts. It blocks the actual shutdown of the program. It immediately calls pac_manager.uninstall(). This goes into the Windows Registry or macOS System Preferences. It completely removes the local HTTP PAC URL. It restores the network settings to their original state. Only after this cleanup is complete does the process call std::process::exit(0). This allows the daemon to safely die. This mechanism guarantees that the user’s internet is not accidentally bricked when they close the application.


Key Pieces

run_server()

#![allow(unused)]
fn main() {
-> See: crates/kinetic-pac/src/main.rs — Lines 281 to 436
}

This is the core execution loop of the entire PAC daemon. When invoked, it initializes the tracing subsystem for logging. It creates the required filesystem directories. It mutates the OS network configurations. It binds the high-performance HTTP listener to 127.0.0.1:16001. It runs continuously as a background process until explicitly terminated.

install_service() and uninstall_service()

#![allow(unused)]
fn main() {
-> See: crates/kinetic-pac/src/main.rs — Lines 231 to 261
}

These crucial management functions leverage the service_manager crate. They register kinetic-pac as a native, persistent background daemon on the host OS. On Linux, this creates a systemd unit. On macOS, it creates a launchd plist file. On Windows, it creates a native Service. The uninstall_service function is particularly robust. It ensures that it manually wipes the OS PAC settings via PacManager::uninstall(). It does this just in case the service crashed or was forcefully killed previously.

The Route Handler Closure

#![allow(unused)]
fn main() {
-> See: crates/kinetic-pac/src/main.rs — Lines 301 to 407
}

This enormous async closure is passed to the axum::routing::get() method. It dictates exactly what happens every single time the OS proxy resolver pings port 16001. It is responsible for the critical Host header security check. It performs the synchronous filesystem I/O to read the proxies directory. It parses the JSON data. It executes the fallback logic. It formats the final raw Javascript string.

The Signal Hook (tokio::select!)

#![allow(unused)]
fn main() {
-> See: crates/kinetic-pac/src/main.rs — Lines 419 to 424
}

This is an advanced asynchronous flow control pattern in Rust. It waits for multiple async events concurrently. It only executes the code block for whichever event finishes first. It listens for either a Unix termination signal or a standard Ctrl+C interrupt. Whichever fires first immediately triggers the cleanup block. This prevents the program from terminating ungracefully.


How This Connects to the Rest of Kinetic

This specific section of the codebase serves as the absolute final consumer of the proxy data within the entire Kinetic ecosystem.

  • Reads from: It continuously monitors the kinetic_global/proxies directory on the local filesystem.
  • Written by: Other completely independent Kinetic daemons (such as kinetic-core or the network layer) are responsible. They make connections and write the .json files into this directory.
  • Controls: It has direct, unchallenged control over the host operating system’s network stack. The Javascript string generated in this file dictates routing. It dictates, on a request-by-request basis, whether a web browser’s traffic routes securely. It decides if it goes into the decentralized Kinetic network. Or, it decides if it bypasses it to the regular internet.

This architecture is deliberately decoupled. The kinetic-pac daemon does not need to know how the network layer works. It does not communicate with the rest of the node via RPC channels. It does not use websockets. It does not use shared memory. It simply blindly serves whatever JSON files happen to exist in the shared folder at that specific millisecond. This makes the proxy layer incredibly resilient to crashes in the networking stack.


Quick Reference

  • Server Bind Address: 127.0.0.1:16001
  • Exposed Route: GET /proxy.pac
  • Primary Data Source: ~/.local/share/kinetic_global/proxies/*.json
  • Security Mechanism: Rejects non-loopback Host headers with a strict 403 Forbidden response.
  • Javascript Output: Dynamically constructs and returns a FindProxyForURL(url, host) function. This maps specific decentralized TLDs to PROXY command strings.
  • Proxy Fallback Chain: Prioritizes Native nodes. It falls back to Atlas relays. It defaults to DIRECT connection.
  • Cleanup Routine: Automatically uninstalls the PAC configuration from the OS registry. It does this upon receiving SIGTERM or Ctrl+C.

Open Questions / Things to Revisit

Note

  • Filesystem I/O on Every Single Request: Currently, the axum route handler executes std::fs::read_dir every single time. It executes this when the operating system requests the PAC file. On high-traffic networks where the OS polls the PAC file frequently, this is a problem. Some operating systems do this every few seconds. This could cause disk thrashing or elevated CPU usage. It would be much more performant to introduce a debounced in-memory cache. Or, use a crate like notify to actively watch the directory for filesystem changes. This is better than synchronously polling the disk inside the async HTTP handler.

Warning

  • Blocking File System Calls in an Async Context: The handler relies on std::fs::read_to_string to parse the JSON files inside an axum async route. In the Tokio runtime ecosystem, utilizing blocking standard library I/O operations inside an asynchronous handler is bad practice. It can unintentionally stall the underlying async executor thread. This logic should be migrated to tokio::fs::read_to_string. This prevents blocking the worker pool.

Important

  • Service Name Hardcoding Fragility: The background service label is generated dynamically. It is generated as format!("{}-pac", kinetic_core::constants::NETWORK_ID). If the NETWORK_ID constant ever changes in the future, older installed services might become orphaned. They would be left running perpetually on the user’s OS. This is because the uninstaller would be looking for a different service label name.

OS Proxy Injection: Linux, macOS, and Windows Configurators

Files: kinetic-pac/src/os/linux.rs, os/macos.rs, os/windows.rs, os/mod.rs Crate: kinetic-pac | Stage: 11 Reading Time: 35 minutes


1. What Is This?

These files contain the actual OS-integration code — the shell commands and registry edits that make the browser route .kin traffic through the Kinetic proxy without any user configuration.

Each file implements the ProxyConfigurator trait for its target platform:

  • linux.rs: Two implementations — KdeConfigurator (using kwriteconfig5 and kreadconfig5) and GnomeConfigurator (using gsettings). Plus detect_linux_configurator() which reads XDG_CURRENT_DESKTOP to choose between them.
  • macos.rs: MacosConfigurator (using networksetup) that sets the PAC URL on every detected network service.
  • windows.rs: WindowsConfigurator (using PowerShell) that writes directly to the Windows registry’s Internet Settings key.

All four methods of the trait (install, uninstall, save_previous_state, restore_state) are implemented for each platform.


2. Why Kinetic Needs This

A PAC file works only if the OS knows about it. Writing a perfect PAC file and serving it at http://127.0.0.1:16001/proxy.pac accomplishes nothing if the browser or OS doesn’t know to fetch it from there.

Each platform has a completely different mechanism for storing this configuration:

  • GNOME stores it in a DConf database accessed via gsettings CLI.
  • KDE Plasma stores it in ~/.config/kioslaverc written via kwriteconfig5 CLI.
  • macOS stores per-service proxy settings queried and set via the networksetup binary.
  • Windows stores it in the user’s registry at HKCU:\Software\Microsoft\Windows\CurrentVersion\Internet Settings.

There is no cross-platform library that abstracts these four mechanisms, so Kinetic shells out to the native tools directly.


3. How It Works — Linux (GNOME)

GnomeConfigurator::install(pac_url)

#![allow(unused)]
fn main() {
-> See: `kinetic-pac/src/os/linux.rs` — Lines 174–191
}

Two gsettings set commands in sequence:

Command 1: Sets the proxy mode to auto. This tells GNOME’s network manager to use a PAC file instead of manual proxy settings or no proxy.

gsettings set org.gnome.system.proxy mode 'auto'

Command 2: Sets the autoconfiguration URL to the Kinetic PAC endpoint.

gsettings set org.gnome.system.proxy autoconfig-url 'http://127.0.0.1:16001/proxy.pac'

Once these two settings are applied, GNOME-based browsers (and most GTK apps) will automatically fetch the PAC file and route .kin traffic through the Kinetic proxy.

GnomeConfigurator::save_previous_state()

#![allow(unused)]
fn main() {
-> See: `kinetic-pac/src/os/linux.rs` — Lines 202–235
}

Runs gsettings get for both mode and autoconfig-url. The output is captured from stdout using .output() (not .status()) so the return value can be stored.

The result is trimmed of whitespace and filtered for empty strings. The autoconfig-url is also filtered for '' (single-quoted empty string, which is what gsettings returns when no URL is set). This prevents storing a garbage value in the SavedState that would later be “restored” as a proxy URL.

GnomeConfigurator::uninstall()

#![allow(unused)]
fn main() {
-> See: `kinetic-pac/src/os/linux.rs` — Lines 193–199
}

Sets mode back to 'none'. Does not touch the autoconfig-url key — it’s not needed since the mode overrides it.

GnomeConfigurator::restore_state()

#![allow(unused)]
fn main() {
-> See: `kinetic-pac/src/os/linux.rs` — Lines 238–254
}

Writes back mode first, then optionally the autoconfig-url. If there was no previous autoconfig-url, it explicitly sets it to '' to clear any value Kinetic left.


4. How It Works — Linux (KDE Plasma)

KdeConfigurator::install(pac_url)

#![allow(unused)]
fn main() {
-> See: `kinetic-pac/src/os/linux.rs` — Lines 10–47
}

Three commands:

Command 1: Sets ProxyType to 2 in kioslaverc. In KDE’s proxy system, type 0 = no proxy, 1 = manual proxy, 2 = PAC script URL. This switches KDE into PAC mode.

kwriteconfig5 --file kioslaverc --group "Proxy Settings" --key ProxyType 2

Command 2: Writes the PAC URL to the Proxy Config Script key.

kwriteconfig5 --file kioslaverc --group "Proxy Settings" --key "Proxy Config Script" <url>

Command 3: Sends a D-Bus signal to KIO/Scheduler to reload its slave configuration. Without this, KDE applications that are already running won’t pick up the change until they restart.

dbus-send --type=signal /KIO/Scheduler org.kde.KIO.Scheduler.reparseSlaveConfiguration string:''

The D-Bus call failure is deliberately ignored (let _ = ...) because KDE applications might not be running (headless or server environments).

KdeConfigurator::save_previous_state()

#![allow(unused)]
fn main() {
-> See: `kinetic-pac/src/os/linux.rs` — Lines 76–124
}

Uses kreadconfig5 to read back both ProxyType and Proxy Config Script. This is the read counterpart to kwriteconfig5. If ProxyType can’t be read, it defaults to "0" (no proxy) so restore always has a valid value.

KdeConfigurator::restore_state()

#![allow(unused)]
fn main() {
-> See: `kinetic-pac/src/os/linux.rs` — Lines 126–167
}

Writes ProxyType back from the saved value, then optionally restores Proxy Config Script. Always sends the D-Bus reload signal at the end so running KDE applications pick up the change immediately.

detect_linux_configurator()

#![allow(unused)]
fn main() {
-> See: `kinetic-pac/src/os/linux.rs` — Lines 259–278
}

Reads the XDG_CURRENT_DESKTOP environment variable and lowercases it. Matches against known desktop environment names:

  • Contains kde or plasmaKdeConfigurator
  • Contains gnome, unity, or budgieGnomeConfigurator
  • Anything else → FallbackConfigurator

The fallback covers headless servers, Sway/Hyprland (Wayland-only), Cinnamon, XFCE, and any other environment without an automatic proxy API.


5. How It Works — macOS

The networksetup approach — multiple services

#![allow(unused)]
fn main() {
-> See: `kinetic-pac/src/os/macos.rs` — Lines 11–27
}

macOS is more complex than Linux because it has multiple distinct “network services” — Wi-Fi, Ethernet, USB Ethernet, VPN adapters. Each service has its own independent proxy configuration. Setting the PAC URL on only the Wi-Fi service would break it when the user switches to Ethernet.

MacosConfigurator::install() runs networksetup -listallnetworkservices first to get the full list of services. It then loops over every service that doesn’t start with * (disabled services are prefixed with * in the output) and calls:

networksetup -setautoproxyurl "<service name>" "http://127.0.0.1:16001/proxy.pac"

Each call is fire-and-forget with let _ = ... because some services may not support proxy settings (VPN tunnels often reject this command).

MacosConfigurator::save_previous_state()

#![allow(unused)]
fn main() {
-> See: `kinetic-pac/src/os/macos.rs` — Lines 47–83
}

Loops over all services again. For each service, runs:

networksetup -getautoproxyurl "<service>"

The output contains both the URL and whether it’s enabled:

URL: http://some-existing-pac.com
Enabled: Yes

The code checks url_str.contains("Enabled: Yes") — only saves the URL if the PAC was actually enabled before Kinetic touched it. This prevents recording a PAC URL that was already configured but disabled.

Saves everything into a HashMap<String, String> keyed by service name.

MacosConfigurator::restore_state()

#![allow(unused)]
fn main() {
-> See: `kinetic-pac/src/os/macos.rs` — Lines 85–98
}

Calls self.uninstall() first (disables PAC on all services), then iterates the macos_services hashmap and re-enables PAC on each service that previously had it, using both -setautoproxyurl (to set the URL) and -setautoproxystate ... on (to re-enable it).


6. How It Works — Windows

Registry-based PAC injection

#![allow(unused)]
fn main() {
-> See: `kinetic-pac/src/os/windows.rs` — Lines 11–30
}

Windows stores proxy settings in the user’s HKCU registry hive, not in a config file. The key path is:

HKCU:\Software\Microsoft\Windows\CurrentVersion\Internet Settings

Step 1: Uses PowerShell Set-ItemProperty to write AutoConfigURL:

Set-ItemProperty -Path 'HKCU:\...' -Name AutoConfigURL -Value 'http://127.0.0.1:16001/proxy.pac'

Step 2: Sets ProxyEnable to 0. This disables any manual HTTP proxy that might have been configured. When AutoConfigURL is set, Windows uses the PAC file regardless of ProxyEnable, but disabling manual proxy prevents conflicts.

WindowsConfigurator::uninstall()

#![allow(unused)]
fn main() {
-> See: `kinetic-pac/src/os/windows.rs` — Lines 32–42
}

Uses Remove-ItemProperty with -ErrorAction SilentlyContinue to delete the AutoConfigURL key entirely. The SilentlyContinue flag prevents PowerShell from failing if the key doesn’t exist.

WindowsConfigurator::save_previous_state()

#![allow(unused)]
fn main() {
-> See: `kinetic-pac/src/os/windows.rs` — Lines 44–68
}

Runs Get-ItemProperty to read the current AutoConfigURL value. Uses String::from_utf8_lossy() (not from_utf8()) because PowerShell output can sometimes contain BOM characters or locale-specific encoding on older Windows versions. The lossy variant replaces invalid UTF-8 sequences with replacement characters rather than returning an error.


7. Key Pieces

KdeConfigurator

Implements the full 4-method ProxyConfigurator contract using kwriteconfig5/kreadconfig5. Uses D-Bus to notify running KDE apps of the change.

#![allow(unused)]
fn main() {
-> See: `kinetic-pac/src/os/linux.rs` — Lines 7–168
}

GnomeConfigurator

Uses gsettings with the org.gnome.system.proxy schema. Two keys: mode (the proxy type) and autoconfig-url (the PAC URL).

#![allow(unused)]
fn main() {
-> See: `kinetic-pac/src/os/linux.rs` — Lines 171–255
}

detect_linux_configurator()

Reads XDG_CURRENT_DESKTOP and dispatches to the right configurator. Returns a Box<dyn ProxyConfigurator> so the caller has a uniform interface.

#![allow(unused)]
fn main() {
-> See: `kinetic-pac/src/os/linux.rs` — Lines 259–278
}

MacosConfigurator

Loops over all macOS network services and applies proxy settings to each one individually. Saves per-service state so each service is restored to exactly its original configuration.

#![allow(unused)]
fn main() {
-> See: `kinetic-pac/src/os/macos.rs` — Lines 10–99
}

WindowsConfigurator

Uses PowerShell to write/read/delete the AutoConfigURL registry key. Uses from_utf8_lossy for robust output parsing.

#![allow(unused)]
fn main() {
-> See: `kinetic-pac/src/os/windows.rs` — Lines 10–84
}

8. Cross-Crate Connections

  • main.rs (SavedState, ProxyConfigurator, PacError, FallbackConfigurator): All OS configurators are children of main.rs types. They reference SavedState as their state snapshot type and PacError as their error type.
  • error.rs (PacError): The error type used in all Result<(), PacError> returns from the OS commands. Contains variants like PacError::Command(String) for subprocess failures.

9. Quick Reference

PlatformTool UsedProxy Setting Location
GNOME/UnitygsettingsDConf schema org.gnome.system.proxy
KDE Plasmakwriteconfig5 / D-Bus~/.config/kioslaverc
macOSnetworksetupPer-service system preference
WindowsPowerShellHKCU:\...\Internet Settings\AutoConfigURL
Other/HeadlessFallbackConfiguratorNo-op, manual user config required

macOS difference: Must loop over ALL network services. Single-service injection leaves other interfaces without the PAC rule.

KDE difference: Requires a D-Bus signal after writing the config file to notify running applications.

Windows difference: PAC URL is a registry value, not a file. PowerShell is the most portable way to write it without a Windows-specific Rust crate.


10. Open Questions

  • XFCE/Cinnamon/Sway: These environments are not detected and fall through to FallbackConfigurator. Users on Sway (Wayland compositor) or XFCE need to manually configure their browser’s proxy.
  • macOS VPN adapters: The networksetup -setautoproxyurl call on VPN services typically fails silently. There is no way to verify success without re-reading the value.
  • Windows UAC: Writing to HKCU (current user) does not require elevation. But if Kinetic is run as a service under SYSTEM, the HKCU hive targeted is the SYSTEM hive, not the logged-in user’s. This would silently fail to configure the correct user’s proxy.
  • Chrome on Linux: Chrome/Chromium on Linux reads proxy settings from GNOME or KDE depending on flags. But if launched with --no-system-proxy, it ignores all of this. The PAC injection has no effect on such invocations.

11. Rust Concepts

  • Command::new(...).output() vs .status(): .status() waits for the subprocess and returns only the exit code. .output() captures both stdout/stderr and the exit code. Used in save_previous_state() where the result of the command needs to be read back.
  • String::from_utf8_lossy(&bytes): Converts bytes to string, replacing invalid UTF-8 sequences with \u{FFFD} instead of returning an error. Critical for Windows PowerShell output which may contain BOMs or non-UTF8 on older systems.
  • #[cfg(target_os = "macos")] on the struct definition: The struct MacosConfigurator itself is only compiled on macOS. The detect_configurator() function uses a second #[cfg] inside the "macos" match arm to conditionally return it. This is belt-and-suspenders: the function can only reach that code path when compiled for macOS.
  • let _ = Command::new(...).status(): The leading let _ = explicitly discards the Result. Used for D-Bus signals and non-critical cleanup commands where failure is acceptable and should not propagate.
  • String::from_utf8(o.stdout).ok().and_then(...): A chain of fallible conversions. .ok() converts Result to Option. .and_then() applies a function to the inner value if it exists, short-circuiting to None on any failure. Cleaner than nested match for sequential conversions.

Crate: kinetic-cli

Stage: 14 — The User-Facing Terminal Interface

Reading Time: 30 mins


What Is This?

The kinetic binary is the user’s command center. It doesn’t run any background services itself — it talks to the already-running kinetic-daemon over HTTP (with Bearer token auth) and delegates process management to other binaries (kinetic-daemon, kinetic-node, etc.).


Command Map

kinetic
├── setup [firefox]          # First-run wizard: generates seed + identity
├── seed init | restore      # BIP-39 seed phrase management
├── name
│   ├── register <name>      # Two-phase PoW + VDF + ML-DSA registration
│   ├── renew <name>         # Renew expiring name
│   ├── publish <name>       # Publish/update DNS zone records
│   └── query <name>         # Resolve a name via daemon API
├── identity
│   ├── create               # Generate ML-DSA-65 KID keypair
│   ├── publish              # Publish KID + capability manifest
│   ├── resolve <did>        # Resolve a did:kin from the network
│   ├── revoke               # Revoke a KID
│   └── rotate-key           # Rotate a KID's controller key
├── governance broadcast     # Broadcast a pre-signed governance JSON
├── dns-tree generate        # Build Merkle DNS tree zone file
├── clock [-l]               # Show Kinetic Network Time (daemon or offline)
├── daemon  install|start|stop|status|logs  # Manage kinetic-daemon
├── host    install|start|stop|status|logs  # Manage kinetic-host
├── node    install|start|stop|status|logs  # Manage kinetic-node
├── dns     install|start|stop|status|logs  # Manage kinetic-dns (sudo)
└── pac     install|start|stop|status|logs  # Manage kinetic-pac

Architecture

Important

Auth: All daemon API calls use a Bearer token read from <base_dir>/tokens/admin.token.

  • HTTP client: Built once per command in utils::build_client(timeout_secs), pre-seeded with the Bearer header.

Note

No service logic: The CLI never manages P2P, storage, or cryptographic state directly — it delegates everything to the daemon API or shells out to the right binary.


Files

  • 02_main_utils.md — Entry point, command dispatch, utils.rs helpers
  • 03_seed_setup.md — BIP-39 seed phrase init/restore, setup wizard, Firefox CA injection
  • 04_name.md — Name registration (two-phase commitment + VDF), renew, publish, query
  • 05_identity.md — KID creation, publish, revoke, rotate
  • 06_service_governance_misc.md — Service delegator, governance broadcast, clock, dns-tree

Entry Point and Utilities

Files: main.rs, utils.rs, commands/mod.rs Crate: kinetic-cli | Stage: 14


main.rs — Dispatch

main() parses the Commands enum with clap, loads KineticConfig, then dispatches. Two patterns:

Commands that need the daemon API — build the authenticated HTTP client first:

#![allow(unused)]
fn main() {
let client = utils::build_client(30)?;
commands::name::handle_name_command(cmd, &config, &client).await?;
}

Commands that manage sub-binaries — call handle_service_command:

#![allow(unused)]
fn main() {
Commands::Dns { cmd } => {
    let bin = format!("{}-dns", NETWORK_ID);
    handle_service_command(&bin, cmd, true).await?; // true = needs sudo
}
}

Note

Commands::Setup and Commands::Seed don’t need the client (they work fully offline — no daemon required).

verify_cli() test: uses clap’s debug_assert() to catch structural bugs (missing required args, conflicting flags) at test time.

#![allow(unused)]
fn main() {
-> See: `kinetic-cli/src/main.rs` — Lines 100–111
}

utils.rs — Three Helpers

build_client(timeout_secs) -> Result<Client>

Important

Reads <base_dir>/tokens/admin.token, inserts it as Authorization: Bearer <token> in default headers, builds a reqwest::Client with the given timeout. Called once per command invocation — not reused across commands.

#![allow(unused)]
fn main() {
-> See: `kinetic-cli/src/utils.rs` — Lines 68–80
}

parse_and_format_api_error(context, status, body) -> String

Tries serde_json::from_str::<kinetic_core::ApiError>(body). On success: "[<code>] <context>: <detail>". On fail (HTML error pages, plain text): "<context>: HTTP <status> - <body>". Used everywhere an API call can return a non-2xx.

#![allow(unused)]
fn main() {
-> See: `kinetic-cli/src/utils.rs` — Lines 13–23
}

save_zone_file(fqdn, zone) -> Result<()>

Validates the apex name via is_valid_apex_name(), then writes <zones_dir>/<fqdn>.json as pretty-printed JSON. Used by name register and name publish to cache the zone locally.

#![allow(unused)]
fn main() {
-> See: `kinetic-cli/src/utils.rs` — Lines 32–47
}

commands/mod.rs — The Commands Enum

All top-level subcommands live here. Daemon, Host, Node, Dns, and Pac all share the same ServiceCommands sub-enum — same 7 lifecycle ops (install, uninstall, run, start, stop, status, logs) delegated to different binaries.

#![allow(unused)]
fn main() {
-> See: `kinetic-cli/src/commands/mod.rs` — Lines 16–72
}

Seed Phrase Management and Setup Wizard

Files: commands/seed.rs, commands/setup.rs Crate: kinetic-cli | Stage: 14


seed.rs — BIP-39 Identity Bootstrap

seed init — First-time identity generation

#![allow(unused)]
fn main() {
-> See: `kinetic-cli/src/commands/seed.rs` — Lines 30–79
}
  1. getrandom::fill(&mut [0u8; 32]) — 256 bits of OS entropy.
  2. Mnemonic::from_entropy_in(Language::English, &entropy) — converts to a 24-word BIP-39 mnemonic.
  3. Prints the phrase to the terminal with a clear warning (“NEVER be able to view this phrase again”).
  4. Backup verification loop: Uses entropy[0] % 24 and entropy[1] % 24 to pick two random word positions from the phrase. Prompts the user to type those two words. If wrong, loops. If correct, confirms and breaks.
  5. save_keypair_from_mnemonic(&path, &phrase, NETWORK_ID) — derives the ML-DSA-65 keypair from the mnemonic and writes it to <base_dir>/identity.key.

Important

The word indices are derived from the entropy itself — not random at display time — so they are deterministic from the same entropy but unpredictable to anyone who hasn’t seen the phrase.

seed restore

Prompts the seed phrase via rpassword::prompt_password() (reads without echoing to terminal), then calls save_keypair_from_mnemonic(). If the phrase is invalid, the error is surfaced and the command fails cleanly.

#![allow(unused)]
fn main() {
-> See: `kinetic-cli/src/commands/seed.rs` — Lines 81–99
}

setup.rs — First-Run Wizard

kinetic setup is a thin wrapper that:

  1. Prints a welcome banner.
  2. Calls handle_seed_command(SeedCommands::Init) — runs the full seed generation flow.
  3. Prints “Setup Complete” with suggested next steps.

kinetic setup firefox — Root CA injection

#![allow(unused)]
fn main() {
-> See: `kinetic-cli/src/commands/setup.rs` — Lines 52–end
}

Note

Injects the Kinetic Root CA (generated by the daemon on first boot at <base_dir>/<NETWORK_ID>.cert.pem) into Firefox’s NSS certificate database so that .kin HTTPS sites work without browser warnings.

Process:

  1. Reads the cert from <base_dir>/<NETWORK_ID>.cert.pem. If missing, instructs the user to start the daemon first.
  2. Finds all Firefox profile directories per platform:
    • Windows: %APPDATA%/Mozilla/Firefox/Profiles/
    • macOS: ~/Library/Application Support/Firefox/Profiles/
    • Linux: ~/.mozilla/firefox/ and ~/.var/app/org.mozilla.firefox/ (Flatpak)
  3. For each profile that has a cert9.db (NSS3 format), calls:
    certutil -A -n "Kinetic Root CA" -t "CT,," -i <cert.pem> -d sql:<profile_dir>
    
  4. Also checks cert8.db (older NSS2) and calls the same command with the legacy format.
  5. Prints success/failure for each profile found.

Name Registration, Renewal, Publish, and Query

Files: commands/name/register.rs, renew.rs, publish.rs, query.rs, mod.rs Crate: kinetic-cli | Stage: 14


name register <name> — The Full Registration Pipeline

This is the most computationally complex CLI command. It runs a multi-step cryptographic protocol against the daemon.

#![allow(unused)]
fn main() {
-> See: `kinetic-cli/src/commands/name/register.rs` — Lines 25–end
}

Step 1: Fetch Drand entropy

DrandClient::new(None).fetch_latest() — gets the latest beacon randomness. The Drand signature is hashed with SHA-256 to produce 32 bytes of verifiable entropy (drand_rand).

Step 2: Build the commitment hash

H(fqdn || salt || drand_rand || pubkey)
  • salt: 32 random bytes from getrandom::fill().
  • pubkey: the ML-DSA-65 public key from identity.key.
  • Result stored in Commitment { hash: [u8; 32] }.

Step 3: POST commitment (Phase 1 of 2)

Important

POST /api/commit with the CommitRequest. The daemon stores this commitment. This is the “lock-in” phase — the user can’t later fake a different name after seeing other commitments.

Step 4: Generate VDF proof (CPU-intensive)

kinetic_vdf::ChiaVdfEngine::new().generate_proof(challenge, iterations) — runs the Chia VDF for iterations steps. Run in tokio::task::spawn_blocking. This is the PoW component — computationally sequential, can’t be parallelized.

Step 5: Create the Reveal

Constructs Reveal { name: fqdn, payload: zone_bytes, salt, drand_kyn, drand_signature, vdf_proof, iterations, pubkey, signature }. Signs signable_bytes(NETWORK_ID) with the ML-DSA-65 key from identity.key.

Step 6: POST reveal (Phase 2 of 2)

POST /api/publish with the Reveal. The daemon verifies the commitment matches, verifies the signature, verifies the VDF proof, then publishes to the DHT.

Zone file

Before POST, calls save_zone_file(fqdn, &zone) to persist the zone locally in <base_dir>/zones/<fqdn>.json.


name renew <name> — Renewal

Same pipeline as register but hits POST /api/renew at the reveal phase instead of POST /api/publish. Also re-fetches the existing zone from the daemon API to carry over records.

#![allow(unused)]
fn main() {
-> See: `kinetic-cli/src/commands/name/renew.rs`
}

name publish <name> — Zone Update

Used after a name is already registered to update DNS zone records (add A records, CNAME, TXT, KID links, etc.).

  1. Reads the local zone file from <zones_dir>/<name>.json (or creates a blank zone).
  2. Prompts the user to add/edit records (interactive CLI prompts).
  3. Signs a new Reveal with the updated zone payload.
  4. POST /api/publish — re-registers the updated zone on the DHT.
#![allow(unused)]
fn main() {
-> See: `kinetic-cli/src/commands/name/publish.rs`
}

name query <name> — Lookup

GET /api/resolve/<name> from the daemon. Prints the returned NameRecord as formatted JSON. If the daemon isn’t running, falls back to constructing the API URL from config and printing an error.

#![allow(unused)]
fn main() {
-> See: `kinetic-cli/src/commands/name/query.rs`
}

name/mod.rs — Dispatcher

Defines NameCommands enum: Register, Renew, Publish, Query. handle_name_command() dispatches to the right handler.

#![allow(unused)]
fn main() {
-> See: `kinetic-cli/src/commands/name/mod.rs`
}

Quick Reference

CommandKey StepsDaemon Endpoint
name registerDrand → VDF → ML-DSA sign → commit → revealPOST /api/commit + POST /api/publish
name renewSame as register but for existing namePOST /api/renew
name publishEdit zone → sign → postPOST /api/publish
name queryDirect API lookupGET /api/resolve/<name>

KID Identity Commands

File: commands/identity.rs Crate: kinetic-cli | Stage: 14


What Is a KID Here?

The CLI creates and manages Kinetic Identity Documents — post-quantum DID credentials using ML-DSA-65. These are separate from the node identity (identity.key) — they are user-facing cryptographic identity documents that link a .kin name to a public key.


identity create

#![allow(unused)]
fn main() {
-> See: `kinetic-cli/src/commands/identity.rs` — Lines 62–~120
}
  1. ml_dsa::SigningKey::<MlDsa65>::generate() — generates a post-quantum ML-DSA-65 keypair.
  2. Derives the DID string: "did:kin:" + hex(SHA-256(public_key_bytes)).
  3. Constructs a KineticDid from the DID string.
  4. Builds a KidDocument (the actual DID document with public key embedded).
  5. Saves the signing key bytes to <output>.key (default: kid.key) with 0o600 permissions.
  6. Saves the DID document as <output>.json (default: kid.json).

Note

This command is fully offline — no daemon required.


identity publish

#![allow(unused)]
fn main() {
-> See: `kinetic-cli/src/commands/identity.rs` — Lines ~120–~220
}
  1. Reads the kid.json file (the DID document).
  2. Reads the manifest.json file if it exists (a capability manifest — list of permissions the KID grants).
  3. POST /api/kid on the daemon with the KID document and manifest.
  4. The daemon stores it and publishes it to the DHT under the name’s zone record.

Required arg: --name saif.kin — the name the KID belongs to.


identity resolve <did>

#![allow(unused)]
fn main() {
-> See: `kinetic-cli/src/commands/identity.rs` — Lines ~220–~250
}

GET /api/kid/<did> from the daemon. Prints the resolved KidDocument JSON. Falls back to printing an error with the daemon URL if unreachable.


identity revoke

#![allow(unused)]
fn main() {
-> See: `kinetic-cli/src/commands/identity.rs` — Lines ~250–~330
}
  1. Reads the KID file and the signing key.
  2. Sets kid.revoked = true and updates the updated_at timestamp.
  3. Signs the updated document with the ML-DSA-65 key.
  4. Saves the revoked document to <output> (default: revoked_kid.json).
  5. POST /api/kid with the revoked document — publishes the revocation to the network.

identity rotate-key

#![allow(unused)]
fn main() {
-> See: `kinetic-cli/src/commands/identity.rs` — Lines ~330–~440
}
  1. Reads the existing KID and the old signing key.
  2. Generates a new ML-DSA-65 keypair.
  3. Updates the KID document to embed the new public key.
  4. Signs with the old key (proving authority to rotate).
  5. Saves the new private key and the updated KID document.
  6. Publishes via POST /api/kid.

Important

Key rotation preserves the DID identifier — the did:kin:<hash> stays the same since it was derived from the original public key. Only the verificationMethod embedded in the document changes.


Quick Reference

CommandOffline?Daemon Endpoint
identity createYesNone
identity publishNoPOST /api/kid
identity resolveNoGET /api/kid/<did>
identity revokeNoPOST /api/kid
identity rotate-keyNoPOST /api/kid

Service, Governance, and Misc Commands

Files: service.rs, governance.rs, clock.rs, dns_tree.rs Crate: kinetic-cli | Stage: 14


service.rs — The System Delegator

#![allow(unused)]
fn main() {
-> See: `kinetic-cli/src/commands/service.rs`
}

The CLI binary doesn’t run the daemon, node, or proxy itself. Instead, it provides a unified interface (kinetic daemon start, kinetic host install) that shells out to the actual binaries (kinetic-daemon start, kinetic-host install).

Key mechanics:

  • Path Verification: Checks if the target binary is installed and on the $PATH using which. If not found, prints a helpful hint explaining what that specific component does before exiting.

Warning

Sudo Support: The kinetic dns commands require port 53. service.rs knows this (via the needs_sudo flag) and automatically prepends sudo to the execution on Unix, warning the user they might be prompted for a password.

  • OS-Aware Status & Logs: When running status or logs, it doesn’t just pass the command down. It translates them into OS-native commands:
OSCommand
Linuxsystemctl is-active <binary>, journalctl -u <binary> -f
macOSlaunchctl list <binary>

governance.rs — Broadcasting Proposals

#![allow(unused)]
fn main() {
-> See: `kinetic-cli/src/commands/governance.rs`
}

Submits a post-quantum governance proposal to the network.

Flow:

  1. Reads a pre-signed governance JSON file from disk (signed_action.json). These files are generated in an offline, air-gapped environment by kinetic-OS (the governance operating system) to keep the cold keys safe.
  2. POST /publish-governance to the local daemon (using the Bearer token for authentication).
  3. The daemon then validates the signatures and broadcasts the proposal via P2P gossip.

clock.rs — Kinetic Network Time (KNT)

#![allow(unused)]
fn main() {
-> See: `kinetic-cli/src/commands/clock.rs`
}

Displays the current Kinetic Network Time (which is strictly synchronized to the Drand beacon epochs).

Two-tier resolution:

ModeBehavior
1. API mode:Attempts to GET /api/time from the local daemon. If successful, prints [Synced] and the time.
2. Offline fallback:If the daemon is unreachable, mathematically calculates the time using SystemTime::now(), DRAND_GENESIS_TIME, and DRAND_PERIOD. Prints [Offline/Mathematical].

With the --listen flag, it enters a loop and prints the updated time every 3 seconds (the Drand beacon interval).


dns_tree.rs — Merkle DNS Tree Generator

#![allow(unused)]
fn main() {
-> See: `kinetic-cli/src/commands/dns_tree.rs`
}

A developer utility to generate Cloudflare-ready DNS Tree zone files for P2P bootstrap node discovery.

PartDescription
Problem:How do you distribute a list of 5,000 bootstrap node IPs to new users without centralizing on a single HTTP endpoint or making the client binary 10MB larger?
Solution:A Merkle tree encoded into standard DNS TXT records (EIP-1459 style).

How it works:

  1. Reads a text file containing one libp2p Multiaddr per line.
  2. Creates kintree-leaf:<addr> records and hashes them.
  3. Groups up to 50 leaf hashes into a branch: kintree-branch:<hash1>,<hash2>... and hashes the branch.
  4. Repeats recursively until there is a single kintree-root hash.
  5. Hashes use SHA-256 encoded as base32 (truncated to 32 chars) so they fit perfectly as DNS subdomain labels.
  6. Outputs a standard BIND zone file that can be imported into Cloudflare or Route53.

Clients can then use standard DNS lookups to traverse the tree and discover bootstrap multiaddrs securely.

Crate: kinetic-wasm

Stage: 15 — Browser Native Web3

Depends On: kinetic-network, kinetic-core


What Is This?

kinetic-wasm compiles the Kinetic P2P network client into a Universal WebAssembly library. It allows web browsers (via extensions or embedded JS) to connect directly to the libp2p network, perform DHT lookups, and fetch content over P2P without needing a local daemon installed.


Universal Multi-TLD Support

It is designed as a “Universal Web3 Extension” driven by the Atlas Registry. It doesn’t just resolve .kin — it resolves any Kinetic network fork registered in Atlas.


How It Works

File:

#![allow(unused)]
fn main() {
kinetic-wasm/src/lib.rs
}

1. Dynamic Registry

#![allow(unused)]
fn main() {
fetch_registry()
}

downloads the global index.json from the kinetic-atlas GitHub repo. It populates a mapping of TLD -> AtlasNetworkConfig (which includes the network’s unique network_id for signatures, and bootstrap_nodes).


2. On-Demand Swarm Spawning

#![allow(unused)]
fn main() {
get_or_spawn_swarm(tld)
}

creates an isolated NetworkEventLoop (a full libp2p swarm) for that specific TLD.

  • It resolves bootstrap nodes via the kintree Merkle DNS tree if configured.
  • Spawns the async loop using wasm_bindgen_futures::spawn_local.
  • Caches the NetworkClient in a HashMap.

3. Resolution and Signature Validation

#![allow(unused)]
fn main() {
resolve_domain(full_domain)
}

:

  • Extracts the TLD and gets the correct swarm.
  • Calls client.resolve_redundant_payload().

Important

  • Crucial: Validates the DHT record signature using the specific network_id configured for that TLD in the Atlas registry. A record signed for .kin is rejected on .alt.

4. Garbage Collection

#![allow(unused)]
fn main() {
start_manager()
}

runs a 60-second loop.

Note

Any swarm unused for 10 minutes is dropped, terminating its WebSockets to conserve browser memory and network connections.