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:
-
The
handle_commitendpoint: Used for locking in a cryptographic claim. -
The
handle_publishendpoint: 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,ApiStatecontains the network client and storage handles. -
tokio::spawn: This is Tokio’s way of executing futures concurrently. It takes a block ofasync 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 returnsOk, Axum automatically sets a200 OKheader. It serializes the success structure into JSON. If it returnsErr, Axum sets the HTTP status code specified in the tuple. It serializes the error structure into JSON. -
std::sync::Mutexvstokio::sync::Mutex: The code uses the standard library mutex forOWNED_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::NameRecordandCommitRequest. -
It uses
kinetic_core::types::normalize_nameto standardize inputs. -
It relies on
kinetic_core::drand::DrandClientto 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_NAMESlists. -
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
0x00bytes. - VDF Staleness:
current_kyn - drand_kynmust not exceedRESQUARING_EPOCH_KYNS. - Redundancy: Payloads are pushed to the
5closest Kademlia DHT nodes. - Quorum Threshold: A minimum of
3out of 5 nodes must confirm storage. - Daemon Capacity: The daemon will automatically heartbeat a maximum of
10,000owned names. - Payload Limits: Payloads failing serialization will result in an
HTTP 500error. - Authorization Limits: Unauthorized requests will result in an
HTTP 403error. - 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:
- 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::Mutexor a concurrentdashmap.
- The Fragile Quorum Sleep:
- The background quorum verification task calls
tokio::time::sleepfor 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
FutureorChannel. - This would signal exactly when the publish operation has completed.
- 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_NAMESlist. - 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.