Concepts
Proof lifecycle
Follow one request through preparation, witness generation, proving, encoding and verification, and learn what each stage binds, what an accepted proof means and how to handle a rejection.
Every proof follows the same five stages. Preparation is shared by all requests of one operation kind and configuration; the other four stages run per request. This page describes what each stage consumes and produces, which values bind a proof to its original request, and what a true verification result establishes.
prepare(kind) once per (operation kind, configuration)
| circuit, derived wiring, full circuit commitment
v
witness per request: operation + Merkle witness + public inputs
| -> a value for every wire of the circuit
v
prove per request: transcript + GKR prover -> GkrProof
|
v
encode (optional) inner-proof-v1 bytes: identity, statement, proof
|
v
verify per request: original request + proof -> true or falseStage 1: Prepare#
StateSyncProver::prepare(kind) returns a PreparedSync that holds three things built once:
- the compiled circuit for the operation kind under the prover's configuration;
- the derived wiring oracle the verifier evaluates at every layer;
- the full circuit commitment that every proof absorbs.
A PreparedSync is immutable and can be shared by all worker threads. Reuse it only for the same operation kind, depth, leaf bound, strategy and configuration, and build a new one after any of them changes. Do not change a prover's configuration while reusing prepared state.
Preparation is where the setup cost sits. In a depth-24 membership profile, computing the circuit commitment took 105.03 ms and compilation took 7.90 ms. In a separate study, direct fresh-versus-prepared measurements at depth 24 showed 3.46–4.45 times lower per-request proving-path latency across the three operations, with encoding, transport and acceptance outside the timer.
Prepare the circuit once. Pay less for every request.
Depth 24, one request per call on a 192-core bare-metal host, three processes of 1,000 timed requests per operation and mode. Prepared material is reusable only for the same operation kind and configuration. Encoding, transport and acceptance are excluded.
prepare_pinned_d24_a_membership(bytes) is a second constructor for one reviewed profile: depth 24, the default leaf bound, strategy A and the Membership kind. It validates the exact material bytes against anchors fixed in the crate (length, framed digest and circuit commitment), rebuilds the derived wiring locally and never falls back to compilation. Any other configuration returns PreparedMaterialError::ConfigMismatch.
Stage 2: Witness#
For each request the prover turns the operation, the Merkle witness and the public inputs into a value for every wire. prove_sync_op_prepared and make_job_prepared do this with compiler::generate_witness, which builds the input vector (leaf pre-image, accumulator chains, siblings, key bits, root and value digest) and evaluates the prepared circuit.
Witness generation fails with SyncError::Witness when the path does not hold exactly d siblings or a leaf encoding is empty or longer than the leaf bound. It does not check whether the operation holds. A request with a wrong root, key, payload or value digest can still produce a witness and a proof. Some of these errors leave every residual at zero and are rejected only by the verifier's public-input checks.
Stage 3: Prove#
The prover seeds a fresh transcript for every request. It observes the domain tag, the circuit shape and full circuit commitment, the request's public inputs and the output layer, in a fixed order, and then runs the GKR prover. Each transcript depends only on its own request, so the worker count and the scheduling order cannot change any proof byte.
Batching runs independent jobs. prove_batch_prepared and prove_batch_parallel return one proof per job, in input order, each identical to the proof the single-request path produces. They do not aggregate proofs. See Batching and parallelism.
Proving succeeds for any well-formed witness, including one whose residual outputs are not zero. A successful proving call is not an acceptance verdict.
What binds a proof to its request#
The public inputs (PublicInputs) are the statement a proof is about. Their field set and order are fixed:
| Field | Type | Meaning |
|---|---|---|
old_root | Digest<BaseField> | Root before the operation |
new_root | Digest<BaseField> | Root after the operation; equal to old_root for membership and non-membership |
op_kind_tag | u8 | 0 for Membership, 1 for NonMembership, 2 for Update (PublicInputs::kind_tag) |
asset_id | AssetId | The key of the slot |
value_digest | Digest<BaseField> | Leaf hash of the leaf the operation asserts; for an update, the new leaf |
The transcript absorbs these values and the full circuit commitment before the first challenge is drawn, so every challenge depends on them. The verifier also requires the public inputs carried with a proof to equal the request's. The witness-derived input vector and the sibling path are not absorbed; the verifier checks them through the final input claims. Formal verification discusses what that ordering means for the soundness argument.
Stage 4: Encode#
encode_sync_result(prepared, result) produces canonical inner-proof-v1 bytes: a header, the circuit identity with the full circuit commitment, the statement and the proof payload. The witness is never part of the encoding. Use it whenever a proof leaves the process. Wire format specifies the layout and every rejection rule.
Encoding fails with EncodeError when a value does not fit its wire field, for example a leaf_max_fields above 65,535.
Stage 5: Verify#
Verification takes the original request as well as the proof, because the verifier rebuilds the input vector from the request's witness. Two entry points share one verifier body:
verify_sync_op_prepared(prepared, request, result)checks an in-memorySyncResult.verify_encoded_sync_op(prepared, request, bytes)first decodes the bytes strictly, then requires the decoded circuit identity to equal the identity computed from the prover's configuration and prepared commitment, and then runs the same verifier on the decoded statement and proof.
Both return false when the request's operation kind differs from the prepared kind. The shared verifier then checks, in order:
- The public inputs carried with the proof equal the request's.
- The operation tag matches the operation, and
asset_idequals the operation's key. - For membership and non-membership,
new_rootequalsold_root. - The key is below 2^d when d is below 64.
value_digestequals the hash of the leaf the operation asserts: the occupied payload, the new leaf, or an empty or tombstone leaf.- For non-membership, the witness leaf is
EmptyorTombstone; for an update, it equalsold_leaf. - The input vector can be rebuilt from the request, which recomputes the leaf hash and the path hashes.
- The transcript replay with an all-zero claimed output layer, and the GKR verification of every layer, succeed.
- Both input claims equal the multilinear extension of the rebuilt input vector at their points.
Every failure is reported as false; verification has no separate error type.
verify_sync_op(request, result) runs the same checks without prepared state: it compiles, derives the wiring and recomputes the commitment on every call. verify_sync_op_reference does the same with the general table oracle and serves as an audit reference. Use the prepared form for repeated verification.
What acceptance means#
A true result from verify_encoded_sync_op means that the complete received encoding is canonical, that it belongs to the circuit of this verifier's prepared state for its own configuration, that it carries exactly the request's public inputs, and that it passes the GKR verifier and both input checks against the request's own witness.
It does not mean that the root belongs to a trusted database, chain or registry, that any state change was committed, or that a destination network reached finality. The measured serving profiles end at this acceptance. Trust boundaries covers the remaining assumptions.
Handling results#
- Treat only a
trueverification result as acceptance. A successful proving or encoding call says nothing about whether the request holds. - Verify against the original request. Keep the request that produced each proof, and with asynchronous or remote workers match every reply to its request through an explicit request identifier.
- Treat
falseas final for that input. Do not retry the same malformed input indefinitely, drop afalseresult silently, weaken the verifier, or replace an expected identity or commitment after a mismatch. - Record the source revision, configuration and error class when a proof is rejected. An honest proof is usually rejected because its request, configuration or prepared state differs from the one used for proving.
- If a tampered or mismatched proof is accepted, stop and report it privately through the security policy (opens in a new tab), not in a public issue.
Errors lists the error types of each stage.
The external wrap path#
wrap_relation and wrap_sync_op connect an encoded inner proof to an outer proof system. The relation checks decoding, circuit identity and inner verification natively before any backend is asked to prove it, and wrap_sync_op rejects a backend result whose statement differs from the locally computed one. The published outer proof, a RISC Zero native succinct proof with zkVerify testnet receipt evidence, covers one fixed depth-24 membership example; it is not a service that wraps arbitrary requests. See Trust boundaries.
Next steps#
- Prepared execution shows the prepare-once pattern in code.
- Encoding and transport covers moving proofs between processes.
- API reference lists every method used on this page.