From 302d3b0e0697b6d135668ce0a8e9e0321037d232 Mon Sep 17 00:00:00 2001 From: Adrian Sutton Date: Thu, 18 Jan 2024 01:38:37 +1000 Subject: [PATCH] op-challenger: Register the oracle used by each game type (#9034) This is prep work for being able to monitor the preimage oracle for large preimages that need to be validated and potentially challenged. --- .../game/fault/contracts/faultdisputegame.go | 12 +++-- .../fault/contracts/faultdisputegame_test.go | 10 ++++ .../game/fault/contracts/gamefactory.go | 9 ++++ .../game/fault/contracts/gamefactory_test.go | 15 ++++++ op-challenger/game/fault/contracts/oracle.go | 8 +++- op-challenger/game/fault/register.go | 47 ++++++++++++++++--- op-challenger/game/registry/registry.go | 19 ++++++-- op-challenger/game/registry/registry_test.go | 29 ++++++++++-- op-challenger/game/service.go | 33 +++++++++---- op-challenger/game/types/types.go | 4 ++ 10 files changed, 160 insertions(+), 26 deletions(-) diff --git a/op-challenger/game/fault/contracts/faultdisputegame.go b/op-challenger/game/fault/contracts/faultdisputegame.go index 706e7befda7d..522bcacbc923 100644 --- a/op-challenger/game/fault/contracts/faultdisputegame.go +++ b/op-challenger/game/fault/contracts/faultdisputegame.go @@ -118,15 +118,19 @@ func (f *FaultDisputeGameContract) addLocalDataTx(claimIdx uint64, data *types.P } func (f *FaultDisputeGameContract) addGlobalDataTx(ctx context.Context, data *types.PreimageOracleData) (txmgr.TxCandidate, error) { - vm, err := f.vm(ctx) + oracle, err := f.GetOracle(ctx) if err != nil { return txmgr.TxCandidate{}, err } - oracle, err := vm.Oracle(ctx) + return oracle.AddGlobalDataTx(data) +} + +func (f *FaultDisputeGameContract) GetOracle(ctx context.Context) (*PreimageOracleContract, error) { + vm, err := f.vm(ctx) if err != nil { - return txmgr.TxCandidate{}, err + return nil, err } - return oracle.AddGlobalDataTx(data) + return vm.Oracle(ctx) } func (f *FaultDisputeGameContract) GetGameDuration(ctx context.Context) (uint64, error) { diff --git a/op-challenger/game/fault/contracts/faultdisputegame_test.go b/op-challenger/game/fault/contracts/faultdisputegame_test.go index 1a1c4422128f..99f9665be70f 100644 --- a/op-challenger/game/fault/contracts/faultdisputegame_test.go +++ b/op-challenger/game/fault/contracts/faultdisputegame_test.go @@ -105,6 +105,16 @@ func TestSimpleGetters(t *testing.T) { } } +func TestGetOracleAddr(t *testing.T) { + stubRpc, game := setupFaultDisputeGameTest(t) + stubRpc.SetResponse(fdgAddr, methodVM, batching.BlockLatest, nil, []interface{}{vmAddr}) + stubRpc.SetResponse(vmAddr, methodOracle, batching.BlockLatest, nil, []interface{}{oracleAddr}) + + actual, err := game.GetOracle(context.Background()) + require.NoError(t, err) + require.Equal(t, oracleAddr, actual.Addr()) +} + func TestGetClaim(t *testing.T) { stubRpc, game := setupFaultDisputeGameTest(t) idx := big.NewInt(2) diff --git a/op-challenger/game/fault/contracts/gamefactory.go b/op-challenger/game/fault/contracts/gamefactory.go index ddfccb3b07e1..983c331d0b22 100644 --- a/op-challenger/game/fault/contracts/gamefactory.go +++ b/op-challenger/game/fault/contracts/gamefactory.go @@ -14,6 +14,7 @@ import ( const ( methodGameCount = "gameCount" methodGameAtIndex = "gameAtIndex" + methodGameImpls = "gameImpls" ) type DisputeGameFactoryContract struct { @@ -48,6 +49,14 @@ func (f *DisputeGameFactoryContract) GetGame(ctx context.Context, idx uint64, bl return f.decodeGame(result), nil } +func (f *DisputeGameFactoryContract) GetGameImpl(ctx context.Context, gameType uint8) (common.Address, error) { + result, err := f.multiCaller.SingleCall(ctx, batching.BlockLatest, f.contract.Call(methodGameImpls, gameType)) + if err != nil { + return common.Address{}, fmt.Errorf("failed to load game impl for type %v: %w", gameType, err) + } + return result.GetAddress(0), nil +} + func (f *DisputeGameFactoryContract) decodeGame(result *batching.CallResult) types.GameMetadata { gameType := result.GetUint8(0) timestamp := result.GetUint64(1) diff --git a/op-challenger/game/fault/contracts/gamefactory_test.go b/op-challenger/game/fault/contracts/gamefactory_test.go index 0886087f58dc..340917869698 100644 --- a/op-challenger/game/fault/contracts/gamefactory_test.go +++ b/op-challenger/game/fault/contracts/gamefactory_test.go @@ -78,6 +78,21 @@ func TestLoadGame(t *testing.T) { } } +func TestGetGameImpl(t *testing.T) { + stubRpc, factory := setupDisputeGameFactoryTest(t) + gameType := uint8(3) + gameImplAddr := common.Address{0xaa} + stubRpc.SetResponse( + factoryAddr, + "gameImpls", + batching.BlockLatest, + []interface{}{gameType}, + []interface{}{gameImplAddr}) + actual, err := factory.GetGameImpl(context.Background(), gameType) + require.NoError(t, err) + require.Equal(t, gameImplAddr, actual) +} + func expectGetGame(stubRpc *batchingTest.AbiBasedRpc, idx int, blockHash common.Hash, game types.GameMetadata) { stubRpc.SetResponse( factoryAddr, diff --git a/op-challenger/game/fault/contracts/oracle.go b/op-challenger/game/fault/contracts/oracle.go index d199d468eb8d..f15e893245f0 100644 --- a/op-challenger/game/fault/contracts/oracle.go +++ b/op-challenger/game/fault/contracts/oracle.go @@ -17,6 +17,7 @@ const ( // PreimageOracleContract is a binding that works with contracts implementing the IPreimageOracle interface type PreimageOracleContract struct { + addr common.Address multiCaller *batching.MultiCaller contract *batching.BoundContract } @@ -28,12 +29,17 @@ func NewPreimageOracleContract(addr common.Address, caller *batching.MultiCaller } return &PreimageOracleContract{ + addr: addr, multiCaller: caller, contract: batching.NewBoundContract(mipsAbi, addr), }, nil } -func (c PreimageOracleContract) AddGlobalDataTx(data *types.PreimageOracleData) (txmgr.TxCandidate, error) { +func (c *PreimageOracleContract) Addr() common.Address { + return c.addr +} + +func (c *PreimageOracleContract) AddGlobalDataTx(data *types.PreimageOracleData) (txmgr.TxCandidate, error) { call := c.contract.Call(methodLoadKeccak256PreimagePart, new(big.Int).SetUint64(uint64(data.OracleOffset)), data.GetPreimageWithoutSize()) return call.ToTxCandidate() } diff --git a/op-challenger/game/fault/register.go b/op-challenger/game/fault/register.go index 0b05cc293b10..07628a17fd60 100644 --- a/op-challenger/game/fault/register.go +++ b/op-challenger/game/fault/register.go @@ -26,7 +26,7 @@ var ( type CloseFunc func() type Registry interface { - RegisterGameType(gameType uint8, creator scheduler.PlayerCreator) + RegisterGameType(gameType uint8, creator scheduler.PlayerCreator, oracle types.LargePreimageOracle) } func RegisterGameTypes( @@ -37,6 +37,7 @@ func RegisterGameTypes( cfg *config.Config, rollupClient outputs.OutputRollupClient, txMgr txmgr.TxManager, + gameFactory *contracts.DisputeGameFactoryContract, caller *batching.MultiCaller, ) (CloseFunc, error) { var closer CloseFunc @@ -50,10 +51,14 @@ func RegisterGameTypes( closer = l2Client.Close } if cfg.TraceTypeEnabled(config.TraceTypeCannon) { - registerCannon(registry, ctx, logger, m, cfg, rollupClient, txMgr, caller, l2Client) + if err := registerCannon(registry, ctx, logger, m, cfg, rollupClient, txMgr, gameFactory, caller, l2Client); err != nil { + return nil, fmt.Errorf("failed to register cannon game type: %w", err) + } } if cfg.TraceTypeEnabled(config.TraceTypeAlphabet) { - registerAlphabet(registry, ctx, logger, m, rollupClient, txMgr, caller) + if err := registerAlphabet(registry, ctx, logger, m, rollupClient, txMgr, gameFactory, caller); err != nil { + return nil, fmt.Errorf("failed to register alphabet game type: %w", err) + } } return closer, nil } @@ -65,8 +70,9 @@ func registerAlphabet( m metrics.Metricer, rollupClient outputs.OutputRollupClient, txMgr txmgr.TxManager, + gameFactory *contracts.DisputeGameFactoryContract, caller *batching.MultiCaller, -) { +) error { playerCreator := func(game types.GameMetadata, dir string) (scheduler.GamePlayer, error) { contract, err := contracts.NewFaultDisputeGameContract(game.Proxy, caller) if err != nil { @@ -92,7 +98,28 @@ func registerAlphabet( genesisValidator := NewPrestateValidator(contract.GetGenesisOutputRoot, prestateProvider) return NewGamePlayer(ctx, logger, m, dir, game.Proxy, txMgr, contract, []Validator{prestateValidator, genesisValidator}, creator) } - registry.RegisterGameType(alphabetGameType, playerCreator) + oracle, err := createOracle(ctx, gameFactory, caller) + if err != nil { + return err + } + registry.RegisterGameType(alphabetGameType, playerCreator, oracle) + return nil +} + +func createOracle(ctx context.Context, gameFactory *contracts.DisputeGameFactoryContract, caller *batching.MultiCaller) (*contracts.PreimageOracleContract, error) { + implAddr, err := gameFactory.GetGameImpl(ctx, alphabetGameType) + if err != nil { + return nil, fmt.Errorf("failed to load alphabet game implementation: %w", err) + } + contract, err := contracts.NewFaultDisputeGameContract(implAddr, caller) + if err != nil { + return nil, err + } + oracle, err := contract.GetOracle(ctx) + if err != nil { + return nil, fmt.Errorf("failed to load oracle address: %w", err) + } + return oracle, nil } func registerCannon( @@ -103,9 +130,10 @@ func registerCannon( cfg *config.Config, rollupClient outputs.OutputRollupClient, txMgr txmgr.TxManager, + gameFactory *contracts.DisputeGameFactoryContract, caller *batching.MultiCaller, l2Client cannon.L2HeaderSource, -) { +) error { playerCreator := func(game types.GameMetadata, dir string) (scheduler.GamePlayer, error) { contract, err := contracts.NewFaultDisputeGameContract(game.Proxy, caller) if err != nil { @@ -131,5 +159,10 @@ func registerCannon( genesisValidator := NewPrestateValidator(contract.GetGenesisOutputRoot, prestateProvider) return NewGamePlayer(ctx, logger, m, dir, game.Proxy, txMgr, contract, []Validator{prestateValidator, genesisValidator}, creator) } - registry.RegisterGameType(cannonGameType, playerCreator) + oracle, err := createOracle(ctx, gameFactory, caller) + if err != nil { + return err + } + registry.RegisterGameType(cannonGameType, playerCreator, oracle) + return nil } diff --git a/op-challenger/game/registry/registry.go b/op-challenger/game/registry/registry.go index 8f9b71d21388..9c7833abf6a5 100644 --- a/op-challenger/game/registry/registry.go +++ b/op-challenger/game/registry/registry.go @@ -6,6 +6,8 @@ import ( "github.com/ethereum-optimism/optimism/op-challenger/game/scheduler" "github.com/ethereum-optimism/optimism/op-challenger/game/types" + "github.com/ethereum/go-ethereum/common" + "golang.org/x/exp/maps" ) var ( @@ -13,22 +15,29 @@ var ( ) type GameTypeRegistry struct { - types map[uint8]scheduler.PlayerCreator + types map[uint8]scheduler.PlayerCreator + oracles map[common.Address]types.LargePreimageOracle } func NewGameTypeRegistry() *GameTypeRegistry { return &GameTypeRegistry{ - types: make(map[uint8]scheduler.PlayerCreator), + types: make(map[uint8]scheduler.PlayerCreator), + oracles: make(map[common.Address]types.LargePreimageOracle), } } // RegisterGameType registers a scheduler.PlayerCreator to use for a specific game type. // Panics if the same game type is registered multiple times, since this indicates a significant programmer error. -func (r *GameTypeRegistry) RegisterGameType(gameType uint8, creator scheduler.PlayerCreator) { +func (r *GameTypeRegistry) RegisterGameType(gameType uint8, creator scheduler.PlayerCreator, oracle types.LargePreimageOracle) { if _, ok := r.types[gameType]; ok { panic(fmt.Errorf("duplicate creator registered for game type: %v", gameType)) } r.types[gameType] = creator + if oracle != nil { + // It's ok to have two game types use the same oracle contract. + // We add them to a map deliberately to deduplicate them. + r.oracles[oracle.Addr()] = oracle + } } // CreatePlayer creates a new game player for the given game, using the specified directory for persisting data. @@ -39,3 +48,7 @@ func (r *GameTypeRegistry) CreatePlayer(game types.GameMetadata, dir string) (sc } return creator(game, dir) } + +func (r *GameTypeRegistry) Oracles() []types.LargePreimageOracle { + return maps.Values(r.oracles) +} diff --git a/op-challenger/game/registry/registry_test.go b/op-challenger/game/registry/registry_test.go index 9f8c81b85d99..f82ab9bd6f43 100644 --- a/op-challenger/game/registry/registry_test.go +++ b/op-challenger/game/registry/registry_test.go @@ -6,6 +6,7 @@ import ( "github.com/ethereum-optimism/optimism/op-challenger/game/scheduler" "github.com/ethereum-optimism/optimism/op-challenger/game/scheduler/test" "github.com/ethereum-optimism/optimism/op-challenger/game/types" + "github.com/ethereum/go-ethereum/common" "github.com/stretchr/testify/require" ) @@ -22,7 +23,7 @@ func TestKnownGameType(t *testing.T) { creator := func(game types.GameMetadata, dir string) (scheduler.GamePlayer, error) { return expectedPlayer, nil } - registry.RegisterGameType(0, creator) + registry.RegisterGameType(0, creator, nil) player, err := registry.CreatePlayer(types.GameMetadata{GameType: 0}, "") require.NoError(t, err) require.Same(t, expectedPlayer, player) @@ -33,8 +34,30 @@ func TestPanicsOnDuplicateGameType(t *testing.T) { creator := func(game types.GameMetadata, dir string) (scheduler.GamePlayer, error) { return nil, nil } - registry.RegisterGameType(0, creator) + registry.RegisterGameType(0, creator, nil) require.Panics(t, func() { - registry.RegisterGameType(0, creator) + registry.RegisterGameType(0, creator, nil) }) } + +func TestDeduplicateOracles(t *testing.T) { + registry := NewGameTypeRegistry() + creator := func(game types.GameMetadata, dir string) (scheduler.GamePlayer, error) { + return nil, nil + } + oracleA := stubPreimageOracle{0xaa} + oracleB := stubPreimageOracle{0xbb} + registry.RegisterGameType(0, creator, oracleA) + registry.RegisterGameType(1, creator, oracleB) + registry.RegisterGameType(2, creator, oracleB) + oracles := registry.Oracles() + require.Len(t, oracles, 2) + require.Contains(t, oracles, oracleA) + require.Contains(t, oracles, oracleB) +} + +type stubPreimageOracle common.Address + +func (s stubPreimageOracle) Addr() common.Address { + return common.Address(s) +} diff --git a/op-challenger/game/service.go b/op-challenger/game/service.go index fb4264db4fea..8f9658030741 100644 --- a/op-challenger/game/service.go +++ b/op-challenger/game/service.go @@ -41,7 +41,9 @@ type Service struct { loader *loader.GameLoader - rollupClient *sources.RollupClient + factoryContract *contracts.DisputeGameFactoryContract + registry *registry.GameTypeRegistry + rollupClient *sources.RollupClient l1Client *ethclient.Client pollClient client.RPC @@ -88,10 +90,16 @@ func (s *Service) initFromConfig(ctx context.Context, cfg *config.Config) error if err := s.initMetricsServer(&cfg.MetricsConfig); err != nil { return fmt.Errorf("failed to init metrics server: %w", err) } - if err := s.initGameLoader(cfg); err != nil { + if err := s.initFactoryContract(cfg); err != nil { + return fmt.Errorf("failed to create factory contract bindings: %w", err) + } + if err := s.initGameLoader(); err != nil { return fmt.Errorf("failed to init game loader: %w", err) } - if err := s.initScheduler(ctx, cfg); err != nil { + if err := s.registerGameTypes(ctx, cfg); err != nil { + return fmt.Errorf("failed to register game types: %w", err) + } + if err := s.initScheduler(cfg); err != nil { return fmt.Errorf("failed to init scheduler: %w", err) } @@ -165,13 +173,18 @@ func (s *Service) initMetricsServer(cfg *opmetrics.CLIConfig) error { return nil } -func (s *Service) initGameLoader(cfg *config.Config) error { +func (s *Service) initFactoryContract(cfg *config.Config) error { factoryContract, err := contracts.NewDisputeGameFactoryContract(cfg.GameFactoryAddress, batching.NewMultiCaller(s.l1Client.Client(), batching.DefaultBatchSize)) if err != nil { return fmt.Errorf("failed to bind the fault dispute game factory contract: %w", err) } - s.loader = loader.NewGameLoader(factoryContract) + s.factoryContract = factoryContract + return nil +} + +func (s *Service) initGameLoader() error { + s.loader = loader.NewGameLoader(s.factoryContract) return nil } @@ -187,17 +200,21 @@ func (s *Service) initRollupClient(ctx context.Context, cfg *config.Config) erro return nil } -func (s *Service) initScheduler(ctx context.Context, cfg *config.Config) error { +func (s *Service) registerGameTypes(ctx context.Context, cfg *config.Config) error { gameTypeRegistry := registry.NewGameTypeRegistry() caller := batching.NewMultiCaller(s.l1Client.Client(), batching.DefaultBatchSize) - closer, err := fault.RegisterGameTypes(gameTypeRegistry, ctx, s.logger, s.metrics, cfg, s.rollupClient, s.txMgr, caller) + closer, err := fault.RegisterGameTypes(gameTypeRegistry, ctx, s.logger, s.metrics, cfg, s.rollupClient, s.txMgr, s.factoryContract, caller) if err != nil { return err } s.faultGamesCloser = closer + s.registry = gameTypeRegistry + return nil +} +func (s *Service) initScheduler(cfg *config.Config) error { disk := newDiskManager(cfg.Datadir) - s.sched = scheduler.NewScheduler(s.logger, s.metrics, disk, cfg.MaxConcurrency, gameTypeRegistry.CreatePlayer) + s.sched = scheduler.NewScheduler(s.logger, s.metrics, disk, cfg.MaxConcurrency, s.registry.CreatePlayer) return nil } diff --git a/op-challenger/game/types/types.go b/op-challenger/game/types/types.go index d69c757b27b3..6b743ba392f8 100644 --- a/op-challenger/game/types/types.go +++ b/op-challenger/game/types/types.go @@ -41,3 +41,7 @@ type GameMetadata struct { Timestamp uint64 Proxy common.Address } + +type LargePreimageOracle interface { + Addr() common.Address +}