Skip navigation
XChain Docs

5 · Security

Last updated: 08/07/20265 min read

Security in XChain is per-asset, not per-chain. Every asset carries a minimum security-class, and a high-value asset simply cannot exist in the data Domain. This makes "downgrade" attacks structurally impossible, and rests the safety of value on cryptography and capital rather than on trusting a small validator set.

Three layers for high value

For high value (T0–T1), three layers combine:

  1. ZK validity proof — even a fully-compromised committee cannot forge invalid state; it loses liveness, not safety.
  2. Global restaking — fraud on any chain slashes global capital.
  3. Watcher / fraud proof — detection and challenge.

Low-value data (T3–T4) is secured by proof-of-publication only. Three chain security-classes span the range, and pessimistic proofs provide failure isolation on top: a faulty chain can lose only its own share.

Security-classApplies toWhat backs itMay it carry high value?
SOVEREIGNT3–T4 · data DomainIts own validators + proof-of-publicationNo — structurally forbidden by UAC
SHAREDT1–T2 · ExecutionPartial Set Security — a borrowed validator setYes, up to its class
ZK_SECUREDT0–T1 · Settlement / ValueValidity proof mandatory + global restaking + watchersYes — the highest class

Figure 5 — Security spent by value: three combined layers for the value Domain, publication proof alone at the edge.

What verification actually costs

"Verify, don't re-execute" is only an argument until someone measures it, so here are the numbers. They come from the real Groth16/BN254 verify path — the one merged into master and proven end to end on devnet nodes. ⚠️ They are not a description of the public testnet, which still runs the pre-upgrade binary with a MOCK prover; that distinction is tracked in Status & Live Network and is not glossed over here.

MeasuredValueWhy it matters
Verifying one state-transition certificate~1.19 ms (one BN254 pairing, pure Go, inside DeliverTx)At a 3,000 ms block cadence, ~2,500 certificates could be verified per block before verification becomes the bottleneck
Computing the public input MiMC(prev,tx,new)~14 µsThe chain derives it itself, so a proof is bound to one transition and cannot be replayed for another
Cost of groth16.Verify vs. circuit sizeConstant — independent of the number of public inputs and of the constraint countThe 1.19 ms figure therefore also represents the real SP1 verifying key, not just the current stand-in circuit
Generating a proof (SP1 wrap → Groth16)~15.97M constraints, ~21.5 GB RAMOff-chain by construction — it never runs on a validator. Proving needs a prover network or a ≥64 GB machine

That asymmetry is the whole point of the architecture. Producing a proof is heavy enough to require dedicated infrastructure; checking one is cheap enough to be irrelevant to block cadence. This is what lets the Root's cost stay nearly constant while edge volume grows — and it is also why prover supply is listed openly as an unsolved economic problem in Open Problems rather than assumed away.

What hardening looks like in practice

Three properties are load-bearing, and each was checked rather than asserted:

Determinism. The verifier uses no floating point and no randomness. This is not a nicety: if one validator computed a different result, the app hash would diverge and the chain would halt. It is verified by repeating the same verification many times with identical results, plus an end-to-end run where two validators reach the same app hash after verifying a real proof inside a block.

Graceful rejection of garbage. Anything arriving from a transaction is hostile input, and inside DeliverTx a panic or a hang is a chain halt. Fuzzing found a real one: the proof decoder read an untrusted nbCommitments field and allocated from it, so an attacker could set it to roughly four billion and trigger a ~256 GB allocation — a per-transaction denial of service that would have stopped the chain. The fix is a deterministic shape guard applied before the proof reaches the cryptographic library (exact expected length, commitment count zero); timeouts were rejected as a fix because they would break determinism. After the patch: 4.26 million clean fuzzing executions, the old hanging input kept as a regression test that now completes in under a second, and a live node that rejects the malicious input while continuing to produce blocks.

Paying for the work you cause. A subtler finding: the gas difference between a real proof and a MOCK one (~48,191 vs ~40,638) came almost entirely from storing a larger proof, not from the ~1.19 ms of CPU the verification actually costs — because gas is metered for storage operations, not for pure computation inside a handler. An attacker could therefore buy real CPU cheaply by spamming well-formed-but-invalid proofs. The fix is to charge for the verification explicitly, before performing it, so the fee reflects the cost imposed.

None of these were found by reasoning about the design; they were found by running it. That is the argument for a public testnet in one paragraph.

Takeaway: binding a security-class to each asset is the keystone — it turns "don't put value in the wrong place" from a guideline into a structural guarantee. Every guarantee named so far — the Domain boundary, the READ/WRITE split, per-asset classes — must be enforced somewhere small enough to audit and trust. That place is the narrow waist: seven primitives, specified next.