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 (
"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-challenger/metrics"
"github.com/ethereum-optimism/optimism/op-service/eth"
"github.com/ethereum/go-ethereum/common"
gethTypes "github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/log"
)
......@@ -21,12 +23,22 @@ type GameInfo interface {
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 {
act actor
loader GameInfo
logger log.Logger
syncValidator SyncValidator
prestateValidators []Validator
status gameTypes.GameStatus
gameL1Head eth.BlockID
}
type GameContract interface {
......@@ -37,6 +49,7 @@ type GameContract interface {
GetStatus(ctx context.Context) (gameTypes.GameStatus, error)
GetMaxGameDepth(ctx context.Context) (types.Depth, 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)
......@@ -50,8 +63,10 @@ func NewGamePlayer(
addr common.Address,
txSender gameTypes.TxSender,
loader GameContract,
syncValidator SyncValidator,
validators []Validator,
creator resourceCreator,
l1HeaderSource L1HeaderSource,
) (*GamePlayer, error) {
logger = logger.New("game", addr)
......@@ -89,6 +104,16 @@ func NewGamePlayer(
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)
if err != nil {
return nil, fmt.Errorf("failed to load min large preimage size: %w", err)
......@@ -107,6 +132,8 @@ func NewGamePlayer(
loader: loader,
logger: logger,
status: status,
gameL1Head: l1Head,
syncValidator: syncValidator,
prestateValidators: validators,
}, nil
}
......@@ -130,13 +157,17 @@ func (g *GamePlayer) ProgressGame(ctx context.Context) gameTypes.GameStatus {
g.logger.Trace("Skipping completed game")
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")
if err := g.act(ctx); err != nil {
g.logger.Error("Error when acting on game", "err", err)
}
status, err := g.loader.GetStatus(ctx)
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
}
g.logGameStatus(ctx, status)
......
......@@ -7,6 +7,7 @@ import (
"testing"
"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/go-ethereum/common"
"github.com/ethereum/go-ethereum/log"
......@@ -16,7 +17,7 @@ import (
var mockValidatorError = fmt.Errorf("mock validator error")
func TestProgressGame_LogErrorFromAct(t *testing.T) {
handler, game, actor := setupProgressGameTest(t)
handler, game, actor, _ := setupProgressGameTest(t)
actor.actErr = errors.New("boom")
status := game.ProgressGame(context.Background())
require.Equal(t, types.GameStatusInProgress, status)
......@@ -60,7 +61,7 @@ func TestProgressGame_LogGameStatus(t *testing.T) {
for _, test := range tests {
test := test
t.Run(test.name, func(t *testing.T) {
handler, game, gameState := setupProgressGameTest(t)
handler, game, gameState, _ := setupProgressGameTest(t)
gameState.status = test.status
status := game.ProgressGame(context.Background())
......@@ -78,7 +79,7 @@ func TestProgressGame_LogGameStatus(t *testing.T) {
func TestDoNotActOnCompleteGame(t *testing.T) {
for _, status := range []types.GameStatus{types.GameStatusChallengerWon, types.GameStatusDefenderWon} {
t.Run(status.String(), func(t *testing.T) {
_, game, gameState := setupProgressGameTest(t)
_, game, gameState, _ := setupProgressGameTest(t)
gameState.status = status
fetched := game.ProgressGame(context.Background())
......@@ -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) {
tests := []struct {
name string
......@@ -142,22 +154,36 @@ type mockValidator struct {
err bool
}
func (m *mockValidator) Validate(ctx context.Context) error {
func (m *mockValidator) Validate(_ context.Context) error {
if m.err {
return mockValidatorError
}
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)
gameState := &stubGameState{claimCount: 1}
syncValidator := &stubSyncValidator{}
game := &GamePlayer{
act: gameState.Act,
loader: gameState,
logger: logger,
act: gameState.Act,
loader: gameState,
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 {
......
......@@ -28,6 +28,11 @@ type Registry interface {
RegisterBondContract(gameType uint32, creator claims.BondContractCreator)
}
type RollupClient interface {
source.OutputRollupClient
SyncStatusProvider
}
func RegisterGameTypes(
registry Registry,
ctx context.Context,
......@@ -35,10 +40,11 @@ func RegisterGameTypes(
logger log.Logger,
m metrics.Metricer,
cfg *config.Config,
rollupClient source.OutputRollupClient,
rollupClient RollupClient,
txSender types.TxSender,
gameFactory *contracts.DisputeGameFactoryContract,
caller *batching.MultiCaller,
l1HeaderSource L1HeaderSource,
) (CloseFunc, error) {
var closer CloseFunc
var l2Client *ethclient.Client
......@@ -51,19 +57,20 @@ func RegisterGameTypes(
closer = l2Client.Close
}
outputSourceCreator := source.NewOutputSourceCreator(logger, rollupClient)
syncValidator := newSyncStatusValidator(rollupClient)
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)
}
}
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)
}
}
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)
}
}
......@@ -76,10 +83,12 @@ func registerAlphabet(
cl faultTypes.ClockReader,
logger log.Logger,
m metrics.Metricer,
syncValidator SyncValidator,
rollupClient source.OutputRollupClient,
txSender types.TxSender,
gameFactory *contracts.DisputeGameFactoryContract,
caller *batching.MultiCaller,
l1HeaderSource L1HeaderSource,
) error {
playerCreator := func(game types.GameMetadata, dir string) (scheduler.GamePlayer, error) {
contract, err := contracts.NewFaultDisputeGameContract(game.Proxy, caller)
......@@ -105,7 +114,7 @@ func registerAlphabet(
}
prestateValidator := NewPrestateValidator("alphabet", contract.GetAbsolutePrestateHash, alphabet.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)
if err != nil {
......@@ -144,11 +153,13 @@ func registerCannon(
logger log.Logger,
m metrics.Metricer,
cfg *config.Config,
syncValidator SyncValidator,
outputSourceCreator *source.OutputSourceCreator,
txSender types.TxSender,
gameFactory *contracts.DisputeGameFactoryContract,
caller *batching.MultiCaller,
l2Client cannon.L2HeaderSource,
l1HeaderSource L1HeaderSource,
) error {
playerCreator := func(game types.GameMetadata, dir string) (scheduler.GamePlayer, error) {
contract, err := contracts.NewFaultDisputeGameContract(game.Proxy, caller)
......@@ -181,7 +192,7 @@ func registerCannon(
}
prestateValidator := NewPrestateValidator("cannon", contract.GetAbsolutePrestateHash, cannon.NewPrestateProvider(cfg.CannonAbsolutePreState))
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)
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
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.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 {
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