Skip to main content
The proof contracts turn offchain proof material into onchain checkpoint games. A game claims an L2 output root for a fixed block interval. The contracts verify the initial proof, accept an optional second proof, allow invalid proof material to be challenged or nullified, resolve the game after the applicable delay, move the anchor state forward, and release the initialization bond. This page specifies the contract behavior used by the proof system:
  • AnchorStateRegistry
  • DelayedWETH
  • DisputeGameFactory
  • AggregateVerifier
  • ZKVerifier
  • TEEVerifier
  • TEEProverRegistry
  • NitroValidator
  • CertManager
  • P384Verifier

Contract Graph

DisputeGameFactory, AnchorStateRegistry, DelayedWETH, and TEEProverRegistry are proxied system contracts. AggregateVerifier is deployed as an implementation and cloned by the factory with immutable arguments. TEEVerifier and ZKVerifier are standalone proof verifiers referenced by the game implementation. The registry references a standalone NitroValidator, which uses CertManager and P384Verifier to validate signer attestations.

Data Model

The contracts share the same dispute-game types: The AggregateVerifier game uses two block intervals:
Block Intervals
BLOCK_INTERVAL is the distance between a parent output root and a proposed output root. INTERMEDIATE_BLOCK_INTERVAL is the spacing between intermediate roots inside that range. BLOCK_INTERVAL and INTERMEDIATE_BLOCK_INTERVAL must be non-zero, and BLOCK_INTERVAL must be divisible by INTERMEDIATE_BLOCK_INTERVAL. The number of intermediate roots in every game is:
Intermediate Root Count
The final intermediate root must equal the game’s rootClaim.

Game Lifecycle

  1. The factory owner configures a game type with an AggregateVerifier implementation and an initialization bond.
  2. The registrar caches Nitro certificates in CertManager, then registers enclave signers through TEEProverRegistry using the attestation and P-384 verification hints.
  3. A proposer creates a game through DisputeGameFactory.createWithInitData(), paying the exact initialization bond and providing an initial TEE or ZK proof.
  4. The game validates its parent, L2 block number, intermediate roots, L1 origin, and proof journal. The bond is deposited into DelayedWETH.
  5. A second proof may be submitted through verifyProposalProof(). If the proposal is invalid, challengers can call challenge() or nullify() with proof material for an intermediate root.
  6. After the expected resolution time, anyone can call resolve(). The result is DEFENDER_WINS for a valid unchallenged game and CHALLENGER_WINS for a successful challenge or invalid parent.
  7. After resolution and the registry finality delay, anyone can call closeGame() to make a best-effort anchor update.
  8. The bond recipient calls claimCredit() twice: once to unlock the DelayedWETH credit, then again after the DelayedWETH delay to withdraw and receive ETH.

DisputeGameFactory

DisputeGameFactory creates and indexes dispute-game clones. Each game is uniquely identified by:
Game UUID
The factory stores that UUID in _disputeGames and also appends a packed GameId to _disputeGameList for index-based discovery. Offchain services use DisputeGameCreated, gameAtIndex(), and findLatestGames() to discover games.

Configuration

Only the factory owner can:
  • set a game implementation with setImplementation(gameType, impl)
  • set a game implementation plus opaque implementation args with setImplementation(gameType, impl, args)
  • set the exact required creation bond with setInitBond(gameType, initBond)
Creation reverts if the implementation is unset, if the paid value differs from initBonds, or if a game with the same UUID already exists.

Clone Arguments

When no implementation args are configured, the clone-with-immutable-args payload is: When implementation args are configured, the payload is: AggregateVerifier uses the standard layout. Its extraData is specified in the AggregateVerifier section below.

AnchorStateRegistry

AnchorStateRegistry is the source of truth for whether a dispute game can be trusted by the proof system. It stores:
  • the SystemConfig
  • the DisputeGameFactory
  • the starting anchor root
  • the current anchor game, if one has been accepted
  • the current respected game type
  • a game blacklist
  • a retirement timestamp
  • a dispute-game finality delay
The initial retirement timestamp is set during first initialization. Games created at or before the retirement timestamp are retired.

Game Predicates

The registry exposes these predicates: isGameProper() does not prove that the root claim is correct. It only means the game has not been invalidated by registry-level controls. Consumers that need claim validity must use isGameClaimValid().

Guardian Controls

The SystemConfig.guardian() can:
  • set the respected game type
  • update the retirement timestamp to the current block timestamp
  • blacklist individual games
These controls are the onchain safety valves for invalidating games before they can become valid claims.

