Commit 9cca805d authored by refcell's avatar refcell Committed by GitHub

feat(op-dispute-mon): Game Resolution Forecasting (#9449)

* fix: typo on cache_test eth_call (#9462)

chore(op-dispute-mon): refactor output validation into a separate component

feat(op-dispute-mon): game forecasting implementation minus testing

fix(op-dispute-mon): bad merge

chore(op-dispute-mon): add more tests

* fix(op-dispute-mon): testing var and use updated log filtering

* fix(op-dispute-mon): functional resolver

* fix(op-dispute-mon): simply bidirectional game tree construction

* fix(op-dispute-mon): remove left bond counter

* fix(op-dispute-mon): construct resolver bidirectional tree

---------
Co-authored-by: default avatarOak <me+git@droak.sh>
parent 21c5fcba
...@@ -13,35 +13,36 @@ import ( ...@@ -13,35 +13,36 @@ import (
"github.com/ethereum-optimism/optimism/op-service/sources/caching" "github.com/ethereum-optimism/optimism/op-service/sources/caching"
) )
const metricsLabel = "binding_creator" const metricsLabel = "game_caller_creator"
type MetadataLoader interface { type GameCaller interface {
GetGameMetadata(context.Context) (uint64, common.Hash, types.GameStatus, error) GetGameMetadata(context.Context) (uint64, common.Hash, types.GameStatus, error)
GetAllClaims(ctx context.Context) ([]faultTypes.Claim, error)
} }
type metadataCreator struct { type gameCallerCreator struct {
cache *caching.LRUCache[common.Address, *contracts.FaultDisputeGameContract] cache *caching.LRUCache[common.Address, *contracts.FaultDisputeGameContract]
caller *batching.MultiCaller caller *batching.MultiCaller
} }
func NewMetadataCreator(m caching.Metrics, caller *batching.MultiCaller) *metadataCreator { func NewGameCallerCreator(m caching.Metrics, caller *batching.MultiCaller) *gameCallerCreator {
return &metadataCreator{ return &gameCallerCreator{
caller: caller, caller: caller,
cache: caching.NewLRUCache[common.Address, *contracts.FaultDisputeGameContract](m, metricsLabel, 100), cache: caching.NewLRUCache[common.Address, *contracts.FaultDisputeGameContract](m, metricsLabel, 100),
} }
} }
func (m *metadataCreator) CreateContract(game types.GameMetadata) (MetadataLoader, error) { func (g *gameCallerCreator) CreateContract(game types.GameMetadata) (GameCaller, error) {
if fdg, ok := m.cache.Get(game.Proxy); ok { if fdg, ok := g.cache.Get(game.Proxy); ok {
return fdg, nil return fdg, nil
} }
switch game.GameType { switch game.GameType {
case faultTypes.CannonGameType, faultTypes.AlphabetGameType: case faultTypes.CannonGameType, faultTypes.AlphabetGameType:
fdg, err := contracts.NewFaultDisputeGameContract(game.Proxy, m.caller) fdg, err := contracts.NewFaultDisputeGameContract(game.Proxy, g.caller)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to create FaultDisputeGameContract: %w", err) return nil, fmt.Errorf("failed to create FaultDisputeGameContract: %w", err)
} }
m.cache.Add(game.Proxy, fdg) g.cache.Add(game.Proxy, fdg)
return fdg, nil return fdg, nil
default: default:
return nil, fmt.Errorf("unsupported game type: %d", game.GameType) return nil, fmt.Errorf("unsupported game type: %d", game.GameType)
......
...@@ -43,7 +43,7 @@ func TestMetadataCreator_CreateContract(t *testing.T) { ...@@ -43,7 +43,7 @@ func TestMetadataCreator_CreateContract(t *testing.T) {
test := test test := test
t.Run(test.name, func(t *testing.T) { t.Run(test.name, func(t *testing.T) {
caller, metrics := setupMetadataLoaderTest(t) caller, metrics := setupMetadataLoaderTest(t)
creator := NewMetadataCreator(metrics, caller) creator := NewGameCallerCreator(metrics, caller)
_, err := creator.CreateContract(test.game) _, err := creator.CreateContract(test.game)
require.Equal(t, test.expectedErr, err) require.Equal(t, test.expectedErr, err)
if test.expectedErr == nil { if test.expectedErr == nil {
......
...@@ -14,8 +14,8 @@ type OutputValidator interface { ...@@ -14,8 +14,8 @@ type OutputValidator interface {
CheckRootAgreement(ctx context.Context, blockNum uint64, root common.Hash) (bool, common.Hash, error) CheckRootAgreement(ctx context.Context, blockNum uint64, root common.Hash) (bool, common.Hash, error)
} }
type MetadataCreator interface { type GameCallerCreator interface {
CreateContract(game types.GameMetadata) (MetadataLoader, error) CreateContract(game types.GameMetadata) (GameCaller, error)
} }
type DetectorMetrics interface { type DetectorMetrics interface {
...@@ -26,11 +26,11 @@ type DetectorMetrics interface { ...@@ -26,11 +26,11 @@ type DetectorMetrics interface {
type detector struct { type detector struct {
logger log.Logger logger log.Logger
metrics DetectorMetrics metrics DetectorMetrics
creator MetadataCreator creator GameCallerCreator
validator OutputValidator validator OutputValidator
} }
func newDetector(logger log.Logger, metrics DetectorMetrics, creator MetadataCreator, validator OutputValidator) *detector { func newDetector(logger log.Logger, metrics DetectorMetrics, creator GameCallerCreator, validator OutputValidator) *detector {
return &detector{ return &detector{
logger: logger, logger: logger,
metrics: metrics, metrics: metrics,
......
...@@ -5,6 +5,7 @@ import ( ...@@ -5,6 +5,7 @@ import (
"errors" "errors"
"testing" "testing"
faultTypes "github.com/ethereum-optimism/optimism/op-challenger/game/fault/types"
"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/testlog" "github.com/ethereum-optimism/optimism/op-service/testlog"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
...@@ -33,7 +34,7 @@ func TestDetector_Detect(t *testing.T) { ...@@ -33,7 +34,7 @@ func TestDetector_Detect(t *testing.T) {
t.Run("CheckAgreementFails", func(t *testing.T) { t.Run("CheckAgreementFails", func(t *testing.T) {
detector, metrics, creator, rollup, _ := setupDetectorTest(t) detector, metrics, creator, rollup, _ := setupDetectorTest(t)
rollup.err = errors.New("boom") rollup.err = errors.New("boom")
creator.loader = &mockMetadataLoader{status: types.GameStatusInProgress} creator.caller = &mockGameCaller{status: types.GameStatusInProgress}
detector.Detect(context.Background(), []types.GameMetadata{{}}) detector.Detect(context.Background(), []types.GameMetadata{{}})
metrics.Equals(t, 1, 0, 0) // Status should still be metriced here! metrics.Equals(t, 1, 0, 0) // Status should still be metriced here!
metrics.Mapped(t, map[string]int{}) metrics.Mapped(t, map[string]int{})
...@@ -41,8 +42,7 @@ func TestDetector_Detect(t *testing.T) { ...@@ -41,8 +42,7 @@ func TestDetector_Detect(t *testing.T) {
t.Run("SingleGame", func(t *testing.T) { t.Run("SingleGame", func(t *testing.T) {
detector, metrics, creator, _, _ := setupDetectorTest(t) detector, metrics, creator, _, _ := setupDetectorTest(t)
loader := &mockMetadataLoader{status: types.GameStatusInProgress} creator.caller = &mockGameCaller{status: types.GameStatusInProgress}
creator.loader = loader
detector.Detect(context.Background(), []types.GameMetadata{{}}) detector.Detect(context.Background(), []types.GameMetadata{{}})
metrics.Equals(t, 1, 0, 0) metrics.Equals(t, 1, 0, 0)
metrics.Mapped(t, map[string]int{"in_progress": 1}) metrics.Mapped(t, map[string]int{"in_progress": 1})
...@@ -50,8 +50,7 @@ func TestDetector_Detect(t *testing.T) { ...@@ -50,8 +50,7 @@ func TestDetector_Detect(t *testing.T) {
t.Run("MultipleGames", func(t *testing.T) { t.Run("MultipleGames", func(t *testing.T) {
detector, metrics, creator, _, _ := setupDetectorTest(t) detector, metrics, creator, _, _ := setupDetectorTest(t)
loader := &mockMetadataLoader{status: types.GameStatusInProgress} creator.caller = &mockGameCaller{status: types.GameStatusInProgress}
creator.loader = loader
detector.Detect(context.Background(), []types.GameMetadata{{}, {}, {}}) detector.Detect(context.Background(), []types.GameMetadata{{}, {}, {}})
metrics.Equals(t, 3, 0, 0) metrics.Equals(t, 3, 0, 0)
metrics.Mapped(t, map[string]int{"in_progress": 3}) metrics.Mapped(t, map[string]int{"in_progress": 3})
...@@ -128,16 +127,14 @@ func TestDetector_FetchGameMetadata(t *testing.T) { ...@@ -128,16 +127,14 @@ func TestDetector_FetchGameMetadata(t *testing.T) {
t.Run("GetGameMetadataFails", func(t *testing.T) { t.Run("GetGameMetadataFails", func(t *testing.T) {
detector, _, creator, _, _ := setupDetectorTest(t) detector, _, creator, _, _ := setupDetectorTest(t)
loader := &mockMetadataLoader{err: errors.New("boom")} creator.caller = &mockGameCaller{err: errors.New("boom")}
creator.loader = loader
_, _, _, err := detector.fetchGameMetadata(context.Background(), types.GameMetadata{}) _, _, _, err := detector.fetchGameMetadata(context.Background(), types.GameMetadata{})
require.Error(t, err) require.Error(t, err)
}) })
t.Run("Success", func(t *testing.T) { t.Run("Success", func(t *testing.T) {
detector, _, creator, _, _ := setupDetectorTest(t) detector, _, creator, _, _ := setupDetectorTest(t)
loader := &mockMetadataLoader{status: types.GameStatusInProgress} creator.caller = &mockGameCaller{status: types.GameStatusInProgress}
creator.loader = loader
_, _, status, err := detector.fetchGameMetadata(context.Background(), types.GameMetadata{}) _, _, status, err := detector.fetchGameMetadata(context.Background(), types.GameMetadata{})
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, types.GameStatusInProgress, status) require.Equal(t, types.GameStatusInProgress, status)
...@@ -229,53 +226,67 @@ func TestDetector_CheckAgreement_Succeeds(t *testing.T) { ...@@ -229,53 +226,67 @@ func TestDetector_CheckAgreement_Succeeds(t *testing.T) {
} }
} }
func setupDetectorTest(t *testing.T) (*detector, *mockDetectorMetricer, *mockMetadataCreator, *stubOutputValidator, *testlog.CapturingHandler) { func setupDetectorTest(t *testing.T) (*detector, *mockDetectorMetricer, *mockGameCallerCreator, *stubOutputValidator, *testlog.CapturingHandler) {
logger, capturedLogs := testlog.CaptureLogger(t, log.LvlDebug) logger, capturedLogs := testlog.CaptureLogger(t, log.LvlDebug)
metrics := &mockDetectorMetricer{} metrics := &mockDetectorMetricer{}
loader := &mockMetadataLoader{} caller := &mockGameCaller{}
creator := &mockMetadataCreator{loader: loader} creator := &mockGameCallerCreator{caller: caller}
validator := &stubOutputValidator{} validator := &stubOutputValidator{}
detector := newDetector(logger, metrics, creator, validator) detector := newDetector(logger, metrics, creator, validator)
return detector, metrics, creator, validator, capturedLogs return detector, metrics, creator, validator, capturedLogs
} }
type stubOutputValidator struct { type stubOutputValidator struct {
err error calls int
err error
} }
func (s *stubOutputValidator) CheckRootAgreement(ctx context.Context, blockNum uint64, rootClaim common.Hash) (bool, common.Hash, error) { func (s *stubOutputValidator) CheckRootAgreement(ctx context.Context, blockNum uint64, rootClaim common.Hash) (bool, common.Hash, error) {
s.calls++
if s.err != nil { if s.err != nil {
return false, common.Hash{}, s.err return false, common.Hash{}, s.err
} }
return rootClaim == mockRootClaim, mockRootClaim, nil return rootClaim == mockRootClaim, mockRootClaim, nil
} }
type mockMetadataCreator struct { type mockGameCallerCreator struct {
calls int calls int
err error err error
loader *mockMetadataLoader caller *mockGameCaller
} }
func (m *mockMetadataCreator) CreateContract(game types.GameMetadata) (MetadataLoader, error) { func (m *mockGameCallerCreator) CreateContract(game types.GameMetadata) (GameCaller, error) {
m.calls++ m.calls++
if m.err != nil { if m.err != nil {
return nil, m.err return nil, m.err
} }
return m.loader, nil return m.caller, nil
} }
type mockMetadataLoader struct { type mockGameCaller struct {
calls int calls int
status types.GameStatus claimsCalls int
err error claims []faultTypes.Claim
status types.GameStatus
rootClaim common.Hash
err error
claimsErr error
} }
func (m *mockMetadataLoader) GetGameMetadata(ctx context.Context) (uint64, common.Hash, types.GameStatus, error) { func (m *mockGameCaller) GetGameMetadata(ctx context.Context) (uint64, common.Hash, types.GameStatus, error) {
m.calls++ m.calls++
if m.err != nil { if m.err != nil {
return 0, common.Hash{}, m.status, m.err return 0, m.rootClaim, m.status, m.err
}
return 0, m.rootClaim, m.status, nil
}
func (m *mockGameCaller) GetAllClaims(ctx context.Context) ([]faultTypes.Claim, error) {
m.claimsCalls++
if m.claimsErr != nil {
return nil, m.claimsErr
} }
return 0, common.Hash{}, m.status, nil return m.claims, nil
} }
type mockDetectorMetricer struct { type mockDetectorMetricer struct {
......
package mon
import (
"context"
"errors"
"fmt"
"github.com/ethereum-optimism/optimism/op-challenger/game/types"
"github.com/ethereum/go-ethereum/log"
)
var (
ErrContractCreation = errors.New("failed to create contract")
ErrMetadataFetch = errors.New("failed to fetch game metadata")
ErrClaimFetch = errors.New("failed to fetch game claims")
ErrResolver = errors.New("failed to resolve game")
ErrRootAgreement = errors.New("failed to check root agreement")
)
type forecast struct {
logger log.Logger
// TODO(client-pod#536): Add forecast metrics.
// These should only fire if a game is in progress.
// otherwise, the detector should record the game status.
creator GameCallerCreator
validator OutputValidator
}
func newForecast(logger log.Logger, creator GameCallerCreator, validator OutputValidator) *forecast {
return &forecast{
logger: logger,
creator: creator,
validator: validator,
}
}
func (f *forecast) Forecast(ctx context.Context, games []types.GameMetadata) {
for _, game := range games {
if err := f.forecastGame(ctx, game); err != nil {
f.logger.Error("Failed to forecast game", "err", err)
}
}
}
func (f *forecast) forecastGame(ctx context.Context, game types.GameMetadata) error {
loader, err := f.creator.CreateContract(game)
if err != nil {
return fmt.Errorf("%w: %w", ErrContractCreation, err)
}
// Get the game status, it must be in progress to forecast.
l2BlockNum, rootClaim, status, err := loader.GetGameMetadata(ctx)
if err != nil {
return fmt.Errorf("%w: %w", ErrMetadataFetch, err)
}
if status != types.GameStatusInProgress {
f.logger.Debug("Game is not in progress, skipping forecast", "game", game, "status", status)
return nil
}
// Load all claims for the game.
claims, err := loader.GetAllClaims(ctx)
if err != nil {
return fmt.Errorf("%w: %w", ErrClaimFetch, err)
}
// Compute the resolution status of the game.
status, err = Resolve(claims)
if err != nil {
return fmt.Errorf("%w: %w", ErrResolver, err)
}
// Check the root agreement.
agreement, expected, err := f.validator.CheckRootAgreement(ctx, l2BlockNum, rootClaim)
if err != nil {
return fmt.Errorf("%w: %w", ErrRootAgreement, err)
}
if agreement {
// If we agree with the output root proposal, the Defender should win, defending that claim.
if status == types.GameStatusChallengerWon {
f.logger.Warn("Forecasting unexpected game result", "status", status, "game", game, "rootClaim", rootClaim, "expected", expected)
} else {
f.logger.Debug("Forecasting expected game result", "status", status, "game", game, "rootClaim", rootClaim, "expected", expected)
}
} else {
// If we disagree with the output root proposal, the Challenger should win, challenging that claim.
if status == types.GameStatusDefenderWon {
f.logger.Warn("Forecasting unexpected game result", "status", status, "game", game, "rootClaim", rootClaim, "expected", expected)
} else {
f.logger.Debug("Forecasting expected game result", "status", status, "game", game, "rootClaim", rootClaim, "expected", expected)
}
}
return nil
}
This diff is collapsed.
...@@ -14,6 +14,7 @@ import ( ...@@ -14,6 +14,7 @@ import (
) )
type Detect func(ctx context.Context, games []types.GameMetadata) type Detect func(ctx context.Context, games []types.GameMetadata)
type Forecast func(ctx context.Context, games []types.GameMetadata)
type BlockHashFetcher func(ctx context.Context, number *big.Int) (common.Hash, error) type BlockHashFetcher func(ctx context.Context, number *big.Int) (common.Hash, error)
type BlockNumberFetcher func(ctx context.Context) (uint64, error) type BlockNumberFetcher func(ctx context.Context) (uint64, error)
type FactoryGameFetcher func(ctx context.Context, blockHash common.Hash, earliestTimestamp uint64) ([]types.GameMetadata, error) type FactoryGameFetcher func(ctx context.Context, blockHash common.Hash, earliestTimestamp uint64) ([]types.GameMetadata, error)
...@@ -30,6 +31,7 @@ type gameMonitor struct { ...@@ -30,6 +31,7 @@ type gameMonitor struct {
monitorInterval time.Duration monitorInterval time.Duration
detect Detect detect Detect
forecast Forecast
fetchGames FactoryGameFetcher fetchGames FactoryGameFetcher
fetchBlockHash BlockHashFetcher fetchBlockHash BlockHashFetcher
fetchBlockNumber BlockNumberFetcher fetchBlockNumber BlockNumberFetcher
...@@ -42,6 +44,7 @@ func newGameMonitor( ...@@ -42,6 +44,7 @@ func newGameMonitor(
monitorInterval time.Duration, monitorInterval time.Duration,
gameWindow time.Duration, gameWindow time.Duration,
detect Detect, detect Detect,
forecast Forecast,
factory FactoryGameFetcher, factory FactoryGameFetcher,
fetchBlockNumber BlockNumberFetcher, fetchBlockNumber BlockNumberFetcher,
fetchBlockHash BlockHashFetcher, fetchBlockHash BlockHashFetcher,
...@@ -54,6 +57,7 @@ func newGameMonitor( ...@@ -54,6 +57,7 @@ func newGameMonitor(
monitorInterval: monitorInterval, monitorInterval: monitorInterval,
gameWindow: gameWindow, gameWindow: gameWindow,
detect: detect, detect: detect,
forecast: forecast,
fetchGames: factory, fetchGames: factory,
fetchBlockNumber: fetchBlockNumber, fetchBlockNumber: fetchBlockNumber,
fetchBlockHash: fetchBlockHash, fetchBlockHash: fetchBlockHash,
...@@ -87,6 +91,7 @@ func (m *gameMonitor) monitorGames() error { ...@@ -87,6 +91,7 @@ func (m *gameMonitor) monitorGames() error {
return fmt.Errorf("failed to load games: %w", err) return fmt.Errorf("failed to load games: %w", err)
} }
m.detect(m.ctx, games) m.detect(m.ctx, games)
m.forecast(m.ctx, games)
return nil return nil
} }
......
...@@ -23,20 +23,20 @@ func TestMonitor_MinGameTimestamp(t *testing.T) { ...@@ -23,20 +23,20 @@ func TestMonitor_MinGameTimestamp(t *testing.T) {
t.Parallel() t.Parallel()
t.Run("ZeroGameWindow", func(t *testing.T) { t.Run("ZeroGameWindow", func(t *testing.T) {
monitor, _, _ := setupMonitorTest(t) monitor, _, _, _ := setupMonitorTest(t)
monitor.gameWindow = time.Duration(0) monitor.gameWindow = time.Duration(0)
require.Equal(t, monitor.minGameTimestamp(), uint64(0)) require.Equal(t, monitor.minGameTimestamp(), uint64(0))
}) })
t.Run("ZeroClock", func(t *testing.T) { t.Run("ZeroClock", func(t *testing.T) {
monitor, _, _ := setupMonitorTest(t) monitor, _, _, _ := setupMonitorTest(t)
monitor.gameWindow = time.Minute monitor.gameWindow = time.Minute
monitor.clock = clock.NewDeterministicClock(time.Unix(0, 0)) monitor.clock = clock.NewDeterministicClock(time.Unix(0, 0))
require.Equal(t, uint64(0), monitor.minGameTimestamp()) require.Equal(t, uint64(0), monitor.minGameTimestamp())
}) })
t.Run("ValidArithmetic", func(t *testing.T) { t.Run("ValidArithmetic", func(t *testing.T) {
monitor, _, _ := setupMonitorTest(t) monitor, _, _, _ := setupMonitorTest(t)
monitor.gameWindow = time.Minute monitor.gameWindow = time.Minute
frozen := time.Unix(int64(time.Hour.Seconds()), 0) frozen := time.Unix(int64(time.Hour.Seconds()), 0)
monitor.clock = clock.NewDeterministicClock(frozen) monitor.clock = clock.NewDeterministicClock(frozen)
...@@ -49,7 +49,7 @@ func TestMonitor_MonitorGames(t *testing.T) { ...@@ -49,7 +49,7 @@ func TestMonitor_MonitorGames(t *testing.T) {
t.Parallel() t.Parallel()
t.Run("FailedFetchBlocknumber", func(t *testing.T) { t.Run("FailedFetchBlocknumber", func(t *testing.T) {
monitor, _, _ := setupMonitorTest(t) monitor, _, _, _ := setupMonitorTest(t)
boom := errors.New("boom") boom := errors.New("boom")
monitor.fetchBlockNumber = func(ctx context.Context) (uint64, error) { monitor.fetchBlockNumber = func(ctx context.Context) (uint64, error) {
return 0, boom return 0, boom
...@@ -59,7 +59,7 @@ func TestMonitor_MonitorGames(t *testing.T) { ...@@ -59,7 +59,7 @@ func TestMonitor_MonitorGames(t *testing.T) {
}) })
t.Run("FailedFetchBlockHash", func(t *testing.T) { t.Run("FailedFetchBlockHash", func(t *testing.T) {
monitor, _, _ := setupMonitorTest(t) monitor, _, _, _ := setupMonitorTest(t)
boom := errors.New("boom") boom := errors.New("boom")
monitor.fetchBlockHash = func(ctx context.Context, number *big.Int) (common.Hash, error) { monitor.fetchBlockHash = func(ctx context.Context, number *big.Int) (common.Hash, error) {
return common.Hash{}, boom return common.Hash{}, boom
...@@ -69,7 +69,7 @@ func TestMonitor_MonitorGames(t *testing.T) { ...@@ -69,7 +69,7 @@ func TestMonitor_MonitorGames(t *testing.T) {
}) })
t.Run("DetectsWithNoGames", func(t *testing.T) { t.Run("DetectsWithNoGames", func(t *testing.T) {
monitor, factory, detector := setupMonitorTest(t) monitor, factory, detector, _ := setupMonitorTest(t)
factory.games = []types.GameMetadata{} factory.games = []types.GameMetadata{}
err := monitor.monitorGames() err := monitor.monitorGames()
require.NoError(t, err) require.NoError(t, err)
...@@ -77,7 +77,7 @@ func TestMonitor_MonitorGames(t *testing.T) { ...@@ -77,7 +77,7 @@ func TestMonitor_MonitorGames(t *testing.T) {
}) })
t.Run("DetectsMultipleGames", func(t *testing.T) { t.Run("DetectsMultipleGames", func(t *testing.T) {
monitor, factory, detector := setupMonitorTest(t) monitor, factory, detector, _ := setupMonitorTest(t)
factory.games = []types.GameMetadata{{}, {}, {}} factory.games = []types.GameMetadata{{}, {}, {}}
err := monitor.monitorGames() err := monitor.monitorGames()
require.NoError(t, err) require.NoError(t, err)
...@@ -89,7 +89,7 @@ func TestMonitor_StartMonitoring(t *testing.T) { ...@@ -89,7 +89,7 @@ func TestMonitor_StartMonitoring(t *testing.T) {
t.Run("MonitorsGames", func(t *testing.T) { t.Run("MonitorsGames", func(t *testing.T) {
addr1 := common.Address{0xaa} addr1 := common.Address{0xaa}
addr2 := common.Address{0xbb} addr2 := common.Address{0xbb}
monitor, factory, detector := setupMonitorTest(t) monitor, factory, detector, _ := setupMonitorTest(t)
factory.games = []types.GameMetadata{newFDG(addr1, 9999), newFDG(addr2, 9999)} factory.games = []types.GameMetadata{newFDG(addr1, 9999), newFDG(addr2, 9999)}
factory.maxSuccess = len(factory.games) // Only allow two successful fetches factory.maxSuccess = len(factory.games) // Only allow two successful fetches
...@@ -102,7 +102,7 @@ func TestMonitor_StartMonitoring(t *testing.T) { ...@@ -102,7 +102,7 @@ func TestMonitor_StartMonitoring(t *testing.T) {
}) })
t.Run("FailsToFetchGames", func(t *testing.T) { t.Run("FailsToFetchGames", func(t *testing.T) {
monitor, factory, detector := setupMonitorTest(t) monitor, factory, detector, _ := setupMonitorTest(t)
factory.fetchErr = errors.New("boom") factory.fetchErr = errors.New("boom")
monitor.StartMonitoring() monitor.StartMonitoring()
...@@ -121,7 +121,7 @@ func newFDG(proxy common.Address, timestamp uint64) types.GameMetadata { ...@@ -121,7 +121,7 @@ func newFDG(proxy common.Address, timestamp uint64) types.GameMetadata {
} }
} }
func setupMonitorTest(t *testing.T) (*gameMonitor, *mockFactory, *mockDetector) { func setupMonitorTest(t *testing.T) (*gameMonitor, *mockFactory, *mockDetector, *mockForecast) {
logger := testlog.Logger(t, log.LvlDebug) logger := testlog.Logger(t, log.LvlDebug)
fetchBlockNum := func(ctx context.Context) (uint64, error) { fetchBlockNum := func(ctx context.Context) (uint64, error) {
return 1, nil return 1, nil
...@@ -134,6 +134,7 @@ func setupMonitorTest(t *testing.T) (*gameMonitor, *mockFactory, *mockDetector) ...@@ -134,6 +134,7 @@ func setupMonitorTest(t *testing.T) (*gameMonitor, *mockFactory, *mockDetector)
cl.Start() cl.Start()
factory := &mockFactory{} factory := &mockFactory{}
detect := &mockDetector{} detect := &mockDetector{}
forecast := &mockForecast{}
monitor := newGameMonitor( monitor := newGameMonitor(
context.Background(), context.Background(),
logger, logger,
...@@ -141,11 +142,20 @@ func setupMonitorTest(t *testing.T) (*gameMonitor, *mockFactory, *mockDetector) ...@@ -141,11 +142,20 @@ func setupMonitorTest(t *testing.T) (*gameMonitor, *mockFactory, *mockDetector)
monitorInterval, monitorInterval,
time.Duration(10*time.Second), time.Duration(10*time.Second),
detect.Detect, detect.Detect,
forecast.Forecast,
factory.GetGamesAtOrAfter, factory.GetGamesAtOrAfter,
fetchBlockNum, fetchBlockNum,
fetchBlockHash, fetchBlockHash,
) )
return monitor, factory, detect return monitor, factory, detect, forecast
}
type mockForecast struct {
calls int
}
func (m *mockForecast) Forecast(ctx context.Context, games []types.GameMetadata) {
m.calls++
} }
type mockDetector struct { type mockDetector struct {
......
package mon
import (
"fmt"
faultTypes "github.com/ethereum-optimism/optimism/op-challenger/game/fault/types"
"github.com/ethereum-optimism/optimism/op-challenger/game/types"
"github.com/ethereum/go-ethereum/common"
)
type BidirectionalClaim struct {
Claim *faultTypes.Claim
Children []*BidirectionalClaim
}
// Resolve creates the bidirectional tree of claims and then computes the resolved game status.
func Resolve(claims []faultTypes.Claim) (types.GameStatus, error) {
flatBidireactionalTree, err := createBidirectionalTree(claims)
if err != nil {
return 0, fmt.Errorf("failed to create bidirectional tree: %w", err)
}
return resolveTree(flatBidireactionalTree), nil
}
// createBidirectionalTree walks backwards through the list of claims and creates a bidirectional
// tree of claims. The root claim must be at index 0. The tree is returned as a flat array so it
// can be easily traversed following the resolution process.
func createBidirectionalTree(claims []faultTypes.Claim) ([]*BidirectionalClaim, error) {
claimMap := make(map[int]*BidirectionalClaim)
res := make([]*BidirectionalClaim, 0, len(claims))
for _, claim := range claims {
claim := claim
bidirectionalClaim := &BidirectionalClaim{
Claim: &claim,
}
claimMap[claim.ContractIndex] = bidirectionalClaim
if !claim.IsRoot() {
// SAFETY: the parent must exist in the list prior to the current claim.
parent := claimMap[claim.ParentContractIndex]
parent.Children = append(parent.Children, bidirectionalClaim)
}
res = append(res, bidirectionalClaim)
}
return res, nil
}
// resolveTree iterates backwards over the bidirectional tree, iteratively
// checking the leftmost counter of each claim, and updating the claim's counter
// claimant. Once the root claim is reached, the resolution game status is returned.
func resolveTree(tree []*BidirectionalClaim) types.GameStatus {
for i := len(tree) - 1; i >= 0; i-- {
claim := tree[i]
counterClaimant := common.Address{}
for _, child := range claim.Children {
if child.Claim.CounteredBy == (common.Address{}) {
counterClaimant = child.Claim.Claimant
}
}
claim.Claim.CounteredBy = counterClaimant
}
if (len(tree) == 0 || tree[0].Claim.CounteredBy == common.Address{}) {
return types.GameStatusDefenderWon
} else {
return types.GameStatusChallengerWon
}
}
package mon
import (
"math"
"math/big"
"testing"
"github.com/ethereum/go-ethereum/common"
"github.com/stretchr/testify/require"
faultTypes "github.com/ethereum-optimism/optimism/op-challenger/game/fault/types"
"github.com/ethereum-optimism/optimism/op-challenger/game/types"
)
func TestResolver_Resolve(t *testing.T) {
t.Run("NoClaims", func(t *testing.T) {
status, err := Resolve([]faultTypes.Claim{})
require.NoError(t, err)
require.Equal(t, types.GameStatusDefenderWon, status)
})
t.Run("SingleClaim", func(t *testing.T) {
status, err := Resolve(createDeepClaimList()[:1])
require.NoError(t, err)
require.Equal(t, types.GameStatusDefenderWon, status)
})
t.Run("MultipleClaims", func(t *testing.T) {
status, err := Resolve(createDeepClaimList()[:2])
require.NoError(t, err)
require.Equal(t, types.GameStatusChallengerWon, status)
})
t.Run("MultipleClaimsAndChildren", func(t *testing.T) {
status, err := Resolve(createDeepClaimList())
require.NoError(t, err)
require.Equal(t, types.GameStatusDefenderWon, status)
})
}
func TestResolver_CreateBidirectionalTree(t *testing.T) {
t.Run("SingleClaim", func(t *testing.T) {
claims := createDeepClaimList()[:1]
claims[0].CounteredBy = common.Address{}
tree, err := createBidirectionalTree(claims)
require.NoError(t, err)
require.Len(t, tree, 1)
require.Equal(t, claims[0], *tree[0].Claim)
require.Empty(t, tree[0].Children)
})
t.Run("MultipleClaims", func(t *testing.T) {
claims := createDeepClaimList()[:2]
claims[1].CounteredBy = common.Address{}
tree, err := createBidirectionalTree(claims)
require.NoError(t, err)
require.Len(t, tree, 2)
require.Equal(t, claims[0], *tree[0].Claim)
require.Len(t, tree[0].Children, 1)
require.Equal(t, claims[1], *tree[0].Children[0].Claim)
require.Equal(t, claims[1], *tree[1].Claim)
require.Empty(t, tree[1].Children)
})
t.Run("MultipleClaimsAndChildren", func(t *testing.T) {
claims := createDeepClaimList()
tree, err := createBidirectionalTree(claims)
require.NoError(t, err)
require.Len(t, tree, 3)
require.Equal(t, claims[0], *tree[0].Claim)
require.Len(t, tree[0].Children, 1)
require.Equal(t, tree[0].Children[0], tree[1])
require.Equal(t, claims[1], *tree[1].Claim)
require.Len(t, tree[1].Children, 1)
require.Equal(t, tree[1].Children[0], tree[2])
require.Equal(t, claims[2], *tree[2].Claim)
require.Empty(t, tree[2].Children)
})
}
func TestResolver_ResolveTree(t *testing.T) {
t.Run("NoClaims", func(t *testing.T) {
status := resolveTree([]*BidirectionalClaim{})
require.Equal(t, types.GameStatusDefenderWon, status)
})
t.Run("SingleRootClaim", func(t *testing.T) {
list := createBidirectionalClaimList()[:1]
list[0].Claim.CounteredBy = common.Address{}
status := resolveTree(list)
require.Equal(t, types.GameStatusDefenderWon, status)
})
t.Run("ChallengerWon", func(t *testing.T) {
list := createBidirectionalClaimList()[:2]
list[1].Claim.CounteredBy = common.Address{}
list[1].Children = make([]*BidirectionalClaim, 0)
status := resolveTree(list)
require.Equal(t, types.GameStatusChallengerWon, status)
})
t.Run("DefenderWon", func(t *testing.T) {
status := resolveTree(createBidirectionalClaimList())
require.Equal(t, types.GameStatusDefenderWon, status)
})
}
func createBidirectionalClaimList() []*BidirectionalClaim {
claimList := createDeepClaimList()
bidirectionalClaimList := make([]*BidirectionalClaim, len(claimList))
bidirectionalClaimList[2] = &BidirectionalClaim{
Claim: &claimList[2],
Children: make([]*BidirectionalClaim, 0),
}
bidirectionalClaimList[1] = &BidirectionalClaim{
Claim: &claimList[1],
Children: []*BidirectionalClaim{bidirectionalClaimList[2]},
}
bidirectionalClaimList[0] = &BidirectionalClaim{
Claim: &claimList[0],
Children: []*BidirectionalClaim{bidirectionalClaimList[1]},
}
return bidirectionalClaimList
}
func createDeepClaimList() []faultTypes.Claim {
return []faultTypes.Claim{
{
ClaimData: faultTypes.ClaimData{
Position: faultTypes.NewPosition(0, big.NewInt(0)),
},
ContractIndex: 0,
CounteredBy: common.HexToAddress("0x222222"),
ParentContractIndex: math.MaxInt64,
Claimant: common.HexToAddress("0x111111"),
},
{
ClaimData: faultTypes.ClaimData{
Position: faultTypes.NewPosition(1, big.NewInt(0)),
},
CounteredBy: common.HexToAddress("0x111111"),
ContractIndex: 1,
ParentContractIndex: 0,
Claimant: common.HexToAddress("0x222222"),
},
{
ClaimData: faultTypes.ClaimData{
Position: faultTypes.NewPosition(2, big.NewInt(0)),
},
ContractIndex: 2,
ParentContractIndex: 1,
Claimant: common.HexToAddress("0x111111"),
},
}
}
...@@ -34,7 +34,8 @@ type Service struct { ...@@ -34,7 +34,8 @@ type Service struct {
cl clock.Clock cl clock.Clock
metadata *metadataCreator forecast *forecast
game *gameCallerCreator
rollupClient *sources.RollupClient rollupClient *sources.RollupClient
detector *detector detector *detector
validator *outputValidator validator *outputValidator
...@@ -78,9 +79,9 @@ func (s *Service) initFromConfig(ctx context.Context, cfg *config.Config) error ...@@ -78,9 +79,9 @@ func (s *Service) initFromConfig(ctx context.Context, cfg *config.Config) error
if err := s.initOutputRollupClient(ctx, cfg); err != nil { if err := s.initOutputRollupClient(ctx, cfg); err != nil {
return fmt.Errorf("failed to init rollup client: %w", err) return fmt.Errorf("failed to init rollup client: %w", err)
} }
s.initMetadataCreator()
s.initOutputValidator() s.initOutputValidator()
s.initDetector() s.initGameCallerCreator()
s.initForecast(cfg)
s.initDetector() s.initDetector()
s.initMonitor(ctx, cfg) s.initMonitor(ctx, cfg)
...@@ -94,8 +95,16 @@ func (s *Service) initOutputValidator() { ...@@ -94,8 +95,16 @@ func (s *Service) initOutputValidator() {
s.validator = newOutputValidator(s.rollupClient) s.validator = newOutputValidator(s.rollupClient)
} }
func (s *Service) initGameCallerCreator() {
s.game = NewGameCallerCreator(s.metrics, batching.NewMultiCaller(s.l1Client.Client(), batching.DefaultBatchSize))
}
func (s *Service) initForecast(cfg *config.Config) {
s.forecast = newForecast(s.logger, s.game, s.validator)
}
func (s *Service) initDetector() { func (s *Service) initDetector() {
s.detector = newDetector(s.logger, s.metrics, s.metadata, s.validator) s.detector = newDetector(s.logger, s.metrics, s.game, s.validator)
} }
func (s *Service) initOutputRollupClient(ctx context.Context, cfg *config.Config) error { func (s *Service) initOutputRollupClient(ctx context.Context, cfg *config.Config) error {
...@@ -107,10 +116,6 @@ func (s *Service) initOutputRollupClient(ctx context.Context, cfg *config.Config ...@@ -107,10 +116,6 @@ func (s *Service) initOutputRollupClient(ctx context.Context, cfg *config.Config
return nil return nil
} }
func (s *Service) initMetadataCreator() {
s.metadata = NewMetadataCreator(s.metrics, batching.NewMultiCaller(s.l1Client.Client(), batching.DefaultBatchSize))
}
func (s *Service) initL1Client(ctx context.Context, cfg *config.Config) error { func (s *Service) initL1Client(ctx context.Context, cfg *config.Config) error {
l1Client, err := dial.DialEthClientWithTimeout(ctx, dial.DefaultDialTimeout, s.logger, cfg.L1EthRpc) l1Client, err := dial.DialEthClientWithTimeout(ctx, dial.DefaultDialTimeout, s.logger, cfg.L1EthRpc)
if err != nil { if err != nil {
...@@ -180,6 +185,7 @@ func (s *Service) initMonitor(ctx context.Context, cfg *config.Config) { ...@@ -180,6 +185,7 @@ func (s *Service) initMonitor(ctx context.Context, cfg *config.Config) {
cfg.MonitorInterval, cfg.MonitorInterval,
cfg.GameWindow, cfg.GameWindow,
s.detector.Detect, s.detector.Detect,
s.forecast.Forecast,
s.factoryContract.GetGamesAtOrAfter, s.factoryContract.GetGamesAtOrAfter,
s.l1Client.BlockNumber, s.l1Client.BlockNumber,
blockHashFetcher, blockHashFetcher,
......
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