Commit abb34d44 authored by Adrian Sutton's avatar Adrian Sutton Committed by GitHub

op-challenger: Require local node to be sufficiently up to date before playing games (#9614)

parent 815684c0
...@@ -10,7 +10,9 @@ import ( ...@@ -10,7 +10,9 @@ import (
"github.com/ethereum-optimism/optimism/op-challenger/game/fault/types" "github.com/ethereum-optimism/optimism/op-challenger/game/fault/types"
gameTypes "github.com/ethereum-optimism/optimism/op-challenger/game/types" gameTypes "github.com/ethereum-optimism/optimism/op-challenger/game/types"
"github.com/ethereum-optimism/optimism/op-challenger/metrics" "github.com/ethereum-optimism/optimism/op-challenger/metrics"
"github.com/ethereum-optimism/optimism/op-service/eth"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
gethTypes "github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/log"
) )
...@@ -21,12 +23,22 @@ type GameInfo interface { ...@@ -21,12 +23,22 @@ type GameInfo interface {
GetClaimCount(context.Context) (uint64, error) GetClaimCount(context.Context) (uint64, error)
} }
type SyncValidator interface {
ValidateNodeSynced(ctx context.Context, gameL1Head eth.BlockID) error
}
type L1HeaderSource interface {
HeaderByHash(context.Context, common.Hash) (*gethTypes.Header, error)
}
type GamePlayer struct { type GamePlayer struct {
act actor act actor
loader GameInfo loader GameInfo
logger log.Logger logger log.Logger
syncValidator SyncValidator
prestateValidators []Validator prestateValidators []Validator
status gameTypes.GameStatus status gameTypes.GameStatus
gameL1Head eth.BlockID
} }
type GameContract interface { type GameContract interface {
...@@ -37,6 +49,7 @@ type GameContract interface { ...@@ -37,6 +49,7 @@ type GameContract interface {
GetStatus(ctx context.Context) (gameTypes.GameStatus, error) GetStatus(ctx context.Context) (gameTypes.GameStatus, error)
GetMaxGameDepth(ctx context.Context) (types.Depth, error) GetMaxGameDepth(ctx context.Context) (types.Depth, error)
GetOracle(ctx context.Context) (*contracts.PreimageOracleContract, error) GetOracle(ctx context.Context) (*contracts.PreimageOracleContract, error)
GetL1Head(ctx context.Context) (common.Hash, error)
} }
type resourceCreator func(ctx context.Context, logger log.Logger, gameDepth types.Depth, dir string) (types.TraceAccessor, error) type resourceCreator func(ctx context.Context, logger log.Logger, gameDepth types.Depth, dir string) (types.TraceAccessor, error)
...@@ -50,8 +63,10 @@ func NewGamePlayer( ...@@ -50,8 +63,10 @@ func NewGamePlayer(
addr common.Address, addr common.Address,
txSender gameTypes.TxSender, txSender gameTypes.TxSender,
loader GameContract, loader GameContract,
syncValidator SyncValidator,
validators []Validator, validators []Validator,
creator resourceCreator, creator resourceCreator,
l1HeaderSource L1HeaderSource,
) (*GamePlayer, error) { ) (*GamePlayer, error) {
logger = logger.New("game", addr) logger = logger.New("game", addr)
...@@ -89,6 +104,16 @@ func NewGamePlayer( ...@@ -89,6 +104,16 @@ func NewGamePlayer(
return nil, fmt.Errorf("failed to load oracle: %w", err) return nil, fmt.Errorf("failed to load oracle: %w", err)
} }
l1HeadHash, err := loader.GetL1Head(ctx)
if err != nil {
return nil, fmt.Errorf("failed to load game L1 head: %w", err)
}
l1Header, err := l1HeaderSource.HeaderByHash(ctx, l1HeadHash)
if err != nil {
return nil, fmt.Errorf("failed to load L1 header %v: %w", l1HeadHash, err)
}
l1Head := eth.HeaderBlockID(l1Header)
minLargePreimageSize, err := oracle.MinLargePreimageSize(ctx) minLargePreimageSize, err := oracle.MinLargePreimageSize(ctx)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to load min large preimage size: %w", err) return nil, fmt.Errorf("failed to load min large preimage size: %w", err)
...@@ -107,6 +132,8 @@ func NewGamePlayer( ...@@ -107,6 +132,8 @@ func NewGamePlayer(
loader: loader, loader: loader,
logger: logger, logger: logger,
status: status, status: status,
gameL1Head: l1Head,
syncValidator: syncValidator,
prestateValidators: validators, prestateValidators: validators,
}, nil }, nil
} }
...@@ -130,13 +157,17 @@ func (g *GamePlayer) ProgressGame(ctx context.Context) gameTypes.GameStatus { ...@@ -130,13 +157,17 @@ func (g *GamePlayer) ProgressGame(ctx context.Context) gameTypes.GameStatus {
g.logger.Trace("Skipping completed game") g.logger.Trace("Skipping completed game")
return g.status return g.status
} }
if err := g.syncValidator.ValidateNodeSynced(ctx, g.gameL1Head); err != nil {
g.logger.Error("Local node not sufficiently up to date", "err", err)
return g.status
}
g.logger.Trace("Checking if actions are required") g.logger.Trace("Checking if actions are required")
if err := g.act(ctx); err != nil { if err := g.act(ctx); err != nil {
g.logger.Error("Error when acting on game", "err", err) g.logger.Error("Error when acting on game", "err", err)
} }
status, err := g.loader.GetStatus(ctx) status, err := g.loader.GetStatus(ctx)
if err != nil { if err != nil {
g.logger.Warn("Unable to retrieve game status", "err", err) g.logger.Error("Unable to retrieve game status", "err", err)
return gameTypes.GameStatusInProgress return gameTypes.GameStatusInProgress
} }
g.logGameStatus(ctx, status) g.logGameStatus(ctx, status)
......
...@@ -7,6 +7,7 @@ import ( ...@@ -7,6 +7,7 @@ import (
"testing" "testing"
"github.com/ethereum-optimism/optimism/op-challenger/game/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/testlog" "github.com/ethereum-optimism/optimism/op-service/testlog"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/log"
...@@ -16,7 +17,7 @@ import ( ...@@ -16,7 +17,7 @@ import (
var mockValidatorError = fmt.Errorf("mock validator error") var mockValidatorError = fmt.Errorf("mock validator error")
func TestProgressGame_LogErrorFromAct(t *testing.T) { func TestProgressGame_LogErrorFromAct(t *testing.T) {
handler, game, actor := setupProgressGameTest(t) handler, game, actor, _ := setupProgressGameTest(t)
actor.actErr = errors.New("boom") actor.actErr = errors.New("boom")
status := game.ProgressGame(context.Background()) status := game.ProgressGame(context.Background())
require.Equal(t, types.GameStatusInProgress, status) require.Equal(t, types.GameStatusInProgress, status)
...@@ -60,7 +61,7 @@ func TestProgressGame_LogGameStatus(t *testing.T) { ...@@ -60,7 +61,7 @@ func TestProgressGame_LogGameStatus(t *testing.T) {
for _, test := range tests { for _, test := range tests {
test := test test := test
t.Run(test.name, func(t *testing.T) { t.Run(test.name, func(t *testing.T) {
handler, game, gameState := setupProgressGameTest(t) handler, game, gameState, _ := setupProgressGameTest(t)
gameState.status = test.status gameState.status = test.status
status := game.ProgressGame(context.Background()) status := game.ProgressGame(context.Background())
...@@ -78,7 +79,7 @@ func TestProgressGame_LogGameStatus(t *testing.T) { ...@@ -78,7 +79,7 @@ func TestProgressGame_LogGameStatus(t *testing.T) {
func TestDoNotActOnCompleteGame(t *testing.T) { func TestDoNotActOnCompleteGame(t *testing.T) {
for _, status := range []types.GameStatus{types.GameStatusChallengerWon, types.GameStatusDefenderWon} { for _, status := range []types.GameStatus{types.GameStatusChallengerWon, types.GameStatusDefenderWon} {
t.Run(status.String(), func(t *testing.T) { t.Run(status.String(), func(t *testing.T) {
_, game, gameState := setupProgressGameTest(t) _, game, gameState, _ := setupProgressGameTest(t)
gameState.status = status gameState.status = status
fetched := game.ProgressGame(context.Background()) fetched := game.ProgressGame(context.Background())
...@@ -93,6 +94,17 @@ func TestDoNotActOnCompleteGame(t *testing.T) { ...@@ -93,6 +94,17 @@ func TestDoNotActOnCompleteGame(t *testing.T) {
} }
} }
func TestValidateLocalNodeSync(t *testing.T) {
_, game, gameState, syncValidator := setupProgressGameTest(t)
game.ProgressGame(context.Background())
require.Equal(t, 1, gameState.callCount, "acts when in sync")
syncValidator.result = errors.New("boom")
game.ProgressGame(context.Background())
require.Equal(t, 1, gameState.callCount, "does not act when not in sync")
}
func TestValidatePrestate(t *testing.T) { func TestValidatePrestate(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
...@@ -142,22 +154,36 @@ type mockValidator struct { ...@@ -142,22 +154,36 @@ type mockValidator struct {
err bool err bool
} }
func (m *mockValidator) Validate(ctx context.Context) error { func (m *mockValidator) Validate(_ context.Context) error {
if m.err { if m.err {
return mockValidatorError return mockValidatorError
} }
return nil return nil
} }
func setupProgressGameTest(t *testing.T) (*testlog.CapturingHandler, *GamePlayer, *stubGameState) { func setupProgressGameTest(t *testing.T) (*testlog.CapturingHandler, *GamePlayer, *stubGameState, *stubSyncValidator) {
logger, logs := testlog.CaptureLogger(t, log.LevelDebug) logger, logs := testlog.CaptureLogger(t, log.LevelDebug)
gameState := &stubGameState{claimCount: 1} gameState := &stubGameState{claimCount: 1}
syncValidator := &stubSyncValidator{}
game := &GamePlayer{ game := &GamePlayer{
act: gameState.Act, act: gameState.Act,
loader: gameState, loader: gameState,
logger: logger, logger: logger,
syncValidator: syncValidator,
gameL1Head: eth.BlockID{
Hash: common.Hash{0x1a},
Number: 32,
},
} }
return logs, game, gameState return logs, game, gameState, syncValidator
}
type stubSyncValidator struct {
result error
}
func (s *stubSyncValidator) ValidateNodeSynced(_ context.Context, _ eth.BlockID) error {
return s.result
} }
type stubGameState struct { type stubGameState struct {
......
...@@ -28,6 +28,11 @@ type Registry interface { ...@@ -28,6 +28,11 @@ type Registry interface {
RegisterBondContract(gameType uint32, creator claims.BondContractCreator) RegisterBondContract(gameType uint32, creator claims.BondContractCreator)
} }
type RollupClient interface {
source.OutputRollupClient
SyncStatusProvider
}
func RegisterGameTypes( func RegisterGameTypes(
registry Registry, registry Registry,
ctx context.Context, ctx context.Context,
...@@ -35,10 +40,11 @@ func RegisterGameTypes( ...@@ -35,10 +40,11 @@ func RegisterGameTypes(
logger log.Logger, logger log.Logger,
m metrics.Metricer, m metrics.Metricer,
cfg *config.Config, cfg *config.Config,
rollupClient source.OutputRollupClient, rollupClient RollupClient,
txSender types.TxSender, txSender types.TxSender,
gameFactory *contracts.DisputeGameFactoryContract, gameFactory *contracts.DisputeGameFactoryContract,
caller *batching.MultiCaller, caller *batching.MultiCaller,
l1HeaderSource L1HeaderSource,
) (CloseFunc, error) { ) (CloseFunc, error) {
var closer CloseFunc var closer CloseFunc
var l2Client *ethclient.Client var l2Client *ethclient.Client
...@@ -51,19 +57,20 @@ func RegisterGameTypes( ...@@ -51,19 +57,20 @@ func RegisterGameTypes(
closer = l2Client.Close closer = l2Client.Close
} }
outputSourceCreator := source.NewOutputSourceCreator(logger, rollupClient) outputSourceCreator := source.NewOutputSourceCreator(logger, rollupClient)
syncValidator := newSyncStatusValidator(rollupClient)
if cfg.TraceTypeEnabled(config.TraceTypeCannon) { if cfg.TraceTypeEnabled(config.TraceTypeCannon) {
if err := registerCannon(faultTypes.CannonGameType, registry, ctx, cl, logger, m, cfg, outputSourceCreator, txSender, gameFactory, caller, l2Client); err != nil { if err := registerCannon(faultTypes.CannonGameType, registry, ctx, cl, logger, m, cfg, syncValidator, outputSourceCreator, txSender, gameFactory, caller, l2Client, l1HeaderSource); err != nil {
return nil, fmt.Errorf("failed to register cannon game type: %w", err) return nil, fmt.Errorf("failed to register cannon game type: %w", err)
} }
} }
if cfg.TraceTypeEnabled(config.TraceTypePermissioned) { if cfg.TraceTypeEnabled(config.TraceTypePermissioned) {
if err := registerCannon(faultTypes.PermissionedGameType, registry, ctx, cl, logger, m, cfg, outputSourceCreator, txSender, gameFactory, caller, l2Client); err != nil { if err := registerCannon(faultTypes.PermissionedGameType, registry, ctx, cl, logger, m, cfg, syncValidator, outputSourceCreator, txSender, gameFactory, caller, l2Client, l1HeaderSource); err != nil {
return nil, fmt.Errorf("failed to register permissioned cannon game type: %w", err) return nil, fmt.Errorf("failed to register permissioned cannon game type: %w", err)
} }
} }
if cfg.TraceTypeEnabled(config.TraceTypeAlphabet) { if cfg.TraceTypeEnabled(config.TraceTypeAlphabet) {
if err := registerAlphabet(registry, ctx, cl, logger, m, rollupClient, txSender, gameFactory, caller); err != nil { if err := registerAlphabet(registry, ctx, cl, logger, m, syncValidator, rollupClient, txSender, gameFactory, caller, l1HeaderSource); err != nil {
return nil, fmt.Errorf("failed to register alphabet game type: %w", err) return nil, fmt.Errorf("failed to register alphabet game type: %w", err)
} }
} }
...@@ -76,10 +83,12 @@ func registerAlphabet( ...@@ -76,10 +83,12 @@ func registerAlphabet(
cl faultTypes.ClockReader, cl faultTypes.ClockReader,
logger log.Logger, logger log.Logger,
m metrics.Metricer, m metrics.Metricer,
syncValidator SyncValidator,
rollupClient source.OutputRollupClient, rollupClient source.OutputRollupClient,
txSender types.TxSender, txSender types.TxSender,
gameFactory *contracts.DisputeGameFactoryContract, gameFactory *contracts.DisputeGameFactoryContract,
caller *batching.MultiCaller, caller *batching.MultiCaller,
l1HeaderSource L1HeaderSource,
) error { ) error {
playerCreator := func(game types.GameMetadata, dir string) (scheduler.GamePlayer, error) { playerCreator := func(game types.GameMetadata, dir string) (scheduler.GamePlayer, error) {
contract, err := contracts.NewFaultDisputeGameContract(game.Proxy, caller) contract, err := contracts.NewFaultDisputeGameContract(game.Proxy, caller)
...@@ -105,7 +114,7 @@ func registerAlphabet( ...@@ -105,7 +114,7 @@ func registerAlphabet(
} }
prestateValidator := NewPrestateValidator("alphabet", contract.GetAbsolutePrestateHash, alphabet.PrestateProvider) prestateValidator := NewPrestateValidator("alphabet", contract.GetAbsolutePrestateHash, alphabet.PrestateProvider)
genesisValidator := NewPrestateValidator("output root", contract.GetGenesisOutputRoot, prestateProvider) genesisValidator := NewPrestateValidator("output root", contract.GetGenesisOutputRoot, prestateProvider)
return NewGamePlayer(ctx, cl, logger, m, dir, game.Proxy, txSender, contract, []Validator{prestateValidator, genesisValidator}, creator) return NewGamePlayer(ctx, cl, logger, m, dir, game.Proxy, txSender, contract, syncValidator, []Validator{prestateValidator, genesisValidator}, creator, l1HeaderSource)
} }
oracle, err := createOracle(ctx, gameFactory, caller, faultTypes.AlphabetGameType) oracle, err := createOracle(ctx, gameFactory, caller, faultTypes.AlphabetGameType)
if err != nil { if err != nil {
...@@ -144,11 +153,13 @@ func registerCannon( ...@@ -144,11 +153,13 @@ func registerCannon(
logger log.Logger, logger log.Logger,
m metrics.Metricer, m metrics.Metricer,
cfg *config.Config, cfg *config.Config,
syncValidator SyncValidator,
outputSourceCreator *source.OutputSourceCreator, outputSourceCreator *source.OutputSourceCreator,
txSender types.TxSender, txSender types.TxSender,
gameFactory *contracts.DisputeGameFactoryContract, gameFactory *contracts.DisputeGameFactoryContract,
caller *batching.MultiCaller, caller *batching.MultiCaller,
l2Client cannon.L2HeaderSource, l2Client cannon.L2HeaderSource,
l1HeaderSource L1HeaderSource,
) error { ) error {
playerCreator := func(game types.GameMetadata, dir string) (scheduler.GamePlayer, error) { playerCreator := func(game types.GameMetadata, dir string) (scheduler.GamePlayer, error) {
contract, err := contracts.NewFaultDisputeGameContract(game.Proxy, caller) contract, err := contracts.NewFaultDisputeGameContract(game.Proxy, caller)
...@@ -181,7 +192,7 @@ func registerCannon( ...@@ -181,7 +192,7 @@ func registerCannon(
} }
prestateValidator := NewPrestateValidator("cannon", contract.GetAbsolutePrestateHash, cannon.NewPrestateProvider(cfg.CannonAbsolutePreState)) prestateValidator := NewPrestateValidator("cannon", contract.GetAbsolutePrestateHash, cannon.NewPrestateProvider(cfg.CannonAbsolutePreState))
genesisValidator := NewPrestateValidator("output root", contract.GetGenesisOutputRoot, prestateProvider) genesisValidator := NewPrestateValidator("output root", contract.GetGenesisOutputRoot, prestateProvider)
return NewGamePlayer(ctx, cl, logger, m, dir, game.Proxy, txSender, contract, []Validator{prestateValidator, genesisValidator}, creator) return NewGamePlayer(ctx, cl, logger, m, dir, game.Proxy, txSender, contract, syncValidator, []Validator{prestateValidator, genesisValidator}, creator, l1HeaderSource)
} }
oracle, err := createOracle(ctx, gameFactory, caller, gameType) oracle, err := createOracle(ctx, gameFactory, caller, gameType)
if err != nil { if err != nil {
......
package fault
import (
"context"
"errors"
"fmt"
"github.com/ethereum-optimism/optimism/op-service/eth"
)
var ErrNotInSync = errors.New("local node too far behind")
type SyncStatusProvider interface {
SyncStatus(context.Context) (*eth.SyncStatus, error)
}
type syncStatusValidator struct {
statusProvider SyncStatusProvider
}
func newSyncStatusValidator(statusProvider SyncStatusProvider) *syncStatusValidator {
return &syncStatusValidator{
statusProvider: statusProvider,
}
}
func (s *syncStatusValidator) ValidateNodeSynced(ctx context.Context, gameL1Head eth.BlockID) error {
syncStatus, err := s.statusProvider.SyncStatus(ctx)
if err != nil {
return fmt.Errorf("failed to retrieve local node sync status: %w", err)
}
if syncStatus.CurrentL1.Number <= gameL1Head.Number {
return fmt.Errorf("%w require L1 block above %v but at %v", ErrNotInSync, gameL1Head.Number, syncStatus.CurrentL1.Number)
}
return nil
}
package fault
import (
"context"
"errors"
"testing"
"github.com/ethereum-optimism/optimism/op-service/eth"
"github.com/stretchr/testify/require"
)
func TestSyncStatusProvider(t *testing.T) {
requestErr := errors.New("boom")
tests := []struct {
name string
gameL1Head eth.BlockID
syncStatus *eth.SyncStatus
statusReqErr error
expected error
}{
{
name: "ErrorFetchingStatus",
gameL1Head: eth.BlockID{Number: 100},
syncStatus: nil,
statusReqErr: requestErr,
expected: requestErr,
},
{
name: "CurrentL1BelowGameL1Head",
gameL1Head: eth.BlockID{Number: 100},
syncStatus: &eth.SyncStatus{
CurrentL1: eth.L1BlockRef{
Number: 99,
},
},
statusReqErr: nil,
expected: ErrNotInSync,
},
{
name: "CurrentL1EqualToGameL1Head",
gameL1Head: eth.BlockID{Number: 100},
syncStatus: &eth.SyncStatus{
CurrentL1: eth.L1BlockRef{
Number: 100,
},
},
statusReqErr: nil,
expected: ErrNotInSync,
},
{
name: "CurrentL1AboveGameL1Head",
gameL1Head: eth.BlockID{Number: 100},
syncStatus: &eth.SyncStatus{
CurrentL1: eth.L1BlockRef{
Number: 101,
},
},
statusReqErr: nil,
expected: nil,
},
}
for _, test := range tests {
test := test
t.Run(test.name, func(t *testing.T) {
provider := &stubSyncStatusProvider{
status: test.syncStatus,
err: test.statusReqErr,
}
validator := newSyncStatusValidator(provider)
err := validator.ValidateNodeSynced(context.Background(), test.gameL1Head)
require.ErrorIs(t, err, test.expected)
})
}
}
type stubSyncStatusProvider struct {
status *eth.SyncStatus
err error
}
func (s *stubSyncStatusProvider) SyncStatus(_ context.Context) (*eth.SyncStatus, error) {
return s.status, s.err
}
...@@ -217,7 +217,7 @@ func (s *Service) initRollupClient(ctx context.Context, cfg *config.Config) erro ...@@ -217,7 +217,7 @@ func (s *Service) initRollupClient(ctx context.Context, cfg *config.Config) erro
func (s *Service) registerGameTypes(ctx context.Context, cfg *config.Config) error { func (s *Service) registerGameTypes(ctx context.Context, cfg *config.Config) error {
gameTypeRegistry := registry.NewGameTypeRegistry() gameTypeRegistry := registry.NewGameTypeRegistry()
caller := batching.NewMultiCaller(s.l1Client.Client(), batching.DefaultBatchSize) caller := batching.NewMultiCaller(s.l1Client.Client(), batching.DefaultBatchSize)
closer, err := fault.RegisterGameTypes(gameTypeRegistry, ctx, s.cl, s.logger, s.metrics, cfg, s.rollupClient, s.txSender, s.factoryContract, caller) closer, err := fault.RegisterGameTypes(gameTypeRegistry, ctx, s.cl, s.logger, s.metrics, cfg, s.rollupClient, s.txSender, s.factoryContract, caller, s.l1Client)
if err != nil { if err != nil {
return err return err
} }
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment