diff --git a/op-devstack/dsl/bridge.go b/op-devstack/dsl/bridge.go index 26ae4f0b84..5dd43f38d3 100644 --- a/op-devstack/dsl/bridge.go +++ b/op-devstack/dsl/bridge.go @@ -296,12 +296,12 @@ func (b *StandardBridge) forGamePublished(l2BlockNumber *big.Int) disputeGame { bindings.WithClient(b.l1Client.EthClient()), bindings.WithTo(game.Proxy), bindings.WithTest(b.t)) - seqNum, err := contractio.Read(gameContract.L2SequenceNumber(), b.ctx) - b.require.NoError(err, "Failed to read sequence number") + seqNum, err := contractio.Read(gameContract.L2BlockNumber(), b.ctx) + b.require.NoError(err, "Failed to read block number") gameSeqNum = seqNum.Uint64() b.log.Info("Found latest game", "index", gameIndex, "seqNum", gameSeqNum) return gameSeqNum >= l2SequenceNumber - }, 90*time.Second, 100*time.Millisecond, "did not find a game of type %v at or after l2 sequence number %v", respectedGameType, l2SequenceNumber) + }, 40*time.Minute, 100*time.Millisecond, "did not find a game of type %v at or after l2 sequence number %v", respectedGameType, l2SequenceNumber) gameBlockNum := gameSeqNum if superRootsActive { @@ -373,25 +373,34 @@ func (w *Withdrawal) Prove(user *EOA) { var params ProvenWithdrawalParameters w.t.Log("proveWithdrawal: proving withdrawal...") - params = w.proveWithdrawalParameters() - tx := bindings.WithdrawalTransaction{ - Nonce: params.Nonce, - Sender: params.Sender, - Target: params.Target, - Value: params.Value, - GasLimit: params.GasLimit, - Data: params.Data, - } - var call bindings.TypedCall[any] - if params.SuperRootProof == nil { - call = w.bridge.l1Portal.ProveWithdrawalTransaction(tx, params.DisputeGameIndex, params.OutputRootProof, params.WithdrawalProof) - } else { - call = w.bridge.l1Portal.ProveWithdrawalTransactionSuperRoot(tx, params.DisputeGameAddress, params.OutputRootIndex, *params.SuperRootProof, params.OutputRootProof, params.WithdrawalProof) - } - // Retry as withdrawals can't be proven in the same block as the game is created. - // estimateGas works against the current head so we may need to retry until it has progressed enough. + // First, wait for at least one suitable game to exist (blocking wait). + // This ensures the proposer has created a game covering the withdrawal block. + w.bridge.forGamePublished(w.initReceipt.BlockNumber) + + // Retry loop that re-fetches parameters on each attempt. + // A new game may be created during retries that includes the withdrawal in its L2 state. + // Re-computing parameters ensures we use the latest game data. w.require.Eventually(func() bool { + // Re-fetch parameters on each attempt to get the latest game (non-blocking) + params = w.proveWithdrawalParametersLatestGame() + + tx := bindings.WithdrawalTransaction{ + Nonce: params.Nonce, + Sender: params.Sender, + Target: params.Target, + Value: params.Value, + GasLimit: params.GasLimit, + Data: params.Data, + } + + var call bindings.TypedCall[any] + if params.SuperRootProof == nil { + call = w.bridge.l1Portal.ProveWithdrawalTransaction(tx, params.DisputeGameIndex, params.OutputRootProof, params.WithdrawalProof) + } else { + call = w.bridge.l1Portal.ProveWithdrawalTransactionSuperRoot(tx, params.DisputeGameAddress, params.OutputRootIndex, *params.SuperRootProof, params.OutputRootProof, params.WithdrawalProof) + } + proveReceipt, err := contractio.Write(call, w.ctx, user.Plan()) if err != nil { w.log.Error("Failed to send prove transaction", "err", err) @@ -406,11 +415,38 @@ func (w *Withdrawal) Prove(user *EOA) { }, 30*time.Second, 1*time.Second, "Sending prove transaction") } -// ProveWithdrawalParameters calls ProveWithdrawalParametersForBlock with the most recent L2 output after the latest game. -// Ported from op-node/withdrawals/utils.go to fit in the op-devstack -func (w *Withdrawal) proveWithdrawalParameters() ProvenWithdrawalParameters { - // Wait for a suitable game to be published - latestGame := w.bridge.forGamePublished(w.initReceipt.BlockNumber) +// proveWithdrawalParametersLatestGame fetches the latest game (non-blocking) and computes withdrawal parameters. +// This should only be called after forGamePublished has confirmed a suitable game exists. +func (w *Withdrawal) proveWithdrawalParametersLatestGame() ProvenWithdrawalParameters { + respectedGameType := w.bridge.RespectedGameType() + superRootsActive := w.bridge.UsesSuperRoots() + + // Get the latest game (non-blocking) + game, gameIndex, err := w.bridge.findLatestGame(respectedGameType) + w.require.NoError(err, "failed to find latest game") + + gameContract := bindings.NewBindings[bindings.FaultDisputeGame]( + bindings.WithClient(w.bridge.l1Client.EthClient()), + bindings.WithTo(game.Proxy), + bindings.WithTest(w.t)) + seqNum, err := contractio.Read(gameContract.L2BlockNumber(), w.ctx) + w.require.NoError(err, "Failed to read block number") + gameSeqNum := seqNum.Uint64() + + gameBlockNum := gameSeqNum + if superRootsActive { + blockNum, err := w.bridge.rollupCfg.TargetBlockNumber(gameSeqNum) + w.require.NoError(err, "Failed to convert game timestamp to block number") + gameBlockNum = blockNum + } + + latestGame := disputeGame{ + Index: gameIndex, + Address: game.Proxy, + L2BlockNumber: gameBlockNum, + SequenceNumber: gameSeqNum, + UsesSuperRoots: superRootsActive, + } // Fetch the block header from the L2 node l2Header, err := w.bridge.l2Client.InfoByNumber(w.ctx, latestGame.L2BlockNumber) diff --git a/op-devstack/sysgo/deployer_succinct.go b/op-devstack/sysgo/deployer_succinct.go index 787e33b1e1..4d2bef6e17 100644 --- a/op-devstack/sysgo/deployer_succinct.go +++ b/op-devstack/sysgo/deployer_succinct.go @@ -18,12 +18,18 @@ import ( "github.com/ethereum-optimism/optimism/op-devstack/devtest" "github.com/ethereum-optimism/optimism/op-devstack/stack" "github.com/ethereum-optimism/optimism/op-e2e/e2eutils/geth" + "github.com/ethereum-optimism/optimism/op-e2e/e2eutils/transactions" "github.com/ethereum-optimism/optimism/op-service/eth" + "github.com/ethereum-optimism/optimism/op-service/txmgr" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/ethclient" "github.com/ethereum/go-ethereum/log" + "github.com/ethereum/go-ethereum/rpc" + "github.com/lmittmann/w3" + w3eth "github.com/lmittmann/w3/module/eth" ) // ============================================================= @@ -451,6 +457,20 @@ func WithDeployOPSuccinctFaultDisputeGamePostDeploy(o *Orchestrator, l2Net.deployment.sp1Verifier = addrs.Sp1Verifier l2Net.deployment.anchorStateRegistry = addrs.AnchorStateRegistry l2Net.deployment.disputeGameFactoryProxy = addrs.FactoryProxy + + // Update the original AnchorStateRegistry's respectedGameType to match OP Succinct (42). + // The original ASR (deployed with the chain) defaults to respectedGameType=1, but + // OP Succinct creates games with type 42. Without this, withdrawals via the original + // OptimismPortal2 won't recognize OP Succinct games as valid. + l1EL, ok := o.GetL1EL(l1ELID) + require.True(ok, "l1 EL node required for setRespectedGameType") + + originalPortalAddr := l2Net.rollupCfg.DepositContractAddress + l1ChainID := l1CLID.ChainID().ToBig() + logger := o.P().Logger().New("component", "succinct-deployer", "chain", l2CLID.ChainID().String()) + + err = setRespectedGameType(o.P(), l1EL.UserRPC(), originalPortalAddr, l1ChainID, o.GetKeys(), logger) + require.NoError(err, "failed to set respected game type on original AnchorStateRegistry") } // deployOpSuccinctFaultDisputeGame deploys an OPSuccinctFaultDisputeGame contract @@ -756,6 +776,74 @@ func parseNamedAddresses(stdoutStr string, names ...string) (map[string]string, return result, nil } +// opSuccinctGameType is the game type used by OP Succinct FaultDisputeGame contracts. +const opSuccinctGameType = 42 + +// setRespectedGameType sets the respected game type on the original AnchorStateRegistry +// to match the OP Succinct game type (42). This is needed because the original +// AnchorStateRegistry (deployed with the chain) defaults to respectedGameType=1, +// but OP Succinct creates games with type 42. +func setRespectedGameType( + p devtest.P, + l1ELRpc string, + originalPortalAddr common.Address, + l1ChainID *big.Int, + keys devkeys.Keys, + logger log.Logger, +) error { + // Connect to L1 + rpcClient, err := rpc.DialContext(p.Ctx(), l1ELRpc) + if err != nil { + return fmt.Errorf("failed to dial L1 RPC: %w", err) + } + defer rpcClient.Close() + + client := ethclient.NewClient(rpcClient) + w3Client := w3.NewClient(rpcClient) + + // Get the original AnchorStateRegistry from the portal + var originalASR common.Address + err = w3Client.Call(w3eth.CallFunc(originalPortalAddr, anchorStateRegistryFn).Returns(&originalASR)) + if err != nil { + return fmt.Errorf("failed to get AnchorStateRegistry from portal: %w", err) + } + + logger.Info("Setting respected game type on original AnchorStateRegistry", + "portal", originalPortalAddr.Hex(), + "asr", originalASR.Hex(), + "gameType", opSuccinctGameType, + ) + + // Get the Guardian key (SuperchainConfigGuardianKey is the Guardian of AnchorStateRegistry) + guardianKey, err := keys.Secret(devkeys.SuperchainConfigGuardianKey.Key(l1ChainID)) + if err != nil { + return fmt.Errorf("failed to get Guardian key: %w", err) + } + + // Encode the call data + data, err := setRespectedGameTypeFn.EncodeArgs(uint32(opSuccinctGameType)) + if err != nil { + return fmt.Errorf("failed to encode setRespectedGameType args: %w", err) + } + + // Send the transaction + candidate := txmgr.TxCandidate{ + To: &originalASR, + TxData: data, + GasLimit: 100_000, + } + _, receipt, err := transactions.SendTx(p.Ctx(), client, candidate, guardianKey) + if err != nil { + return fmt.Errorf("failed to send setRespectedGameType tx: %w", err) + } + if receipt.Status != types.ReceiptStatusSuccessful { + return fmt.Errorf("setRespectedGameType tx failed: status=%d", receipt.Status) + } + + logger.Info("Successfully set respected game type", "txHash", receipt.TxHash.Hex()) + return nil +} + // WriteEnvFile writes key-value pairs to a file in .env format. func WriteEnvFile(path string, kv map[string]string) error { var keys []string diff --git a/op-devstack/sysgo/superroot.go b/op-devstack/sysgo/superroot.go index 3b358ec770..3c6c19ca0b 100644 --- a/op-devstack/sysgo/superroot.go +++ b/op-devstack/sysgo/superroot.go @@ -279,17 +279,18 @@ const ( ) var ( - optimismPortalFn = w3.MustNewFunc("optimismPortal()", "address") - disputeGameFactoryFn = w3.MustNewFunc("disputeGameFactory()", "address") - gameImplsFn = w3.MustNewFunc("gameImpls(uint32)", "address") - gameArgsFn = w3.MustNewFunc("gameArgs(uint32)", "bytes") - ownerFn = w3.MustNewFunc("owner()", "address") - proxyAdminFn = w3.MustNewFunc("proxyAdmin()", "address") - adminFn = w3.MustNewFunc("admin()", "address") - proxyAdminOwnerFn = w3.MustNewFunc("proxyAdminOwner()", "address") - ethLockboxFn = w3.MustNewFunc("ethLockbox()", "address") - anchorStateRegistryFn = w3.MustNewFunc("anchorStateRegistry()", "address") - transferOwnershipFn = w3.MustNewFunc("transferOwnership(address)", "") + optimismPortalFn = w3.MustNewFunc("optimismPortal()", "address") + disputeGameFactoryFn = w3.MustNewFunc("disputeGameFactory()", "address") + gameImplsFn = w3.MustNewFunc("gameImpls(uint32)", "address") + gameArgsFn = w3.MustNewFunc("gameArgs(uint32)", "bytes") + ownerFn = w3.MustNewFunc("owner()", "address") + proxyAdminFn = w3.MustNewFunc("proxyAdmin()", "address") + adminFn = w3.MustNewFunc("admin()", "address") + proxyAdminOwnerFn = w3.MustNewFunc("proxyAdminOwner()", "address") + ethLockboxFn = w3.MustNewFunc("ethLockbox()", "address") + anchorStateRegistryFn = w3.MustNewFunc("anchorStateRegistry()", "address") + transferOwnershipFn = w3.MustNewFunc("transferOwnership(address)", "") + setRespectedGameTypeFn = w3.MustNewFunc("setRespectedGameType(uint32)", "") ) func getOptimismPortal(t devtest.CommonT, client *w3.Client, systemConfigProxy common.Address) common.Address { diff --git a/op-service/txintent/bindings/FaultDisputeGame.go b/op-service/txintent/bindings/FaultDisputeGame.go index 6aa5ede0bf..e63a157cf1 100644 --- a/op-service/txintent/bindings/FaultDisputeGame.go +++ b/op-service/txintent/bindings/FaultDisputeGame.go @@ -44,6 +44,7 @@ type FaultDisputeGame struct { // IDisputeGame.sol read methods L1Head func() TypedCall[common.Hash] `sol:"l1Head"` L2SequenceNumber func() TypedCall[*big.Int] `sol:"l2SequenceNumber"` + L2BlockNumber func() TypedCall[*big.Int] `sol:"l2BlockNumber"` Status func() TypedCall[uint8] `sol:"status"` GameType func() TypedCall[uint32] `sol:"gameType"`