Anchor Updates

getAnchorRoot() returns the starting anchor root until an anchor game is accepted. After that, it returns the root claim and L2 block number of anchorGame. setAnchorState(game) accepts a new anchor game only when:
  • isGameClaimValid(game) is true
  • the game’s L2 sequence number is greater than the current anchor root’s sequence number
The update is permissionless and self-validating.

DelayedWETH

DelayedWETH is WETH with delayed withdrawals. It escrows game bonds and forces a two-step credit claim:
  1. The game calls unlock(subAccount, amount) for the bond recipient.
  2. After delay() seconds, the game calls withdraw(subAccount, amount) and sends ETH to the recipient.
Unlocks are keyed by:
Withdrawal Request Key
For proof games, msg.sender is the AggregateVerifier game contract and subAccount is the current bondRecipient. Withdrawals revert while the system is paused. The proxy admin owner also has emergency recovery powers:
  • recover(amount) sends up to amount ETH from the contract to the owner.
  • hold(account) or hold(account, amount) pulls WETH from an account into the owner address.

AggregateVerifier

AggregateVerifier is the dispute-game implementation for checkpoint proofs. Every factory-created game is a clone with immutable game data. The implementation owns no per-game storage except the clone’s storage.

Constructor Configuration

An implementation fixes these values for all clones of that game type: PROOF_THRESHOLD controls resolution, not proof submission. The game can store one TEE proof, one ZK proof, or both.

Game Extra Data

AggregateVerifier.extraData() is encoded as: where:
Extra Data Root Count
The final intermediate output root must equal rootClaim.

Initialization

initializeWithInitData(proof) can only run once. It verifies the calldata size so that unused bytes cannot create multiple factory UUIDs for the same logical proposal. During initialization the game:
  1. Checks that the final intermediate root matches rootClaim.
  2. Resolves the starting root. If parentAddress is the registry address, the starting root is AnchorStateRegistry.getStartingAnchorRoot(). Otherwise the parent must be a valid registered game.
  3. Requires:
  4. Records createdAt, wasRespectedGameTypeWhenCreated, and an initial expectedResolution.
  5. Verifies the claimed L1 origin hash in the initialization proof against either blockhash() or EIP-2935 history.
  6. Verifies the supplied TEE or ZK proof.
  7. Records the initial prover, sets bondRecipient to gameCreator, and deposits the bond into DelayedWETH.
The initialization proof format is: The L1 origin block must be in the past. Native blockhash() is used for block ages up to 256 blocks. EIP-2935 history is used up to 8191 blocks. Older or unavailable L1 origin blocks revert.

Additional Proofs

verifyProposalProof(proofBytes) adds the missing proof type while a game is in progress and not over. It does not re-read a new L1 origin from calldata. Instead, it uses the l1Head() captured by the factory at clone creation. The additional proof format is: A game cannot store more than one proof of the same type.

Proof Journals

TEE and ZK proofs commit to the same transition shape:
Proof Journal Fields
For TEE proofs, the final field is TEE_IMAGE_HASH and the journal is checked by TEEVerifier. The game calls:
TEE Journal Verification Call
For ZK proofs, the final field is ZK_RANGE_HASH and the proof is checked by ZKVerifier. The game calls:
ZK Journal Verification Call

Resolution Delay

expectedResolution is derived from the number of currently accepted proofs: Adding a proof can only decrease expectedResolution. Nullifying a proof can increase it. A challenge with a ZK proof sets expectedResolution to 7 days from the challenge so the challenge can itself be nullified.

Challenge

challenge(proofBytes, intermediateRootIndex, intermediateRootToProve) challenges a TEE-backed proposal with a ZK proof for one intermediate interval. The call is accepted only when:
  • the game is still IN_PROGRESS
  • the game itself is valid according to the registry
  • the parent has not resolved with CHALLENGER_WINS
  • the game has a TEE proof
  • the game does not already have a ZK proof
  • the supplied proof type is ZK
  • the challenged index is in range
  • the supplied root differs from the currently proposed intermediate root
If the ZK proof verifies, the game records the ZK prover, increments proofCount, stores the 1-based countered intermediate index, and emits Challenged. When the game resolves, the challenger receives the bond and the game status becomes CHALLENGER_WINS.

Nullification

nullify(proofBytes, intermediateRootIndex, intermediateRootToProve) removes an already accepted proof by proving a contradictory intermediate root. For an unchallenged game, the target root must differ from the proposed intermediate root. For a challenged game, only the challenged index can be nullified, only with a ZK proof, and the supplied root must match the original proposed intermediate root. After a successful nullification:
  • the prover slot for that proof type is deleted
  • proofCount decreases
  • expectedResolution is recalculated
  • the countered index is cleared if the ZK challenge was nullified
  • the corresponding verifier contract is nullified
