Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Migrating from Optimistic to ZK Proofs

This guide walks through migrating an existing OP Stack chain from optimistic fault proofs (e.g., Cannon) to ZK proofs using OP Succinct. The migration is a hot swap — the L2 chain continues producing blocks normally with zero downtime. Only the L1 verification mechanism changes.

Warning

This guide assumes your chain already uses DisputeGameFactory, AnchorStateRegistry, and OptimismPortal2. If your chain uses the legacy L2OutputOracle, you must migrate to the fault proof system first (separate migration).

How It Works

The OP Stack's DisputeGameFactory supports multiple game types simultaneously. Migration works by registering a new ZK game type alongside the existing optimistic game type, then switching the chain's "respected" game type.

Withdrawals remain safe throughout the migration. Each game records an immutable wasRespectedGameTypeWhenCreated flag at creation time. Games created while their type was respected retain that flag forever, so in-flight withdrawals proven against old games can still finalize after the switch. No user action is required.

Choose Your Target Mode

OP Succinct offers two ZK proving modes. Choose one before proceeding:

OP Succinct (Validity)OP Succinct Lite (Fault Proofs)
Proof typeFull validity proofs — every block is provenZK fault dispute games — single-round interactive
Game type6 (OP_SUCCINCT)42 (OP_SUCCINCT_FAULT_DISPUTE_GAME)
Core contractsOPSuccinctL2OutputOracle + OPSuccinctDisputeGameOPSuccinctFaultDisputeGame
Challenger neededNoYes
FinalizationAfter proof submitted + finality delayAfter challenge window + finality delay

The rest of this guide uses <GAME_TYPE> to refer to either 6 or 42 depending on your choice.

Prerequisites

Existing Infrastructure

Your chain must already be running a modern OP Stack with:

  • DisputeGameFactory (proxy)
  • AnchorStateRegistry (proxy)
  • OptimismPortal2
  • op-proposer and op-challenger (OP native)

Required Access

The migration requires two distinct privileged roles:

RoleUsed ForHow to Identify
Factory OwnersetImplementation, setInitBond on DisputeGameFactoryDisputeGameFactory.owner()
GuardiansetRespectedGameType on AnchorStateRegistrySystemConfig.guardian()

These are typically different keys. Confirm you have access to both before proceeding.

Infrastructure & Tooling

Rollback Plan

Before starting, understand how to revert if needed:

  1. Stop the OP Succinct proposer (and challenger, if fault proof mode).
  2. Revert the respected game type (Guardian):
    cast send $ANCHOR_STATE_REGISTRY_ADDRESS "setRespectedGameType(uint32)" <OLD_GAME_TYPE> \
      --rpc-url $L1_RPC --private-key $GUARDIAN_PRIVATE_KEY
    
  3. Restart the old op-proposer and op-challenger.

ZK games created during the migration window remain valid but no new ones will be created after rollback. The registered ZK implementation stays in the factory (there is no removeImplementation) but becomes inert once the respected type is switched back.

Migration Steps

Phase 1: Deploy the ZK Game Implementation

This phase registers the new ZK game type in your existing DisputeGameFactory. No on-chain behavior changes yet — the old optimistic system continues operating normally.

1.1 Clone and build

git clone https://github.com/succinctlabs/op-succinct.git
cd op-succinct/contracts
forge build
cd ..

If migrating to OP Succinct (Validity Mode, game type 6)

1.2 Configure environment

Create a .env file in the project root. See Environment Variables for the full reference.

Key variables for migration:

VariableMigration Value
L1_RPC, L2_RPC, L2_NODE_RPCYour node endpoints
PRIVATE_KEYMust be the Factory Owner key
1.3 Deploy OPSuccinctL2OutputOracle

Deploy the oracle contract following the validity deployment guide:

just deploy-oracle

Save the proxy address from the output.

1.4 Deploy OPSuccinctDisputeGame and register in factory

Since your chain already has a DisputeGameFactory, you need to deploy the OPSuccinctDisputeGame wrapper and register it manually — do not use just deploy-dispute-game-factory as that creates a new factory. See OptimismPortal2 Support — Existing DisputeGameFactory for details.

  1. Deploy the OPSuccinctDisputeGame contract (using forge create or a custom script with L2OO_ADDRESS set to the oracle proxy from step 1.3).

  2. Register the game implementation in your existing factory:

    cast send $FACTORY_ADDRESS "setImplementation(uint32,address)" 6 $DISPUTE_GAME_ADDRESS \
      --rpc-url $L1_RPC --private-key $PRIVATE_KEY
    
  3. Link the factory to the oracle:

    cast send $L2OO_ADDRESS "setDisputeGameFactory(address)" $FACTORY_ADDRESS \
      --rpc-url $L1_RPC --private-key $PRIVATE_KEY
    
  4. Set the initial bond:

    cast send $FACTORY_ADDRESS "setInitBond(uint32,uint256)" 6 $INITIAL_BOND_WEI \
      --rpc-url $L1_RPC --private-key $PRIVATE_KEY
    
