Skip to main content
The proposer is an offchain service that turns canonical L2 checkpoint ranges into AggregateVerifier games on L1. It selects the next checkpoint from the latest onchain parent state, obtains a TEE proof for that range, validates the proof against canonical L2 state, and creates the next dispute game through DisputeGameFactory. The production proposer is controlled by its configured L1 transaction signer. Its output is still self-validating: each game is uniquely identified by the game type, claimed output root, parent, L2 block number, and intermediate output roots, and the proof can be checked by the onchain verifier and by independent challengers.

Responsibilities

A conforming proposer performs the following work:
  1. Read the active AggregateVerifier implementation and proposal parameters from L1.
  2. Recover the latest onchain parent state from AnchorStateRegistry and DisputeGameFactory.
  3. Select the next checkpoint block that is no later than the chosen safe head.
  4. Build a prover_prove request for the checkpoint range.
  5. Accept only TEE proof results for proposal creation.
  6. Revalidate the aggregate output root and all intermediate roots against canonical L2 state immediately before L1 submission.
  7. Optionally pre-check the TEE signer against TEEProverRegistry.
  8. Submit DisputeGameFactory.createWithInitData() with the required bond.
  9. Retry transient proof, RPC, and transaction failures without creating out-of-order games.
The proposer does not challenge games, resolve games, claim bonds, or decide withdrawal finality. Those responsibilities belong to the challenger and proof contracts.

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
  • AnchorStateRegistry
  • DisputeGameFactory
  • an optional TEEProverRegistry
The proposer reads the game implementation address from:
Game Implementation Lookup
The implementation address must be non-zero. Before it creates each game, the proposer reads the bond and the proposal intervals for that game:
Per-Game Reads
intervalsForStartingBlock returns the (BLOCK_INTERVAL, INTERMEDIATE_BLOCK_INTERVAL) pair that applies to a game starting at parentL2BlockNumber. Implementations earlier than version 0.3.0 expose only BLOCK_INTERVAL() and INTERMEDIATE_BLOCK_INTERVAL(). See Proposal Interval Selection. On this page, BLOCK_INTERVAL and INTERMEDIATE_BLOCK_INTERVAL mean that per-game pair. 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 in a proposal is:
Intermediate Root Count
The proposer defaults to finalized L2 state. If explicitly configured to allow non-finalized proposals, it may use the rollup node’s safe L2 state instead.

Proposal Interval Selection

The proposal interval is not a fixed process-wide constant. AggregateVerifier version 0.3.0 and later stores two interval pairs and picks one from each game’s starting block:
Interval Pairs
The slow pair applies to games that start before the L2 block-time speedup activation in the ProtocolVersions schedule. The fast pair applies to games that start at or after it. While the speedup is not scheduled, every game uses the slow pair. Both pairs must yield the same number of intermediate roots, so the extraData layout does not change at the activation. The proposer resolves the pair for every game it creates, reconstructs, or looks up. It reads the active implementation from DisputeGameFactory.gameImpls(gameType) and calls intervalsForStartingBlock each time instead of caching the result at startup. A cached value would stop matching games once the implementation is upgraded or the activation block is reached. AggregateVerifier versions before 0.3.0 instead expose a single fixed pair through BLOCK_INTERVAL() and INTERMEDIATE_BLOCK_INTERVAL(). The proposer reads version() to choose which interface to call. The interval for new games on a network is the value that intervalsForStartingBlock(startingBlock) returns on the implementation at DisputeGameFactory.gameImpls(gameType). If that implementation reports a version earlier than 0.3.0, the interval is the value of BLOCK_INTERVAL(). The time from an L2 block to its L1 checkpoint also depends on the L2 block time, on how far the chosen safe or finalized head lags the target block, and on proof generation and L1 inclusion. This specification does not set a target proposal cadence.

Parent Recovery

The proposer recovers the latest onchain parent state from L1 before planning new work. The parent state is:
Parent State Fields
If no matching games exist, the parent is the anchor root from AnchorStateRegistry:
Anchor Parent State
If games exist, the proposer performs a deterministic forward walk from the anchor root, or from a cached recovered tip when the cache is still valid. At each step:
  1. Compute:
  2. Fetch the canonical output root for every intermediate checkpoint:
    for i in 1..=BLOCK_INTERVAL / INTERMEDIATE_BLOCK_INTERVAL.
  3. Treat the final intermediate root as the canonical root claim for expectedBlock.
  4. Encode extraData from expectedBlock, parentAddress, and the ordered intermediate roots.
  5. Look up the expected game:
  6. If the lookup returns address(0), stop. The current parent is the latest recovered state.
  7. Otherwise, advance the parent to the returned game proxy and continue.
