Proposer
The proposer is the offchain service that promotes a canonical L2 checkpoint range into an AggregateVerifier game on L1. Each cycle it reads the latest onchain parent state, picks the next checkpoint, asks a TEE prover for a proof over that range, revalidates the proof against canonical L2 state, and opens a new dispute game through DisputeGameFactory.
The production proposer is gated by the L1 signer it is configured with. Its output remains self-validating end to end: a game is uniquely keyed by game type, claimed output root, parent, L2 block number, and the ordered intermediate output roots, and the proof itself can be re-checked by the onchain verifier and by independent challengers.
Responsibilities
Section titled “Responsibilities”A conforming proposer is expected to:
- Load the active
AggregateVerifierimplementation and proposal parameters from L1. - Recover the latest onchain parent state from
AnchorStateRegistryandDisputeGameFactory. - Pick the next checkpoint block that is at or before the configured safe head.
- Build a
prover_proverequest covering that checkpoint range. - Accept only TEE proof results for proposal creation.
- Re-check the aggregate output root and every intermediate root against canonical L2 state right before L1 submission.
- Optionally pre-validate the TEE signer against
TEEProverRegistry. - Submit
DisputeGameFactory.createWithInitData()with the required bond. - Retry transient proof, RPC, and transaction failures without ever creating games out of order.
The proposer does not challenge games, resolve them, claim bonds, or decide withdrawal finality — those duties live with the challenger and the proof contracts.
Startup Configuration
Section titled “Startup Configuration”At startup the proposer connects to:
- an L1 execution RPC for contract reads and transaction submission
- an L2 execution RPC for agreed L2 block headers
- a rollup RPC for sync status and output roots
- a prover RPC that implements
prover_prove AnchorStateRegistryDisputeGameFactory- an optional
TEEProverRegistry
The game implementation address comes from:
DisputeGameFactory.gameImpls(gameType)That address must be non-zero. From there the proposer reads:
AggregateVerifier.BLOCK_INTERVAL()AggregateVerifier.INTERMEDIATE_BLOCK_INTERVAL()DisputeGameFactory.initBonds(gameType)BLOCK_INTERVAL must be at least 2, INTERMEDIATE_BLOCK_INTERVAL must be non-zero, and BLOCK_INTERVAL % INTERMEDIATE_BLOCK_INTERVAL must be 0. The number of intermediate roots a proposal carries is:
BLOCK_INTERVAL / INTERMEDIATE_BLOCK_INTERVALBy default the proposer targets finalized L2 state. Operators may opt into safe-but-not-finalized proposals, in which case the rollup node’s safe L2 head is used instead.
Parent recovery
Section titled “Parent recovery”Before planning new work, the proposer walks forward from L1 to find the current tip of the proposal chain. Parent state is captured as:
parentAddressparentOutputRootparentL2BlockNumberIf no games have been created yet, the parent is the anchor root from AnchorStateRegistry:
parentAddress = AnchorStateRegistry addressparentOutputRoot = AnchorStateRegistry.getAnchorRoot().rootparentL2BlockNumber = AnchorStateRegistry.getAnchorRoot().l2BlockNumberOtherwise the proposer performs a deterministic forward walk from the anchor root (or from a cached recovered tip when the cache is still fresh). At each step it:
-
Computes:
expectedBlock = parentL2BlockNumber + BLOCK_INTERVAL -
Fetches the canonical output root for every intermediate checkpoint at:
parentL2BlockNumber + INTERMEDIATE_BLOCK_INTERVAL * ifor
iin1..=BLOCK_INTERVAL / INTERMEDIATE_BLOCK_INTERVAL. -
Takes the final intermediate root as the canonical root claim for
expectedBlock. -
Builds
extraDatafromexpectedBlock,parentAddress, and the ordered intermediate roots. -
Looks the expected game up:
DisputeGameFactory.games(gameType, rootClaim, extraData) -
If the lookup returns
address(0), walk stops and the current parent is the recovered tip. -
Otherwise it advances the parent to the returned game proxy and repeats.
This recovery does not scan the factory for a “best” game. It uses each game’s unique factory key, so only the canonical next game for the recovered parent can advance the parent chain. Any game that disagrees on root, parent, L2 block number, or intermediate roots has a different key and is invisible to recovery.
Checkpoint selection
Section titled “Checkpoint selection”After recovery, the next proposal target is:
targetBlock = parentL2BlockNumber + BLOCK_INTERVALThe proposer must not request or submit a proof for targetBlock unless:
targetBlock <= safeHeadwhere safeHead is either:
finalized_l2.number, by default, orsafe_l2.number, only when non-finalized proposals are explicitly enabled.
When parallel proving is on, multiple proof requests can be in flight for future checkpoints, but L1 submissions are still strictly sequential — at most one proposal transaction is pending at a time, and the next one waits until earlier checkpoint games are recovered or confirmed.
Proof request
Section titled “Proof request”For a checkpoint range the proposer builds a ProofRequest with:
| Field | Value |
|---|---|
l1_head | Hash of the latest L1 block at request construction time |
l1_head_number | Number of the latest L1 block at request construction time |
agreed_l2_head_hash | L2 block hash at parentL2BlockNumber |
agreed_l2_output_root | Parent output root recovered from L1 |
claimed_l2_output_root | Rollup RPC output root at targetBlock |
claimed_l2_block_number | targetBlock |
proposer | L1 address that will submit the proposal transaction |
intermediate_block_interval | INTERMEDIATE_BLOCK_INTERVAL |
image_hash | Expected TEE image hash |
The prover RPC method is:
prover_prove(ProofRequest) -> ProofResultOnly ProofResult::Tee is accepted by the proposer. ZK proof results are not valid input on this path.
TEE proposal journal
Section titled “TEE proposal journal”The TEE prover returns:
- one aggregate proposal that covers the entire checkpoint range
- per-block proposals for each block inside that range
The aggregate proposal carries:
outputRootsignaturel1OriginHashl1OriginNumberl2BlockNumberprevOutputRootconfigHashThe TEE signature is taken over:
keccak256(journal)where journal is packed as:
proposer(20)|| l1OriginHash(32)|| prevOutputRoot(32)|| startingL2Block(8)|| outputRoot(32)|| endingL2Block(8)|| intermediateRoots(32 * N)|| configHash(32)|| teeImageHash(32)For aggregate proposals:
startingL2Block = parentL2BlockNumberendingL2Block = targetBlockprevOutputRoot = parentOutputRootoutputRoot = claimed root at targetBlockintermediateRoots are sampled every INTERMEDIATE_BLOCK_INTERVAL blocks and include the final target block root.
Pre-submission validation
Section titled “Pre-submission validation”Right before sending to L1, the proposer re-validates the proof against canonical L2 state:
- Fetch the rollup output root at
targetBlock. - Require it to equal the aggregate proposal’s
outputRoot. - Extract the intermediate roots from the per-block proposals.
- Fetch the canonical output root for every intermediate checkpoint.
- Require every proposed intermediate root to equal its canonical counterpart.
If the aggregate root or any intermediate root no longer matches canonical state, the proposer drops the pending work and restarts recovery. This is the guard against stale proof results after L1 or L2 reorgs.
When a TEEProverRegistry is configured, the proposer should recover the TEE signer from the aggregate proposal signature and call:
TEEProverRegistry.isValidSigner(signer)A false return means the proof must not be submitted; discard it and request a new one. If the registry call itself fails for RPC or deployment reasons, the proposer may continue to submission and let the onchain verifier enforce signer validity.
Game creation
Section titled “Game creation”The proposer creates the game with:
DisputeGameFactory.createWithInitData{value: initBond}( gameType, rootClaim, extraData, initData)where:
rootClaim = aggregateProposal.outputRootextraData is packed, not ABI-encoded:
l2BlockNumber(32) || parentAddress(20) || intermediateRoots(32 * N)l2BlockNumber is a 32-byte big-endian integer. parentAddress is the recovered parent game proxy, or the AnchorStateRegistry address for the first game after the anchor.
initData is the TEE proof bytes for AggregateVerifier.initializeWithInitData():
proofType(1) || l1OriginHash(32) || l1OriginNumber(32) || signature(65)For TEE proofs:
proofType = 0The ECDSA v value must be normalized to 27 or 28 before submission. initBond is read from DisputeGameFactory.initBonds(gameType) at startup and is sent as the transaction value. The L1 transaction manager owns nonce management, fee bumping, signing, and resubmission.
Duplicate games
Section titled “Duplicate games”A game’s factory key is:
gameType || rootClaim || extraDataIf createWithInitData() reverts with GameAlreadyExists, the proposer treats the target as already submitted, refreshes recovery from L1, and continues from the new tip. This covers both the case where a prior transaction succeeded but the receipt was missed, and the case where another valid proposer beat us to the same game.
Retry behavior
Section titled “Retry behavior”Transient failures are retried on later ticks:
| Failure | Required behavior |
|---|---|
| Recovery RPC or contract read failure | Skip the current tick and retry recovery on the next tick |
| Proof request failure | Retry the target on a later tick |
| Repeated proof failure | Reset pipeline state and recover from L1 |
| L1 submission failure | Keep the proved result and retry submission on a later tick |
| L1 submission timeout | Treat as a submission failure and retry after recovery |
GameAlreadyExists | Treat as success, refresh recovery, and continue |
| Canonical root mismatch | Reset pipeline state and re-prove from recovered L1 state |
| Invalid TEE signer | Discard the proof and request a new one |
The current implementation retries a single proof target up to three times before resetting pipeline state, and bounds proposal submission with a ten-minute timeout.
Admin interface
Section titled “Admin interface”The proposer may expose an optional JSON-RPC admin interface. When enabled, it provides:
| Method | Result |
|---|---|
admin_startProposer | Starts the proving pipeline |
admin_stopProposer | Stops the proving pipeline |
admin_proposerRunning | Returns whether the pipeline is running |
Starting a proposer that is already running, or stopping one that is already stopped, are both errors.
Dry run mode
Section titled “Dry run mode”In dry run mode the proposer still performs recovery, checkpoint selection, proof sourcing, and pre-submission validation, but it does not send L1 transactions — it just logs the game that would have been created. Useful for validating prover and RPC behavior, but it does not advance the onchain proposal chain.