From 6593815d3d60bf598fb75dbf9fa00f41463ec663 Mon Sep 17 00:00:00 2001 From: Adrian Sutton Date: Thu, 27 Nov 2025 11:19:14 +1000 Subject: [PATCH 1/2] op-challenger: Update list-games sub command to support optimistic zk games. Starts to build out the OptimisticZK contract bindings and make challenger not require everything to look like a FaultDisputeGame. --- op-challenger/cmd/credits.go | 2 +- op-challenger/cmd/list_claims.go | 2 +- op-challenger/cmd/list_games.go | 18 +- .../game/fault/contracts/disputegame.go | 59 ++++++ .../game/fault/contracts/faultdisputegame.go | 45 ++++- .../fault/contracts/faultdisputegame0180.go | 4 +- .../fault/contracts/faultdisputegame080.go | 4 +- .../fault/contracts/faultdisputegame_test.go | 35 +++- .../contracts/optimisticzkdisputegame.go | 135 ++++++++++++++ .../contracts/optimisticzkdisputegame_test.go | 174 ++++++++++++++++++ .../fault/contracts/superfaultdisputegame.go | 34 +++- op-challenger/game/fault/types/types.go | 3 + op-dispute-mon/mon/extract/caller.go | 2 +- op-dispute-mon/mon/extract/extractor.go | 2 +- op-dispute-mon/mon/extract/extractor_test.go | 2 +- .../contracts-bedrock/snapshots/abi_loader.go | 11 ++ 16 files changed, 501 insertions(+), 31 deletions(-) create mode 100644 op-challenger/game/fault/contracts/disputegame.go create mode 100644 op-challenger/game/fault/contracts/optimisticzkdisputegame.go create mode 100644 op-challenger/game/fault/contracts/optimisticzkdisputegame_test.go diff --git a/op-challenger/cmd/credits.go b/op-challenger/cmd/credits.go index 6c9aa3e5b914c..4d14e71291463 100644 --- a/op-challenger/cmd/credits.go +++ b/op-challenger/cmd/credits.go @@ -53,7 +53,7 @@ func listCredits(ctx context.Context, game contracts.FaultDisputeGameContract) e if err != nil { return fmt.Errorf("failed to load claims: %w", err) } - metadata, err := game.GetGameMetadata(ctx, rpcblock.Latest) + metadata, err := game.GetExtendedMetadata(ctx, rpcblock.Latest) if err != nil { return fmt.Errorf("failed to load metadata: %w", err) } diff --git a/op-challenger/cmd/list_claims.go b/op-challenger/cmd/list_claims.go index bcabf60b27720..83818de512600 100644 --- a/op-challenger/cmd/list_claims.go +++ b/op-challenger/cmd/list_claims.go @@ -65,7 +65,7 @@ func ListClaims(ctx *cli.Context) error { } func listClaims(ctx context.Context, game contracts.FaultDisputeGameContract, verbose bool) error { - metadata, err := game.GetGameMetadata(ctx, rpcblock.Latest) + metadata, err := game.GetExtendedMetadata(ctx, rpcblock.Latest) if err != nil { return fmt.Errorf("failed to retrieve metadata: %w", err) } diff --git a/op-challenger/cmd/list_games.go b/op-challenger/cmd/list_games.go index a904760508839..75aca55fae541 100644 --- a/op-challenger/cmd/list_games.go +++ b/op-challenger/cmd/list_games.go @@ -103,7 +103,7 @@ func listGames(ctx context.Context, caller *batching.MultiCaller, factory *contr var wg sync.WaitGroup for idx, game := range games { idx := idx - gameContract, err := contracts.NewFaultDisputeGameContract(ctx, metrics.NoopContractMetrics, game.Proxy, caller) + gameContract, err := contracts.NewDisputeGameContractForGame(ctx, metrics.NoopContractMetrics, caller, game) if err != nil { return fmt.Errorf("failed to create dispute game contract: %w", err) } @@ -112,20 +112,22 @@ func listGames(ctx context.Context, caller *batching.MultiCaller, factory *contr wg.Add(1) go func() { defer wg.Done() - metadata, err := gameContract.GetGameMetadata(ctx, rpcblock.ByHash(block)) + metadata, err := gameContract.GetMetadata(ctx, rpcblock.ByHash(block)) if err != nil { infos[idx].err = fmt.Errorf("failed to retrieve metadata for game %v: %w", gameProxy, err) return } infos[idx].status = metadata.Status infos[idx].l2BlockNum = metadata.L2SequenceNum - infos[idx].rootClaim = metadata.RootClaim - claimCount, err := gameContract.GetClaimCount(ctx) - if err != nil { - infos[idx].err = fmt.Errorf("failed to retrieve claim count for game %v: %w", gameProxy, err) - return + infos[idx].rootClaim = metadata.ProposedRoot + if fdg, ok := gameContract.(contracts.FaultDisputeGameContract); ok { + claimCount, err := fdg.GetClaimCount(ctx) + if err != nil { + infos[idx].err = fmt.Errorf("failed to retrieve claim count for game %v: %w", gameProxy, err) + return + } + infos[idx].claimCount = claimCount } - infos[idx].claimCount = claimCount }() } wg.Wait() diff --git a/op-challenger/game/fault/contracts/disputegame.go b/op-challenger/game/fault/contracts/disputegame.go new file mode 100644 index 0000000000000..b1e9ee616f2f8 --- /dev/null +++ b/op-challenger/game/fault/contracts/disputegame.go @@ -0,0 +1,59 @@ +package contracts + +import ( + "context" + "errors" + "time" + + "github.com/ethereum-optimism/optimism/op-challenger/game/fault/contracts/metrics" + "github.com/ethereum-optimism/optimism/op-challenger/game/fault/types" + gameTypes "github.com/ethereum-optimism/optimism/op-challenger/game/types" + "github.com/ethereum-optimism/optimism/op-service/sources/batching" + "github.com/ethereum-optimism/optimism/op-service/sources/batching/rpcblock" + "github.com/ethereum-optimism/optimism/op-service/txmgr" + "github.com/ethereum/go-ethereum/common" +) + +var ErrUnsupportedGameType = errors.New("unsupported game type") + +type GenericGameMetadata struct { + L1Head common.Hash + L2SequenceNum uint64 + ProposedRoot common.Hash + Status gameTypes.GameStatus +} + +type DisputeGameContract interface { + GetL1Head(ctx context.Context) (common.Hash, error) + GetStatus(ctx context.Context) (gameTypes.GameStatus, error) + GetGameRange(ctx context.Context) (prestateBlock uint64, poststateBlock uint64, retErr error) + GetMetadata(ctx context.Context, block rpcblock.Block) (GenericGameMetadata, error) + + GetResolvedAt(ctx context.Context, block rpcblock.Block) (time.Time, error) + CallResolve(ctx context.Context) (gameTypes.GameStatus, error) + ResolveTx() (txmgr.TxCandidate, error) +} + +func NewDisputeGameContractForGame(ctx context.Context, metrics metrics.ContractMetricer, caller *batching.MultiCaller, game gameTypes.GameMetadata) (DisputeGameContract, error) { + return NewDisputeGameContract(ctx, metrics, caller, types.GameType(game.GameType), game.Proxy) +} + +func NewDisputeGameContract(ctx context.Context, metrics metrics.ContractMetricer, caller *batching.MultiCaller, gameType types.GameType, addr common.Address) (DisputeGameContract, error) { + switch gameType { + case types.SuperCannonGameType, types.SuperCannonKonaGameType, types.SuperPermissionedGameType, types.SuperAsteriscKonaGameType: + return NewSuperFaultDisputeGameContract(ctx, metrics, addr, caller) + + case types.CannonGameType, + types.PermissionedGameType, + types.CannonKonaGameType, + types.AsteriscGameType, + types.AlphabetGameType, + types.FastGameType, + types.AsteriscKonaGameType: + return NewPreInteropFaultDisputeGameContract(ctx, metrics, addr, caller) + case types.OptimisticZKGameType: + return NewOptimisticZKDisputeGameContract(metrics, addr, caller) + default: + return nil, ErrUnsupportedGameType + } +} diff --git a/op-challenger/game/fault/contracts/faultdisputegame.go b/op-challenger/game/fault/contracts/faultdisputegame.go index db904b05841d0..526c7f0280ac1 100644 --- a/op-challenger/game/fault/contracts/faultdisputegame.go +++ b/op-challenger/game/fault/contracts/faultdisputegame.go @@ -208,9 +208,9 @@ type GameMetadata struct { L2BlockNumberChallenger common.Address } -// GetGameMetadata returns the game's L1 head, L2 block number, root claim, status, max clock duration, and is l2 block number challenged. -func (f *FaultDisputeGameContractLatest) GetGameMetadata(ctx context.Context, block rpcblock.Block) (GameMetadata, error) { - defer f.metrics.StartContractRequest("GetGameMetadata")() +// GetExtendedMetadata returns the game's L1 head, L2 block number, root claim, status, max clock duration, and is l2 block number challenged. +func (f *FaultDisputeGameContractLatest) GetExtendedMetadata(ctx context.Context, block rpcblock.Block) (GameMetadata, error) { + defer f.metrics.StartContractRequest("GetExtendedMetadata")() results, err := f.multiCaller.Call(ctx, block, f.contract.Call(methodL1Head), f.contract.Call(methodL2BlockNumber), @@ -247,6 +247,36 @@ func (f *FaultDisputeGameContractLatest) GetGameMetadata(ctx context.Context, bl }, nil } +// GetMetadata returns the basic game metadata +func (f *FaultDisputeGameContractLatest) GetMetadata(ctx context.Context, block rpcblock.Block) (GenericGameMetadata, error) { + defer f.metrics.StartContractRequest("GetMetadata")() + results, err := f.multiCaller.Call(ctx, block, + f.contract.Call(methodL1Head), + f.contract.Call(methodL2BlockNumber), + f.contract.Call(methodRootClaim), + f.contract.Call(methodStatus), + ) + if err != nil { + return GenericGameMetadata{}, fmt.Errorf("failed to retrieve game metadata: %w", err) + } + if len(results) != 4 { + return GenericGameMetadata{}, fmt.Errorf("expected 4 results but got %v", len(results)) + } + l1Head := results[0].GetHash(0) + l2BlockNumber := results[1].GetBigInt(0).Uint64() + rootClaim := results[2].GetHash(0) + status, err := gameTypes.GameStatusFromUint8(results[3].GetUint8(0)) + if err != nil { + return GenericGameMetadata{}, fmt.Errorf("failed to convert game status: %w", err) + } + return GenericGameMetadata{ + L1Head: l1Head, + L2SequenceNum: l2BlockNumber, + ProposedRoot: rootClaim, + Status: status, + }, nil +} + func (f *FaultDisputeGameContractLatest) GetResolvedAt(ctx context.Context, block rpcblock.Block) (time.Time, error) { defer f.metrics.StartContractRequest("GetResolvedAt")() result, err := f.multiCaller.SingleCall(ctx, block, f.contract.Call(methodResolvedAt)) @@ -642,10 +672,9 @@ func (f *FaultDisputeGameContractLatest) decodeClaim(result *batching.CallResult } type FaultDisputeGameContract interface { + DisputeGameContract GetBalanceAndDelay(ctx context.Context, block rpcblock.Block) (*big.Int, time.Duration, common.Address, error) - GetGameRange(ctx context.Context) (prestateBlock uint64, poststateBlock uint64, retErr error) - GetGameMetadata(ctx context.Context, block rpcblock.Block) (GameMetadata, error) - GetResolvedAt(ctx context.Context, block rpcblock.Block) (time.Time, error) + GetExtendedMetadata(ctx context.Context, block rpcblock.Block) (GameMetadata, error) GetStartingRootHash(ctx context.Context) (common.Hash, error) GetSplitDepth(ctx context.Context) (types.Depth, error) GetCredit(ctx context.Context, recipient common.Address) (*big.Int, gameTypes.GameStatus, error) @@ -660,8 +689,6 @@ type FaultDisputeGameContract interface { GetClockExtension(ctx context.Context) (time.Duration, error) GetMaxGameDepth(ctx context.Context) (types.Depth, error) GetAbsolutePrestateHash(ctx context.Context) (common.Hash, error) - GetL1Head(ctx context.Context) (common.Hash, error) - GetStatus(ctx context.Context) (gameTypes.GameStatus, error) GetClaimCount(ctx context.Context) (uint64, error) GetClaim(ctx context.Context, idx uint64) (types.Claim, error) GetAllClaims(ctx context.Context, block rpcblock.Block) ([]types.Claim, error) @@ -673,8 +700,6 @@ type FaultDisputeGameContract interface { StepTx(claimIdx uint64, isAttack bool, stateData []byte, proof []byte) (txmgr.TxCandidate, error) CallResolveClaim(ctx context.Context, claimIdx uint64) error ResolveClaimTx(claimIdx uint64) (txmgr.TxCandidate, error) - CallResolve(ctx context.Context) (gameTypes.GameStatus, error) - ResolveTx() (txmgr.TxCandidate, error) Vm(ctx context.Context) (*VMContract, error) GetBondDistributionMode(ctx context.Context, block rpcblock.Block) (types.BondDistributionMode, error) } diff --git a/op-challenger/game/fault/contracts/faultdisputegame0180.go b/op-challenger/game/fault/contracts/faultdisputegame0180.go index 6c0bc61caeafb..97cead1460552 100644 --- a/op-challenger/game/fault/contracts/faultdisputegame0180.go +++ b/op-challenger/game/fault/contracts/faultdisputegame0180.go @@ -21,8 +21,8 @@ type FaultDisputeGameContract0180 struct { } // GetGameMetadata returns the game's L1 head, L2 block number, root claim, status, and max clock duration. -func (f *FaultDisputeGameContract0180) GetGameMetadata(ctx context.Context, block rpcblock.Block) (GameMetadata, error) { - defer f.metrics.StartContractRequest("GetGameMetadata")() +func (f *FaultDisputeGameContract0180) GetExtendedMetadata(ctx context.Context, block rpcblock.Block) (GameMetadata, error) { + defer f.metrics.StartContractRequest("GetExtendedMetadata")() results, err := f.multiCaller.Call(ctx, block, f.contract.Call(methodL1Head), f.contract.Call(methodL2BlockNumber), diff --git a/op-challenger/game/fault/contracts/faultdisputegame080.go b/op-challenger/game/fault/contracts/faultdisputegame080.go index 492e160c16e5d..17beede451421 100644 --- a/op-challenger/game/fault/contracts/faultdisputegame080.go +++ b/op-challenger/game/fault/contracts/faultdisputegame080.go @@ -29,8 +29,8 @@ type FaultDisputeGameContract080 struct { } // GetGameMetadata returns the game's L1 head, L2 block number, root claim, status, and max clock duration. -func (f *FaultDisputeGameContract080) GetGameMetadata(ctx context.Context, block rpcblock.Block) (GameMetadata, error) { - defer f.metrics.StartContractRequest("GetGameMetadata")() +func (f *FaultDisputeGameContract080) GetExtendedMetadata(ctx context.Context, block rpcblock.Block) (GameMetadata, error) { + defer f.metrics.StartContractRequest("GetExtendedMetadata")() results, err := f.multiCaller.Call(ctx, block, f.contract.Call(methodL1Head), f.contract.Call(methodL2BlockNumber), diff --git a/op-challenger/game/fault/contracts/faultdisputegame_test.go b/op-challenger/game/fault/contracts/faultdisputegame_test.go index e5368ebd050e9..2cc98abc551fa 100644 --- a/op-challenger/game/fault/contracts/faultdisputegame_test.go +++ b/op-challenger/game/fault/contracts/faultdisputegame_test.go @@ -536,7 +536,7 @@ func expectGetClaim(stubRpc *batchingTest.AbiBasedRpc, block rpcblock.Block, cla }) } -func TestGetBlockRange(t *testing.T) { +func TestGetGameRange(t *testing.T) { for _, version := range versions { version := version t.Run(version.String(), func(t *testing.T) { @@ -601,7 +601,7 @@ func TestGetGameMetadata(t *testing.T) { } else { t.Skip("Can't have challenged L2 block number on this contract version") } - actual, err := contract.GetGameMetadata(context.Background(), block) + actual, err := contract.GetExtendedMetadata(context.Background(), block) expected := GameMetadata{ L1Head: expectedL1Head, L2SequenceNum: expectedL2BlockNumber, @@ -617,6 +617,37 @@ func TestGetGameMetadata(t *testing.T) { } } +func TestGetMetadata(t *testing.T) { + for _, version := range versions { + version := version + t.Run(version.String(), func(t *testing.T) { + stubRpc, contract := setupFaultDisputeGameTest(t, version) + expectedL1Head := common.Hash{0x0a, 0x0b} + expectedL2BlockNumber := uint64(123) + expectedRootClaim := common.Hash{0x01, 0x02} + expectedStatus := types.GameStatusChallengerWon + block := rpcblock.ByNumber(889) + stubRpc.SetResponse(fdgAddr, methodL1Head, block, nil, []interface{}{expectedL1Head}) + if version.IsSuperGame() { + stubRpc.SetResponse(fdgAddr, methodL2SequenceNumber, block, nil, []interface{}{new(big.Int).SetUint64(expectedL2BlockNumber)}) + } else { + stubRpc.SetResponse(fdgAddr, methodL2BlockNumber, block, nil, []interface{}{new(big.Int).SetUint64(expectedL2BlockNumber)}) + } + stubRpc.SetResponse(fdgAddr, methodRootClaim, block, nil, []interface{}{expectedRootClaim}) + stubRpc.SetResponse(fdgAddr, methodStatus, block, nil, []interface{}{expectedStatus}) + actual, err := contract.GetMetadata(context.Background(), block) + expected := GenericGameMetadata{ + L1Head: expectedL1Head, + L2SequenceNum: expectedL2BlockNumber, + ProposedRoot: expectedRootClaim, + Status: expectedStatus, + } + require.NoError(t, err) + require.Equal(t, expected, actual) + }) + } +} + func TestGetStartingRootHash(t *testing.T) { for _, version := range versions { version := version diff --git a/op-challenger/game/fault/contracts/optimisticzkdisputegame.go b/op-challenger/game/fault/contracts/optimisticzkdisputegame.go new file mode 100644 index 0000000000000..0a497d99ed2f9 --- /dev/null +++ b/op-challenger/game/fault/contracts/optimisticzkdisputegame.go @@ -0,0 +1,135 @@ +package contracts + +import ( + "context" + "fmt" + "time" + + "github.com/ethereum-optimism/optimism/op-challenger/game/fault/contracts/metrics" + gameTypes "github.com/ethereum-optimism/optimism/op-challenger/game/types" + "github.com/ethereum-optimism/optimism/op-service/sources/batching" + "github.com/ethereum-optimism/optimism/op-service/sources/batching/rpcblock" + "github.com/ethereum-optimism/optimism/op-service/txmgr" + "github.com/ethereum-optimism/optimism/packages/contracts-bedrock/snapshots" + "github.com/ethereum/go-ethereum/common" +) + +type OptimisticZKDisputeGameContract interface { + DisputeGameContract +} + +type OptimisticZKDisputeGameContractLatest struct { + metrics metrics.ContractMetricer + multiCaller *batching.MultiCaller + contract *batching.BoundContract +} + +func NewOptimisticZKDisputeGameContract( + m metrics.ContractMetricer, + addr common.Address, + caller *batching.MultiCaller, +) (*OptimisticZKDisputeGameContractLatest, error) { + abi := snapshots.LoadZKDisputeGameABI() + return &OptimisticZKDisputeGameContractLatest{ + metrics: m, + multiCaller: caller, + contract: batching.NewBoundContract(abi, addr), + }, nil +} + +// GetMetadata returns the basic game metadata +func (g *OptimisticZKDisputeGameContractLatest) GetMetadata(ctx context.Context, block rpcblock.Block) (GenericGameMetadata, error) { + defer g.metrics.StartContractRequest("GetMetadata")() + results, err := g.multiCaller.Call(ctx, block, + g.contract.Call(methodL1Head), + g.contract.Call(methodL2SequenceNumber), + g.contract.Call(methodRootClaim), + g.contract.Call(methodStatus), + ) + if err != nil { + return GenericGameMetadata{}, fmt.Errorf("failed to retrieve game metadata: %w", err) + } + if len(results) != 4 { + return GenericGameMetadata{}, fmt.Errorf("expected 4 results but got %v", len(results)) + } + l1Head := results[0].GetHash(0) + l2SequenceNumber := results[1].GetBigInt(0).Uint64() + rootClaim := results[2].GetHash(0) + status, err := gameTypes.GameStatusFromUint8(results[3].GetUint8(0)) + if err != nil { + return GenericGameMetadata{}, fmt.Errorf("failed to convert game status: %w", err) + } + return GenericGameMetadata{ + L1Head: l1Head, + L2SequenceNum: l2SequenceNumber, + ProposedRoot: rootClaim, + Status: status, + }, nil +} + +func (g *OptimisticZKDisputeGameContractLatest) GetL1Head(ctx context.Context) (common.Hash, error) { + defer g.metrics.StartContractRequest("GetL1Head")() + result, err := g.multiCaller.SingleCall(ctx, rpcblock.Latest, g.contract.Call(methodL1Head)) + if err != nil { + return common.Hash{}, fmt.Errorf("failed to fetch L1 head: %w", err) + } + return result.GetHash(0), nil +} + +func (g *OptimisticZKDisputeGameContractLatest) GetStatus(ctx context.Context) (gameTypes.GameStatus, error) { + defer g.metrics.StartContractRequest("GetStatus")() + result, err := g.multiCaller.SingleCall(ctx, rpcblock.Latest, g.contract.Call(methodStatus)) + if err != nil { + return 0, fmt.Errorf("failed to fetch status: %w", err) + } + return gameTypes.GameStatusFromUint8(result.GetUint8(0)) +} + +func (g *OptimisticZKDisputeGameContractLatest) GetGameRange(ctx context.Context) (prestateBlock uint64, poststateBlock uint64, retErr error) { + defer g.metrics.StartContractRequest("GetGameRange")() + results, err := g.multiCaller.Call(ctx, rpcblock.Latest, + g.contract.Call(methodStartingBlockNumber), + g.contract.Call(methodL2SequenceNumber)) + if err != nil { + retErr = fmt.Errorf("failed to retrieve game block range: %w", err) + return + } + if len(results) != 2 { + retErr = fmt.Errorf("expected 2 results but got %v", len(results)) + return + } + prestateBlock = results[0].GetBigInt(0).Uint64() + poststateBlock = results[1].GetBigInt(0).Uint64() + return +} + +func (g *OptimisticZKDisputeGameContractLatest) GetResolvedAt(ctx context.Context, block rpcblock.Block) (time.Time, error) { + defer g.metrics.StartContractRequest("GetResolvedAt")() + result, err := g.multiCaller.SingleCall(ctx, block, g.contract.Call(methodResolvedAt)) + if err != nil { + return time.Time{}, fmt.Errorf("failed to retrieve resolution time: %w", err) + } + resolvedAt := time.Unix(int64(result.GetUint64(0)), 0) + return resolvedAt, nil +} + +func (g *OptimisticZKDisputeGameContractLatest) CallResolve(ctx context.Context) (gameTypes.GameStatus, error) { + defer g.metrics.StartContractRequest("CallResolve")() + call := g.resolveCall() + result, err := g.multiCaller.SingleCall(ctx, rpcblock.Latest, call) + if err != nil { + return gameTypes.GameStatusInProgress, fmt.Errorf("failed to call resolve: %w", err) + } + return gameTypes.GameStatusFromUint8(result.GetUint8(0)) +} + +func (g *OptimisticZKDisputeGameContractLatest) ResolveTx() (txmgr.TxCandidate, error) { + call := g.resolveCall() + return call.ToTxCandidate() +} + +func (g *OptimisticZKDisputeGameContractLatest) resolveCall() *batching.ContractCall { + return g.contract.Call(methodResolve) +} + +var _ DisputeGameContract = (*OptimisticZKDisputeGameContractLatest)(nil) diff --git a/op-challenger/game/fault/contracts/optimisticzkdisputegame_test.go b/op-challenger/game/fault/contracts/optimisticzkdisputegame_test.go new file mode 100644 index 0000000000000..aba346184294f --- /dev/null +++ b/op-challenger/game/fault/contracts/optimisticzkdisputegame_test.go @@ -0,0 +1,174 @@ +package contracts + +import ( + "context" + "math/big" + "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/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/packages/contracts-bedrock/snapshots" + "github.com/ethereum/go-ethereum/common" + "github.com/stretchr/testify/require" +) + +const ( + versZKLatest = "0.0.0" +) + +var zkVersions = []contractVersion{ + { + version: versZKLatest, + gameType: faultTypes.OptimisticZKGameType, + loadAbi: snapshots.LoadZKDisputeGameABI, + }, +} + +func TestZKSimpleGetters(t *testing.T) { + tests := []struct { + methodAlias string + method string + args []interface{} + result interface{} + expected interface{} // Defaults to expecting the same as result + call func(game OptimisticZKDisputeGameContract) (any, error) + applies func(version contractVersion) bool + }{ + { + methodAlias: "status", + method: methodStatus, + result: types.GameStatusChallengerWon, + call: func(game OptimisticZKDisputeGameContract) (any, error) { + return game.GetStatus(context.Background()) + }, + }, + { + methodAlias: "l1Head", + method: methodL1Head, + result: common.Hash{0xdd, 0xbb}, + call: func(game OptimisticZKDisputeGameContract) (any, error) { + return game.GetL1Head(context.Background()) + }, + }, + { + methodAlias: "resolve", + method: methodResolve, + result: types.GameStatusInProgress, + call: func(game OptimisticZKDisputeGameContract) (any, error) { + return game.CallResolve(context.Background()) + }, + }, + { + methodAlias: "resolvedAt", + method: methodResolvedAt, + result: uint64(240402), + expected: time.Unix(240402, 0), + call: func(game OptimisticZKDisputeGameContract) (any, error) { + return game.GetResolvedAt(context.Background(), rpcblock.Latest) + }, + }, + } + for _, version := range zkVersions { + version := version + t.Run(version.String(), func(t *testing.T) { + for _, test := range tests { + test := test + t.Run(test.methodAlias, func(t *testing.T) { + if test.applies != nil && !test.applies(version) { + t.Skip("Skipping for this version") + } + stubRpc, game := setupZKDisputeGameTest(t, version) + stubRpc.SetResponse(fdgAddr, test.method, rpcblock.Latest, nil, []interface{}{test.result}) + status, err := test.call(game) + require.NoError(t, err) + expected := test.expected + if expected == nil { + expected = test.result + } + require.Equal(t, expected, status) + }) + } + }) + } +} + +func TestZKGetMetadata(t *testing.T) { + for _, version := range zkVersions { + version := version + t.Run(version.String(), func(t *testing.T) { + stubRpc, contract := setupZKDisputeGameTest(t, version) + expectedL1Head := common.Hash{0x0a, 0x0b} + expectedL2BlockNumber := uint64(123) + expectedRootClaim := common.Hash{0x01, 0x02} + expectedStatus := types.GameStatusChallengerWon + block := rpcblock.ByNumber(889) + stubRpc.SetResponse(fdgAddr, methodL1Head, block, nil, []interface{}{expectedL1Head}) + stubRpc.SetResponse(fdgAddr, methodL2SequenceNumber, block, nil, []interface{}{new(big.Int).SetUint64(expectedL2BlockNumber)}) + stubRpc.SetResponse(fdgAddr, methodRootClaim, block, nil, []interface{}{expectedRootClaim}) + stubRpc.SetResponse(fdgAddr, methodStatus, block, nil, []interface{}{expectedStatus}) + actual, err := contract.GetMetadata(context.Background(), block) + expected := GenericGameMetadata{ + L1Head: expectedL1Head, + L2SequenceNum: expectedL2BlockNumber, + ProposedRoot: expectedRootClaim, + Status: expectedStatus, + } + require.NoError(t, err) + require.Equal(t, expected, actual) + }) + } +} + +func TestZKGetGameRange(t *testing.T) { + for _, version := range zkVersions { + version := version + t.Run(version.String(), func(t *testing.T) { + stubRpc, contract := setupZKDisputeGameTest(t, version) + expectedStart := uint64(65) + expectedEnd := uint64(102) + stubRpc.SetResponse(fdgAddr, methodStartingBlockNumber, rpcblock.Latest, nil, []interface{}{new(big.Int).SetUint64(expectedStart)}) + stubRpc.SetResponse(fdgAddr, methodL2SequenceNumber, rpcblock.Latest, nil, []interface{}{new(big.Int).SetUint64(expectedEnd)}) + start, end, err := contract.GetGameRange(context.Background()) + require.NoError(t, err) + require.Equal(t, expectedStart, start) + require.Equal(t, expectedEnd, end) + }) + } +} + +func TestZKResolveTx(t *testing.T) { + for _, version := range zkVersions { + version := version + t.Run(version.String(), func(t *testing.T) { + stubRpc, game := setupZKDisputeGameTest(t, version) + stubRpc.SetResponse(fdgAddr, methodResolve, rpcblock.Latest, nil, nil) + tx, err := game.ResolveTx() + require.NoError(t, err) + stubRpc.VerifyTxCandidate(tx) + }) + } +} + +func setupZKDisputeGameTest(t *testing.T, version contractVersion) (*batchingTest.AbiBasedRpc, OptimisticZKDisputeGameContract) { + fdgAbi := version.loadAbi() + + vmAbi := snapshots.LoadMIPSABI() + oracleAbi := snapshots.LoadPreimageOracleABI() + + stubRpc := batchingTest.NewAbiBasedRpc(t, fdgAddr, fdgAbi) + stubRpc.AddContract(vmAddr, vmAbi) + stubRpc.AddContract(oracleAddr, oracleAbi) + caller := batching.NewMultiCaller(stubRpc, batching.DefaultBatchSize) + + stubRpc.SetResponse(fdgAddr, methodGameType, rpcblock.Latest, nil, []interface{}{uint32(version.gameType)}) + stubRpc.SetResponse(fdgAddr, methodVersion, rpcblock.Latest, nil, []interface{}{version.version}) + stubRpc.SetResponse(oracleAddr, methodVersion, rpcblock.Latest, nil, []interface{}{oracleLatest}) + game, err := NewOptimisticZKDisputeGameContract(contractMetrics.NoopContractMetrics, fdgAddr, caller) + require.NoError(t, err) + return stubRpc, game +} diff --git a/op-challenger/game/fault/contracts/superfaultdisputegame.go b/op-challenger/game/fault/contracts/superfaultdisputegame.go index ed0561c7a71f4..3b92824145695 100644 --- a/op-challenger/game/fault/contracts/superfaultdisputegame.go +++ b/op-challenger/game/fault/contracts/superfaultdisputegame.go @@ -35,8 +35,8 @@ func NewSuperFaultDisputeGameContract(ctx context.Context, metrics metrics.Contr } // GetGameMetadata returns the game's L1 head, L2 block number, root claim, status, max clock duration, and is l2 block number challenged. -func (f *SuperFaultDisputeGameContractLatest) GetGameMetadata(ctx context.Context, block rpcblock.Block) (GameMetadata, error) { - defer f.metrics.StartContractRequest("GetGameMetadata")() +func (f *SuperFaultDisputeGameContractLatest) GetExtendedMetadata(ctx context.Context, block rpcblock.Block) (GameMetadata, error) { + defer f.metrics.StartContractRequest("GetExtendedMetadata")() results, err := f.multiCaller.Call(ctx, block, f.contract.Call(methodL1Head), f.contract.Call(methodL2SequenceNumber), @@ -67,6 +67,36 @@ func (f *SuperFaultDisputeGameContractLatest) GetGameMetadata(ctx context.Contex }, nil } +// GetMetadata returns the basic game metadata +func (f *SuperFaultDisputeGameContractLatest) GetMetadata(ctx context.Context, block rpcblock.Block) (GenericGameMetadata, error) { + defer f.metrics.StartContractRequest("GetMetadata")() + results, err := f.multiCaller.Call(ctx, block, + f.contract.Call(methodL1Head), + f.contract.Call(methodL2SequenceNumber), + f.contract.Call(methodRootClaim), + f.contract.Call(methodStatus), + ) + if err != nil { + return GenericGameMetadata{}, fmt.Errorf("failed to retrieve game metadata: %w", err) + } + if len(results) != 4 { + return GenericGameMetadata{}, fmt.Errorf("expected 4 results but got %v", len(results)) + } + l1Head := results[0].GetHash(0) + l2BlockNumber := results[1].GetBigInt(0).Uint64() + rootClaim := results[2].GetHash(0) + status, err := gameTypes.GameStatusFromUint8(results[3].GetUint8(0)) + if err != nil { + return GenericGameMetadata{}, fmt.Errorf("failed to convert game status: %w", err) + } + return GenericGameMetadata{ + L1Head: l1Head, + L2SequenceNum: l2BlockNumber, + ProposedRoot: rootClaim, + Status: status, + }, nil +} + func (f *SuperFaultDisputeGameContractLatest) IsL2BlockNumberChallenged(ctx context.Context, block rpcblock.Block) (bool, error) { return false, nil } diff --git a/op-challenger/game/fault/types/types.go b/op-challenger/game/fault/types/types.go index 914a63d313fd1..51fe0cf9e13aa 100644 --- a/op-challenger/game/fault/types/types.go +++ b/op-challenger/game/fault/types/types.go @@ -37,6 +37,7 @@ const ( SuperAsteriscKonaGameType GameType = 7 CannonKonaGameType GameType = 8 SuperCannonKonaGameType GameType = 9 + OptimisticZKGameType GameType = 10 FastGameType GameType = 254 AlphabetGameType GameType = 255 KailuaGameType GameType = 1337 @@ -69,6 +70,8 @@ func (t GameType) String() string { return "cannon-kona" case SuperCannonKonaGameType: return "super-cannon-kona" + case OptimisticZKGameType: + return "optimistic-zk" case FastGameType: return "fast" case AlphabetGameType: diff --git a/op-dispute-mon/mon/extract/caller.go b/op-dispute-mon/mon/extract/caller.go index 9658c2533c963..ea6ee0c3af084 100644 --- a/op-dispute-mon/mon/extract/caller.go +++ b/op-dispute-mon/mon/extract/caller.go @@ -25,7 +25,7 @@ type GameCallerMetrics interface { type GameCaller interface { GetWithdrawals(context.Context, rpcblock.Block, ...common.Address) ([]*contracts.WithdrawalRequest, error) - GetGameMetadata(context.Context, rpcblock.Block) (contracts.GameMetadata, error) + GetExtendedMetadata(context.Context, rpcblock.Block) (contracts.GameMetadata, error) GetAllClaims(context.Context, rpcblock.Block) ([]faultTypes.Claim, error) BondCaller BalanceCaller diff --git a/op-dispute-mon/mon/extract/extractor.go b/op-dispute-mon/mon/extract/extractor.go index a1497de640470..aa10c6df15921 100644 --- a/op-dispute-mon/mon/extract/extractor.go +++ b/op-dispute-mon/mon/extract/extractor.go @@ -136,7 +136,7 @@ func (e *Extractor) enrichGame(ctx context.Context, blockHash common.Hash, game if err != nil { return nil, fmt.Errorf("failed to create contracts: %w", err) } - meta, err := caller.GetGameMetadata(ctx, rpcblock.ByHash(blockHash)) + meta, err := caller.GetExtendedMetadata(ctx, rpcblock.ByHash(blockHash)) if err != nil { return nil, fmt.Errorf("failed to fetch game metadata: %w", err) } diff --git a/op-dispute-mon/mon/extract/extractor_test.go b/op-dispute-mon/mon/extract/extractor_test.go index de11f6acc5160..09e408d76d9aa 100644 --- a/op-dispute-mon/mon/extract/extractor_test.go +++ b/op-dispute-mon/mon/extract/extractor_test.go @@ -315,7 +315,7 @@ func (m *mockGameCaller) GetWithdrawals(_ context.Context, _ rpcblock.Block, _ . }, nil } -func (m *mockGameCaller) GetGameMetadata(_ context.Context, _ rpcblock.Block) (contracts.GameMetadata, error) { +func (m *mockGameCaller) GetExtendedMetadata(_ context.Context, _ rpcblock.Block) (contracts.GameMetadata, error) { m.metadataCalls++ if m.metadataErr != nil { return contracts.GameMetadata{}, m.metadataErr diff --git a/packages/contracts-bedrock/snapshots/abi_loader.go b/packages/contracts-bedrock/snapshots/abi_loader.go index d014c6604e050..8a69f9321a9de 100644 --- a/packages/contracts-bedrock/snapshots/abi_loader.go +++ b/packages/contracts-bedrock/snapshots/abi_loader.go @@ -16,6 +16,9 @@ var superFaultDisputeGame []byte //go:embed abi/FaultDisputeGame.json var faultDisputeGame []byte +//go:embed abi/OPSuccinctFaultDisputeGame.json +var zkDisputeGame []byte + //go:embed abi/PreimageOracle.json var preimageOracle []byte @@ -34,18 +37,26 @@ var crossL2Inbox []byte func LoadDisputeGameFactoryABI() *abi.ABI { return loadABI(disputeGameFactory) } + func LoadSuperFaultDisputeGameABI() *abi.ABI { return loadABI(superFaultDisputeGame) } + func LoadFaultDisputeGameABI() *abi.ABI { return loadABI(faultDisputeGame) } + func LoadPreimageOracleABI() *abi.ABI { return loadABI(preimageOracle) } + func LoadMIPSABI() *abi.ABI { return loadABI(mips) } +func LoadZKDisputeGameABI() *abi.ABI { + return loadABI(zkDisputeGame) +} + func LoadDelayedWETHABI() *abi.ABI { return loadABI(delayedWETH) } From 1f104e2a26e50e274c7f601127795290207dcd85 Mon Sep 17 00:00:00 2001 From: Adrian Sutton Date: Thu, 27 Nov 2025 13:18:59 +1000 Subject: [PATCH 2/2] Fix variable name --- op-challenger/game/fault/contracts/superfaultdisputegame.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/op-challenger/game/fault/contracts/superfaultdisputegame.go b/op-challenger/game/fault/contracts/superfaultdisputegame.go index 3b92824145695..f8543be6b61af 100644 --- a/op-challenger/game/fault/contracts/superfaultdisputegame.go +++ b/op-challenger/game/fault/contracts/superfaultdisputegame.go @@ -83,7 +83,7 @@ func (f *SuperFaultDisputeGameContractLatest) GetMetadata(ctx context.Context, b return GenericGameMetadata{}, fmt.Errorf("expected 4 results but got %v", len(results)) } l1Head := results[0].GetHash(0) - l2BlockNumber := results[1].GetBigInt(0).Uint64() + l2SequenceNumber := results[1].GetBigInt(0).Uint64() rootClaim := results[2].GetHash(0) status, err := gameTypes.GameStatusFromUint8(results[3].GetUint8(0)) if err != nil { @@ -91,7 +91,7 @@ func (f *SuperFaultDisputeGameContractLatest) GetMetadata(ctx context.Context, b } return GenericGameMetadata{ L1Head: l1Head, - L2SequenceNum: l2BlockNumber, + L2SequenceNum: l2SequenceNumber, ProposedRoot: rootClaim, Status: status, }, nil