1.5 Verify registration
# Should return the OPSuccinctDisputeGame address (non-zero)
cast call $FACTORY_ADDRESS "gameImpls(uint32)" 6 --rpc-url $L1_RPC

# Should return the bond amount
cast call $FACTORY_ADDRESS "initBonds(uint32)" 6 --rpc-url $L1_RPC

If migrating to OP Succinct Lite (Fault Proof Mode, game type 42)

1.2 Configure environment

Create a .env file in the project root. This uses the same environment variables as a fresh deployment — see Contract Configuration for the full reference.

Key differences for migration (vs. greenfield deploy):

VariableMigration Value
FACTORY_ADDRESSYour existing DisputeGameFactory proxy address
OPTIMISM_PORTAL2_ADDRESSYour existing OptimismPortal2 address
ANCHOR_STATE_REGISTRYYour existing AnchorStateRegistry proxy address
PRIVATE_KEYMust be the Factory Owner key
GAME_TYPE42
1.3 Access control configuration

Configure proposer and challenger access control as described in the deployment guide. Permissioned mode (PERMISSIONLESS_MODE=false) is recommended for initial migration. See Fallback Timeout Mechanism for details on permissionless fallback behavior.

1.4 Deploy and register

Use the upgrade script to deploy OPSuccinctFaultDisputeGame and register it in your existing factory. The script calls DisputeGameFactory.setImplementation(42, newImpl).

# Dry run first to verify
DRY_RUN=true just upgrade-fault-dispute-game

# Execute
DRY_RUN=false just upgrade-fault-dispute-game

You must also set the initial bond for the new game type. If the upgrade script does not call setInitBond, run it manually:

cast send $FACTORY_ADDRESS "setInitBond(uint32,uint256)" 42 $INITIAL_BOND_WEI \
  --rpc-url $L1_RPC --private-key $PRIVATE_KEY

For bond sizing guidance, see the deployment guide.

1.5 Verify registration
# Should return the new implementation address (non-zero)
cast call $FACTORY_ADDRESS "gameImpls(uint32)" 42 --rpc-url $L1_RPC

# Should return the bond amount
cast call $FACTORY_ADDRESS "initBonds(uint32)" 42 --rpc-url $L1_RPC

See the upgrade verification guide for more details.


Phase 2: Activate the ZK Game Type

This is the cutover step. After this, the chain recognizes ZK games as the canonical proof type.

2.1 Set respected game type

Call setRespectedGameType on the AnchorStateRegistry. This requires the Guardian key. Use 6 for validity mode or 42 for fault proof mode:

cast send $ANCHOR_STATE_REGISTRY_ADDRESS "setRespectedGameType(uint32)" <GAME_TYPE> \
  --rpc-url $L1_RPC --private-key $GUARDIAN_PRIVATE_KEY

Verify:

cast call $ANCHOR_STATE_REGISTRY_ADDRESS "respectedGameType()" --rpc-url $L1_RPC

Important

Fault proof mode only: Only start the proposer after setRespectedGameType is confirmed. OPSuccinctFaultDisputeGame dynamically checks the respected game type at initialization — games created before this call will have wasRespectedGameTypeWhenCreated = false and cannot be used for withdrawal proofs.

Validity mode: OPSuccinctDisputeGame always sets wasRespectedGameTypeWhenCreated = true, so proposer start order relative to setRespectedGameType does not affect game validity. However, you should still set the respected game type first so that OptimismPortal2 recognizes the new games for withdrawal finalization.

2.2 Start the proposer

Validity mode: Create a .env file with proposer configuration. See Proposer Configuration for the full list of variables.

Migration-specific notes:

  • Set L2OO_ADDRESS to the OPSuccinctL2OutputOracle proxy deployed in Phase 1.
  • Set DGF_ADDRESS to your existing DisputeGameFactory address. This makes the proposer create dispute games via the factory (required for OptimismPortal2 compatibility).
  • Consider starting with OP_SUCCINCT_MOCK=true for initial validation, then switching to real proofs.
docker compose up

