A confidential payment hides the amount. We shipped that earlier this month: shield a plaintext UTXO into a private note, transfer it with the value sealed inside a Pedersen commitment, unshield it back out, all non-custodial. Observers see a commitment, not a number.
That layer has one honest caveat, and this post is about closing it.
The caveat: hiding is not the same as soundness
A confidential scheme has to do two separate jobs.
The first is hiding. A Pedersen commitment C = v*G + r*H reveals nothing about the
value v as long as the blinding r is random. That property is information theoretic.
It does not depend on any computational assumption, so a quantum computer does not weaken
it. The amount stays hidden forever.
The second job is soundness, which is the promise that nobody can create coins out of thin air. In a Pedersen and Bulletproof system, that promise rests on the discrete logarithm assumption: the reason you cannot open a commitment to a value you do not actually hold is that you cannot solve discrete log on the curve. Shor's algorithm solves discrete log. So the soundness of every Pedersen and Bulletproof confidential scheme is classical. A sufficiently large quantum computer could forge a balance proof and mint value, even though it could never see the hidden amounts.
For a chain whose entire thesis is post-quantum security, that is the wrong place to leave a discrete-log assumption. The amounts were quantum-safe. The accounting was not.
The fix: move soundness onto a hash
The standard way to get post-quantum soundness is to stop relying on number-theoretic hard problems and rely on a hash function instead. STARKs do exactly that. A STARK proves that a computation was carried out correctly, and its only cryptographic assumption is that the hash function it is built on is collision resistant. Hash functions are not broken by Shor. The transparent setup is a bonus: there is no trusted ceremony, no toxic waste, nothing to trust except the hash.
We built the confidential transfer proof on Winterfell, an audited Rust STARK library, with the Rescue Prime hash as the algebraic primitive inside the circuit. The proof establishes, without revealing any amount, that:
- each input note commitment opens to some value the sender knows,
- each output note commitment opens to some value in a valid range (no negative or wrapped amounts),
- and the inputs balance the outputs plus the public fee, so no value is created.
The verifier is small and deterministic, so it runs directly inside the chain runtime. There is no new host function and no node binary to roll out. The proof verification is part of the runtime logic that every validator already runs identically.
The trap we walked into: a STARK is not private by default
Here is the part that is easy to get wrong, and we nearly did.
A STARK proves a statement is true. It does not, on its own, promise to reveal nothing else. The proof works by committing to a set of low-degree polynomials that encode the execution trace of the computation, and then answering a handful of random spot-check queries about those polynomials. Each query opens a few points of the trace.
If the secret you are hiding lives in a trace that is short and low degree, those opened points are enough for an observer to interpolate the polynomial and recover the secret. In plain terms: a naive STARK confidential transfer would be sound but would leak the amounts through its own proof. It would be the opposite failure of the Pedersen scheme. Perfect accounting, no privacy.
We caught this by attacking our own first version before shipping it. The lesson is worth stating plainly: proving a statement and revealing nothing beyond it are two different properties, and you have to engineer the second one on purpose.
Adding zero knowledge without a fork
The fix is to make the trace polynomials underdetermined from the verifier's point of view, so the spot-check openings never pin them down. We did three things, all inside the proving and verification logic, with no change to the chain's trust model:
-
The proof is committed over an extended domain that does not include the real trace rows, so a query never lands on an actual witness value.
-
The hash computation runs continuously over a long trace with a uniform low-degree transition rule, instead of a short circuit with special-cased rows. Special-cased rows are exactly the high-degree fingerprints that make a trace easy to reconstruct.
-
The trailing rows of the trace are left unconstrained and filled with cryptographic randomness drawn from the prover's own secure generator. Because there are more random masking rows than there are total query openings, the trace polynomials stay statistically hidden. Two proofs of the same transfer come out different, which is the visible signature of the randomization working.
The result is a proof that is sound against a quantum adversary and reveals nothing about the amounts, built only from a hash. We verified all of it: an opening still matches the native hash, two proofs of the same witness differ, tampering is rejected, and the masking margin is asserted in the test suite.
The proof is generated in your browser
Privacy is only real if the secret never leaves your device. So the prover compiles to WebAssembly and runs client side. When you make a private transfer, your browser takes your note values and blinding factors, generates the STARK locally, and sends out only the commitments, the public fee, and an opaque proof of about fifty kilobytes. The relayer that submits the transaction and pays the outer fee never sees an amount. Neither does the chain.
It is fast. Proving a two-input, two-output transfer takes roughly seventy five milliseconds in the browser. The relay endpoint reconstructs the exact confidential transfer call from the fields it was given, so it can never be coaxed into relaying anything else, and the in-runtime verifier is the final authority on every proof.
Live, and tested like we wanted to break it
This shipped as an additive runtime upgrade. The earlier confidential calls still work. There was no fork and no migration. We then ran it on the live chain the way an attacker would.
- A valid transfer is accepted. Only the fee is public. The amounts are hidden.
- Re-submitting a spent transfer is rejected, because the input note is no longer in the unspent set.
- A transfer with a single byte flipped in the proof is rejected by the runtime verifier.
- A fresh valid transfer right after the rejections is accepted, proving the rejections were the tampering and not a flaky path.
The full path was exercised end to end through the public stack: a proof generated in a browser, posted to the public API, relayed, and finalized on chain as a confidential transfer, with the amount never appearing anywhere.
In the wallet on qbc.network you will find a Private transfer panel that does all of this. It manages your private notes locally, generates the proof in your browser, and hands you the single note to share with your recipient. The blinding factors stay on your machine.
What is honest to say, and what is next
Two things we are explicit about.
First, this is new cryptography built on an audited core. The STARK library underneath is audited; the circuit constraints that encode our specific transfer are the reviewable delta and they are soundness critical for money. We will put them through an external audit before we ever attach a "private, mainnet value" label. Until then, treat it as live and real but not yet externally audited.
Second, the on-ramp. The post-quantum private transfer moves value between private notes today. The dedicated post-quantum path for moving plaintext QBC into and out of these notes is the next step we are building. In the meantime the existing confidential shield and unshield handle the plaintext on and off ramp.
The headline stands on its own. A private transfer on QBC now hides the amount with information theoretic security and proves the books balance with a hash based zero-knowledge proof that a quantum computer cannot forge, generated on your own device, verified by every validator, live on chain.