This recovery method does not scan factory indices for a “best” game. It uses the game’s unique factory key, so only the canonical next game for the recovered parent can advance the chain of parents. A game with the wrong root, parent, L2 block number, or intermediate roots has a different key and is ignored by parent recovery.

Checkpoint Selection

After recovery, the next proposal target is:
Target Block Formula
The proposer must not request or submit a proof for targetBlock unless:
Safe Head Constraint
where safeHead is either:
  • finalized_l2.number, by default
  • safe_l2.number, only when non-finalized proposals are explicitly enabled
When parallel proving is enabled, the proposer may request proofs for multiple future checkpoint targets, but L1 submissions remain strictly sequential. At most one proposal transaction is in flight, and the next transaction is not submitted until all earlier checkpoint games are recovered or confirmed.

Proof Request

For a checkpoint range, the proposer builds a ProofRequest with: The prover RPC method is:
Prover RPC Method
The proposer accepts ProofResult::Tee for proposal creation. A ZK proof result is not valid input for the current proposer path.

TEE Proposal Journal

The TEE prover returns:
  • an aggregate proposal for the full checkpoint range
  • per-block proposals for the blocks in that range
The aggregate proposal contains:
Aggregate Proposal Fields
The TEE signature is over:
TEE Signature Preimage
where journal is packed as:
Journal Packing
For aggregate proposals:
Aggregate Proposal Values
The ordered intermediateRoots are sampled every INTERMEDIATE_BLOCK_INTERVAL blocks and include the final target block root.

Pre-Submission Validation

Immediately before submitting to L1, the proposer must re-check the proof against canonical L2 state:
  1. Fetch the rollup output root at targetBlock.
  2. Require it to equal the aggregate proposal’s outputRoot.
  3. Extract the intermediate roots from the per-block proposals.
  4. Fetch the canonical output root for each intermediate checkpoint.
  5. Require every proposed intermediate root to equal its canonical root.
If the aggregate root or any intermediate root no longer matches canonical state, the proposer discards the pending work and restarts recovery. This protects against stale proof results after L1 or L2 reorgs. When TEEProverRegistry is configured, the proposer should recover the TEE signer from the aggregate proposal signature and call:
Signer Validity Check
If the registry returns false, the proposer must not submit that proof. It should discard the proof and request a new one. If the registry check itself fails because of an RPC or deployment issue, the proposer may continue to submission and rely on the onchain verifier to enforce signer validity.

Game Creation

The proposer creates a game with:
Game Creation Call
where:
Root Claim
extraData is packed, not ABI-encoded:
Extra Data Packing
l2BlockNumber is encoded as a 32-byte big-endian integer. parentAddress is the recovered parent game proxy address, or the AnchorStateRegistry address for the first game after the anchor. initData is the TEE proof bytes for AggregateVerifier.initializeWithInitData():
Init Data Packing
For TEE proofs:
TEE Proof Type
The ECDSA v value in the signature must be normalized to 27 or 28 before submission. initBond is read from DisputeGameFactory.initBonds(gameType) before each game is created and is sent as the transaction value. Nonce management, fee bumping, signing, and transaction resubmission are handled by the L1 transaction manager.

Duplicate Games

The factory key for a game is:
Factory Game Key
If createWithInitData() reverts with GameAlreadyExists, the proposer treats the target as already submitted. It refreshes recovery from L1 and continues from the recovered tip. This handles the case where a previous transaction succeeded but the proposer did not observe the receipt, or where another valid proposer submitted the same game first.

Retry Behavior

The proposer retries transient failures on later ticks: The current implementation retries a single proof target up to three times before resetting pipeline state. Proposal submission is bounded by a ten minute timeout.

Admin Interface

The proposer may expose an optional JSON-RPC admin interface. When enabled, it provides: Starting an already running proposer and stopping a stopped proposer are errors.

Dry Run Mode

In dry run mode, the proposer performs recovery, checkpoint selection, proof sourcing, and pre-submission validation, but it does not submit L1 transactions. Instead, it logs the game that would have been created. Dry run mode is useful for validating prover and RPC behavior, but it does not advance the onchain proposal chain.