The challenger is an offchain service that protects the proof system by independently checking in-progressDocumentation Index
Fetch the complete documentation index at: https://base-a060aa97-leopoldjoy-migrate-tee-provers-spec.mintlify.app/llms.txt
Use this file to discover all available pages before exploring further.
AggregateVerifier games against canonical L2 state. When it finds an invalid
checkpoint root, it obtains the proof material required by the game contract and submits a dispute
transaction on L1.
The challenger is permissionless in the ZK path: any operator with access to canonical L1 and L2
RPCs, a ZK proving service, and an L1 transaction signer can run it. Base may also run a challenger
with access to a TEE proof endpoint so invalid TEE-backed games can be nullified on a faster path
before falling back to ZK.
Responsibilities
A conforming challenger performs the following work:- Scan recent
DisputeGameFactorygames. - Select games that are still
IN_PROGRESSand have proof state that may require action. - Recompute the relevant checkpoint output roots from an L2 node.
- Identify the first invalid checkpoint root, or determine whether a ZK challenge targeted a valid checkpoint.
- Source a TEE or ZK proof for the checkpoint interval that must be proven.
- Submit
nullify()orchallenge()to the game contract. - Track the resulting bond lifecycle when configured to claim bonds.
Game Selection
The challenger reads the currentAnchorStateRegistry.anchorGame(), locates that game in the
factory index array, and scans every later factory index. If the registry is still at the starting
anchor, or if the anchor game cannot be found in the factory, scanning starts at index 0. Games
observed IN_PROGRESS remain tracked until they resolve or are fully nullified, so metrics reflect
the live post-anchor set. Each scan re-evaluates the full post-anchor range so games can move
between categories as new proofs, challenges, or nullifications are posted onchain. Individual game
query failures are logged and retried on the next scan; they do not abort the full scan.
A game is selected only when status() == IN_PROGRESS. The challenger then reads:
teeProver()zkProver()counteredByIntermediateRootIndexPlusOne()rootClaim()l2SequenceNumber()startingBlockNumber()l1Head()INTERMEDIATE_BLOCK_INTERVAL()from the game implementation for the game type
(teeProver, zkProver, countered index) tuple determines the candidate category.
| TEE prover | ZK prover | Countered index | Category | Challenger action |
|---|---|---|---|---|
| non-zero | zero | 0 | Invalid TEE proposal | Validate all checkpoint roots. If invalid, prefer TEE nullification and fall back to ZK challenge(). |
| non-zero | non-zero | > 0 | Fraudulent ZK challenge | Validate only the challenged checkpoint. If the challenged root is correct, submit ZK nullify(). |
| zero | non-zero | 0 | Invalid ZK proposal | Validate all checkpoint roots. If invalid, submit ZK nullify(). |
| non-zero | non-zero | 0 | Invalid dual proposal | Validate all checkpoint roots. If invalid, nullify the TEE proof first, then rescan to handle the remaining ZK proof. |
Output Root Validation
For an unchallenged proposal, the challenger validates the submitted intermediate roots. For indexi, the checkpoint block is:
- Fetch the L2 block header by block number.
- Verify that the RPC-provided header hash equals the hash computed from the consensus header.
- Fetch an
eth_getProofaccount proof forL2ToL1MessagePasserat that block hash. - Verify the account proof against the header state root.
- Build the output root from the L2 state root,
L2ToL1MessagePasserstorage root, and L2 block hash. - Compare the computed root to the root stored in the game.
intermediateRootIndex and intermediateRootToProve used in the
dispute transaction. intermediateRootToProve is the locally computed correct root for the invalid
checkpoint.
When the requested L2 block is not yet available, the challenger skips the game for that scan tick.
The game remains eligible and will be retried on the next scan.
Fraudulent ZK Challenge Validation
When a TEE proposal has been challenged by a ZK proof, the game stores a 1-based countered index. The challenger converts it to a 0-based checkpoint index and validates only that checkpoint. If the onchain root at the challenged index does not match the locally computed root, the ZK challenge was legitimate and the challenger takes no action. If the onchain root matches the local root, the ZK challenge targeted a correct checkpoint and is fraudulent. The challenger then obtains a ZK proof for that checkpoint interval and submitsnullify().
This validation is intentionally local to the challenged index. Earlier invalid roots do not make a
challenge against a later valid root legitimate.
Proof Sourcing
The challenger proves only the interval that contains the invalid checkpoint. The trusted anchor is the prior checkpoint root, or the game’sstartingBlockNumber state when the invalid checkpoint is
index 0.
For a ZK proof request:
start_block_numberis the start of the invalid checkpoint interval.number_of_blocks_to_proveisINTERMEDIATE_BLOCK_INTERVAL.proof_typeis Groth16 SNARK.session_idis deterministic from(game address, invalid checkpoint index).prover_addressis the L1 address that will submit the transaction.l1_headis the L1 head hash stored in the game at creation.
l1Head, the
corresponding L1 block number, the locally computed agreed L2 output at the start of the interval,
and the expected output root at the invalid checkpoint. The challenger accepts the TEE result only
if the enclave output root equals the locally computed expected root, then encodes the TEE dispute
proof bytes for nullify().
If the TEE request fails or times out, the challenger falls back to ZK. If a TEE proof is obtained
but the TEE nullify() transaction fails, the pending entry transitions to a ZK proof request
instead of retrying the same TEE transaction indefinitely.
Dispute Transactions
The challenger submits one of two game calls:| Intent | Contract call | Used when |
|---|---|---|
| Nullify | nullify(proofBytes, intermediateRootIndex, intermediateRootToProve) | Removing an invalid TEE proof, removing an invalid ZK proof, or refuting a fraudulent ZK challenge. |
| Challenge | challenge(proofBytes, intermediateRootIndex, intermediateRootToProve) | Challenging an invalid TEE proposal with a ZK proof. |
nullify(). ZK proofs can target either challenge() or nullify()
depending on the candidate category.
Before submitting or retrying a failed proof, the challenger rechecks the game status and prover
slots. If the game has already resolved, has already been challenged, or the targeted prover slot
has already been zeroed, the pending proof is dropped. This prevents duplicate transactions when
another actor has already handled the game.
Pending Proof Lifecycle
Each pending proof is keyed by game address and tracks:- proof kind: TEE or ZK
- invalid checkpoint index
- expected root for that checkpoint
- dispute intent
- retry count
- phase
ReadyToSubmit immediately after it is obtained;
if its transaction fails, the challenger immediately requests the pre-built ZK fallback proof when
one is available. If no fallback request exists, the entry is dropped; if the fallback prove_block
call fails, the entry remains in NeedsRetry until the next tick. A proof that remains pending, a
failed ZK transaction, or a failed prove_block retry leaves the proof in its current phase until
the next tick. A pending proof causes no contract reads for that game on that tick.
Bond Claiming
Bond claiming is optional and is enabled by configuring claim addresses. When enabled, the challenger tracks games whosebondRecipient() or pre-resolution zkProver() matches one of those addresses.
This allows a challenger to recover claimable games after restart and to discover games handled by
other actors.
The bond lifecycle is:
NeedsResolve: wait forgameOver(), then submitresolve().NeedsUnlock: submit the firstclaimCredit()to unlock theDelayedWETHcredit.AwaitingDelay: wait for theDelayedWETHdelay.NeedsWithdraw: submit the secondclaimCredit()to withdraw the credit.
bondRecipient() and stops tracking the game if the bond
is no longer claimable by a configured address. For games that resolve as DEFENDER_WINS, it also
attempts a best-effort AnchorStateRegistry.setAnchorState(game) update. The registry call is
permissionless and self-validating; premature or ineligible calls can revert and be retried.
Service Lifecycle
At startup, the challenger:- Creates L1 and L2 RPC clients.
- Creates the L1 transaction manager from the configured signer.
- Creates
DisputeGameFactoryandAggregateVerifierclients. - Creates the ZK proof client and optional TEE proof client.
- Starts the health server.
- Starts the driver loop.
- Polls pending proof sessions and submits ready disputes.
- Discovers claimable bonds and advances tracked bond claims.
- Scans for in-progress candidate games.
- Validates and initiates proofs for new candidates.
Operator Inputs
A challenger needs:- L1 RPC endpoint.
- L2 execution RPC endpoint.
DisputeGameFactoryaddress.AnchorStateRegistryaddress.- ZK proof RPC endpoint.
- L1 transaction signer.
- Poll interval.
- TEE proof RPC endpoint and timeout, enabling TEE-first nullification for TEE-backed games.
- Bond claim addresses, bond discovery interval, and bond discovery lookback window, enabling automatic bond recovery and claiming.
- Metrics and health server settings.
Safety Requirements
A challenger implementation must preserve these safety properties:- Do not dispute a game from the game’s own claimed roots alone; recompute roots from L2 headers and
verified
L2ToL1MessagePasseraccount proofs. - Use the game’s stored L1 head when requesting dispute proofs, so proof journals match the game context verified onchain.
- For fraudulent ZK challenges, validate the challenged checkpoint itself rather than the first invalid checkpoint in the whole proposal.
- Recheck game state before submitting a ready proof, because another challenger or prover may have already changed the game.
- Treat unavailable L2 blocks and transient RPC failures as retryable scan conditions rather than final validation results.