Fault proof mode: Create .env.proposer in the fault-proof directory. See Proposer Configuration for the full list of variables.

Migration-specific notes:

  • Use the ANCHOR_STATE_REGISTRY_ADDRESS and FACTORY_ADDRESS from your existing deployment.
  • Set GAME_TYPE=42.
  • Consider starting with MOCK_MODE=true for initial validation, then switching to real proofs.
cd fault-proof
cargo run --bin proposer

Watch for logs confirming ZK games are being created (e.g., Game created successfully).

2.3 Start the challenger (fault proof mode only)

This step only applies to OP Succinct Lite (fault proof mode). Validity mode does not require a separate challenger.

Create .env.challenger in the fault-proof directory. See Challenger Configuration for the full reference.

cargo run --bin challenger

2.4 Verify migration

What to CheckHow
New impl registeredcast call $FACTORY_ADDRESS "gameImpls(uint32)" <GAME_TYPE> returns non-zero
Respected type updatedcast call $ANCHOR_STATE_REGISTRY_ADDRESS "respectedGameType()" returns your game type
ZK proposer creating gamesProposer logs show games being created
Old proposer stopped creatingNo new games with old type appearing

Phase 3: Wind Down the Old System

3.1 Stop the old op-proposer

Once the OP Succinct proposer is creating games successfully, stop the OP native op-proposer. Any new optimistic games it creates after setRespectedGameType will have wasRespectedGameTypeWhenCreated = false and won't be usable for withdrawals.

# Stop the OP native proposer (method depends on your deployment)
# e.g., systemctl stop op-proposer, docker stop op-proposer, etc.

3.2 Wait for old games to resolve

Old optimistic games created before the game type switch still have wasRespectedGameTypeWhenCreated = true and must be allowed to complete their lifecycle. This ensures any in-flight withdrawals proven against those games can finalize normally.

The maximum wait time depends on your existing optimistic game type's parameters (not the new ZK game's config). Check your current game implementation's timing values — typically this is on the order of 1–2 weeks.

3.3 Stop the old op-challenger

Once all old optimistic games have resolved, stop the OP native op-challenger.

3.4 (Optional) Retire old games

To blanket-invalidate all games created before a certain point, the Guardian can set a retirement timestamp:

cast send $ANCHOR_STATE_REGISTRY_ADDRESS "updateRetirementTimestamp()" \
  --rpc-url $L1_RPC --private-key $GUARDIAN_PRIVATE_KEY

This marks all games created at or before block.timestamp as retired (isGameRetired() = true), preventing them from being used for new withdrawal proofs. Only do this after confirming all legitimate withdrawals from old games have finalized.

Post-migration monitoring

What to CheckHow
Games resolving/finalizing normallyProposer logs show resolution
Bonds being claimedLog: Claimed bond (fault proof mode)
Challenger activeLog: Game challenged successfully (fault proof mode only)
Old games winding downDecreasing count of unresolved old-type games

For metrics endpoints, see Validity Proposer (METRICS_PORT) or FP Proposer (PROPOSER_METRICS_PORT) and FP Challenger (CHALLENGER_METRICS_PORT).

Withdrawal Safety Details

For operators who want a deeper understanding of withdrawal behavior during migration:

How withdrawals work across the game type switch

  • Old games (created before switch): wasRespectedGameTypeWhenCreated = true (immutable). Withdrawals can be proven and finalized against these games normally.
  • New ZK games (fault proof mode): wasRespectedGameTypeWhenCreated is dynamically set at game creation based on the current respected type. Games created after setRespectedGameType will have the flag set to true.
  • New ZK games (validity mode): OPSuccinctDisputeGame always hardcodes wasRespectedGameTypeWhenCreated = true, regardless of the respected game type at creation time.

Withdrawal timeline

T0  Game created
T1  User calls proveWithdrawalTransaction() (can happen any time after T0)
T2  Game resolved (DEFENDER_WINS)
T3  T2 + DISPUTE_GAME_FINALITY_DELAY → game finalized
T4  max(T3, T1 + PROOF_MATURITY_DELAY) → withdrawal finalizable
T5  User calls finalizeWithdrawalTransaction()

Note: withdrawal proving (proveWithdrawalTransaction) does not require the game to be finalized — it can be called at any point after game creation. The finality and maturity checks only apply at finalization time.

Emergency invalidation

If old games need to be invalidated immediately (e.g., security incident), the Guardian can:

  • Call updateRetirementTimestamp() to retire all pre-migration games
  • Call blacklistDisputeGame(address) to target specific games

Both may block pending withdrawals — use with caution.