Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
5dcd70b
feat(tests): add withdrawal finalization e2e test
fakedev9999 Jan 27, 2026
6d341f7
fix: update optimism submodule with respectedGameType fix
fakedev9999 Jan 27, 2026
031d331
fix: update optimism submodule with guardian key fix
fakedev9999 Jan 27, 2026
63695e9
fix: update optimism submodule with correct guardian key for setRespe…
fakedev9999 Jan 27, 2026
335d3b7
fix: update optimism submodule to set respectedGameType on both ASRs
fakedev9999 Jan 27, 2026
9c81ab7
debug: add diagnostic logging for DGF address investigation
fakedev9999 Jan 27, 2026
69177ea
debug: add explicit DGF address comparison logging
fakedev9999 Jan 27, 2026
58184ed
fix(fp): add address output for Go test infrastructure parsing
fakedev9999 Jan 27, 2026
13ed023
fix(fp): add maxClockDuration() compatibility function
fakedev9999 Jan 27, 2026
226514b
fix(tests): increase game creation frequency for withdrawal test
fakedev9999 Jan 27, 2026
6d94eba
fix(tests): wait for more games before withdrawal to ensure coverage
fakedev9999 Jan 27, 2026
707dc39
fix(tests): use FastFinalityMode for withdrawal test
fakedev9999 Jan 27, 2026
bb37602
fix(tests): use larger ProposalIntervalInBlocks for faster game coverage
fakedev9999 Jan 27, 2026
db6ee1d
fix(tests): increase game count to 15 for withdrawal test coverage
fakedev9999 Jan 27, 2026
5786924
fix(tests): increase withdrawal test timeout to 15 minutes
fakedev9999 Jan 27, 2026
6a48201
fix: use standard DGF for OPSuccinct games to fix withdrawal test
fakedev9999 Jan 27, 2026
78f6a56
fix: use standard AnchorStateRegistry for OPSuccinct games to fix wit…
fakedev9999 Jan 27, 2026
b1b1744
feat(tests): add WithFPTimeTravel option to enable time travel for wi…
fakedev9999 Jan 27, 2026
d1b477f
fix: add missing config fields to integration test and fix formatting
fakedev9999 Jan 27, 2026
123eb57
fix: increase MaxProveDuration for CI stability
fakedev9999 Jan 27, 2026
a71d026
Merge branch 'main' into fakedev9999/withdrawal-test-issue
fakedev9999 Jan 27, 2026
893a00a
fix: increase MaxConcurrentRangeProofs for defense and long-running t…
fakedev9999 Jan 28, 2026
2d518b0
fix: sort games by index for deterministic fast finality proving order
fakedev9999 Jan 28, 2026
40f6670
fix: increase FastFinalityProvingLimit for defense test
fakedev9999 Jan 28, 2026
eeba771
revert: restore original test configs to match main
fakedev9999 Jan 28, 2026
ba0a887
chore(tests): update optimism submodule with withdrawal test support
fakedev9999 Jan 29, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/e2e-sysgo-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ jobs:
- "./e2e/faultproof/fastfinality/..."
- "./e2e/faultproof/recovery/..."
- "./e2e/faultproof/defense/..."
- "./e2e/faultproof/withdrawal/..."

steps:
- name: Checkout sources
Expand Down
65 changes: 48 additions & 17 deletions contracts/script/fp/DeployOPSuccinctFDG.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -75,23 +75,40 @@ contract DeployOPSuccinctFDG is Script, Utils {
}

