跳到主要内容
cryptographyAugust 26, 2026·8 min read

What replaced HAWK: ML-DSA, a live testnet, and the step it sets up

On 29 July we said HAWK was leaving our stack and that the replacement would be published separately. It is ML-DSA, and the chain running it is live: four validators, ML-DSA-65 for consensus, ML-DSA-44 for spends, over 136,000 blocks. Here is why we picked it, why an address now commits to several schemes at once, why we took the block prover off the node, and why this is deliberately a floor rather than a destination.

N

Nixeon

NixProtocol

分享:

On 29 July we wrote that HAWK was leaving our stack entirely, and that "the replacement is being selected now and we will publish the choice separately." This is that post.

The replacement is ML-DSA, and the chain running it is live. ML-DSA gives us a standards-grade authorization layer we did not write ourselves. It is the step before authorization moves into a zero-knowledge relation, not the end of the work.

What is actually running

pqe-testnet-3 runs four permissioned validators at f=1 fault tolerance, with ML-DSA-65 (FIPS 204) for consensus votes and node identity, ML-DSA-44 for spend authorization, and SHA3-256 for block and transaction hashing. It has produced over 136,000 blocks.

You do not have to take the first claim on trust. The node's status endpoint reports its validator scheme and public key, and that key is 1,952 bytes, which is ML-DSA-65's public key size and nothing else's.

curl -s https://pq-node.nixprotocol.com/consensus/status

The wallet and explorer are at pq.nixprotocol.com, and a faucet will fund a fresh address so you can push a transaction through and watch it settle.

Why ML-DSA, and why not Falcon

For consensus votes and the P2P handshake the requirements are narrow. Those keys are rotatable and hold no user funds, so what matters is a finished standard, a conservative underlying assumption, and an implementation somebody competent has already scrutinized. Signature size is close to irrelevant at authority-vote rates, where a handful of signatures per block run against a configurable block budget.

schemeNIST categorypublic keysignature
ML-DSA-4421,3122,420
ML-DSA-6531,9523,309
Falcon-5121 (claimed)897666 fixed
HAWK-512 (former)1, withdrawn1,024555

Falcon is a reasonable choice, and Solana selected it in April 2026, with Anza and Firedancer converging on it independently. Jump Crypto's reasoning is sound for Solana, where Falcon's small signatures answer a real bandwidth constraint. That constraint is not ours. We are not fighting for bytes in a validator vote, so we would be paying for Falcon's harder implementation story, floating-point Gaussian sampling and all, to buy something we do not need. One footnote is worth keeping straight. Falcon-512's provable bound is frequently quoted at around 113 bits, which is below 128, so it is not accurate to describe Falcon-512 as a 128-bit scheme, and we do not.

Which scheme we picked matters less than how it arrives. ML-DSA arrives as a dependency rather than a port.

We hand-ported HAWK-512, byte for byte, and that port is precisely what the attack landed on. The lesson was less that HAWK was the wrong pick and more that we should not have been the ones writing it. So the chain now depends on fips204, which is pure Rust with no unsafe, constant-time key generation and signing, no_std, and no heap allocation. We specifically avoided RustCrypto's ml-dsa crate, which has never been independently audited and which shipped a medium-severity bug in v0.1.0-rc.2 where verification accepted signatures carrying duplicate hint indices.

ML-DSA also hands us two properties we would otherwise have had to build. Key generation is canonical from a 32-byte seed, so our derivation discipline extends to authority keys without a special case. And the scheme binds the public key into the message hash directly, computing mu = H(tr || M) with tr = H(pk), so no BUFF wrapper is needed to get non-resignability. We wrote and shipped a BUFF wrapper for HAWK, and here the standard already did it.

One address, several spend paths

Having replaced a signature scheme once, we expect to do it again, so addresses are no longer the hash of a single public key.

An address is now SHA3(domain || root), where root is a Merkle commitment over a fixed number of slots, each holding one authorization scheme's public key. A spend presents its public key plus the Merkle path to the root. The address stays the same whichever committed scheme authorizes the spend.

Slot 0 is ML-DSA-44, a lattice scheme, and slot 1 is SLH-DSA-SHAKE-128s, which is hash-based. The two families are deliberate. A second lattice scheme in slot 1 would be insurance that pays out only in the worlds where it was not needed, because a structural result against module lattices takes every lattice leaf at the same instant. Slot 1 is the only element of this design that survives a lattice break, and it will look like a dead leaf for as long as it is working.

The limitation is permanent and worth stating. A Merkle commitment fixes every leaf when the address is created. A slot cannot be filled in later, because changing a leaf changes the root, which changes the address. The agility this buys is exactly the set of schemes committed before genesis. A scheme adopted afterwards is an address migration for every user, which is the outcome the whole design exists to avoid. Slots 2 and 3 hold filler and will stay that way. They buy tree shape, constant-length paths and no leak of how many schemes an address really supports, and nothing more.

