# On-chain ZK recovery proof — live Preview test A real, client-generated 3,450,403-constraint Groth16 recovery proof verified by a live Cardano Preview node inside a real transaction. The node ran the BLS12-381 commitment-aware verifier over the genuine proof, accepted it, and released the locked funds. A tampered proof was rejected on-chain. Date: 2026-06-27. Network: Preview (testnet-magic 2). Branch: `feat/vending-machine`. ## What was proven The recovery circuit proves: a master XPrv derives (CIP-1852) to the leaf credential `C`, `C` is included in the eligibility Merkle snapshot, and a single public input `pub` binds the snapshot root, the credential, the entitlement, and the payout destination `D`. The proof is generated client-side (the master/mnemonic never leaves the client); only the 192/336-byte proof and the public destination go on-chain. The circuit uses gnark lookup-table hashing (`std/math/uints` in every Blake2b/SHA512/ed25519 gadget), which forces a single gnark BSB22 **Pedersen commitment** in the Groth16 proof. The on-chain verifier is therefore commitment-aware, not vanilla. ## Components | Item | Value | |------|-------| | Circuit constraints | 3,450,403 (K=22, 1 public input, 1239 secret) | | Setup / prove | 12m18s setup, 43s prove, peak RSS 7.3 GB (local WSL, 24 GB) | | Verifying key (on-chain) | `vk_c.hex` — 672 B (vanilla 432 ‖ K2 G1 48 ‖ CommitmentKey.G G2 96 ‖ GSigmaNeg G2 96) | | Proof (on-chain) | `proof_c.hex` — 336 B (vanilla A‖B‖C 192 ‖ commitment uncompressed 96 ‖ PoK G1 48) | | Public input | `pub.hex` — 32 B (LE Blake2b-256(DOM) digest; reduced mod r on-chain) | | Gate script | Plutus V3, addr `addr_test1wzak5fqsa8ztaatzrfrqcfcqnrlrpjmdhqajn394pvc2h9sdyqte3` | | Gate script hash | `bb6a2410e9c4bef5621a460c270098fe30cb6db83b29c4b50b30ab96` | | Funded wallet | `addr_test1vpw0dhd0k6rq0qrpeul0km0xtl5tguqk8f9q8v5p5rzghdq05nhzv` (~9,998 tADA) | ## On-chain verifier (`groth16VerifyCommitted`, `Recovery/Verify.hs`) 1. Reconstruct the commitment's compressed form from its uncompressed X + the ZCash sign bit, then `bls12_381_G1_uncompress` (validates on-curve + subgroup, yields the pairing point). 2. Pedersen proof-of-knowledge pairing: `e(commitment, GSigmaNeg) · e(PoK, G) == 1`. 3. Commitment challenge: `e_cmt = expand_message_xmd(SHA-256, uncompressed96(commitment), "bsb22-commitment", 48) mod r` — three `sha2_256` builtin calls, RFC 9380. Byte-exact vs gnark `fr.Hash` (cross-checked in `prover/ecmt_test.go`). 4. Fold: `vk_x = K0 + pub·K1 + e_cmt·K2 + commitment`. 5. Groth16 check: `e(A, B) == e(α, β) · e(vk_x, γ) · e(C, δ)`. `PublicAndCommitmentCommitted = [[]]`, so the hashed message is just the 96-byte commitment. ## Steps 1. **Generate the real proof** locally — `go run ./prover/cmd/realprove`: golden wallet (path 0/0/0, `C = 19e07fbc…e4`) → single-wallet snapshot (root `86c946f2…`) → assemble the full `Recovery` witness → compile + Setup + Prove the 3.45M circuit → off-chain `VerifyProof` OK → serialize `vk_c`/`proof_c`/`pub`. 2. **Build the verify-gate** — a Plutus V3 validator (`Gate.hs`) parameterized by `(vk, pub)`, spendable iff `groth16VerifyCommitted (VerifyingKey vk) (Proof redeemer) (Scalar pub)`; serialize to `.plutus`; derive the script address. 3. **Lock** 5 tADA from the funded wallet at the gate script. 4. **Spend (valid)** — submit the real 336-byte proof in the redeemer; the node runs the verifier; it returns True; the 5 tADA (minus fee) is released. 5. **Spend (tampered)** — flip one byte of the proof; the node's verifier fails; the spend is rejected. 6. **Pull Koios data** — confirmations, fees, the fund flow. ## Transactions | # | Tx hash | Action | Fee | Confirmations | |---|---------|--------|-----|---------------| | 1 | `168b4de3c9b63eae48722af2992db8881b7f0a1d5343fa90dc1ec5e03cfcb4ef` | Lock 5 tADA at the gate | 1.0 tADA | 28 | | 2 | `7f7f41e0db53892ce2c216732fad1ad78a5b795fa2d50fd297dd7518bcda82af` | Valid spend — real proof verified on-chain, funds released | 1.5 tADA | 15 | | 3 | `0279ff4c1e220a2b1ce314db77e32f956489c0acb306dba4ae4a83e7186a07c7` | Tampered-proof spend — rejected by the node | — (rejected) | — | Explorer (Preview): - Lock: https://preview.cexplorer.io/tx/168b4de3c9b63eae48722af2992db8881b7f0a1d5343fa90dc1ec5e03cfcb4ef - Valid spend: https://preview.cexplorer.io/tx/7f7f41e0db53892ce2c216732fad1ad78a5b795fa2d50fd297dd7518bcda82af ## Fund flow ``` funded wallet addr_test1vpw0dhd0k6rq0qrpeul0km0xtl5tguqk8f9q8v5p5rzghdq05nhzv | input: 9,994 tADA (UTxO 33b2125e…#1) | |==[ TX1 lock, fee 1.0 tADA ]=========================================+ | v | gate script addr_test1wzak5fqsa8ztaatz…vc2h9sdyqte3 | holds 5 tADA (UTxO 168b4de3…#0) | change 9,988 tADA back | | | TX2 redeemer = real 336-byte proof | | node runs groth16VerifyCommitted -> TRUE | v (fee 1.5 tADA) +============================= 3.5 tADA back to funded wallet (output of 7f7f41e0…) TX3 (negative): gate UTxO --[ redeemer = TAMPERED proof ]--> node verifier FAILS PlutusV3 script error at bls12_381_G1_uncompress -> spend REJECTED ``` ## Result - **Valid:** gate UTxO `168b4de3…#0` is `is_spent: true`. The Preview node executed the Plutus V3 script — the full commitment-aware BLS12-381 Groth16 verification — and it returned True, so the spend was valid and 3.5 tADA moved to the funded wallet. - **Tampered:** the node's evaluation failed precisely at `bls12_381_G1_uncompress` (the flipped byte produced an invalid G1 point); the spend was rejected. The verifier discriminates real vs tampered proofs on-chain. ## Execution units | | ExCPU (steps) | ExMemory (units) | |--|---------------|------------------| | Per-tx budget (Preview, params.json) | 10,000,000,000 | 16,500,000 | | Committed verify (off-chain CEK) | 3,236,238,137 (32.4%) | 35,242 (0.21%) | | Declared in the valid spend | 6,000,000,000 | 800,000 | The on-chain Groth16 commitment verification fits one Preview transaction with wide margin.