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

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?