The filler is derived from the account seed rather than being a constant. A constant would make every unused slot identical across all users, so a spend that revealed a path would confirm which sibling positions were unused. Seed-derived filler makes an unused slot indistinguishable from a used one to anybody without the seed.

The seed anchor

The root of key derivation is Argon2id rather than a single hash pass, at 64 MiB of memory and three passes, which is RFC 9106's second recommended profile, run at a single lane rather than that profile's four. Lanes only buy parallelism for a defender who actually threads the derivation, and we do not.

The reasoning is narrow. A mnemonic and optional passphrase is the one secret a human actually holds, and it is the one input an attacker can guess offline at whatever rate their hardware allows. A memory-hard root makes each guess cost memory rather than a hash invocation. It does nothing about a compromised device and it is not a substitute for entropy in the mnemonic. What it changes is the economics of guessing a weak passphrase.

This was a breaking change, and addresses derived under the previous single-pass derivation do not survive it. Doing it on a fresh testnet with no real funds is the entire reason to do it now rather than later.

What came out

HAWK is off every path. The crate remains in the tree and is published separately as nixprotocol/pqe-hawk for anyone who wants the port, but nothing in the chain depends on it.

The LatticeFold-style block prover also came off the node. We should have been louder about this earlier, because the folded block proof was never on the consensus path. Validators confirm state the way they always did, by re-executing every transaction and comparing the resulting application hash. The prover ran after commit and produced proofs that nothing verified. Taking it off the node removed a component that was doing no work on the live chain.

It remains built and benchmarked. The measured folded block proof is about 2.3 MB at our shipped soundness parameters. That figure is worth stating precisely, because an early plan document estimated 2.6 KB and was wrong by roughly three orders of magnitude.

Why this is a stepping stone and not a destination

ML-DSA gives us a chain running on nothing that has been withdrawn, with implementations we did not write. That is worth having and it was the right thing to do first. It does not fix the structural problem the HAWK result exposed.

The Straznickas-Weis reduction needs exactly one input, the public key. ML-DSA is not vulnerable to that reduction, but the shape of the exposure is unchanged. A spend publishes a verification key, and any future key-recovery result against whichever lattice scheme we happen to run will be able to read that key off the chain indefinitely.

The scheme tree narrows the window. Funds sitting at rest publish no verification key at all. A spend publishes only the slot it used, so the hash-based parachute in slot 1 stays unpublished for as long as slot 0 holds.

The end state is different in kind. Authorization goes through a zero-knowledge relation, where an output locks funds to a lattice commitment to the owner's key material, and spending proves in zero knowledge that the wallet knows the opening of that commitment, with the proof bound to the transaction it authorizes. The chain verifies one fixed algebraic relation, in batches. No key that guards a user's funds appears on the wire or in state, so the reduction has no input to read.

We have built this for amounts already, and the costs are measured rather than projected.

measured
proof size, 2 inputs / 2 outputs3.61 MB
batch verify, 8 cores (the consensus path)6.0 to 8.3 ms per transaction, 121 to 168 tx/s
verify one proof alone139 to 159 ms
prove natively, 1 output6.99 s
prove that same shape in the browser16.33 s
prove natively, 3 inputs / 3 outputs8.71 s

The browser row is a real browser timing the proving call itself, rather than a native number multiplied by a guessed wasm penalty. Against the native row directly above it, on the identical shape, the measured penalty is 2.34x. Every proving figure comes from one run at the same commit, because quoting proving times measured at different commits is how you end up publishing a speedup nobody achieved. The verify rows are older, measured on 12 August, and predate an amount candidate pool that has since landed. We have not re-measured verification against it, so those two rows are the last figure we actually took rather than the current one. We publish these numbers because the honest cost of client-side proving is the part that usually gets waved away, and because multi-megabyte proofs and double-digit-second proving times are the real engineering problem between this design and production.

None of that runs on the live testnet today, and the branch testnet-3 runs contains none of those crates. The sequencing is deliberate. Put the chain on standardized signatures with implementations we did not write, then move authorization into the proof system, rather than doing both at once and having no idea which half broke.

What we are not claiming

  • No external audit. No independent cryptographic audit of this chain has been performed.
  • This is a testnet. No real funds, four permissioned validators, f=1. It is not a decentralized network and nothing about it should be read as production hardness.
  • Privacy is not live. The shielded lane is closed at genesis. The confidential machinery quoted above is built and measured on a separate branch.
  • The block proof is not verified by anything on the live chain, and never has been.
  • ML-DSA-44 spends still publish a public key. Removing that is what the next step is for.

If you want to check any of this, the status endpoint and the faucet are open, and the numbers above came out of benchmarks rather than a slide.

继续阅读

探索更多关于隐私基础设施和零知识证明的研究文章。

查看所有文章