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
Commitmentstruct 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_kynfield (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:
- Confirm the commitment hash matches the submitted name and salt
- Verify the drand randomness is real and from the right epoch
- Accept the VDF proof as valid work
- Verify the ML-DSA-65 signature over everything above
- 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:
- 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. protocol_version— 1 byte.name— 4-byte big-endian length, then the UTF-8 name bytes.payload— 4-byte big-endian length, then the payload bytes.salt— exactly 32 bytes (no length prefix — it is always 32).drand_kyn— 8 bytes, big-endian.drand_signature— 4-byte length, then the hex string bytes.iterations— 8 bytes, big-endian.vdf_proof.proof_bytes— 4-byte length, then the proof bytes.pubkey— 4-byte length, then the ML-DSA-65 public key bytes.previous_proofoption flag — 1 byte:1if present,0if absent. If present: 4-byte length + the serializedPreviousProof.proof_bytes().miner_pubkeyoption flag — 1 byte:1if present,0if 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 manyiterationsare 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 newReveal.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. ReturnsOk(())on success or aVdfVerifyErroron 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
| Type | Phase | Role |
|---|---|---|
Commitment | 1 | 32-byte SHA-256 hash of name+salt; establishes priority |
CommitRequest | 1 | Full Phase 1 submission: name + commitment |
VdfProof | 2 | Raw chiavdf output bytes proving sequential computation |
PreviousProof | 2 | Chained prior registration for renewals |
Reveal | 2 | Full Phase 2 payload; everything signed and verified |
VdfJobRequest | Async | Parameters sent to a miner to start VDF computation |
VdfVerifyError | 2 | Structured errors from ML-DSA-65 verification |
| Error Variant | Code | Severity | Meaning |
|---|---|---|---|
MalformedPublicKey | KIN-VDF-040 | Warning | Public key bytes invalid |
MalformedSignature | KIN-VDF-041 | Warning | Signature bytes invalid |
InvalidSignature | KIN-VDF-042 | Error | Crypto 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.
VdfJobRequestcarriesname_lengthso the miner can deriveiterations, 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.
Revealcontainsnameandsalt, which together should reproduce the originalCommitment.hash. Theverify_signature()method does not do this check — it only verifies the ML-DSA-65 signature. Somewhere inkinetic-verify, the SHA-256 reproduction check must happen. Confirm where.3. Miner compensation flow.
miner_pubkeyis 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.
payloadfield semantics.Reveal.payloadis 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 insidepayloadfor DNS records?6. No expiry field.
Revealhas noexpires_atordurationfield. Does the network derive name expiry from the drand timestamp, from the number of VDF iterations, or from some external configuration? Renewal viaPreviousProofimplies names do expire — but the mechanic is opaque from this file alone.7.
hashcash_noncevalidation.VdfJobRequestcontains ahashcash_noncedescribed 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?