diff --git a/op-challenger/game/fault/contracts/abis/FaultDisputeGame-0.18.1.json b/op-challenger/game/fault/contracts/abis/FaultDisputeGame-0.18.1.json new file mode 100644 index 0000000000000..e8c4133bb3170 --- /dev/null +++ b/op-challenger/game/fault/contracts/abis/FaultDisputeGame-0.18.1.json @@ -0,0 +1,926 @@ +[ + { + "inputs": [ + { + "internalType": "GameType", + "name": "_gameType", + "type": "uint32" + }, + { + "internalType": "Claim", + "name": "_absolutePrestate", + "type": "bytes32" + }, + { + "internalType": "uint256", + "name": "_maxGameDepth", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "_splitDepth", + "type": "uint256" + }, + { + "internalType": "Duration", + "name": "_clockExtension", + "type": "uint64" + }, + { + "internalType": "Duration", + "name": "_maxClockDuration", + "type": "uint64" + }, + { + "internalType": "contract IBigStepper", + "name": "_vm", + "type": "address" + }, + { + "internalType": "contract IDelayedWETH", + "name": "_weth", + "type": "address" + }, + { + "internalType": "contract IAnchorStateRegistry", + "name": "_anchorStateRegistry", + "type": "address" + }, + { + "internalType": "uint256", + "name": "_l2ChainId", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "inputs": [], + "name": "absolutePrestate", + "outputs": [ + { + "internalType": "Claim", + "name": "absolutePrestate_", + "type": "bytes32" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "_ident", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "_execLeafIdx", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "_partOffset", + "type": "uint256" + } + ], + "name": "addLocalData", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "anchorStateRegistry", + "outputs": [ + { + "internalType": "contract IAnchorStateRegistry", + "name": "registry_", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "_parentIndex", + "type": "uint256" + }, + { + "internalType": "Claim", + "name": "_claim", + "type": "bytes32" + } + ], + "name": "attack", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_recipient", + "type": "address" + } + ], + "name": "claimCredit", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "name": "claimData", + "outputs": [ + { + "internalType": "uint32", + "name": "parentIndex", + "type": "uint32" + }, + { + "internalType": "address", + "name": "counteredBy", + "type": "address" + }, + { + "internalType": "address", + "name": "claimant", + "type": "address" + }, + { + "internalType": "uint128", + "name": "bond", + "type": "uint128" + }, + { + "internalType": "Claim", + "name": "claim", + "type": "bytes32" + }, + { + "internalType": "Position", + "name": "position", + "type": "uint128" + }, + { + "internalType": "Clock", + "name": "clock", + "type": "uint128" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "claimDataLen", + "outputs": [ + { + "internalType": "uint256", + "name": "len_", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "Hash", + "name": "", + "type": "bytes32" + } + ], + "name": "claims", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "clockExtension", + "outputs": [ + { + "internalType": "Duration", + "name": "clockExtension_", + "type": "uint64" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "createdAt", + "outputs": [ + { + "internalType": "Timestamp", + "name": "", + "type": "uint64" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "name": "credit", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "_parentIndex", + "type": "uint256" + }, + { + "internalType": "Claim", + "name": "_claim", + "type": "bytes32" + } + ], + "name": "defend", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [], + "name": "extraData", + "outputs": [ + { + "internalType": "bytes", + "name": "extraData_", + "type": "bytes" + } + ], + "stateMutability": "pure", + "type": "function" + }, + { + "inputs": [], + "name": "gameCreator", + "outputs": [ + { + "internalType": "address", + "name": "creator_", + "type": "address" + } + ], + "stateMutability": "pure", + "type": "function" + }, + { + "inputs": [], + "name": "gameData", + "outputs": [ + { + "internalType": "GameType", + "name": "gameType_", + "type": "uint32" + }, + { + "internalType": "Claim", + "name": "rootClaim_", + "type": "bytes32" + }, + { + "internalType": "bytes", + "name": "extraData_", + "type": "bytes" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "gameType", + "outputs": [ + { + "internalType": "GameType", + "name": "gameType_", + "type": "uint32" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "_claimIndex", + "type": "uint256" + } + ], + "name": "getChallengerDuration", + "outputs": [ + { + "internalType": "Duration", + "name": "duration_", + "type": "uint64" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "_claimIndex", + "type": "uint256" + } + ], + "name": "getNumToResolve", + "outputs": [ + { + "internalType": "uint256", + "name": "numRemainingChildren_", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "Position", + "name": "_position", + "type": "uint128" + } + ], + "name": "getRequiredBond", + "outputs": [ + { + "internalType": "uint256", + "name": "requiredBond_", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "initialize", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [], + "name": "l1Head", + "outputs": [ + { + "internalType": "Hash", + "name": "l1Head_", + "type": "bytes32" + } + ], + "stateMutability": "pure", + "type": "function" + }, + { + "inputs": [], + "name": "l2BlockNumber", + "outputs": [ + { + "internalType": "uint256", + "name": "l2BlockNumber_", + "type": "uint256" + } + ], + "stateMutability": "pure", + "type": "function" + }, + { + "inputs": [], + "name": "l2ChainId", + "outputs": [ + { + "internalType": "uint256", + "name": "l2ChainId_", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "maxClockDuration", + "outputs": [ + { + "internalType": "Duration", + "name": "maxClockDuration_", + "type": "uint64" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "maxGameDepth", + "outputs": [ + { + "internalType": "uint256", + "name": "maxGameDepth_", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "_challengeIndex", + "type": "uint256" + }, + { + "internalType": "Claim", + "name": "_claim", + "type": "bytes32" + }, + { + "internalType": "bool", + "name": "_isAttack", + "type": "bool" + } + ], + "name": "move", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "name": "resolutionCheckpoints", + "outputs": [ + { + "internalType": "bool", + "name": "initialCheckpointComplete", + "type": "bool" + }, + { + "internalType": "uint32", + "name": "subgameIndex", + "type": "uint32" + }, + { + "internalType": "Position", + "name": "leftmostPosition", + "type": "uint128" + }, + { + "internalType": "address", + "name": "counteredBy", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "resolve", + "outputs": [ + { + "internalType": "enum GameStatus", + "name": "status_", + "type": "uint8" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "_claimIndex", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "_numToResolve", + "type": "uint256" + } + ], + "name": "resolveClaim", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "resolvedAt", + "outputs": [ + { + "internalType": "Timestamp", + "name": "", + "type": "uint64" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "name": "resolvedSubgames", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "rootClaim", + "outputs": [ + { + "internalType": "Claim", + "name": "rootClaim_", + "type": "bytes32" + } + ], + "stateMutability": "pure", + "type": "function" + }, + { + "inputs": [], + "name": "splitDepth", + "outputs": [ + { + "internalType": "uint256", + "name": "splitDepth_", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "startingBlockNumber", + "outputs": [ + { + "internalType": "uint256", + "name": "startingBlockNumber_", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "startingOutputRoot", + "outputs": [ + { + "internalType": "Hash", + "name": "root", + "type": "bytes32" + }, + { + "internalType": "uint256", + "name": "l2BlockNumber", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "startingRootHash", + "outputs": [ + { + "internalType": "Hash", + "name": "startingRootHash_", + "type": "bytes32" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "status", + "outputs": [ + { + "internalType": "enum GameStatus", + "name": "", + "type": "uint8" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "_claimIndex", + "type": "uint256" + }, + { + "internalType": "bool", + "name": "_isAttack", + "type": "bool" + }, + { + "internalType": "bytes", + "name": "_stateData", + "type": "bytes" + }, + { + "internalType": "bytes", + "name": "_proof", + "type": "bytes" + } + ], + "name": "step", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "name": "subgames", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "version", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "vm", + "outputs": [ + { + "internalType": "contract IBigStepper", + "name": "vm_", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "weth", + "outputs": [ + { + "internalType": "contract IDelayedWETH", + "name": "weth_", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint256", + "name": "parentIndex", + "type": "uint256" + }, + { + "indexed": true, + "internalType": "Claim", + "name": "claim", + "type": "bytes32" + }, + { + "indexed": true, + "internalType": "address", + "name": "claimant", + "type": "address" + } + ], + "name": "Move", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "enum GameStatus", + "name": "status", + "type": "uint8" + } + ], + "name": "Resolved", + "type": "event" + }, + { + "inputs": [], + "name": "AlreadyInitialized", + "type": "error" + }, + { + "inputs": [], + "name": "AnchorRootNotFound", + "type": "error" + }, + { + "inputs": [], + "name": "BondTransferFailed", + "type": "error" + }, + { + "inputs": [], + "name": "CannotDefendRootClaim", + "type": "error" + }, + { + "inputs": [], + "name": "ClaimAboveSplit", + "type": "error" + }, + { + "inputs": [], + "name": "ClaimAlreadyExists", + "type": "error" + }, + { + "inputs": [], + "name": "ClaimAlreadyResolved", + "type": "error" + }, + { + "inputs": [], + "name": "ClockNotExpired", + "type": "error" + }, + { + "inputs": [], + "name": "ClockTimeExceeded", + "type": "error" + }, + { + "inputs": [], + "name": "DuplicateStep", + "type": "error" + }, + { + "inputs": [], + "name": "GameDepthExceeded", + "type": "error" + }, + { + "inputs": [], + "name": "GameNotInProgress", + "type": "error" + }, + { + "inputs": [], + "name": "IncorrectBondAmount", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidClockExtension", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidLocalIdent", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidParent", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidPrestate", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidSplitDepth", + "type": "error" + }, + { + "inputs": [], + "name": "MaxDepthTooLarge", + "type": "error" + }, + { + "inputs": [], + "name": "NoCreditToClaim", + "type": "error" + }, + { + "inputs": [], + "name": "OutOfOrderResolution", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "Claim", + "name": "rootClaim", + "type": "bytes32" + } + ], + "name": "UnexpectedRootClaim", + "type": "error" + }, + { + "inputs": [], + "name": "ValidStep", + "type": "error" + } +] \ No newline at end of file diff --git a/op-challenger/game/fault/contracts/faultdisputegame.go b/op-challenger/game/fault/contracts/faultdisputegame.go index 1a07ca4b82875..b2eaf639a6904 100644 --- a/op-challenger/game/fault/contracts/faultdisputegame.go +++ b/op-challenger/game/fault/contracts/faultdisputegame.go @@ -19,37 +19,40 @@ import ( "github.com/ethereum-optimism/optimism/packages/contracts-bedrock/snapshots" "github.com/ethereum/go-ethereum/accounts/abi" "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/rlp" ) // The maximum number of children that will be processed during a call to `resolveClaim` var maxChildChecks = big.NewInt(512) var ( - methodVersion = "version" - methodMaxClockDuration = "maxClockDuration" - methodMaxGameDepth = "maxGameDepth" - methodAbsolutePrestate = "absolutePrestate" - methodStatus = "status" - methodRootClaim = "rootClaim" - methodClaimCount = "claimDataLen" - methodClaim = "claimData" - methodL1Head = "l1Head" - methodResolvedSubgames = "resolvedSubgames" - methodResolve = "resolve" - methodResolveClaim = "resolveClaim" - methodAttack = "attack" - methodDefend = "defend" - methodStep = "step" - methodAddLocalData = "addLocalData" - methodVM = "vm" - methodStartingBlockNumber = "startingBlockNumber" - methodStartingRootHash = "startingRootHash" - methodSplitDepth = "splitDepth" - methodL2BlockNumber = "l2BlockNumber" - methodRequiredBond = "getRequiredBond" - methodClaimCredit = "claimCredit" - methodCredit = "credit" - methodWETH = "weth" + methodVersion = "version" + methodMaxClockDuration = "maxClockDuration" + methodMaxGameDepth = "maxGameDepth" + methodAbsolutePrestate = "absolutePrestate" + methodStatus = "status" + methodRootClaim = "rootClaim" + methodClaimCount = "claimDataLen" + methodClaim = "claimData" + methodL1Head = "l1Head" + methodResolvedSubgames = "resolvedSubgames" + methodResolve = "resolve" + methodResolveClaim = "resolveClaim" + methodAttack = "attack" + methodDefend = "defend" + methodStep = "step" + methodAddLocalData = "addLocalData" + methodVM = "vm" + methodStartingBlockNumber = "startingBlockNumber" + methodStartingRootHash = "startingRootHash" + methodSplitDepth = "splitDepth" + methodL2BlockNumber = "l2BlockNumber" + methodRequiredBond = "getRequiredBond" + methodClaimCredit = "claimCredit" + methodCredit = "credit" + methodWETH = "weth" + methodL2BlockNumberChallenged = "l2BlockNumberChallenged" + methodChallengeRootL2Block = "challengeRootL2Block" ) var ( @@ -68,6 +71,14 @@ type Proposal struct { OutputRoot common.Hash } +// outputRootProof is designed to match the solidity OutputRootProof struct. +type outputRootProof struct { + Version [32]byte + StateRoot [32]byte + MessagePasserStorageRoot [32]byte + LatestBlockhash [32]byte +} + func NewFaultDisputeGameContract(ctx context.Context, metrics metrics.ContractMetricer, addr common.Address, caller *batching.MultiCaller) (FaultDisputeGameContract, error) { contractAbi := snapshots.LoadFaultDisputeGameABI() @@ -87,6 +98,16 @@ func NewFaultDisputeGameContract(ctx context.Context, metrics metrics.ContractMe contract: batching.NewBoundContract(legacyAbi, addr), }, }, nil + } else if strings.HasPrefix(version, "0.18.") { + // Detected an older version of contracts, use a compatibility shim. + legacyAbi := mustParseAbi(faultDisputeGameAbi0180) + return &FaultDisputeGameContract0180{ + FaultDisputeGameContractLatest: FaultDisputeGameContractLatest{ + metrics: metrics, + multiCaller: caller, + contract: batching.NewBoundContract(legacyAbi, addr), + }, + }, nil } else { return &FaultDisputeGameContractLatest{ metrics: metrics, @@ -414,11 +435,25 @@ func (f *FaultDisputeGameContractLatest) vm(ctx context.Context) (*VMContract, e } func (f *FaultDisputeGameContractLatest) IsL2BlockNumberChallenged(ctx context.Context, block rpcblock.Block) (bool, error) { - return false, nil + defer f.metrics.StartContractRequest("IsL2BlockNumberChallenged")() + result, err := f.multiCaller.SingleCall(ctx, block, f.contract.Call(methodL2BlockNumberChallenged)) + if err != nil { + return false, fmt.Errorf("failed to fetch block number challenged: %w", err) + } + return result.GetBool(0), nil } func (f *FaultDisputeGameContractLatest) ChallengeL2BlockNumberTx(challenge *types.InvalidL2BlockNumberChallenge) (txmgr.TxCandidate, error) { - return txmgr.TxCandidate{}, ErrChallengeL2BlockNotSupported + headerRlp, err := rlp.EncodeToBytes(challenge.Header) + if err != nil { + return txmgr.TxCandidate{}, fmt.Errorf("failed to serialize header: %w", err) + } + return f.contract.Call(methodChallengeRootL2Block, outputRootProof{ + Version: challenge.Output.Version, + StateRoot: challenge.Output.StateRoot, + MessagePasserStorageRoot: challenge.Output.WithdrawalStorageRoot, + LatestBlockhash: challenge.Output.BlockRef.Hash, + }, headerRlp).ToTxCandidate() } func (f *FaultDisputeGameContractLatest) AttackTx(parentContractIndex uint64, pivot common.Hash) (txmgr.TxCandidate, error) { diff --git a/op-challenger/game/fault/contracts/faultdisputegame0180.go b/op-challenger/game/fault/contracts/faultdisputegame0180.go new file mode 100644 index 0000000000000..d7b23f73a3393 --- /dev/null +++ b/op-challenger/game/fault/contracts/faultdisputegame0180.go @@ -0,0 +1,25 @@ +package contracts + +import ( + "context" + _ "embed" + + "github.com/ethereum-optimism/optimism/op-challenger/game/fault/types" + "github.com/ethereum-optimism/optimism/op-service/sources/batching/rpcblock" + "github.com/ethereum-optimism/optimism/op-service/txmgr" +) + +//go:embed abis/FaultDisputeGame-0.18.1.json +var faultDisputeGameAbi0180 []byte + +type FaultDisputeGameContract0180 struct { + FaultDisputeGameContractLatest +} + +func (f *FaultDisputeGameContract0180) IsL2BlockNumberChallenged(_ context.Context, _ rpcblock.Block) (bool, error) { + return false, nil +} + +func (f *FaultDisputeGameContract0180) ChallengeL2BlockNumberTx(_ *types.InvalidL2BlockNumberChallenge) (txmgr.TxCandidate, error) { + return txmgr.TxCandidate{}, ErrChallengeL2BlockNotSupported +} diff --git a/op-challenger/game/fault/contracts/faultdisputegame080.go b/op-challenger/game/fault/contracts/faultdisputegame080.go index 18ed05d20c02b..b76f105f4c590 100644 --- a/op-challenger/game/fault/contracts/faultdisputegame080.go +++ b/op-challenger/game/fault/contracts/faultdisputegame080.go @@ -132,3 +132,11 @@ func (f *FaultDisputeGameContract080) ResolveClaimTx(claimIdx uint64) (txmgr.TxC func (f *FaultDisputeGameContract080) resolveClaimCall(claimIdx uint64) *batching.ContractCall { return f.contract.Call(methodResolveClaim, new(big.Int).SetUint64(claimIdx)) } + +func (f *FaultDisputeGameContract080) IsL2BlockNumberChallenged(_ context.Context, _ rpcblock.Block) (bool, error) { + return false, nil +} + +func (f *FaultDisputeGameContract080) ChallengeL2BlockNumberTx(_ *types.InvalidL2BlockNumberChallenge) (txmgr.TxCandidate, error) { + return txmgr.TxCandidate{}, ErrChallengeL2BlockNotSupported +} diff --git a/op-challenger/game/fault/contracts/faultdisputegame_test.go b/op-challenger/game/fault/contracts/faultdisputegame_test.go index 86c8b066822b5..9d7901edca44f 100644 --- a/op-challenger/game/fault/contracts/faultdisputegame_test.go +++ b/op-challenger/game/fault/contracts/faultdisputegame_test.go @@ -3,21 +3,26 @@ package contracts import ( "context" "errors" + "fmt" "math" "math/big" + "math/rand" "testing" "time" contractMetrics "github.com/ethereum-optimism/optimism/op-challenger/game/fault/contracts/metrics" faultTypes "github.com/ethereum-optimism/optimism/op-challenger/game/fault/types" "github.com/ethereum-optimism/optimism/op-challenger/game/types" + "github.com/ethereum-optimism/optimism/op-service/eth" "github.com/ethereum-optimism/optimism/op-service/sources/batching" "github.com/ethereum-optimism/optimism/op-service/sources/batching/rpcblock" batchingTest "github.com/ethereum-optimism/optimism/op-service/sources/batching/test" + "github.com/ethereum-optimism/optimism/op-service/testutils" "github.com/ethereum-optimism/optimism/op-service/txmgr" "github.com/ethereum-optimism/optimism/packages/contracts-bedrock/snapshots" "github.com/ethereum/go-ethereum/accounts/abi" "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/rlp" "github.com/stretchr/testify/require" ) @@ -34,7 +39,8 @@ type contractVersion struct { const ( vers080 = "0.8.0" - versLatest = "0.18.0" + vers0180 = "0.18.0" + versLatest = "1.1.0" ) var versions = []contractVersion{ @@ -44,6 +50,12 @@ var versions = []contractVersion{ return mustParseAbi(faultDisputeGameAbi020) }, }, + { + version: vers0180, + loadAbi: func() *abi.ABI { + return mustParseAbi(faultDisputeGameAbi0180) + }, + }, { version: versLatest, loadAbi: snapshots.LoadFaultDisputeGameABI, @@ -655,12 +667,22 @@ func TestFaultDisputeGame_IsResolved(t *testing.T) { func TestFaultDisputeGameContractLatest_IsL2BlockNumberChallenged(t *testing.T) { for _, version := range versions { version := version - t.Run(version.version, func(t *testing.T) { - _, game := setupFaultDisputeGameTest(t, version) - challenged, err := game.IsL2BlockNumberChallenged(context.Background(), rpcblock.Latest) - require.NoError(t, err) - require.False(t, challenged) - }) + for _, expected := range []bool{true, false} { + expected := expected + t.Run(fmt.Sprintf("%v-%v", version.version, expected), func(t *testing.T) { + block := rpcblock.ByHash(common.Hash{0x43}) + stubRpc, game := setupFaultDisputeGameTest(t, version) + supportsL2BlockNumChallenge := version.version != vers080 && version.version != vers0180 + if supportsL2BlockNumChallenge { + stubRpc.SetResponse(fdgAddr, methodL2BlockNumberChallenged, block, nil, []interface{}{expected}) + } else if expected { + t.Skip("Can't have challenged L2 block number on this contract version") + } + challenged, err := game.IsL2BlockNumberChallenged(context.Background(), block) + require.NoError(t, err) + require.Equal(t, expected, challenged) + }) + } } } @@ -668,10 +690,40 @@ func TestFaultDisputeGameContractLatest_ChallengeL2BlockNumberTx(t *testing.T) { for _, version := range versions { version := version t.Run(version.version, func(t *testing.T) { - _, game := setupFaultDisputeGameTest(t, version) - tx, err := game.ChallengeL2BlockNumberTx(&faultTypes.InvalidL2BlockNumberChallenge{}) - require.ErrorIs(t, err, ErrChallengeL2BlockNotSupported) - require.Equal(t, txmgr.TxCandidate{}, tx) + rng := rand.New(rand.NewSource(0)) + stubRpc, game := setupFaultDisputeGameTest(t, version) + challenge := &faultTypes.InvalidL2BlockNumberChallenge{ + Output: ð.OutputResponse{ + Version: eth.Bytes32{}, + OutputRoot: eth.Bytes32{0xaa}, + BlockRef: eth.L2BlockRef{Hash: common.Hash{0xbb}}, + WithdrawalStorageRoot: common.Hash{0xcc}, + StateRoot: common.Hash{0xdd}, + }, + Header: testutils.RandomHeader(rng), + } + supportsL2BlockNumChallenge := version.version != vers080 && version.version != vers0180 + if supportsL2BlockNumChallenge { + headerRlp, err := rlp.EncodeToBytes(challenge.Header) + require.NoError(t, err) + stubRpc.SetResponse(fdgAddr, methodChallengeRootL2Block, rpcblock.Latest, []interface{}{ + outputRootProof{ + Version: challenge.Output.Version, + StateRoot: challenge.Output.StateRoot, + MessagePasserStorageRoot: challenge.Output.WithdrawalStorageRoot, + LatestBlockhash: challenge.Output.BlockRef.Hash, + }, + headerRlp, + }, nil) + } + tx, err := game.ChallengeL2BlockNumberTx(challenge) + if supportsL2BlockNumChallenge { + require.NoError(t, err) + stubRpc.VerifyTxCandidate(tx) + } else { + require.ErrorIs(t, err, ErrChallengeL2BlockNotSupported) + require.Equal(t, txmgr.TxCandidate{}, tx) + } }) } } diff --git a/op-e2e/e2eutils/disputegame/output_game_helper.go b/op-e2e/e2eutils/disputegame/output_game_helper.go index b15db138be5ad..124412e6cd019 100644 --- a/op-e2e/e2eutils/disputegame/output_game_helper.go +++ b/op-e2e/e2eutils/disputegame/output_game_helper.go @@ -442,6 +442,19 @@ func (g *OutputGameHelper) WaitForInactivity(ctx context.Context, numInactiveBlo } } +func (g *OutputGameHelper) WaitForL2BlockNumberChallenged(ctx context.Context) { + g.T.Logf("Waiting for game %v to have L2 block number challenged", g.Addr) + caller := batching.NewMultiCaller(g.System.NodeClient("l1").Client(), batching.DefaultBatchSize) + contract, err := contracts.NewFaultDisputeGameContract(ctx, contractMetrics.NoopContractMetrics, g.Addr, caller) + g.Require.NoError(err) + timedCtx, cancel := context.WithTimeout(ctx, 30*time.Second) + defer cancel() + err = wait.For(timedCtx, time.Second, func() (bool, error) { + return contract.IsL2BlockNumberChallenged(ctx, rpcblock.Latest) + }) + g.Require.NoError(err, "L2 block number was not challenged in time") +} + // Mover is a function that either attacks or defends the claim at parentClaimIdx type Mover func(parent *ClaimHelper) *ClaimHelper diff --git a/op-e2e/faultproofs/output_cannon_test.go b/op-e2e/faultproofs/output_cannon_test.go index 27a64d996a7a3..292b207c38dd9 100644 --- a/op-e2e/faultproofs/output_cannon_test.go +++ b/op-e2e/faultproofs/output_cannon_test.go @@ -784,3 +784,35 @@ func TestInvalidateProposalForFutureBlock(t *testing.T) { }) } } + +func TestInvalidateCorrectProposalFutureBlock(t *testing.T) { + op_e2e.InitParallel(t, op_e2e.UsesCannon) + ctx := context.Background() + // Spin up the system without the batcher so the safe head doesn't advance + sys, l1Client := StartFaultDisputeSystem(t, WithBatcherStopped(), WithSequencerWindowSize(100000)) + t.Cleanup(sys.Close) + + // Create a dispute game factory helper. + disputeGameFactory := disputegame.NewFactoryHelper(t, ctx, sys) + + // No batches submitted so safe head is genesis + output, err := sys.RollupClient("sequencer").OutputAtBlock(ctx, 0) + require.NoError(t, err, "Failed to get output at safe head") + // Create a dispute game with an output root that is valid at `safeHead`, but that claims to correspond to block + // `safeHead.Number + 10000`. This is dishonest, because this block does not exist yet. + game := disputeGameFactory.StartOutputCannonGame(ctx, "sequencer", 10_000, common.Hash(output.OutputRoot), disputegame.WithFutureProposal()) + + // Start the honest challenger. + game.StartChallenger(ctx, "Honest", challenger.WithPrivKey(sys.Cfg.Secrets.Bob)) + + game.WaitForL2BlockNumberChallenged(ctx) + + // Time travel past when the game will be resolvable. + sys.TimeTravelClock.AdvanceTime(game.MaxClockDuration(ctx)) + require.NoError(t, wait.ForNextBlock(ctx, l1Client)) + + // The game should resolve as `CHALLENGER_WINS` always, because the root claim signifies a claim that does not exist + // yet in the L2 chain. + game.WaitForGameStatus(ctx, disputegame.StatusChallengerWins) + game.LogGameData(ctx) +}