Commit e8af7c66 authored by OptimismBot's avatar OptimismBot Committed by GitHub

Merge pull request #6927 from ethereum-optimism/refcell/recent-game-loading

feat(op-challenger): Only Load Recent Games
parents d8e589fc 5755e6b2
......@@ -4,6 +4,7 @@ import (
"context"
"fmt"
"testing"
"time"
"github.com/ethereum-optimism/optimism/op-challenger/config"
"github.com/ethereum-optimism/optimism/op-node/chaincfg"
......@@ -233,6 +234,23 @@ func TestCannonSnapshotFreq(t *testing.T) {
})
}
func TestGameWindow(t *testing.T) {
t.Run("UsesDefault", func(t *testing.T) {
cfg := configForArgs(t, addRequiredArgs(config.TraceTypeAlphabet))
require.Equal(t, config.DefaultGameWindow, cfg.GameWindow)
})
t.Run("Valid", func(t *testing.T) {
cfg := configForArgs(t, addRequiredArgs(config.TraceTypeAlphabet, "--game-window=1m"))
require.Equal(t, time.Duration(time.Minute), cfg.GameWindow)
})
t.Run("ParsesDefault", func(t *testing.T) {
cfg := configForArgs(t, addRequiredArgs(config.TraceTypeAlphabet, "--game-window=264h"))
require.Equal(t, config.DefaultGameWindow, cfg.GameWindow)
})
}
func TestRequireEitherCannonNetworkOrRollupAndGenesis(t *testing.T) {
verifyArgsInvalid(
t,
......
......@@ -3,6 +3,7 @@ package config
import (
"errors"
"fmt"
"time"
"github.com/ethereum/go-ethereum/common"
......@@ -73,7 +74,14 @@ func ValidTraceType(value TraceType) bool {
return false
}
const DefaultCannonSnapshotFreq = uint(1_000_000_000)
const (
DefaultCannonSnapshotFreq = uint(1_000_000_000)
// DefaultGameWindow is the default maximum time duration in the past
// that the challenger will look for games to progress.
// The default value is 11 days, which is a 4 day resolution buffer
// plus the 7 day game finalization window.
DefaultGameWindow = time.Duration(11 * 24 * time.Hour)
)
// Config is a well typed config that is parsed from the CLI params.
// This also contains config options for auxiliary services.
......@@ -82,8 +90,8 @@ type Config struct {
L1EthRpc string // L1 RPC Url
GameFactoryAddress common.Address // Address of the dispute game factory
GameAllowlist []common.Address // Allowlist of fault game addresses
GameWindow time.Duration // Maximum time duration to look for games to progress
AgreeWithProposedOutput bool // Temporary config if we agree or disagree with the posted output
TraceType TraceType // Type of trace
// Specific to the alphabet trace provider
......@@ -124,6 +132,7 @@ func NewConfig(
PprofConfig: oppprof.DefaultCLIConfig(),
CannonSnapshotFreq: DefaultCannonSnapshotFreq,
GameWindow: DefaultGameWindow,
}
}
......
......@@ -43,7 +43,7 @@ func NewGameLoader(caller MinimalDisputeGameFactoryCaller) *gameLoader {
}
// FetchAllGamesAtBlock fetches all dispute games from the factory at a given block number.
func (l *gameLoader) FetchAllGamesAtBlock(ctx context.Context, blockNumber *big.Int) ([]FaultDisputeGame, error) {
func (l *gameLoader) FetchAllGamesAtBlock(ctx context.Context, earliestTimestamp uint64, blockNumber *big.Int) ([]FaultDisputeGame, error) {
if blockNumber == nil {
return nil, ErrMissingBlockNumber
}
......@@ -56,14 +56,19 @@ func (l *gameLoader) FetchAllGamesAtBlock(ctx context.Context, blockNumber *big.
return nil, fmt.Errorf("failed to fetch game count: %w", err)
}
games := make([]FaultDisputeGame, gameCount.Uint64())
for i := uint64(0); i < gameCount.Uint64(); i++ {
game, err := l.caller.GameAtIndex(callOpts, big.NewInt(int64(i)))
games := make([]FaultDisputeGame, 0)
if gameCount.Uint64() == 0 {
return games, nil
}
for i := gameCount.Uint64(); i > 0; i-- {
game, err := l.caller.GameAtIndex(callOpts, big.NewInt(int64(i-1)))
if err != nil {
return nil, fmt.Errorf("failed to fetch game at index %d: %w", i, err)
}
games[i] = game
if game.Timestamp < earliestTimestamp {
break
}
games = append(games, game)
}
return games, nil
......
......@@ -25,6 +25,7 @@ func TestGameLoader_FetchAllGames(t *testing.T) {
tests := []struct {
name string
caller *mockMinimalDisputeGameFactoryCaller
earliest uint64
blockNumber *big.Int
expectedErr error
expectedLen int
......@@ -33,35 +34,36 @@ func TestGameLoader_FetchAllGames(t *testing.T) {
name: "success",
caller: newMockMinimalDisputeGameFactoryCaller(10, false, false),
blockNumber: big.NewInt(1),
expectedErr: nil,
expectedLen: 10,
},
{
name: "expired game ignored",
caller: newMockMinimalDisputeGameFactoryCaller(10, false, false),
earliest: 500,
blockNumber: big.NewInt(1),
expectedLen: 5,
},
{
name: "game count error",
caller: newMockMinimalDisputeGameFactoryCaller(10, true, false),
blockNumber: big.NewInt(1),
expectedErr: gameCountErr,
expectedLen: 0,
},
{
name: "game index error",
caller: newMockMinimalDisputeGameFactoryCaller(10, false, true),
blockNumber: big.NewInt(1),
expectedErr: gameIndexErr,
expectedLen: 0,
},
{
name: "no games",
caller: newMockMinimalDisputeGameFactoryCaller(0, false, false),
blockNumber: big.NewInt(1),
expectedErr: nil,
expectedLen: 0,
},
{
name: "missing block number",
caller: newMockMinimalDisputeGameFactoryCaller(0, false, false),
expectedErr: ErrMissingBlockNumber,
expectedLen: 0,
},
}
......@@ -72,10 +74,11 @@ func TestGameLoader_FetchAllGames(t *testing.T) {
t.Parallel()
loader := NewGameLoader(test.caller)
games, err := loader.FetchAllGamesAtBlock(context.Background(), test.blockNumber)
games, err := loader.FetchAllGamesAtBlock(context.Background(), test.earliest, test.blockNumber)
require.ErrorIs(t, err, test.expectedErr)
require.Len(t, games, test.expectedLen)
expectedGames := test.caller.games
expectedGames = expectedGames[len(expectedGames)-test.expectedLen:]
if test.expectedErr != nil {
expectedGames = make([]FaultDisputeGame, 0)
}
......@@ -90,7 +93,7 @@ func generateMockGames(count uint64) []FaultDisputeGame {
for i := uint64(0); i < count; i++ {
games[i] = FaultDisputeGame{
Proxy: common.BigToAddress(big.NewInt(int64(i))),
Timestamp: i,
Timestamp: i * 100,
}
}
......
......@@ -20,24 +20,26 @@ type blockNumberFetcher func(ctx context.Context) (uint64, error)
// gameSource loads information about the games available to play
type gameSource interface {
FetchAllGamesAtBlock(ctx context.Context, blockNumber *big.Int) ([]FaultDisputeGame, error)
FetchAllGamesAtBlock(ctx context.Context, earliest uint64, blockNumber *big.Int) ([]FaultDisputeGame, error)
}
type gameMonitor struct {
logger log.Logger
clock clock.Clock
source gameSource
gameWindow time.Duration
createPlayer playerCreator
fetchBlockNumber blockNumberFetcher
allowedGames []common.Address
players map[common.Address]gamePlayer
}
func newGameMonitor(logger log.Logger, cl clock.Clock, fetchBlockNumber blockNumberFetcher, allowedGames []common.Address, source gameSource, createGame playerCreator) *gameMonitor {
func newGameMonitor(logger log.Logger, gameWindow time.Duration, cl clock.Clock, fetchBlockNumber blockNumberFetcher, allowedGames []common.Address, source gameSource, createGame playerCreator) *gameMonitor {
return &gameMonitor{
logger: logger,
clock: cl,
source: source,
gameWindow: gameWindow,
createPlayer: createGame,
fetchBlockNumber: fetchBlockNumber,
allowedGames: allowedGames,
......@@ -57,12 +59,24 @@ func (m *gameMonitor) allowedGame(game common.Address) bool {
return false
}
func (m *gameMonitor) minGameTimestamp() uint64 {
if m.gameWindow.Seconds() == 0 {
return 0
}
// time: "To compute t-d for a duration d, use t.Add(-d)."
// https://pkg.go.dev/time#Time.Sub
if m.clock.Now().Unix() > int64(m.gameWindow.Seconds()) {
return uint64(m.clock.Now().Add(-m.gameWindow).Unix())
}
return 0
}
func (m *gameMonitor) progressGames(ctx context.Context) error {
blockNum, err := m.fetchBlockNumber(ctx)
if err != nil {
return fmt.Errorf("failed to load current block number: %w", err)
}
games, err := m.source.FetchAllGamesAtBlock(ctx, new(big.Int).SetUint64(blockNum))
games, err := m.source.FetchAllGamesAtBlock(ctx, m.minGameTimestamp(), new(big.Int).SetUint64(blockNum))
if err != nil {
return fmt.Errorf("failed to load games: %w", err)
}
......
......@@ -4,6 +4,7 @@ import (
"context"
"math/big"
"testing"
"time"
"github.com/ethereum-optimism/optimism/op-service/clock"
"github.com/ethereum/go-ethereum/common"
......@@ -13,6 +14,32 @@ import (
"github.com/ethereum-optimism/optimism/op-node/testlog"
)
func TestMonitorMinGameTimestamp(t *testing.T) {
t.Parallel()
t.Run("zero game window returns zero", func(t *testing.T) {
monitor, _, _ := setupMonitorTest(t, []common.Address{})
monitor.gameWindow = time.Duration(0)
require.Equal(t, monitor.minGameTimestamp(), uint64(0))
})
t.Run("non-zero game window with zero clock", func(t *testing.T) {
monitor, _, _ := setupMonitorTest(t, []common.Address{})
monitor.gameWindow = time.Minute
monitor.clock = clock.NewDeterministicClock(time.Unix(0, 0))
require.Equal(t, monitor.minGameTimestamp(), uint64(0))
})
t.Run("minimum computed correctly", func(t *testing.T) {
monitor, _, _ := setupMonitorTest(t, []common.Address{})
monitor.gameWindow = time.Minute
frozen := time.Unix(int64(time.Hour.Seconds()), 0)
monitor.clock = clock.NewDeterministicClock(frozen)
expected := uint64(frozen.Add(-time.Minute).Unix())
require.Equal(t, monitor.minGameTimestamp(), expected)
})
}
func TestMonitorExitsWhenContextDone(t *testing.T) {
monitor, _, _ := setupMonitorTest(t, []common.Address{common.Address{}})
ctx, cancel := context.WithCancel(context.Background())
......@@ -87,7 +114,7 @@ func setupMonitorTest(t *testing.T, allowedGames []common.Address) (*gameMonitor
fetchBlockNum := func(ctx context.Context) (uint64, error) {
return 1234, nil
}
monitor := newGameMonitor(logger, clock.SystemClock, fetchBlockNum, allowedGames, source, games.CreateGame)
monitor := newGameMonitor(logger, time.Duration(0), clock.SystemClock, fetchBlockNum, allowedGames, source, games.CreateGame)
return monitor, source, games
}
......@@ -95,7 +122,7 @@ type stubGameSource struct {
games []FaultDisputeGame
}
func (s *stubGameSource) FetchAllGamesAtBlock(ctx context.Context, blockNumber *big.Int) ([]FaultDisputeGame, error) {
func (s *stubGameSource) FetchAllGamesAtBlock(ctx context.Context, earliest uint64, blockNumber *big.Int) ([]FaultDisputeGame, error) {
return s.games, nil
}
......
......@@ -73,7 +73,7 @@ func NewService(ctx context.Context, logger log.Logger, cfg *config.Config) (*se
}
loader := NewGameLoader(factory)
monitor := newGameMonitor(logger, cl, client.BlockNumber, cfg.GameAllowlist, loader, func(addr common.Address) (gamePlayer, error) {
monitor := newGameMonitor(logger, cfg.GameWindow, cl, client.BlockNumber, cfg.GameAllowlist, loader, func(addr common.Address) (gamePlayer, error) {
return NewGamePlayer(ctx, logger, cfg, addr, txMgr, client)
})
......
......@@ -109,6 +109,12 @@ var (
EnvVars: prefixEnvVars("CANNON_SNAPSHOT_FREQ"),
Value: config.DefaultCannonSnapshotFreq,
}
GameWindowFlag = &cli.DurationFlag{
Name: "game-window",
Usage: "The time window which the challenger will look for games to progress.",
EnvVars: prefixEnvVars("GAME_WINDOW"),
Value: config.DefaultGameWindow,
}
)
// requiredFlags are checked by [CheckRequired]
......@@ -132,6 +138,7 @@ var optionalFlags = []cli.Flag{
CannonDatadirFlag,
CannonL2Flag,
CannonSnapshotFreqFlag,
GameWindowFlag,
}
func init() {
......@@ -222,6 +229,7 @@ func NewConfigFromCLI(ctx *cli.Context) (*config.Config, error) {
TraceType: traceTypeFlag,
GameFactoryAddress: gameFactoryAddress,
GameAllowlist: allowedGames,
GameWindow: ctx.Duration(GameWindowFlag.Name),
AlphabetTrace: ctx.String(AlphabetFlag.Name),
CannonNetwork: ctx.String(CannonNetworkFlag.Name),
CannonRollupConfigPath: ctx.String(CannonRollupConfigFlag.Name),
......
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