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

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.