Verifier nullification is a global safety stop. Once TEE_VERIFIER.nullify() or ZK_VERIFIER.nullify() succeeds, future proof verification through that verifier reverts until the system is upgraded or reconfigured.

Resolve, Close, and Bonds

resolve() can be called by anyone. The parent must be resolved unless the parent is the registry itself. If the parent resolved with CHALLENGER_WINS, or later became blacklisted or retired, the child also resolves with CHALLENGER_WINS. Otherwise the game must be over and must have at least PROOF_THRESHOLD accepted proofs. If the game was challenged, resolve() sets CHALLENGER_WINS and moves the bond recipient to the ZK prover. Otherwise it sets DEFENDER_WINS. closeGame() is permissionless. It reverts while the registry is paused, requires the game to be resolved and finalized by the registry, and then attempts AnchorStateRegistry.setAnchorState(). The anchor update is best-effort: if the registry rejects the game because it is no longer the newest valid claim, closeGame() swallows that registry revert. claimCredit() has two phases:
  1. Unlock the bond in DelayedWETH.
  2. After the DelayedWETH delay, withdraw WETH and send ETH to bondRecipient.
If accepted proofs have been nullified and expectedResolution is reset to the never-resolvable sentinel, claimCredit() is blocked until 14 days after createdAt. This prevents a stuck game from locking the bond forever.

ZKVerifier

ZKVerifier adapts the Succinct SP1 verifier gateway to the common IVerifier interface used by AggregateVerifier. The call:
ZKVerifier Verify Call
performs:
SP1 Verification Call
and returns true if the SP1 gateway does not revert. imageId is the aggregate program verification key supplied by the game, and journal is the hash of the public inputs assembled by the game. ZKVerifier inherits verifier nullification. After a proper respected game nullifies the verifier, all future verify() calls revert.

TEEVerifier

TEEVerifier verifies TEE proof signatures against the TEEProverRegistry. The proof bytes passed to TEEVerifier are: The signature is recovered over the journal hash directly. It is not wrapped with the Ethereum signed-message prefix. A TEE proof is valid only when:
  • the proof is at least 85 bytes
  • the signature recovers cleanly
  • the proposer is allowlisted in TEEProverRegistry
  • the recovered signer is registered in TEEProverRegistry
  • the signer’s registered image hash equals the imageId supplied by the calling game
The image-hash check prevents an enclave registered for one image from producing accepted proofs for a game type or upgrade that expects another image. TEEVerifier also inherits verifier nullification.

TEEProverRegistry

TEEProverRegistry manages TEE signer registration and proposer allowlisting. The registry has:
  • an owner
  • a manager
  • an immutable NitroValidator reference
  • a DisputeGameFactory
  • a configurable gameType
  • registered signer state
  • proposer allowlist state
The owner can set proposer addresses and update the gameType. The owner or manager can register and deregister signers.

Expected Image Hash

The registry reads the expected TEE image hash from the current game implementation:
Expected Image Hash Lookup
setGameType() validates that this call succeeds and returns a non-zero hash. isValidSigner() returns true only when the signer is registered and its stored image hash matches the current expected hash. Registration does not compare PCR0 with the current TEE_IMAGE_HASH. This lets operators pre-register signers for a future image before a game-type migration. Those signers do not become valid for proof submission until the game’s expected image hash matches their registered image hash.

Signer Registration

registerSigner(attestationTbs, signature, hints) calls:
Attestation Verification Call
The attestation’s certificate chain must already be verified and cached in CertManager. The validator returns field pointers into the signed attestation, and the registry applies Base-specific checks. The attestation timestamp, converted from milliseconds to seconds, must be strictly earlier than block.timestamp and less than MAX_AGE (3,600 seconds) old. PCR0 must be present at index zero, exactly 48 bytes, and not the all-zero debug-mode measurement. The public key must be exactly 65 bytes in uncompressed ANSI X9.62 form:
Uncompressed Public Key Layout
The registry derives the signer address as:
Signer Address Derivation
The registry extracts the 48-byte PCR0 measurement from the attestation and stores:
Signer Image Hash
It then marks the signer as registered, adds it to an enumerable signer set, and emits SignerRegistered.

Deregistration