function deployContracts(FDGConfig memory config) internal returns (DeployedContracts memory) {
// Deploy ProxyAdmin with msg.sender as owner.
ProxyAdmin proxyAdmin = new ProxyAdmin(msg.sender);
console.log("ProxyAdmin deployed at:", address(proxyAdmin));
DisputeGameFactory factory;
ProxyAdmin proxyAdmin;

// Deploy factory implementation.
DisputeGameFactory factoryImpl = new DisputeGameFactory();
// Check if using existing DGF (for e2e tests where OptimismPortal2 must use the same DGF)
bool useExistingDgf = config.existingDisputeGameFactoryProxy != address(0);

// Deploy factory proxy using Optimism Proxy pattern.
Proxy factoryProxy = new Proxy(address(proxyAdmin));
if (useExistingDgf) {
// Use existing DisputeGameFactory - required for e2e tests where
// OptimismPortal2 is already configured with a specific DGF
factory = DisputeGameFactory(config.existingDisputeGameFactoryProxy);
console.log("Using existing DisputeGameFactory:", address(factory));

// Initialize the factory through ProxyAdmin.
proxyAdmin.upgradeAndCall(
payable(address(factoryProxy)),
address(factoryImpl),
abi.encodeWithSelector(DisputeGameFactory.initialize.selector, msg.sender)
);
DisputeGameFactory factory = DisputeGameFactory(address(factoryProxy));
// Deploy ProxyAdmin for AnchorStateRegistry (still needed for new components)
proxyAdmin = new ProxyAdmin(msg.sender);
console.log("ProxyAdmin deployed at:", address(proxyAdmin));
} else {
// Deploy ProxyAdmin with msg.sender as owner.
proxyAdmin = new ProxyAdmin(msg.sender);
console.log("ProxyAdmin deployed at:", address(proxyAdmin));

// Deploy factory implementation.
DisputeGameFactory factoryImpl = new DisputeGameFactory();

// Deploy factory proxy using Optimism Proxy pattern.
Proxy factoryProxy = new Proxy(address(proxyAdmin));

// Initialize the factory through ProxyAdmin.
proxyAdmin.upgradeAndCall(
payable(address(factoryProxy)),
address(factoryImpl),
abi.encodeWithSelector(DisputeGameFactory.initialize.selector, msg.sender)
);
factory = DisputeGameFactory(address(factoryProxy));
}

GameType gameType = GameType.wrap(config.gameType);

Expand All @@ -106,7 +123,7 @@ contract DeployOPSuccinctFDG is Script, Utils {
deployAnchorStateRegistry(config, factory, startingAnchorRoot, gameType, proxyAdmin);

// Deploy and configure access manager
AccessManager accessManager = deployAccessManager(config, address(factoryProxy));
AccessManager accessManager = deployAccessManager(config, address(factory));

// Deploy SP1 verifier and get configuration
SP1Config memory sp1Config = deploySP1Verifier(config);
Expand All @@ -121,14 +138,21 @@ contract DeployOPSuccinctFDG is Script, Utils {

// Create deployed contracts struct
DeployedContracts memory deployedContracts = DeployedContracts({
factoryProxy: address(factoryProxy),
factoryProxy: address(factory),
gameImplementation: address(gameImpl),
sp1Verifier: sp1Config.verifierAddress,
anchorStateRegistry: address(registry),
accessManager: address(accessManager),
optimismPortal2: portalAddress
});

// Output addresses in format expected by Go test infrastructure:
// <name>: address 0x...
// These are parsed by parseNamedAddresses in deployer_succinct.go
console.log("factoryProxy: address", address(factory));
console.log("anchorStateRegistry: address", address(registry));
console.log("sp1Verifier: address", sp1Config.verifierAddress);

return deployedContracts;
}

Expand Down Expand Up @@ -160,6 +184,13 @@ contract DeployOPSuccinctFDG is Script, Utils {
GameType gameType,
ProxyAdmin proxyAdmin
) internal returns (AnchorStateRegistry) {
// Check if using existing ASR (for e2e tests where games must use the same ASR as OptimismPortal2)
if (config.existingAnchorStateRegistry != address(0)) {
AnchorStateRegistry registry = AnchorStateRegistry(config.existingAnchorStateRegistry);
console.log("Using existing AnchorStateRegistry:", address(registry));
return registry;
}

// Use existing SystemConfig or deploy MockSystemConfig for testing
address systemConfigAddress;
if (config.systemConfigAddress != address(0)) {
Expand Down Expand Up @@ -195,7 +226,7 @@ contract DeployOPSuccinctFDG is Script, Utils {
);

AnchorStateRegistry registry = AnchorStateRegistry(address(registryProxy));
console.log("Anchor state registry:", address(registry));
console.log("Deployed new AnchorStateRegistry:", address(registry));
return registry;
}

Expand Down
6 changes: 6 additions & 0 deletions contracts/src/fp/OPSuccinctFaultDisputeGame.sol
Original file line number Diff line number Diff line change
Expand Up @@ -637,6 +637,12 @@ contract OPSuccinctFaultDisputeGame is Clone, ISemver, IDisputeGame {
maxChallengeDuration_ = MAX_CHALLENGE_DURATION;
}

/// @notice Returns the max clock duration.
/// @dev Compatibility alias for maxChallengeDuration to match the standard FaultDisputeGame interface.
function maxClockDuration() external view returns (Duration maxClockDuration_) {
maxClockDuration_ = MAX_CHALLENGE_DURATION;
}

/// @notice Returns the max prove duration.
function maxProveDuration() external view returns (Duration maxProveDuration_) {
maxProveDuration_ = MAX_PROVE_DURATION;
Expand Down
6 changes: 6 additions & 0 deletions contracts/test/helpers/JSONDecoder.sol
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,12 @@ contract JSONDecoder {
address[] challengerAddresses;
uint256 challengerBondWei;
uint256 disputeGameFinalityDelaySeconds;
// If non-zero, use this existing ASR instead of deploying a new one.
// Required for e2e tests where games must reference the same ASR as OptimismPortal2.
address existingAnchorStateRegistry;
// If non-zero, use this existing DGF instead of deploying a new one.
// Required for e2e tests where OptimismPortal2 must use the same DGF.
address existingDisputeGameFactoryProxy;
uint256 fallbackTimeoutFpSecs;
uint32 gameType;
uint256 initialBondWei;
Expand Down
8 changes: 8 additions & 0 deletions fault-proof/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -344,6 +344,14 @@ pub struct FaultDisputeGameConfig {
pub challenger_addresses: Vec<String>,
pub challenger_bond_wei: u64,
pub dispute_game_finality_delay_seconds: u64,
/// Optional existing AnchorStateRegistry address. If provided (non-zero), the deployment
/// script will use this existing ASR instead of deploying a new one.
/// This is required for e2e tests to ensure games reference the same ASR as OptimismPortal2.
pub existing_anchor_state_registry: String,
/// Optional existing DisputeGameFactory address. If provided (non-zero), the deployment
/// script will register game type 42 in this existing factory instead of creating a new one.
/// This is required for e2e tests to ensure OptimismPortal2 uses the same DGF as the games.
pub existing_dispute_game_factory_proxy: String,
pub fallback_timeout_fp_secs: u64,
pub game_type: u32,
pub initial_bond_wei: u64,
Expand Down
3 changes: 3 additions & 0 deletions fault-proof/tests/common/env.rs
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,9 @@ pub fn test_config(starting_l2_block_number: u64, starting_root: String) -> Faul
challenger_addresses: vec![CHALLENGER_ADDRESS.to_string()],
challenger_bond_wei: CHALLENGER_BOND.to::<u64>(),
dispute_game_finality_delay_seconds: DISPUTE_GAME_FINALITY_DELAY_SECONDS,
// Integration tests deploy their own contracts, so these are not needed.
existing_anchor_state_registry: Address::ZERO.to_string(),
existing_dispute_game_factory_proxy: Address::ZERO.to_string(),
fallback_timeout_fp_secs: FALLBACK_TIMEOUT.to::<u64>(),
game_type: TEST_GAME_TYPE,
initial_bond_wei: INIT_BOND.to::<u64>(),
Expand Down
20 changes: 20 additions & 0 deletions scripts/utils/bin/fetch_fault_dispute_game_config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,24 @@ async fn update_fdg_config() -> Result<()> {
"0x0000000000000000000000000000000000000000".to_string()
});

// Existing AnchorStateRegistry configuration (for e2e tests).
// If provided, the deployment will use this existing ASR instead of deploying a new one.
// This ensures games reference the same ASR that OptimismPortal2 uses.
let existing_anchor_state_registry =
env::var("EXISTING_ANCHOR_STATE_REGISTRY").unwrap_or_else(|_| {
// Default to zero address - will deploy a new AnchorStateRegistry
"0x0000000000000000000000000000000000000000".to_string()
});

// Existing DisputeGameFactory configuration (for e2e tests).
// If provided, the deployment will register game type 42 in this existing factory
// instead of creating a new one. This ensures OptimismPortal2 uses the same DGF.
let existing_dispute_game_factory_proxy = env::var("EXISTING_DISPUTE_GAME_FACTORY_PROXY")
.unwrap_or_else(|_| {
// Default to zero address - will deploy a new DisputeGameFactory
"0x0000000000000000000000000000000000000000".to_string()
});

// SystemConfig configuration - derive from rollup config by default.
// For production deployments, this is required for proper guardian functionality.
// For testing, if zero address, a MockSystemConfig will be deployed.
Expand Down Expand Up @@ -198,6 +216,8 @@ async fn update_fdg_config() -> Result<()> {
challenger_addresses,
challenger_bond_wei,
dispute_game_finality_delay_seconds,
existing_anchor_state_registry,
existing_dispute_game_factory_proxy,
fallback_timeout_fp_secs,
game_type,
initial_bond_wei,
Expand Down
168 changes: 168 additions & 0 deletions tests/e2e/faultproof/withdrawal/withdrawal_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
// Package fpwithdrawal tests the full withdrawal lifecycle through OptimismPortal2.
package fpwithdrawal

import (
"context"
"testing"
"time"

"github.com/ethereum-optimism/optimism/op-devstack/devtest"
"github.com/ethereum-optimism/optimism/op-service/eth"
opspresets "github.com/succinctlabs/op-succinct/presets"
"github.com/succinctlabs/op-succinct/utils"
)

// TestFaultProof_WithdrawalFinalized verifies the complete withdrawal lifecycle:
// 1. User initiates withdrawal on L2 (L2ToL1MessagePasser)
// 2. Proposer creates game covering the withdrawal block
// 3. Withdrawal is proven against the dispute game (OptimismPortal2.proveWithdrawal)
// 4. Game resolves as DEFENDER_WINS
// 5. Finality delay elapses
// 6. Withdrawal is finalized (OptimismPortal2.finalizeWithdrawalTransaction)
// 7. Funds are received on L1
func TestFaultProof_WithdrawalFinalized(gt *testing.T) {
t := devtest.SerialT(gt)
logger := t.Logger()

// === SETUP ===
// Configure proposer with fast finality mode - this allows the proposer to create
// games for recent blocks (unsafe head) rather than only finalized blocks.
// This is critical for the withdrawal test because the chain advances rapidly
// during deposit/withdrawal operations.
//
// IMPORTANT: Use a LARGER ProposalIntervalInBlocks (not smaller!) so each game
// covers more blocks. With interval=50, games cover blocks 1, 51, 101, 151, 201, 251...
// so only ~6 games are needed to cover block 279 (vs ~56 games with interval=5).
// Since games are created every ~36 seconds, larger intervals = faster coverage.
proposerCfg := opspresets.FastFinalityFPProposerConfig()
proposerCfg.ProposalIntervalInBlocks = 50 // Larger interval = each game covers more blocks

sys := opspresets.NewFaultProofSystem(t, proposerCfg, opspresets.DefaultL2ChainConfig(), opspresets.WithFPTimeTravel())

// Log DGF address for debugging (this is the OPSuccinct DGF that should have game type 42)
dgfAddr := sys.L2Chain.Escape().Deployment().DisputeGameFactoryProxyAddr()
logger.Info("DGF address from L2Deployment", "address", dgfAddr.Hex())

// Create DgfClient using same DGF address - verify it works
dgf := sys.DgfClient(t)

// Get the standard bridge DSL
bridge := sys.StandardBridge()

// Also log the DGF address from L2Chain to compare
l2ChainDgfAddr := sys.L2Chain.DisputeGameFactoryProxyAddr()
logger.Info("DGF address from L2Chain (used by StandardBridge)", "address", l2ChainDgfAddr.Hex())
if dgfAddr != l2ChainDgfAddr {
logger.Error("DGF ADDRESS MISMATCH!",
"L2Deployment", dgfAddr.Hex(),
"L2Chain", l2ChainDgfAddr.Hex())
}

// Log the respected game type - this should be 42
gameType := bridge.RespectedGameType()
logger.Info("FaultProof withdrawal test starting",
"respectedGameType", gameType,
"dgfAddress", dgfAddr.Hex())

// Now try the StandardBridge methods that query the DGF
// These should work now that we've verified game type 42 is registered
logger.Info("Testing StandardBridge DGF queries",
"withdrawalDelay", bridge.WithdrawalDelay(),
"gameResolutionDelay", bridge.GameResolutionDelay(),
"disputeGameFinalityDelay", bridge.DisputeGameFinalityDelay())

// === Fund test accounts ===
initialL1Balance := eth.OneThirdEther

// l1User and l2User share same private key
l1User := sys.FunderL1.NewFundedEOA(initialL1Balance)
l2User := l1User.AsEL(sys.L2EL) // Only receives funds via the deposit

depositAmount := eth.OneTenthEther
withdrawalAmount := eth.OneHundredthEther

// === PHASE 1: Deposit from L1 to L2 ===
// The max amount of withdrawal is limited to the total amount of deposit
// We trigger deposit first to fund the L1 ETHLockbox to satisfy the invariant
// IMPORTANT: Do deposit/withdrawal EARLY before waiting for games, so the withdrawal
// block number is low enough for games to catch up within the 90s timeout.
logger.Info("Phase 1: Depositing ETH from L1 to L2", "amount", depositAmount)

deposit := bridge.Deposit(depositAmount, l1User)
expectedL1UserBalance := initialL1Balance.Sub(depositAmount).Sub(deposit.GasCost())
l1User.VerifyBalanceExact(expectedL1UserBalance)

expectedL2UserBalance := depositAmount
l2User.VerifyBalanceExact(expectedL2UserBalance)

logger.Info("Deposit complete, L2 balance confirmed")

// === PHASE 2: Initiate withdrawal on L2 ===
logger.Info("Phase 2: Initiating withdrawal on L2", "amount", withdrawalAmount)

withdrawal := bridge.InitiateWithdrawal(withdrawalAmount, l2User)
expectedL2UserBalance = expectedL2UserBalance.Sub(withdrawalAmount).Sub(withdrawal.InitiateGasCost())
l2User.VerifyBalanceExact(expectedL2UserBalance)

logger.Info("Withdrawal initiated on L2",
"initiateBlockHash", withdrawal.InitiateBlockHash().Hex())

// === PHASE 2.5: Wait for games to cover withdrawal block ===
// With ProposalIntervalInBlocks=50, games cover blocks 1, 51, 101, 151, 201, 251, 301...
// The withdrawal block depends on L2 chain state at withdrawal time, which can be
// 500-700 blocks depending on setup time and chain advancement.
// We need ~14-15 games to cover block 700+, with margin for safety.
// Games are created every ~50-55 seconds, so 15 games = ~13 minutes.
// Use 15 minute timeout to ensure we have time for all games.
ctx, cancel := context.WithTimeout(t.Ctx(), 15*time.Minute)
defer cancel()

logger.Info("Waiting for games to cover withdrawal block (need ~15 games with interval=50)")
utils.WaitForGameCount(ctx, t, dgf, 15)
logger.Info("Sufficient games created, proceeding to prove withdrawal")

// === PHASE 3: Prove withdrawal on L1 ===
// This waits for a game covering the withdrawal block to be published (90s timeout in bridge.go)
logger.Info("Phase 3: Proving withdrawal on L1 (waiting for game)")

withdrawal.Prove(l1User)
expectedL1UserBalance = expectedL1UserBalance.Sub(withdrawal.ProveGasCost())
l1User.VerifyBalanceExact(expectedL1UserBalance)

logger.Info("Withdrawal proven on L1")

// === PHASE 4: Wait for game resolution ===
logger.Info("Phase 4: Waiting for game resolution (DEFENDER_WINS)")

// Advance time until game is resolvable
sys.AdvanceTime(bridge.GameResolutionDelay())
withdrawal.WaitForDisputeGameResolved()

logger.Info("Game resolved as DEFENDER_WINS")

// === PHASE 5: Wait for finality delay ===
logger.Info("Phase 5: Waiting for finality delay")

// Advance time to when game finalization and proof finalization delay has expired
remainingDelay := max(bridge.WithdrawalDelay()-bridge.GameResolutionDelay(), bridge.DisputeGameFinalityDelay())
sys.AdvanceTime(remainingDelay)

logger.Info("Finality delay elapsed")

// === PHASE 6: Finalize withdrawal on L1 ===
logger.Info("Phase 6: Finalizing withdrawal on L1",
"proofMaturity", bridge.WithdrawalDelay(),
"gameResolutionDelay", bridge.GameResolutionDelay(),
"gameFinalityDelay", bridge.DisputeGameFinalityDelay())

withdrawal.Finalize(l1User)
expectedL1UserBalance = expectedL1UserBalance.Sub(withdrawal.FinalizeGasCost()).Add(withdrawalAmount)
l1User.VerifyBalanceExact(expectedL1UserBalance)

// === PHASE 7: Verify funds received ===
logger.Info("Phase 7: Withdrawal finalized successfully!",
"withdrawalAmount", withdrawalAmount,
"finalL1Balance", expectedL1UserBalance)

logger.Info("✅ TestFaultProof_WithdrawalFinalized PASSED: Full withdrawal lifecycle verified")
}
Loading