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 (
"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)
GetAllClaims(ctx context.Context) ([]faultTypes.Claim, error)
}
type metadataCreator struct {
type gameCallerCreator struct {
cache *caching.LRUCache[common.Address, *contracts.FaultDisputeGameContract]
caller *batching.MultiCaller
}
func NewMetadataCreator(m caching.Metrics, caller *batching.MultiCaller) *metadataCreator {
return &metadataCreator{
func NewGameCallerCreator(m caching.Metrics, caller *batching.MultiCaller) *gameCallerCreator {
return &gameCallerCreator{
caller: caller,
cache: caching.NewLRUCache[common.Address, *contracts.FaultDisputeGameContract](m, metricsLabel, 100),
}
}
func (m *metadataCreator) CreateContract(game types.GameMetadata) (MetadataLoader, error) {
if fdg, ok := m.cache.Get(game.Proxy); ok {
func (g *gameCallerCreator) CreateContract(game types.GameMetadata) (GameCaller, error) {
if fdg, ok := g.cache.Get(game.Proxy); ok {
return fdg, nil
}
switch game.GameType {
case faultTypes.CannonGameType, faultTypes.AlphabetGameType:
fdg, err := contracts.NewFaultDisputeGameContract(game.Proxy, m.caller)
fdg, err := contracts.NewFaultDisputeGameContract(game.Proxy, g.caller)
if err != nil {
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
default:
return nil, fmt.Errorf("unsupported game type: %d", game.GameType)
......
......@@ -43,7 +43,7 @@ func TestMetadataCreator_CreateContract(t *testing.T) {
test := test
t.Run(test.name, func(t *testing.T) {
caller, metrics := setupMetadataLoaderTest(t)
creator := NewMetadataCreator(metrics, caller)
creator := NewGameCallerCreator(metrics, caller)
_, err := creator.CreateContract(test.game)
require.Equal(t, test.expectedErr, err)
if test.expectedErr == nil {
......
......@@ -14,8 +14,8 @@ type OutputValidator interface {
CheckRootAgreement(ctx context.Context, blockNum uint64, root common.Hash) (bool, common.Hash, error)
}
type MetadataCreator interface {
CreateContract(game types.GameMetadata) (MetadataLoader, error)
type GameCallerCreator interface {
CreateContract(game types.GameMetadata) (GameCaller, error)
}
type DetectorMetrics interface {
......@@ -26,11 +26,11 @@ type DetectorMetrics interface {
type detector struct {
logger log.Logger
metrics DetectorMetrics
creator MetadataCreator
creator GameCallerCreator
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{
logger: logger,
metrics: metrics,
......
......@@ -5,6 +5,7 @@ import (
"errors"
"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-service/testlog"
"github.com/ethereum/go-ethereum/common"
......@@ -33,7 +34,7 @@ func TestDetector_Detect(t *testing.T) {
t.Run("CheckAgreementFails", func(t *testing.T) {
detector, metrics, creator, rollup, _ := setupDetectorTest(t)
rollup.err = errors.New("boom")
creator.loader = &mockMetadataLoader{status: types.GameStatusInProgress}
creator.caller = &mockGameCaller{status: types.GameStatusInProgress}
detector.Detect(context.Background(), []types.GameMetadata{{}})
metrics.Equals(t, 1, 0, 0) // Status should still be metriced here!
metrics.Mapped(t, map[string]int{})
......@@ -41,8 +42,7 @@ func TestDetector_Detect(t *testing.T) {
t.Run("SingleGame", func(t *testing.T) {
detector, metrics, creator, _, _ := setupDetectorTest(t)
loader := &mockMetadataLoader{status: types.GameStatusInProgress}
creator.loader = loader
creator.caller = &mockGameCaller{status: types.GameStatusInProgress}
detector.Detect(context.Background(), []types.GameMetadata{{}})
metrics.Equals(t, 1, 0, 0)
metrics.Mapped(t, map[string]int{"in_progress": 1})
......@@ -50,8 +50,7 @@ func TestDetector_Detect(t *testing.T) {
t.Run("MultipleGames", func(t *testing.T) {
detector, metrics, creator, _, _ := setupDetectorTest(t)
loader := &mockMetadataLoader{status: types.GameStatusInProgress}
creator.loader = loader
creator.caller = &mockGameCaller{status: types.GameStatusInProgress}
detector.Detect(context.Background(), []types.GameMetadata{{}, {}, {}})
metrics.Equals(t, 3, 0, 0)
metrics.Mapped(t, map[string]int{"in_progress": 3})
......@@ -128,16 +127,14 @@ func TestDetector_FetchGameMetadata(t *testing.T) {
t.Run("GetGameMetadataFails", func(t *testing.T) {
detector, _, creator, _, _ := setupDetectorTest(t)
loader := &mockMetadataLoader{err: errors.New("boom")}
creator.loader = loader
creator.caller = &mockGameCaller{err: errors.New("boom")}
_, _, _, err := detector.fetchGameMetadata(context.Background(), types.GameMetadata{})
require.Error(t, err)
})
t.Run("Success", func(t *testing.T) {
detector, _, creator, _, _ := setupDetectorTest(t)
loader := &mockMetadataLoader{status: types.GameStatusInProgress}
creator.loader = loader
creator.caller = &mockGameCaller{status: types.GameStatusInProgress}
_, _, status, err := detector.fetchGameMetadata(context.Background(), types.GameMetadata{})
require.NoError(t, err)
require.Equal(t, types.GameStatusInProgress, status)
......@@ -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)
metrics := &mockDetectorMetricer{}
loader := &mockMetadataLoader{}
creator := &mockMetadataCreator{loader: loader}
caller := &mockGameCaller{}
creator := &mockGameCallerCreator{caller: caller}
validator := &stubOutputValidator{}
detector := newDetector(logger, metrics, creator, validator)
return detector, metrics, creator, validator, capturedLogs
}
type stubOutputValidator struct {
calls int
err error
}
func (s *stubOutputValidator) CheckRootAgreement(ctx context.Context, blockNum uint64, rootClaim common.Hash) (bool, common.Hash, error) {
s.calls++
if s.err != nil {
return false, common.Hash{}, s.err
}
return rootClaim == mockRootClaim, mockRootClaim, nil
}
type mockMetadataCreator struct {
type mockGameCallerCreator struct {
calls int
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++
if m.err != nil {
return nil, m.err
}
return m.loader, nil
return m.caller, nil
}
type mockMetadataLoader struct {
type mockGameCaller struct {
calls int
claimsCalls int
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++
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 {
......
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
}
package mon
import (
"context"
"errors"
"fmt"
"testing"
"github.com/ethereum-optimism/optimism/op-challenger/game/types"
"github.com/ethereum-optimism/optimism/op-service/testlog"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/log"
"github.com/stretchr/testify/require"
)
var (
failedForecaseLog = "Failed to forecast game"
expectedInProgressLog = "Game is not in progress, skipping forecast"
unexpectedResultLog = "Forecasting unexpected game result"
expectedResultLog = "Forecasting expected game result"
)
func TestForecast_Forecast_BasicTests(t *testing.T) {
t.Parallel()
t.Run("NoGames", func(t *testing.T) {
forecast, creator, rollup, logs := setupForecastTest(t)
forecast.Forecast(context.Background(), []types.GameMetadata{})
require.Equal(t, 0, creator.calls)
require.Equal(t, 0, creator.caller.calls)
require.Equal(t, 0, creator.caller.claimsCalls)
require.Equal(t, 0, rollup.calls)
levelFilter := testlog.NewLevelFilter(log.LevelError)
messageFilter := testlog.NewMessageFilter(failedForecaseLog)
require.Nil(t, logs.FindLog(levelFilter, messageFilter))
})
t.Run("ContractCreationFails", func(t *testing.T) {
forecast, creator, rollup, logs := setupForecastTest(t)
creator.err = errors.New("boom")
forecast.Forecast(context.Background(), []types.GameMetadata{{}})
require.Equal(t, 1, creator.calls)
require.Equal(t, 0, creator.caller.calls)
require.Equal(t, 0, creator.caller.claimsCalls)
require.Equal(t, 0, rollup.calls)
levelFilter := testlog.NewLevelFilter(log.LevelError)
messageFilter := testlog.NewMessageFilter(failedForecaseLog)
l := logs.FindLog(levelFilter, messageFilter)
require.NotNil(t, l)
err := l.AttrValue("err")
expectedErr := fmt.Errorf("%w: %w", ErrContractCreation, creator.err)
require.Equal(t, expectedErr, err)
})
t.Run("MetadataFetchFails", func(t *testing.T) {
forecast, creator, rollup, logs := setupForecastTest(t)
creator.caller = &mockGameCaller{status: types.GameStatusInProgress}
creator.caller.err = errors.New("boom")
forecast.Forecast(context.Background(), []types.GameMetadata{{}})
require.Equal(t, 1, creator.calls)
require.Equal(t, 1, creator.caller.calls)
require.Equal(t, 0, creator.caller.claimsCalls)
require.Equal(t, 0, rollup.calls)
levelFilter := testlog.NewLevelFilter(log.LevelError)
messageFilter := testlog.NewMessageFilter(failedForecaseLog)
l := logs.FindLog(levelFilter, messageFilter)
require.NotNil(t, l)
err := l.AttrValue("err")
expectedErr := fmt.Errorf("%w: %w", ErrMetadataFetch, creator.caller.err)
require.Equal(t, expectedErr, err)
})
t.Run("ClaimsFetchFails", func(t *testing.T) {
forecast, creator, rollup, logs := setupForecastTest(t)
creator.caller = &mockGameCaller{status: types.GameStatusInProgress}
creator.caller.claimsErr = errors.New("boom")
forecast.Forecast(context.Background(), []types.GameMetadata{{}})
require.Equal(t, 1, creator.calls)
require.Equal(t, 1, creator.caller.calls)
require.Equal(t, 1, creator.caller.claimsCalls)
require.Equal(t, 0, rollup.calls)
levelFilter := testlog.NewLevelFilter(log.LevelError)
messageFilter := testlog.NewMessageFilter(failedForecaseLog)
l := logs.FindLog(levelFilter, messageFilter)
require.NotNil(t, l)
err := l.AttrValue("err")
expectedErr := fmt.Errorf("%w: %w", ErrClaimFetch, creator.caller.claimsErr)
require.Equal(t, expectedErr, err)
})
t.Run("RollupFetchFails", func(t *testing.T) {
forecast, creator, rollup, logs := setupForecastTest(t)
rollup.err = errors.New("boom")
forecast.Forecast(context.Background(), []types.GameMetadata{{}})
require.Equal(t, 1, creator.calls)
require.Equal(t, 1, creator.caller.calls)
require.Equal(t, 1, creator.caller.claimsCalls)
require.Equal(t, 1, rollup.calls)
levelFilter := testlog.NewLevelFilter(log.LevelError)
messageFilter := testlog.NewMessageFilter(failedForecaseLog)
l := logs.FindLog(levelFilter, messageFilter)
require.NotNil(t, l)
err := l.AttrValue("err")
expectedErr := fmt.Errorf("%w: %w", ErrRootAgreement, rollup.err)
require.Equal(t, expectedErr, err)
})
t.Run("ChallengerWonGameSkipped", func(t *testing.T) {
forecast, creator, rollup, logs := setupForecastTest(t)
creator.caller = &mockGameCaller{status: types.GameStatusChallengerWon}
creator.caller.claims = createDeepClaimList()[:1]
expectedGame := types.GameMetadata{}
forecast.Forecast(context.Background(), []types.GameMetadata{expectedGame})
require.Equal(t, 1, creator.calls)
require.Equal(t, 1, creator.caller.calls)
require.Equal(t, 0, creator.caller.claimsCalls)
require.Equal(t, 0, rollup.calls)
levelFilter := testlog.NewLevelFilter(log.LevelError)
messageFilter := testlog.NewMessageFilter(failedForecaseLog)
require.Nil(t, logs.FindLog(levelFilter, messageFilter))
levelFilter = testlog.NewLevelFilter(log.LevelDebug)
messageFilter = testlog.NewMessageFilter(expectedInProgressLog)
l := logs.FindLog(levelFilter, messageFilter)
require.NotNil(t, l)
require.Equal(t, expectedGame, l.AttrValue("game"))
require.Equal(t, types.GameStatusChallengerWon, l.AttrValue("status"))
})
t.Run("DefenderWonGameSkipped", func(t *testing.T) {
forecast, creator, rollup, logs := setupForecastTest(t)
creator.caller = &mockGameCaller{status: types.GameStatusDefenderWon}
creator.caller.claims = createDeepClaimList()[:1]
expectedGame := types.GameMetadata{}
forecast.Forecast(context.Background(), []types.GameMetadata{expectedGame})
require.Equal(t, 1, creator.calls)
require.Equal(t, 1, creator.caller.calls)
require.Equal(t, 0, creator.caller.claimsCalls)
require.Equal(t, 0, rollup.calls)
levelFilter := testlog.NewLevelFilter(log.LevelError)
messageFilter := testlog.NewMessageFilter(failedForecaseLog)
require.Nil(t, logs.FindLog(levelFilter, messageFilter))
levelFilter = testlog.NewLevelFilter(log.LevelDebug)
messageFilter = testlog.NewMessageFilter(expectedInProgressLog)
l := logs.FindLog(levelFilter, messageFilter)
require.NotNil(t, l)
require.Equal(t, expectedGame, l.AttrValue("game"))
require.Equal(t, types.GameStatusDefenderWon, l.AttrValue("status"))
})
t.Run("SingleGame", func(t *testing.T) {
forecast, creator, rollup, logs := setupForecastTest(t)
creator.caller = &mockGameCaller{status: types.GameStatusInProgress}
creator.caller.claims = createDeepClaimList()[:1]
forecast.Forecast(context.Background(), []types.GameMetadata{{}})
require.Equal(t, 1, creator.calls)
require.Equal(t, 1, creator.caller.calls)
require.Equal(t, 1, creator.caller.claimsCalls)
require.Equal(t, 1, rollup.calls)
levelFilter := testlog.NewLevelFilter(log.LevelError)
messageFilter := testlog.NewMessageFilter(failedForecaseLog)
require.Nil(t, logs.FindLog(levelFilter, messageFilter))
levelFilter = testlog.NewLevelFilter(log.LevelDebug)
messageFilter = testlog.NewMessageFilter(expectedInProgressLog)
require.Nil(t, logs.FindLog(levelFilter, messageFilter))
})
t.Run("MultipleGames", func(t *testing.T) {
forecast, creator, rollup, logs := setupForecastTest(t)
creator.caller.claims = createDeepClaimList()[:1]
creator.caller = &mockGameCaller{status: types.GameStatusInProgress}
forecast.Forecast(context.Background(), []types.GameMetadata{{}, {}, {}})
require.Equal(t, 3, creator.calls)
require.Equal(t, 3, creator.caller.calls)
require.Equal(t, 3, creator.caller.claimsCalls)
require.Equal(t, 3, rollup.calls)
levelFilter := testlog.NewLevelFilter(log.LevelError)
messageFilter := testlog.NewMessageFilter(failedForecaseLog)
require.Nil(t, logs.FindLog(levelFilter, messageFilter))
levelFilter = testlog.NewLevelFilter(log.LevelDebug)
messageFilter = testlog.NewMessageFilter(expectedInProgressLog)
require.Nil(t, logs.FindLog(levelFilter, messageFilter))
})
}
func TestForecast_Forecast_EndLogs(t *testing.T) {
t.Parallel()
t.Run("AgreeDefenderWins", func(t *testing.T) {
forecast, creator, rollup, logs := setupForecastTest(t)
creator.caller = &mockGameCaller{status: types.GameStatusInProgress}
creator.caller.claims = createDeepClaimList()[:1]
forecast.Forecast(context.Background(), []types.GameMetadata{{}})
require.Equal(t, 1, creator.calls)
require.Equal(t, 1, creator.caller.calls)
require.Equal(t, 1, creator.caller.claimsCalls)
require.Equal(t, 1, rollup.calls)
levelFilter := testlog.NewLevelFilter(log.LevelError)
messageFilter := testlog.NewMessageFilter(failedForecaseLog)
require.Nil(t, logs.FindLog(levelFilter, messageFilter))
levelFilter = testlog.NewLevelFilter(log.LevelDebug)
messageFilter = testlog.NewMessageFilter(expectedInProgressLog)
require.Nil(t, logs.FindLog(levelFilter, messageFilter))
levelFilter = testlog.NewLevelFilter(log.LevelWarn)
messageFilter = testlog.NewMessageFilter(unexpectedResultLog)
l := logs.FindLog(levelFilter, messageFilter)
require.NotNil(t, l)
require.Equal(t, common.Hash{}, l.AttrValue("rootClaim"))
require.Equal(t, mockRootClaim, l.AttrValue("expected"))
require.Equal(t, types.GameStatusDefenderWon, l.AttrValue("status"))
})
t.Run("AgreeChallengerWins", func(t *testing.T) {
forecast, creator, rollup, logs := setupForecastTest(t)
creator.caller = &mockGameCaller{status: types.GameStatusInProgress}
creator.caller.claims = createDeepClaimList()[:2]
forecast.Forecast(context.Background(), []types.GameMetadata{{}})
require.Equal(t, 1, creator.calls)
require.Equal(t, 1, creator.caller.calls)
require.Equal(t, 1, creator.caller.claimsCalls)
require.Equal(t, 1, rollup.calls)
levelFilter := testlog.NewLevelFilter(log.LevelError)
messageFilter := testlog.NewMessageFilter(failedForecaseLog)
require.Nil(t, logs.FindLog(levelFilter, messageFilter))
levelFilter = testlog.NewLevelFilter(log.LevelDebug)
messageFilter = testlog.NewMessageFilter(expectedInProgressLog)
require.Nil(t, logs.FindLog(levelFilter, messageFilter))
levelFilter = testlog.NewLevelFilter(log.LevelDebug)
messageFilter = testlog.NewMessageFilter(expectedResultLog)
l := logs.FindLog(levelFilter, messageFilter)
require.NotNil(t, l)
require.Equal(t, common.Hash{}, l.AttrValue("rootClaim"))
require.Equal(t, mockRootClaim, l.AttrValue("expected"))
require.Equal(t, types.GameStatusChallengerWon, l.AttrValue("status"))
})
t.Run("DisagreeChallengerWins", func(t *testing.T) {
forecast, creator, rollup, logs := setupForecastTest(t)
creator.caller = &mockGameCaller{status: types.GameStatusInProgress}
creator.caller.rootClaim = common.Hash{}
creator.caller.claims = createDeepClaimList()[:2]
forecast.Forecast(context.Background(), []types.GameMetadata{{}})
require.Equal(t, 1, creator.calls)
require.Equal(t, 1, creator.caller.calls)
require.Equal(t, 1, creator.caller.claimsCalls)
require.Equal(t, 1, rollup.calls)
levelFilter := testlog.NewLevelFilter(log.LevelError)
messageFilter := testlog.NewMessageFilter(failedForecaseLog)
require.Nil(t, logs.FindLog(levelFilter, messageFilter))
levelFilter = testlog.NewLevelFilter(log.LevelDebug)
messageFilter = testlog.NewMessageFilter(expectedInProgressLog)
require.Nil(t, logs.FindLog(levelFilter, messageFilter))
levelFilter = testlog.NewLevelFilter(log.LevelDebug)
messageFilter = testlog.NewMessageFilter(expectedResultLog)
l := logs.FindLog(levelFilter, messageFilter)
require.NotNil(t, l)
require.Equal(t, common.Hash{}, l.AttrValue("rootClaim"))
require.Equal(t, mockRootClaim, l.AttrValue("expected"))
require.Equal(t, types.GameStatusChallengerWon, l.AttrValue("status"))
})
t.Run("DisagreeDefenderWins", func(t *testing.T) {
forecast, creator, rollup, logs := setupForecastTest(t)
creator.caller = &mockGameCaller{status: types.GameStatusInProgress}
creator.caller.rootClaim = common.Hash{}
creator.caller.claims = createDeepClaimList()[:1]
forecast.Forecast(context.Background(), []types.GameMetadata{{}})
require.Equal(t, 1, creator.calls)
require.Equal(t, 1, creator.caller.calls)
require.Equal(t, 1, creator.caller.claimsCalls)
require.Equal(t, 1, rollup.calls)
levelFilter := testlog.NewLevelFilter(log.LevelError)
messageFilter := testlog.NewMessageFilter(failedForecaseLog)
require.Nil(t, logs.FindLog(levelFilter, messageFilter))
levelFilter = testlog.NewLevelFilter(log.LevelDebug)
messageFilter = testlog.NewMessageFilter(expectedInProgressLog)
require.Nil(t, logs.FindLog(levelFilter, messageFilter))
levelFilter = testlog.NewLevelFilter(log.LevelWarn)
messageFilter = testlog.NewMessageFilter(unexpectedResultLog)
l := logs.FindLog(levelFilter, messageFilter)
require.NotNil(t, l)
require.Equal(t, common.Hash{}, l.AttrValue("rootClaim"))
require.Equal(t, mockRootClaim, l.AttrValue("expected"))
require.Equal(t, types.GameStatusDefenderWon, l.AttrValue("status"))
})
}
func setupForecastTest(t *testing.T) (*forecast, *mockGameCallerCreator, *stubOutputValidator, *testlog.CapturingHandler) {
logger, capturedLogs := testlog.CaptureLogger(t, log.LvlDebug)
validator := &stubOutputValidator{}
caller := &mockGameCaller{rootClaim: mockRootClaim}
creator := &mockGameCallerCreator{caller: caller}
return newForecast(logger, creator, validator), creator, validator, capturedLogs
}
......@@ -14,6 +14,7 @@ import (
)
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 BlockNumberFetcher func(ctx context.Context) (uint64, error)
type FactoryGameFetcher func(ctx context.Context, blockHash common.Hash, earliestTimestamp uint64) ([]types.GameMetadata, error)
......@@ -30,6 +31,7 @@ type gameMonitor struct {
monitorInterval time.Duration
detect Detect
forecast Forecast
fetchGames FactoryGameFetcher
fetchBlockHash BlockHashFetcher
fetchBlockNumber BlockNumberFetcher
......@@ -42,6 +44,7 @@ func newGameMonitor(
monitorInterval time.Duration,
gameWindow time.Duration,
detect Detect,
forecast Forecast,
factory FactoryGameFetcher,
fetchBlockNumber BlockNumberFetcher,
fetchBlockHash BlockHashFetcher,
......@@ -54,6 +57,7 @@ func newGameMonitor(
monitorInterval: monitorInterval,
gameWindow: gameWindow,
detect: detect,
forecast: forecast,
fetchGames: factory,
fetchBlockNumber: fetchBlockNumber,
fetchBlockHash: fetchBlockHash,
......@@ -87,6 +91,7 @@ func (m *gameMonitor) monitorGames() error {
return fmt.Errorf("failed to load games: %w", err)
}
m.detect(m.ctx, games)
m.forecast(m.ctx, games)
return nil
}
......
......@@ -23,20 +23,20 @@ func TestMonitor_MinGameTimestamp(t *testing.T) {
t.Parallel()
t.Run("ZeroGameWindow", func(t *testing.T) {
monitor, _, _ := setupMonitorTest(t)
monitor, _, _, _ := setupMonitorTest(t)
monitor.gameWindow = time.Duration(0)
require.Equal(t, monitor.minGameTimestamp(), uint64(0))
})
t.Run("ZeroClock", func(t *testing.T) {
monitor, _, _ := setupMonitorTest(t)
monitor, _, _, _ := setupMonitorTest(t)
monitor.gameWindow = time.Minute
monitor.clock = clock.NewDeterministicClock(time.Unix(0, 0))
require.Equal(t, uint64(0), monitor.minGameTimestamp())
})
t.Run("ValidArithmetic", func(t *testing.T) {
monitor, _, _ := setupMonitorTest(t)
monitor, _, _, _ := setupMonitorTest(t)
monitor.gameWindow = time.Minute
frozen := time.Unix(int64(time.Hour.Seconds()), 0)
monitor.clock = clock.NewDeterministicClock(frozen)
......@@ -49,7 +49,7 @@ func TestMonitor_MonitorGames(t *testing.T) {
t.Parallel()
t.Run("FailedFetchBlocknumber", func(t *testing.T) {
monitor, _, _ := setupMonitorTest(t)
monitor, _, _, _ := setupMonitorTest(t)
boom := errors.New("boom")
monitor.fetchBlockNumber = func(ctx context.Context) (uint64, error) {
return 0, boom
......@@ -59,7 +59,7 @@ func TestMonitor_MonitorGames(t *testing.T) {
})
t.Run("FailedFetchBlockHash", func(t *testing.T) {
monitor, _, _ := setupMonitorTest(t)
monitor, _, _, _ := setupMonitorTest(t)
boom := errors.New("boom")
monitor.fetchBlockHash = func(ctx context.Context, number *big.Int) (common.Hash, error) {
return common.Hash{}, boom
......@@ -69,7 +69,7 @@ func TestMonitor_MonitorGames(t *testing.T) {
})
t.Run("DetectsWithNoGames", func(t *testing.T) {
monitor, factory, detector := setupMonitorTest(t)
monitor, factory, detector, _ := setupMonitorTest(t)
factory.games = []types.GameMetadata{}
err := monitor.monitorGames()
require.NoError(t, err)
......@@ -77,7 +77,7 @@ func TestMonitor_MonitorGames(t *testing.T) {
})
t.Run("DetectsMultipleGames", func(t *testing.T) {
monitor, factory, detector := setupMonitorTest(t)
monitor, factory, detector, _ := setupMonitorTest(t)
factory.games = []types.GameMetadata{{}, {}, {}}
err := monitor.monitorGames()
require.NoError(t, err)
......@@ -89,7 +89,7 @@ func TestMonitor_StartMonitoring(t *testing.T) {
t.Run("MonitorsGames", func(t *testing.T) {
addr1 := common.Address{0xaa}
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.maxSuccess = len(factory.games) // Only allow two successful fetches
......@@ -102,7 +102,7 @@ func TestMonitor_StartMonitoring(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")
monitor.StartMonitoring()
......@@ -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)
fetchBlockNum := func(ctx context.Context) (uint64, error) {
return 1, nil
......@@ -134,6 +134,7 @@ func setupMonitorTest(t *testing.T) (*gameMonitor, *mockFactory, *mockDetector)
cl.Start()
factory := &mockFactory{}
detect := &mockDetector{}
forecast := &mockForecast{}
monitor := newGameMonitor(
context.Background(),
logger,
......@@ -141,11 +142,20 @@ func setupMonitorTest(t *testing.T) (*gameMonitor, *mockFactory, *mockDetector)
monitorInterval,
time.Duration(10*time.Second),
detect.Detect,
forecast.Forecast,
factory.GetGamesAtOrAfter,
fetchBlockNum,
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 {
......
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 {
cl clock.Clock
metadata *metadataCreator
forecast *forecast
game *gameCallerCreator
rollupClient *sources.RollupClient
detector *detector
validator *outputValidator
......@@ -78,9 +79,9 @@ func (s *Service) initFromConfig(ctx context.Context, cfg *config.Config) error
if err := s.initOutputRollupClient(ctx, cfg); err != nil {
return fmt.Errorf("failed to init rollup client: %w", err)
}
s.initMetadataCreator()
s.initOutputValidator()
s.initDetector()
s.initGameCallerCreator()
s.initForecast(cfg)
s.initDetector()
s.initMonitor(ctx, cfg)
......@@ -94,8 +95,16 @@ func (s *Service) initOutputValidator() {
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() {
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 {
......@@ -107,10 +116,6 @@ func (s *Service) initOutputRollupClient(ctx context.Context, cfg *config.Config
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 {
l1Client, err := dial.DialEthClientWithTimeout(ctx, dial.DefaultDialTimeout, s.logger, cfg.L1EthRpc)
if err != nil {
......@@ -180,6 +185,7 @@ func (s *Service) initMonitor(ctx context.Context, cfg *config.Config) {
cfg.MonitorInterval,
cfg.GameWindow,
s.detector.Detect,
s.forecast.Forecast,
s.factoryContract.GetGamesAtOrAfter,
s.l1Client.BlockNumber,
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