ZK Prover
The ZK prover is an offchain service that takes block-range proof requests, drives SP1 programs to completion, and returns receipts a caller can hand to AggregateVerifier. The service persists request state across restarts, submits the underlying work to its configured SP1 backend, and exposes proof status and receipt retrieval over gRPC.
The ZK path is permissionless: any operator with canonical L1 and L2 RPC access, a working SP1 backend, and an L1 transaction signer can both request proofs and submit valid proof material onchain.
Responsibilities
Section titled “Responsibilities”A conforming ZK prover stack:
- Accept proving requests for L2 block ranges.
- Generate witness input from canonical L1, L2, and beacon RPCs.
- Prove the range program with SP1.
- For Groth16 requests, aggregate the completed range proof into an onchain-verifiable SNARK.
- Persist proof request and backend session state so work can recover across process restarts.
- Expose proof status and receipt retrieval over gRPC.
- Encode receipts in the format expected by challengers, proposers, and
ZKVerifier.
The ZK prover never decides whether a game is valid. Proposers and challengers pick the range, recompute canonical roots themselves, and recheck game state before submitting proof material onchain.
Proving service API
Section titled “Proving service API”The proving service exposes:
ProveBlock(ProveBlockRequest) -> ProveBlockResponseGetProof(GetProofRequest) -> GetProofResponseProveBlock enqueues a proof request and returns a session_id. GetProof returns the current status and, once complete, the requested receipt bytes.
ProveBlock request
Section titled “ProveBlock request”ProveBlockRequest carries:
| Field | Meaning |
|---|---|
start_block_number | L2 block whose output root is the trusted starting state for the range. |
number_of_blocks_to_prove | Number of L2 blocks to prove after start_block_number. |
sequence_window | Optional L1 block lookahead used when deriving an L1 head for witness generation. |
proof_type | PROOF_TYPE_COMPRESSED or PROOF_TYPE_SNARK_GROTH16. |
session_id | Optional caller-supplied UUID used for idempotent requests. |
prover_address | L1 address committed into the Groth16 journal so a proof cannot be replayed by another sender. Required for Groth16. |
l1_head | Optional 32-byte hex L1 block hash used for witness generation. |
A caller that supplies the same session_id twice receives the existing session in response. Challengers exploit this by deriving the UUID deterministically from (game address, invalid checkpoint index) so retries across process restarts are safe.
Pass l1_head when the proof journal must match an L1 head already committed onchain — dispute proofs against an existing game, for example. With l1_head omitted, the service picks one from the L2 block’s L1 origin plus the request or service sequence window, which is the right behavior for fresh proposals where the caller has not yet committed to an L1 head.
For PROOF_TYPE_SNARK_GROTH16 the prover_address is mandatory: the aggregation program embeds it into the journal digest and AggregateVerifier re-checks the same digest, so a Groth16 receipt is bound to the L1 sender that requested it.
Proof types
Section titled “Proof types”Two proof types are supported:
| Proof type | Backend sessions | Result |
|---|---|---|
PROOF_TYPE_COMPRESSED | STARK | A compressed SP1 range proof. |
PROOF_TYPE_SNARK_GROTH16 | STARK, SNARK | A range proof plus a Groth16 aggregation proof suitable for onchain use. |
A PROOF_TYPE_SNARK_GROTH16 request runs in two stages: the range program is submitted as a compressed STARK session, and once that session lands the aggregation program is submitted as a Groth16 SNARK session.
Request lifecycle
Section titled “Request lifecycle”Each proof request starts in CREATED once the request and its outbox entry are durably stored. A worker claims the outbox task and the request moves to PENDING while it prepares and submits backend work. The state advances to RUNNING as soon as at least one backend session exists. The terminal states are SUCCEEDED — every session required by the proof type completed and the receipt bytes are stored — or FAILED, set when validation, witness generation, backend submission, backend execution, receipt download, or retry recovery gives up.
Backend sessions advance through RUNNING, COMPLETED, or FAILED independently of the proof request itself. A compressed request lands when its STARK session lands. A Groth16 request lands only after both STARK and SNARK sessions complete. Any failed session fails the parent request.
Receipt retrieval
Section titled “Receipt retrieval”GetProofRequest carries:
| Field | Meaning |
|---|---|
session_id | UUID returned by ProveBlock. |
receipt_type | Optional receipt selector. Defaults to RECEIPT_TYPE_STARK. |
Receipt selectors:
| Receipt type | Response bytes |
|---|---|
RECEIPT_TYPE_STARK | Serialized SP1 proof-with-public-values for the range proof. |
RECEIPT_TYPE_SNARK | Serialized SP1 proof-with-public-values for the aggregation proof. |
RECEIPT_TYPE_ON_CHAIN_SNARK | Onchain proof bytes extracted from the stored SNARK receipt for the SP1 Groth16 verifier. |
While a request is CREATED, PENDING, or RUNNING, GetProof returns empty receipt bytes. A failed request returns STATUS_FAILED and the stored error message. A success always carries non-empty bytes; if the stored request is Succeeded but the requested receipt kind is missing, GetProof returns gRPC NOT_FOUND rather than an empty success.
Wrapping returned receipt bytes in the AggregateVerifier proof format is the caller’s job. For challenge, nullification, and additional-proof submission the caller prefixes the ZK proof-type byte. For game initialization the caller also adds the L1 origin fields required by initializeWithInitData(). See Proof Contracts for the verifier-side framing.
Backend modes
Section titled “Backend modes”Three backend modes are available:
| Mode | Purpose |
|---|---|
mock | Produces fake receipts for local tests without witness generation. |
cluster | Submits work to a self-hosted SP1 cluster with Redis or S3 artifacts. |
network | Submits work to the SP1 Network with the configured fulfillment policy. |
The cluster and network backends share the same witness generation; only submission, polling, and artifact retrieval differ. The mock backend skips witness generation entirely.
SP1 range program
Section titled “SP1 range program”The range program proves a Base L2 state transition over a contiguous block range. Its stdin contains:
rkyv(DefaultWitnessData)intermediateRootIntervalThe program reconstructs the preimage oracle and beacon blob provider from the witness, runs the Ethereum DA witness executor, and commits a BootInfoStruct.
The committed boot info contains:
| Field | Meaning |
|---|---|
l2PreRoot | Output root for the trusted starting L2 block. |
l2PreBlockNumber | Starting L2 block number. |
l2PostRoot | Output root after executing the requested range. |
l2BlockNumber | Ending L2 block number. |
l1Head | L1 block hash used for derivation data. |
rollupConfigHash | Hash of the rollup configuration used during execution. |
intermediateRoots | Ordered output roots sampled every intermediate-root interval. |
The final intermediate root must correspond to the ending L2 block for the range being proven.
SP1 aggregation program
Section titled “SP1 aggregation program”The aggregation program collapses completed range proofs into the journal digest used by onchain verification. Inputs:
AggregationInputs (sp1_zkvm::io::read)L1 headers (CBOR-encoded) (sp1_zkvm::io::read_vec)compressed range proofs (SP1 proof-input channel)The compressed range proofs travel over SP1’s proof-input mechanism rather than plain stdin bytes, and the program verifies them internally with sp1_lib::verify::verify_sp1_proof.
AggregationInputs bundles the range boot infos, the latest L1 checkpoint head, the range-program verification key, and the prover address.
The aggregation program checks:
-
At least one range boot info is present.
-
Adjacent range boot infos are sequential:
previous.l2PostRoot == next.l2PreRootprevious.l2BlockNumber == next.l2PreBlockNumber -
Every range uses the same
rollupConfigHash. -
Every compressed range proof verifies against the supplied range verification key.
-
The supplied L1 headers form a linked chain ending at
latest_l1_checkpoint_head. -
Every range
l1Headappears in that header chain.
After verification, the program concatenates all intermediate roots and builds one aggregate output:
proverAddressl1Headl2PreRootstartingL2SequenceNumberl2PostRootendingL2SequenceNumberintermediateRootsrollupConfigHashimageHashimageHash is the range-program verification key commitment. The aggregation program commits:
keccak256(abi.encodePacked(AggregationOutputs))That digest matches the journal hash AggregateVerifier assembles for ZK proof verification. In Proof Contracts terminology, imageHash is ZK_RANGE_HASH and the aggregation verification key configured on ZKVerifier is ZK_AGGREGATE_HASH.
ELF reproducibility
Section titled “ELF reproducibility”SP1 ELF binaries are built on demand rather than committed. The repository pins expected ELF SHA-256 hashes in crates/proof/succinct/elf/manifest.toml. Any code change touching either SP1 program must rebuild the ELFs and update manifest.toml in the same change.
The range verification key commitment (ZK_RANGE_HASH) and aggregation verification key hash (ZK_AGGREGATE_HASH) are onchain security parameters. Operators must deploy or configure verifier contracts with values derived from the same ELFs the proving service runs.
Retry behavior
Section titled “Retry behavior”Transient conditions are retried without changing the logical proof request:
| Condition | Required behavior |
|---|---|
| Outbox task already claimed | Skip the duplicate worker. |
Stuck PENDING request without an active session | Reset to CREATED with a new outbox entry until the retry limit is exhausted. |
| Backend status polling error | Leave the request RUNNING and retry on a later poll. |
| Proof artifact unavailable after backend success | Leave the session RUNNING or retry download on a later poll. |
| Backend reports failed or unfulfillable work | Mark the session and proof request FAILED. |
| Groth16 stage-two submission fails after STARK | Mark the proof request FAILED. |
Callers should treat FAILED as terminal for the stored request. If the proof is still wanted, resubmit or retry the same logical request using its deterministic session_id.
Service lifecycle
Section titled “Service lifecycle”At startup, the proving service:
- Connects to Postgres.
- Optionally starts rate-limited local proxies for L1, L2, and beacon RPCs.
- Loads rollup configuration from the rollup RPC.
- Computes the range and aggregation proving and verifying keys.
- Initializes the configured backend.
- Starts the outbox processor.
- Starts the status poller.
- Starts the gRPC server and reflection service.
The outbox processor converts persisted requests into backend sessions. The status poller keeps running sessions in sync, downloads receipts, triggers Groth16 stage two when needed, and either retries or fails stuck requests.
Operator inputs
Section titled “Operator inputs”A ZK prover service needs:
- L1 execution RPC endpoint.
- L1 beacon RPC endpoint.
- L2 execution RPC endpoint.
- Rollup RPC endpoint.
- Postgres connection settings.
- SP1 backend configuration.
- Artifact storage configuration for cluster mode.
- Poll intervals, stuck-request timeout, and retry limits.
- Metrics and logging configuration.
Network mode also needs an SP1 Network signer or KMS requester configuration. Cluster mode also needs an SP1 cluster endpoint and exactly one artifact storage backend.
Onchain expectations
Section titled “Onchain expectations”ZK proof bytes are submitted to AggregateVerifier with proof type ZK. The game assembles the expected journal from proposal or dispute context and calls ZKVerifier.verify() with the configured aggregation verification key.
A valid Groth16 receipt proves the aggregation program committed the expected journal digest. It does not substitute for caller-side state checks. Proposers and challengers must still recompute canonical roots and recheck game state before submitting proof material.
Safety requirements
Section titled “Safety requirements”A conforming ZK prover preserves these safety properties:
- Use the caller-provided
l1_headwhen present so dispute proofs match the game context stored onchain. - Require
prover_addressfor Groth16 proofs because the aggregation journal commits it. - Keep request creation idempotent for deterministic
session_idvalues. - Never return onchain SNARK bytes unless the stored SNARK receipt deserializes successfully.
- Persist backend session metadata before relying on asynchronous backend completion.
- Pin ELF hashes so verification keys and onchain configuration cannot silently drift.
- Treat unavailable RPC data, backend polling failures, and artifact download failures as retryable service conditions, not proof validity results.