Commit 49c3a85d authored by Adrian Sutton's avatar Adrian Sutton Committed by GitHub

op-challenger: Move loading games after timestamp into contract binding (#9379)

* op-challenger: Add method in game factory to retrieve games after a timestamp

* op-challenger: Remove game loader
parent e82da406
......@@ -68,6 +68,49 @@ func (f *DisputeGameFactoryContract) GetGameImpl(ctx context.Context, gameType u
return result.GetAddress(0), nil
}
func (f *DisputeGameFactoryContract) GetGamesAtOrAfter(ctx context.Context, blockHash common.Hash, earliestTimestamp uint64) ([]types.GameMetadata, error) {
count, err := f.GetGameCount(ctx, blockHash)
if err != nil {
return nil, err
}
batchSize := uint64(f.multiCaller.BatchSize())
rangeEnd := count
var games []types.GameMetadata
for {
if rangeEnd == uint64(0) {
// rangeEnd is exclusive so if its 0 we've reached the end.
return games, nil
}
rangeStart := uint64(0)
if rangeEnd > batchSize {
rangeStart = rangeEnd - batchSize
}
calls := make([]*batching.ContractCall, 0, rangeEnd-rangeStart)
for i := rangeEnd - 1; ; i-- {
calls = append(calls, f.contract.Call(methodGameAtIndex, new(big.Int).SetUint64(i)))
// Break once we've added the last call to avoid underflow when rangeStart == 0
if i == rangeStart {
break
}
}
results, err := f.multiCaller.Call(ctx, batching.BlockByHash(blockHash), calls...)
if err != nil {
return nil, fmt.Errorf("failed to fetch games: %w", err)
}
for _, result := range results {
game := f.decodeGame(result)
if game.Timestamp < earliestTimestamp {
return games, nil
}
games = append(games, game)
}
rangeEnd = rangeStart
}
}
func (f *DisputeGameFactoryContract) GetAllGames(ctx context.Context, blockHash common.Hash) ([]types.GameMetadata, error) {
count, err := f.GetGameCount(ctx, blockHash)
if err != nil {
......
......@@ -2,7 +2,9 @@ package contracts
import (
"context"
"fmt"
"math/big"
"slices"
"testing"
"github.com/ethereum-optimism/optimism/op-bindings/bindings"
......@@ -13,7 +15,10 @@ import (
"github.com/stretchr/testify/require"
)
var factoryAddr = common.HexToAddress("0x24112842371dFC380576ebb09Ae16Cb6B6caD7CB")
var (
factoryAddr = common.HexToAddress("0x24112842371dFC380576ebb09Ae16Cb6B6caD7CB")
batchSize = 5
)
func TestDisputeGameFactorySimpleGetters(t *testing.T) {
blockHash := common.Hash{0xbb, 0xcd}
......@@ -105,6 +110,57 @@ func TestGetAllGames(t *testing.T) {
require.Equal(t, expectedGames, actualGames)
}
func TestGetAllGamesAtOrAfter(t *testing.T) {
tests := []struct {
gameCount int
earliestGameIdx int
}{
{gameCount: batchSize * 4, earliestGameIdx: batchSize + 3},
{gameCount: 0, earliestGameIdx: 0},
{gameCount: batchSize * 2, earliestGameIdx: batchSize},
{gameCount: batchSize * 2, earliestGameIdx: batchSize + 1},
{gameCount: batchSize * 2, earliestGameIdx: batchSize - 1},
{gameCount: batchSize * 2, earliestGameIdx: batchSize * 2},
{gameCount: batchSize * 2, earliestGameIdx: batchSize*2 + 1},
{gameCount: batchSize - 2, earliestGameIdx: batchSize - 3},
}
for _, test := range tests {
test := test
t.Run(fmt.Sprintf("Count_%v_Start_%v", test.gameCount, test.earliestGameIdx), func(t *testing.T) {
blockHash := common.Hash{0xbb, 0xce}
stubRpc, factory := setupDisputeGameFactoryTest(t)
var allGames []types.GameMetadata
for i := 0; i < test.gameCount; i++ {
allGames = append(allGames, types.GameMetadata{
GameType: uint32(i),
Timestamp: uint64(i),
Proxy: common.Address{byte(i)},
})
}
stubRpc.SetResponse(factoryAddr, methodGameCount, batching.BlockByHash(blockHash), nil, []interface{}{big.NewInt(int64(len(allGames)))})
for idx, expected := range allGames {
expectGetGame(stubRpc, idx, blockHash, expected)
}
// Set an earliest timestamp that's in the middle of a batch
earliestTimestamp := uint64(test.earliestGameIdx)
actualGames, err := factory.GetGamesAtOrAfter(context.Background(), blockHash, earliestTimestamp)
require.NoError(t, err)
// Games come back in descending timestamp order
var expectedGames []types.GameMetadata
if test.earliestGameIdx < len(allGames) {
expectedGames = slices.Clone(allGames[test.earliestGameIdx:])
}
slices.Reverse(expectedGames)
require.Equal(t, len(expectedGames), len(actualGames))
if len(expectedGames) != 0 {
// Don't assert equal for empty arrays, we accept nil or empty array
require.Equal(t, expectedGames, actualGames)
}
})
}
}
func TestGetGameFromParameters(t *testing.T) {
stubRpc, factory := setupDisputeGameFactoryTest(t)
traceType := uint32(123)
......@@ -166,7 +222,7 @@ func setupDisputeGameFactoryTest(t *testing.T) (*batchingTest.AbiBasedRpc, *Disp
require.NoError(t, err)
stubRpc := batchingTest.NewAbiBasedRpc(t, factoryAddr, fdgAbi)
caller := batching.NewMultiCaller(stubRpc, 100)
caller := batching.NewMultiCaller(stubRpc, batchSize)
factory, err := NewDisputeGameFactoryContract(factoryAddr, caller)
require.NoError(t, err)
return stubRpc, factory
......
package loader
import (
"context"
"fmt"
"github.com/ethereum-optimism/optimism/op-challenger/game/types"
"github.com/ethereum/go-ethereum/common"
)
// MinimalDisputeGameFactoryCaller is a minimal interface around [bindings.DisputeGameFactoryCaller].
// This needs to be updated if the [bindings.DisputeGameFactoryCaller] interface changes.
type MinimalDisputeGameFactoryCaller interface {
GetGameCount(ctx context.Context, blockHash common.Hash) (uint64, error)
GetGame(ctx context.Context, idx uint64, blockHash common.Hash) (types.GameMetadata, error)
}
type GameLoader struct {
caller MinimalDisputeGameFactoryCaller
}
// NewGameLoader creates a new services that can be used to fetch on chain dispute games.
func NewGameLoader(caller MinimalDisputeGameFactoryCaller) *GameLoader {
return &GameLoader{
caller: caller,
}
}
// FetchAllGamesAtBlock fetches all dispute games from the factory at a given block number.
func (l *GameLoader) FetchAllGamesAtBlock(ctx context.Context, earliestTimestamp uint64, blockHash common.Hash) ([]types.GameMetadata, error) {
gameCount, err := l.caller.GetGameCount(ctx, blockHash)
if err != nil {
return nil, fmt.Errorf("failed to fetch game count: %w", err)
}
games := make([]types.GameMetadata, 0, gameCount)
for i := gameCount; i > 0; i-- {
game, err := l.caller.GetGame(ctx, i-1, blockHash)
if err != nil {
return nil, fmt.Errorf("failed to fetch game at index %d: %w", i-1, err)
}
if game.Timestamp < earliestTimestamp {
break
}
games = append(games, game)
}
return games, nil
}
package loader
import (
"context"
"errors"
"math/big"
"testing"
"github.com/ethereum-optimism/optimism/op-challenger/game/types"
"github.com/ethereum/go-ethereum/common"
"github.com/stretchr/testify/require"
)
var (
gameCountErr = errors.New("game count error")
gameIndexErr = errors.New("game index error")
)
// TestGameLoader_FetchAllGames tests that the game loader correctly fetches all games.
func TestGameLoader_FetchAllGames(t *testing.T) {
t.Parallel()
tests := []struct {
name string
caller *mockMinimalDisputeGameFactoryCaller
earliest uint64
blockHash common.Hash
expectedErr error
expectedLen int
}{
{
name: "success",
caller: newMockMinimalDisputeGameFactoryCaller(10, false, false),
blockHash: common.Hash{0x01},
expectedLen: 10,
},
{
name: "expired game ignored",
caller: newMockMinimalDisputeGameFactoryCaller(10, false, false),
earliest: 500,
blockHash: common.Hash{0x01},
expectedLen: 5,
},
{
name: "game count error",
caller: newMockMinimalDisputeGameFactoryCaller(10, true, false),
blockHash: common.Hash{0x01},
expectedErr: gameCountErr,
},
{
name: "game index error",
caller: newMockMinimalDisputeGameFactoryCaller(10, false, true),
blockHash: common.Hash{0x01},
expectedErr: gameIndexErr,
},
{
name: "no games",
caller: newMockMinimalDisputeGameFactoryCaller(0, false, false),
blockHash: common.Hash{0x01},
},
}
for _, test := range tests {
test := test
t.Run(test.name, func(t *testing.T) {
t.Parallel()
loader := NewGameLoader(test.caller)
games, err := loader.FetchAllGamesAtBlock(context.Background(), test.earliest, test.blockHash)
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([]types.GameMetadata, 0)
}
require.ElementsMatch(t, expectedGames, translateGames(games))
})
}
}
func generateMockGames(count uint64) []types.GameMetadata {
games := make([]types.GameMetadata, count)
for i := uint64(0); i < count; i++ {
games[i] = types.GameMetadata{
Proxy: common.BigToAddress(big.NewInt(int64(i))),
Timestamp: i * 100,
}
}
return games
}
func translateGames(games []types.GameMetadata) []types.GameMetadata {
translated := make([]types.GameMetadata, len(games))
for i, game := range games {
translated[i] = translateFaultDisputeGame(game)
}
return translated
}
func translateFaultDisputeGame(game types.GameMetadata) types.GameMetadata {
return types.GameMetadata{
Proxy: game.Proxy,
Timestamp: game.Timestamp,
}
}
func generateMockGameErrors(count uint64, injectErrors bool) []bool {
errors := make([]bool, count)
if injectErrors {
for i := uint64(0); i < count; i++ {
errors[i] = true
}
}
return errors
}
type mockMinimalDisputeGameFactoryCaller struct {
gameCountErr bool
indexErrors []bool
gameCount uint64
games []types.GameMetadata
}
func newMockMinimalDisputeGameFactoryCaller(count uint64, gameCountErr bool, indexErrors bool) *mockMinimalDisputeGameFactoryCaller {
return &mockMinimalDisputeGameFactoryCaller{
indexErrors: generateMockGameErrors(count, indexErrors),
gameCountErr: gameCountErr,
gameCount: count,
games: generateMockGames(count),
}
}
func (m *mockMinimalDisputeGameFactoryCaller) GetGameCount(_ context.Context, _ common.Hash) (uint64, error) {
if m.gameCountErr {
return 0, gameCountErr
}
return m.gameCount, nil
}
func (m *mockMinimalDisputeGameFactoryCaller) GetGame(_ context.Context, index uint64, _ common.Hash) (types.GameMetadata, error) {
if m.indexErrors[index] {
return struct {
GameType uint32
Timestamp uint64
Proxy common.Address
}{}, gameIndexErr
}
return m.games[index], nil
}
......@@ -22,7 +22,7 @@ type blockNumberFetcher func(ctx context.Context) (uint64, error)
// gameSource loads information about the games available to play
type gameSource interface {
FetchAllGamesAtBlock(ctx context.Context, earliest uint64, blockHash common.Hash) ([]types.GameMetadata, error)
GetGamesAtOrAfter(ctx context.Context, blockHash common.Hash, earliestTimestamp uint64) ([]types.GameMetadata, error)
}
type RWClock interface {
......@@ -120,7 +120,7 @@ func (m *gameMonitor) minGameTimestamp() uint64 {
}
func (m *gameMonitor) progressGames(ctx context.Context, blockHash common.Hash, blockNumber uint64) error {
games, err := m.source.FetchAllGamesAtBlock(ctx, m.minGameTimestamp(), blockHash)
games, err := m.source.GetGamesAtOrAfter(ctx, blockHash, m.minGameTimestamp())
if err != nil {
return fmt.Errorf("failed to load games: %w", err)
}
......
......@@ -272,10 +272,10 @@ type stubGameSource struct {
games []types.GameMetadata
}
func (s *stubGameSource) FetchAllGamesAtBlock(
func (s *stubGameSource) GetGamesAtOrAfter(
_ context.Context,
_ uint64,
_ common.Hash,
_ uint64,
) ([]types.GameMetadata, error) {
if s.fetchErr != nil {
return nil, s.fetchErr
......
......@@ -18,7 +18,6 @@ import (
"github.com/ethereum-optimism/optimism/op-challenger/game/fault"
"github.com/ethereum-optimism/optimism/op-challenger/game/fault/claims"
"github.com/ethereum-optimism/optimism/op-challenger/game/fault/contracts"
"github.com/ethereum-optimism/optimism/op-challenger/game/loader"
"github.com/ethereum-optimism/optimism/op-challenger/game/registry"
"github.com/ethereum-optimism/optimism/op-challenger/game/scheduler"
"github.com/ethereum-optimism/optimism/op-challenger/metrics"
......@@ -48,8 +47,6 @@ type Service struct {
cl *clock.SimpleClock
loader *loader.GameLoader
claimer *claims.BondClaimScheduler
factoryContract *contracts.DisputeGameFactoryContract
......@@ -105,9 +102,6 @@ func (s *Service) initFromConfig(ctx context.Context, cfg *config.Config) error
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.registerGameTypes(ctx, cfg); err != nil {
return fmt.Errorf("failed to register game types: %w", err)
}
......@@ -202,11 +196,6 @@ func (s *Service) initFactoryContract(cfg *config.Config) error {
return nil
}
func (s *Service) initGameLoader() error {
s.loader = loader.NewGameLoader(s.factoryContract)
return nil
}
func (s *Service) initBondClaims() error {
claimer := claims.NewBondClaimer(s.logger, s.metrics, s.registry.CreateBondContract, s.txSender)
s.claimer = claims.NewBondClaimScheduler(s.logger, s.metrics, claimer)
......@@ -252,7 +241,7 @@ func (s *Service) initLargePreimages() error {
}
func (s *Service) initMonitor(cfg *config.Config) {
s.monitor = newGameMonitor(s.logger, s.cl, s.loader, s.sched, s.preimages, cfg.GameWindow, s.claimer, s.l1Client.BlockNumber, cfg.GameAllowlist, s.pollClient)
s.monitor = newGameMonitor(s.logger, s.cl, s.factoryContract, s.sched, s.preimages, cfg.GameWindow, s.claimer, s.l1Client.BlockNumber, cfg.GameAllowlist, s.pollClient)
}
func (s *Service) Start(ctx context.Context) error {
......
......@@ -29,6 +29,10 @@ func NewMultiCaller(rpc EthRpc, batchSize int) *MultiCaller {
}
}
func (m *MultiCaller) BatchSize() int {
return m.batchSize
}
func (m *MultiCaller) SingleCall(ctx context.Context, block Block, call *ContractCall) (*CallResult, error) {
results, err := m.Call(ctx, block, call)
if err != nil {
......
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