deregisterSigner(signer) deletes the signer’s registration and image hash, removes the signer from the enumerable set, and emits SignerDeregistered. getRegisteredSigners() returns the current enumerable set. Ordering is not guaranteed.

NitroValidator

NitroValidator validates AWS Nitro attestations using immutable CertManager and P384Verifier references.
Hinted Attestation Validation
The call:
  1. Parses the signed COSE Sig_structure and validates the Nitro payload structure.
  2. Re-walks the certificate chain through CertManager, requiring a complete, unexpired, unrevoked path to the pinned AWS Nitro root.
  3. Verifies the 96-byte P-384 attestation signature over SHA384(attestationTbs) with the leaf certificate’s public key and the supplied inverse hints.
  4. Returns Ptrs, containing the timestamp and CBOR field pointers into attestationTbs.
The certificate-chain walk supplies no certificate hints, so every certificate must already be cached. decodeAttestationTbs() separates a raw COSE_Sign1 document into its signed TBS bytes and signature; decoding alone does not validate the attestation. NitroValidator authenticates the signed fields but does not enforce Base’s freshness window, signer-key format, or accepted enclave image. The registry applies the timestamp, public-key, and PCR0 checks. The registrar’s challenge policy checks the nonce offchain, and TEEVerifier enforces the game’s expected image at proof submission. The deprecated unhinted validateAttestation() entry point always reverts.

CertManager

CertManager pins the AWS Nitro root at deployment and caches verified CA and leaf certificates. Its active verification methods are:
Certificate Cache Calls
Both methods are permissionless. For non-root certificates, the parent must already be cached. Cold verification checks the certificate signature through P384Verifier. Cached reuse checks the certificate’s CA or leaf role, expiry, original parent binding, and unrevoked path to the root. The CA method returns the certificate’s cache key; the leaf method returns VerifiedCert metadata. Non-root cache keys are keccak256(TBSCertificate DER), excluding the outer signature. The root uses its pinned keccak256(root DER) key. loadVerified() is a raw cache read: its result can be expired or revoked and is not itself evidence that a certificate remains usable. Revocation uses a separate identity: computeCertId() returns the non-root issuer/serial identity, and isRevoked() reads its revocation status. The root uses the pinned root hash instead. Revocation is checked during cold verification, cached reuse, and final attestation validation. See the registration plan for the exact cache-key and revocation-identity encodings. Revoking the root blocks new signer registrations. Certificate revocation and expiration do not invalidate previously registered signers; affected signers must be deregistered separately through TEEProverRegistry.deregisterSigner(). The deprecated unhinted verifyCACert() and verifyClientCert() entry points always revert.

P384Verifier

Hinted P-384 Signature Verification
P384Verifier verifies P-384 ECDSA signatures for both certificates and attestations. It consumes 48-byte big-endian inverse hints and checks b * hint == 1 (mod m) before using each inverse. Incorrect, truncated, or surplus hints revert; hints cannot make an invalid signature valid. See P-384 Hints for the generation and encoding requirements. Low-S is not enforced, so equivalent signatures can have different bytes. Signature bytes must not be used as unique certificate or attestation identifiers.

NitroEnclaveVerifier

NitroEnclaveVerifier is the legacy, pre-Cobalt ZK attestation verifier. The current registry uses NitroValidator, CertManager, and P384Verifier instead. See Hinted Registration Migration for the upgrade and preserved registry state.

Cross-Contract Safety Properties

The proof contracts rely on the following cross-contract properties:
  • Factory uniqueness: a logical (gameType, rootClaim, extraData) can create at most one game.
  • Parent validity: non-anchor games can only start from a registered, respected, non-retired, non-blacklisted parent that has not lost.
  • Monotonic checkpoints: each child game must advance exactly BLOCK_INTERVAL L2 blocks from its starting root.
  • Intermediate accountability: every proposal commits to all intermediate roots, so challengers can target the first invalid checkpoint interval.
  • Verifier separation: TEE and ZK proofs use different verifier contracts and different journal domain separators (TEE_IMAGE_HASH versus ZK_RANGE_HASH).
  • Fast finality requires diversity: a game with two accepted proof types can resolve after one day, while a game with one proof waits five days since Beryl.
  • Registry finality is separate from game resolution: a game can resolve before the AnchorStateRegistry accepts it as a valid claim.
  • Safety controls fail closed: pause, blacklist, retirement, and verifier nullification prevent acceptance rather than expanding trust.

Administrative Surfaces

These surfaces are intentionally narrow but high impact. Operational changes to them can affect which games are respected, which proofs verify, and which attestations can register new TEE signers.