Commit d67991a3 authored by Adrian Sutton's avatar Adrian Sutton Committed by GitHub

op-dispute-mon: Use game data from previous update cycle if update fails (#12481)

parent 27f97044
...@@ -183,6 +183,8 @@ type Metricer interface { ...@@ -183,6 +183,8 @@ type Metricer interface {
RecordL2Challenges(agreement bool, count int) RecordL2Challenges(agreement bool, count int)
RecordOldestGameUpdateTime(t time.Time)
caching.Metrics caching.Metrics
contractMetrics.ContractMetricer contractMetrics.ContractMetricer
} }
...@@ -216,6 +218,7 @@ type Metrics struct { ...@@ -216,6 +218,7 @@ type Metrics struct {
honestWithdrawableAmounts prometheus.GaugeVec honestWithdrawableAmounts prometheus.GaugeVec
lastOutputFetch prometheus.Gauge lastOutputFetch prometheus.Gauge
oldestGameUpdateTime prometheus.Gauge
gamesAgreement prometheus.GaugeVec gamesAgreement prometheus.GaugeVec
latestValidProposalL2Block prometheus.Gauge latestValidProposalL2Block prometheus.Gauge
...@@ -269,6 +272,12 @@ func NewMetrics() *Metrics { ...@@ -269,6 +272,12 @@ func NewMetrics() *Metrics {
Name: "last_output_fetch", Name: "last_output_fetch",
Help: "Timestamp of the last output fetch", Help: "Timestamp of the last output fetch",
}), }),
oldestGameUpdateTime: factory.NewGauge(prometheus.GaugeOpts{
Namespace: Namespace,
Name: "oldest_game_update_time",
Help: "Timestamp the least recently updated game " +
"or the time of the last update cycle if there were no games in the monitoring window",
}),
honestActorClaims: *factory.NewGaugeVec(prometheus.GaugeOpts{ honestActorClaims: *factory.NewGaugeVec(prometheus.GaugeOpts{
Namespace: Namespace, Namespace: Namespace,
Name: "honest_actor_claims", Name: "honest_actor_claims",
...@@ -499,6 +508,10 @@ func (m *Metrics) RecordOutputFetchTime(timestamp float64) { ...@@ -499,6 +508,10 @@ func (m *Metrics) RecordOutputFetchTime(timestamp float64) {
m.lastOutputFetch.Set(timestamp) m.lastOutputFetch.Set(timestamp)
} }
func (m *Metrics) RecordOldestGameUpdateTime(t time.Time) {
m.oldestGameUpdateTime.Set(float64(t.Unix()))
}
func (m *Metrics) RecordGameAgreement(status GameAgreementStatus, count int) { func (m *Metrics) RecordGameAgreement(status GameAgreementStatus, count int) {
m.gamesAgreement.WithLabelValues(labelValuesFor(status)...).Set(float64(count)) m.gamesAgreement.WithLabelValues(labelValuesFor(status)...).Set(float64(count))
} }
......
...@@ -36,6 +36,8 @@ func (*NoopMetricsImpl) RecordWithdrawalRequests(_ common.Address, _ bool, _ int ...@@ -36,6 +36,8 @@ func (*NoopMetricsImpl) RecordWithdrawalRequests(_ common.Address, _ bool, _ int
func (*NoopMetricsImpl) RecordOutputFetchTime(_ float64) {} func (*NoopMetricsImpl) RecordOutputFetchTime(_ float64) {}
func (*NoopMetricsImpl) RecordOldestGameUpdateTime(_ time.Time) {}
func (*NoopMetricsImpl) RecordGameAgreement(_ GameAgreementStatus, _ int) {} func (*NoopMetricsImpl) RecordGameAgreement(_ GameAgreementStatus, _ int) {}
func (*NoopMetricsImpl) RecordLatestValidProposalL2Block(_ uint64) {} func (*NoopMetricsImpl) RecordLatestValidProposalL2Block(_ uint64) {}
......
...@@ -9,9 +9,11 @@ import ( ...@@ -9,9 +9,11 @@ import (
gameTypes "github.com/ethereum-optimism/optimism/op-challenger/game/types" gameTypes "github.com/ethereum-optimism/optimism/op-challenger/game/types"
monTypes "github.com/ethereum-optimism/optimism/op-dispute-mon/mon/types" monTypes "github.com/ethereum-optimism/optimism/op-dispute-mon/mon/types"
"github.com/ethereum-optimism/optimism/op-service/clock"
"github.com/ethereum-optimism/optimism/op-service/sources/batching/rpcblock" "github.com/ethereum-optimism/optimism/op-service/sources/batching/rpcblock"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/log"
"golang.org/x/exp/maps"
) )
var ( var (
...@@ -29,20 +31,23 @@ type Enricher interface { ...@@ -29,20 +31,23 @@ type Enricher interface {
type Extractor struct { type Extractor struct {
logger log.Logger logger log.Logger
clock clock.Clock
createContract CreateGameCaller createContract CreateGameCaller
fetchGames FactoryGameFetcher fetchGames FactoryGameFetcher
maxConcurrency int maxConcurrency int
enrichers []Enricher enrichers []Enricher
ignoredGames map[common.Address]bool ignoredGames map[common.Address]bool
latestGameData map[common.Address]*monTypes.EnrichedGameData
} }
func NewExtractor(logger log.Logger, creator CreateGameCaller, fetchGames FactoryGameFetcher, ignoredGames []common.Address, maxConcurrency uint, enrichers ...Enricher) *Extractor { func NewExtractor(logger log.Logger, cl clock.Clock, creator CreateGameCaller, fetchGames FactoryGameFetcher, ignoredGames []common.Address, maxConcurrency uint, enrichers ...Enricher) *Extractor {
ignored := make(map[common.Address]bool) ignored := make(map[common.Address]bool)
for _, game := range ignoredGames { for _, game := range ignoredGames {
ignored[game] = true ignored[game] = true
} }
return &Extractor{ return &Extractor{
logger: logger, logger: logger,
clock: cl,
createContract: creator, createContract: creator,
fetchGames: fetchGames, fetchGames: fetchGames,
maxConcurrency: int(maxConcurrency), maxConcurrency: int(maxConcurrency),
...@@ -61,7 +66,6 @@ func (e *Extractor) Extract(ctx context.Context, blockHash common.Hash, minTimes ...@@ -61,7 +66,6 @@ func (e *Extractor) Extract(ctx context.Context, blockHash common.Hash, minTimes
} }
func (e *Extractor) enrichGames(ctx context.Context, blockHash common.Hash, games []gameTypes.GameMetadata) ([]*monTypes.EnrichedGameData, int, int) { func (e *Extractor) enrichGames(ctx context.Context, blockHash common.Hash, games []gameTypes.GameMetadata) ([]*monTypes.EnrichedGameData, int, int) {
var enrichedGames []*monTypes.EnrichedGameData
var ignored atomic.Int32 var ignored atomic.Int32
var failed atomic.Int32 var failed atomic.Int32
...@@ -101,8 +105,14 @@ func (e *Extractor) enrichGames(ctx context.Context, blockHash common.Hash, game ...@@ -101,8 +105,14 @@ func (e *Extractor) enrichGames(ctx context.Context, blockHash common.Hash, game
}() }()
} }
// Push each game into the channel // Create a new store for game data. This ensures any games no longer in the monitoring set are dropped.
updatedGameData := make(map[common.Address]*monTypes.EnrichedGameData)
// Push each game into the channel and store the latest cached game data as a default if fetching fails
for _, game := range games { for _, game := range games {
previousData := e.latestGameData[game.Proxy]
if previousData != nil {
updatedGameData[game.Proxy] = previousData
}
gameCh <- game gameCh <- game
} }
close(gameCh) close(gameCh)
...@@ -112,9 +122,10 @@ func (e *Extractor) enrichGames(ctx context.Context, blockHash common.Hash, game ...@@ -112,9 +122,10 @@ func (e *Extractor) enrichGames(ctx context.Context, blockHash common.Hash, game
// Read the results // Read the results
for enrichedGame := range enrichedCh { for enrichedGame := range enrichedCh {
enrichedGames = append(enrichedGames, enrichedGame) updatedGameData[enrichedGame.Proxy] = enrichedGame
} }
return enrichedGames, int(ignored.Load()), int(failed.Load()) e.latestGameData = updatedGameData
return maps.Values(updatedGameData), int(ignored.Load()), int(failed.Load())
} }
func (e *Extractor) enrichGame(ctx context.Context, blockHash common.Hash, game gameTypes.GameMetadata) (*monTypes.EnrichedGameData, error) { func (e *Extractor) enrichGame(ctx context.Context, blockHash common.Hash, game gameTypes.GameMetadata) (*monTypes.EnrichedGameData, error) {
...@@ -138,6 +149,7 @@ func (e *Extractor) enrichGame(ctx context.Context, blockHash common.Hash, game ...@@ -138,6 +149,7 @@ func (e *Extractor) enrichGame(ctx context.Context, blockHash common.Hash, game
enrichedClaims[i] = monTypes.EnrichedClaim{Claim: claim} enrichedClaims[i] = monTypes.EnrichedClaim{Claim: claim}
} }
enrichedGame := &monTypes.EnrichedGameData{ enrichedGame := &monTypes.EnrichedGameData{
LastUpdateTime: e.clock.Now(),
GameMetadata: game, GameMetadata: game,
L1Head: meta.L1Head, L1Head: meta.L1Head,
L2BlockNumber: meta.L2BlockNum, L2BlockNumber: meta.L2BlockNum,
......
...@@ -9,6 +9,7 @@ import ( ...@@ -9,6 +9,7 @@ import (
"github.com/ethereum-optimism/optimism/op-challenger/game/fault/contracts" "github.com/ethereum-optimism/optimism/op-challenger/game/fault/contracts"
monTypes "github.com/ethereum-optimism/optimism/op-dispute-mon/mon/types" monTypes "github.com/ethereum-optimism/optimism/op-dispute-mon/mon/types"
"github.com/ethereum-optimism/optimism/op-service/clock"
"github.com/ethereum-optimism/optimism/op-service/sources/batching/rpcblock" "github.com/ethereum-optimism/optimism/op-service/sources/batching/rpcblock"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
...@@ -26,7 +27,7 @@ var ( ...@@ -26,7 +27,7 @@ var (
func TestExtractor_Extract(t *testing.T) { func TestExtractor_Extract(t *testing.T) {
t.Run("FetchGamesError", func(t *testing.T) { t.Run("FetchGamesError", func(t *testing.T) {
extractor, _, games, _ := setupExtractorTest(t) extractor, _, games, _, _ := setupExtractorTest(t)
games.err = errors.New("boom") games.err = errors.New("boom")
_, _, _, err := extractor.Extract(context.Background(), common.Hash{}, 0) _, _, _, err := extractor.Extract(context.Background(), common.Hash{}, 0)
require.ErrorIs(t, err, games.err) require.ErrorIs(t, err, games.err)
...@@ -34,7 +35,7 @@ func TestExtractor_Extract(t *testing.T) { ...@@ -34,7 +35,7 @@ func TestExtractor_Extract(t *testing.T) {
}) })
t.Run("CreateGameErrorLog", func(t *testing.T) { t.Run("CreateGameErrorLog", func(t *testing.T) {
extractor, creator, games, logs := setupExtractorTest(t) extractor, creator, games, logs, _ := setupExtractorTest(t)
games.games = []gameTypes.GameMetadata{{}} games.games = []gameTypes.GameMetadata{{}}
creator.err = errors.New("boom") creator.err = errors.New("boom")
enriched, ignored, failed, err := extractor.Extract(context.Background(), common.Hash{}, 0) enriched, ignored, failed, err := extractor.Extract(context.Background(), common.Hash{}, 0)
...@@ -50,7 +51,7 @@ func TestExtractor_Extract(t *testing.T) { ...@@ -50,7 +51,7 @@ func TestExtractor_Extract(t *testing.T) {
}) })
t.Run("MetadataFetchErrorLog", func(t *testing.T) { t.Run("MetadataFetchErrorLog", func(t *testing.T) {
extractor, creator, games, logs := setupExtractorTest(t) extractor, creator, games, logs, _ := setupExtractorTest(t)
games.games = []gameTypes.GameMetadata{{}} games.games = []gameTypes.GameMetadata{{}}
creator.caller.metadataErr = errors.New("boom") creator.caller.metadataErr = errors.New("boom")
enriched, ignored, failed, err := extractor.Extract(context.Background(), common.Hash{}, 0) enriched, ignored, failed, err := extractor.Extract(context.Background(), common.Hash{}, 0)
...@@ -66,7 +67,7 @@ func TestExtractor_Extract(t *testing.T) { ...@@ -66,7 +67,7 @@ func TestExtractor_Extract(t *testing.T) {
}) })
t.Run("ClaimsFetchErrorLog", func(t *testing.T) { t.Run("ClaimsFetchErrorLog", func(t *testing.T) {
extractor, creator, games, logs := setupExtractorTest(t) extractor, creator, games, logs, _ := setupExtractorTest(t)
games.games = []gameTypes.GameMetadata{{}} games.games = []gameTypes.GameMetadata{{}}
creator.caller.claimsErr = errors.New("boom") creator.caller.claimsErr = errors.New("boom")
enriched, ignored, failed, err := extractor.Extract(context.Background(), common.Hash{}, 0) enriched, ignored, failed, err := extractor.Extract(context.Background(), common.Hash{}, 0)
...@@ -82,7 +83,7 @@ func TestExtractor_Extract(t *testing.T) { ...@@ -82,7 +83,7 @@ func TestExtractor_Extract(t *testing.T) {
}) })
t.Run("Success", func(t *testing.T) { t.Run("Success", func(t *testing.T) {
extractor, creator, games, _ := setupExtractorTest(t) extractor, creator, games, _, _ := setupExtractorTest(t)
games.games = []gameTypes.GameMetadata{{}} games.games = []gameTypes.GameMetadata{{}}
enriched, ignored, failed, err := extractor.Extract(context.Background(), common.Hash{}, 0) enriched, ignored, failed, err := extractor.Extract(context.Background(), common.Hash{}, 0)
require.NoError(t, err) require.NoError(t, err)
...@@ -97,7 +98,7 @@ func TestExtractor_Extract(t *testing.T) { ...@@ -97,7 +98,7 @@ func TestExtractor_Extract(t *testing.T) {
t.Run("EnricherFails", func(t *testing.T) { t.Run("EnricherFails", func(t *testing.T) {
enricher := &mockEnricher{err: errors.New("whoops")} enricher := &mockEnricher{err: errors.New("whoops")}
extractor, _, games, logs := setupExtractorTest(t, enricher) extractor, _, games, logs, _ := setupExtractorTest(t, enricher)
games.games = []gameTypes.GameMetadata{{}} games.games = []gameTypes.GameMetadata{{}}
enriched, ignored, failed, err := extractor.Extract(context.Background(), common.Hash{}, 0) enriched, ignored, failed, err := extractor.Extract(context.Background(), common.Hash{}, 0)
require.NoError(t, err) require.NoError(t, err)
...@@ -110,7 +111,7 @@ func TestExtractor_Extract(t *testing.T) { ...@@ -110,7 +111,7 @@ func TestExtractor_Extract(t *testing.T) {
t.Run("EnricherSuccess", func(t *testing.T) { t.Run("EnricherSuccess", func(t *testing.T) {
enricher := &mockEnricher{} enricher := &mockEnricher{}
extractor, _, games, _ := setupExtractorTest(t, enricher) extractor, _, games, _, _ := setupExtractorTest(t, enricher)
games.games = []gameTypes.GameMetadata{{}} games.games = []gameTypes.GameMetadata{{}}
enriched, ignored, failed, err := extractor.Extract(context.Background(), common.Hash{}, 0) enriched, ignored, failed, err := extractor.Extract(context.Background(), common.Hash{}, 0)
require.NoError(t, err) require.NoError(t, err)
...@@ -123,8 +124,8 @@ func TestExtractor_Extract(t *testing.T) { ...@@ -123,8 +124,8 @@ func TestExtractor_Extract(t *testing.T) {
t.Run("MultipleEnrichersMultipleGames", func(t *testing.T) { t.Run("MultipleEnrichersMultipleGames", func(t *testing.T) {
enricher1 := &mockEnricher{} enricher1 := &mockEnricher{}
enricher2 := &mockEnricher{} enricher2 := &mockEnricher{}
extractor, _, games, _ := setupExtractorTest(t, enricher1, enricher2) extractor, _, games, _, _ := setupExtractorTest(t, enricher1, enricher2)
games.games = []gameTypes.GameMetadata{{}, {}} games.games = []gameTypes.GameMetadata{{Proxy: common.Address{0xaa}}, {Proxy: common.Address{0xbb}}}
enriched, ignored, failed, err := extractor.Extract(context.Background(), common.Hash{}, 0) enriched, ignored, failed, err := extractor.Extract(context.Background(), common.Hash{}, 0)
require.NoError(t, err) require.NoError(t, err)
require.Zero(t, ignored) require.Zero(t, ignored)
...@@ -136,7 +137,7 @@ func TestExtractor_Extract(t *testing.T) { ...@@ -136,7 +137,7 @@ func TestExtractor_Extract(t *testing.T) {
t.Run("IgnoreGames", func(t *testing.T) { t.Run("IgnoreGames", func(t *testing.T) {
enricher1 := &mockEnricher{} enricher1 := &mockEnricher{}
extractor, _, games, logs := setupExtractorTest(t, enricher1) extractor, _, games, logs, _ := setupExtractorTest(t, enricher1)
// Two games, one of which is ignored // Two games, one of which is ignored
games.games = []gameTypes.GameMetadata{{Proxy: ignoredGames[0]}, {Proxy: common.Address{0xaa}}} games.games = []gameTypes.GameMetadata{{Proxy: ignoredGames[0]}, {Proxy: common.Address{0xaa}}}
enriched, ignored, failed, err := extractor.Extract(context.Background(), common.Hash{}, 0) enriched, ignored, failed, err := extractor.Extract(context.Background(), common.Hash{}, 0)
...@@ -152,6 +153,47 @@ func TestExtractor_Extract(t *testing.T) { ...@@ -152,6 +153,47 @@ func TestExtractor_Extract(t *testing.T) {
testlog.NewMessageFilter("Ignoring game"), testlog.NewMessageFilter("Ignoring game"),
testlog.NewAttributesFilter("game", ignoredGames[0].Hex()))) testlog.NewAttributesFilter("game", ignoredGames[0].Hex())))
}) })
t.Run("UseCachedValueOnFailure", func(t *testing.T) {
enricher := &mockEnricher{}
extractor, _, games, _, cl := setupExtractorTest(t, enricher)
gameA := common.Address{0xaa}
gameB := common.Address{0xbb}
games.games = []gameTypes.GameMetadata{{Proxy: gameA}, {Proxy: gameB}}
// First fetch succeeds and the results should be cached
enriched, ignored, failed, err := extractor.Extract(context.Background(), common.Hash{}, 0)
require.NoError(t, err)
require.Zero(t, ignored)
require.Zero(t, failed)
require.Len(t, enriched, 2)
require.Equal(t, 2, enricher.calls)
firstUpdateTime := cl.Now()
require.Equal(t, firstUpdateTime, enriched[0].LastUpdateTime)
require.Equal(t, firstUpdateTime, enriched[1].LastUpdateTime)
cl.AdvanceTime(2 * time.Minute)
secondUpdateTime := cl.Now()
enricher.action = func(game *monTypes.EnrichedGameData) error {
if game.Proxy == gameA {
return errors.New("boom")
}
// Updated games will have a different status
game.Status = gameTypes.GameStatusChallengerWon
return nil
}
// Second fetch fails for one of the two games, it's cached value should be used.
enriched, ignored, failed, err = extractor.Extract(context.Background(), common.Hash{}, 0)
require.NoError(t, err)
require.Zero(t, ignored)
require.Equal(t, 1, failed)
require.Len(t, enriched, 2)
require.Equal(t, 4, enricher.calls)
require.Equal(t, enriched[0].Status, gameTypes.GameStatusInProgress) // Uses cached value from game A
require.Equal(t, enriched[1].Status, gameTypes.GameStatusChallengerWon) // Updates game B
require.Equal(t, firstUpdateTime, enriched[0].LastUpdateTime)
require.Equal(t, secondUpdateTime, enriched[1].LastUpdateTime)
})
} }
func verifyLogs(t *testing.T, logs *testlog.CapturingHandler, createErr, metadataErr, claimsErr, durationErr int) { func verifyLogs(t *testing.T, logs *testlog.CapturingHandler, createErr, metadataErr, claimsErr, durationErr int) {
...@@ -170,20 +212,22 @@ func verifyLogs(t *testing.T, logs *testlog.CapturingHandler, createErr, metadat ...@@ -170,20 +212,22 @@ func verifyLogs(t *testing.T, logs *testlog.CapturingHandler, createErr, metadat
require.Len(t, l, durationErr) require.Len(t, l, durationErr)
} }
func setupExtractorTest(t *testing.T, enrichers ...Enricher) (*Extractor, *mockGameCallerCreator, *mockGameFetcher, *testlog.CapturingHandler) { func setupExtractorTest(t *testing.T, enrichers ...Enricher) (*Extractor, *mockGameCallerCreator, *mockGameFetcher, *testlog.CapturingHandler, *clock.DeterministicClock) {
logger, capturedLogs := testlog.CaptureLogger(t, log.LvlDebug) logger, capturedLogs := testlog.CaptureLogger(t, log.LvlDebug)
games := &mockGameFetcher{} games := &mockGameFetcher{}
caller := &mockGameCaller{rootClaim: mockRootClaim} caller := &mockGameCaller{rootClaim: mockRootClaim}
creator := &mockGameCallerCreator{caller: caller} creator := &mockGameCallerCreator{caller: caller}
cl := clock.NewDeterministicClock(time.Unix(48294294, 58))
extractor := NewExtractor( extractor := NewExtractor(
logger, logger,
cl,
creator.CreateGameCaller, creator.CreateGameCaller,
games.FetchGames, games.FetchGames,
ignoredGames, ignoredGames,
5, 5,
enrichers..., enrichers...,
) )
return extractor, creator, games, capturedLogs return extractor, creator, games, capturedLogs, cl
} }
type mockGameFetcher struct { type mockGameFetcher struct {
...@@ -313,9 +357,13 @@ func (m *mockGameCaller) IsResolved(_ context.Context, _ rpcblock.Block, claims ...@@ -313,9 +357,13 @@ func (m *mockGameCaller) IsResolved(_ context.Context, _ rpcblock.Block, claims
type mockEnricher struct { type mockEnricher struct {
err error err error
calls int calls int
action func(game *monTypes.EnrichedGameData) error
} }
func (m *mockEnricher) Enrich(_ context.Context, _ rpcblock.Block, _ GameCaller, _ *monTypes.EnrichedGameData) error { func (m *mockEnricher) Enrich(_ context.Context, _ rpcblock.Block, _ GameCaller, game *monTypes.EnrichedGameData) error {
m.calls++ m.calls++
if m.action != nil {
return m.action(game)
}
return m.err return m.err
} }
...@@ -14,8 +14,6 @@ import ( ...@@ -14,8 +14,6 @@ import (
) )
type ForecastResolution func(games []*types.EnrichedGameData, ignoredCount, failedCount int) type ForecastResolution func(games []*types.EnrichedGameData, ignoredCount, failedCount int)
type Bonds func(games []*types.EnrichedGameData)
type Resolutions func(games []*types.EnrichedGameData)
type Monitor func(games []*types.EnrichedGameData) type Monitor func(games []*types.EnrichedGameData)
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)
...@@ -38,11 +36,7 @@ type gameMonitor struct { ...@@ -38,11 +36,7 @@ type gameMonitor struct {
monitorInterval time.Duration monitorInterval time.Duration
forecast ForecastResolution forecast ForecastResolution
bonds Bonds monitors []Monitor
resolutions Resolutions
claims Monitor
withdrawals Monitor
l2Challenges Monitor
extract Extract extract Extract
fetchBlockHash BlockHashFetcher fetchBlockHash BlockHashFetcher
fetchBlockNumber BlockNumberFetcher fetchBlockNumber BlockNumberFetcher
...@@ -55,16 +49,11 @@ func newGameMonitor( ...@@ -55,16 +49,11 @@ func newGameMonitor(
metrics MonitorMetrics, metrics MonitorMetrics,
monitorInterval time.Duration, monitorInterval time.Duration,
gameWindow time.Duration, gameWindow time.Duration,
forecast ForecastResolution,
bonds Bonds,
resolutions Resolutions,
claims Monitor,
withdrawals Monitor,
l2Challenges Monitor,
extract Extract,
fetchBlockNumber BlockNumberFetcher,
fetchBlockHash BlockHashFetcher, fetchBlockHash BlockHashFetcher,
) *gameMonitor { fetchBlockNumber BlockNumberFetcher,
extract Extract,
forecast ForecastResolution,
monitors ...Monitor) *gameMonitor {
return &gameMonitor{ return &gameMonitor{
logger: logger, logger: logger,
clock: cl, clock: cl,
...@@ -74,11 +63,7 @@ func newGameMonitor( ...@@ -74,11 +63,7 @@ func newGameMonitor(
monitorInterval: monitorInterval, monitorInterval: monitorInterval,
gameWindow: gameWindow, gameWindow: gameWindow,
forecast: forecast, forecast: forecast,
bonds: bonds, monitors: monitors,
resolutions: resolutions,
claims: claims,
withdrawals: withdrawals,
l2Challenges: l2Challenges,
extract: extract, extract: extract,
fetchBlockNumber: fetchBlockNumber, fetchBlockNumber: fetchBlockNumber,
fetchBlockHash: fetchBlockHash, fetchBlockHash: fetchBlockHash,
...@@ -101,12 +86,10 @@ func (m *gameMonitor) monitorGames() error { ...@@ -101,12 +86,10 @@ func (m *gameMonitor) monitorGames() error {
if err != nil { if err != nil {
return fmt.Errorf("failed to load games: %w", err) return fmt.Errorf("failed to load games: %w", err)
} }
m.resolutions(enrichedGames)
m.forecast(enrichedGames, ignored, failed) m.forecast(enrichedGames, ignored, failed)
m.bonds(enrichedGames) for _, monitor := range m.monitors {
m.claims(enrichedGames) monitor(enrichedGames)
m.withdrawals(enrichedGames) }
m.l2Challenges(enrichedGames)
timeTaken := m.clock.Since(start) timeTaken := m.clock.Since(start)
m.metrics.RecordMonitorDuration(timeTaken) m.metrics.RecordMonitorDuration(timeTaken)
m.logger.Info("Completed monitoring update", "blockNumber", blockNumber, "blockHash", blockHash, "duration", timeTaken, "games", len(enrichedGames), "ignored", ignored, "failed", failed) m.logger.Info("Completed monitoring update", "blockNumber", blockNumber, "blockHash", blockHash, "duration", timeTaken, "games", len(enrichedGames), "ignored", ignored, "failed", failed)
......
...@@ -25,7 +25,7 @@ func TestMonitor_MonitorGames(t *testing.T) { ...@@ -25,7 +25,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
...@@ -35,7 +35,7 @@ func TestMonitor_MonitorGames(t *testing.T) { ...@@ -35,7 +35,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
...@@ -45,29 +45,25 @@ func TestMonitor_MonitorGames(t *testing.T) { ...@@ -45,29 +45,25 @@ func TestMonitor_MonitorGames(t *testing.T) {
}) })
t.Run("MonitorsWithNoGames", func(t *testing.T) { t.Run("MonitorsWithNoGames", func(t *testing.T) {
monitor, factory, forecast, bonds, withdrawals, resolutions, claims, l2Challenges := setupMonitorTest(t) monitor, factory, forecast, monitors := setupMonitorTest(t)
factory.games = []*monTypes.EnrichedGameData{} factory.games = []*monTypes.EnrichedGameData{}
err := monitor.monitorGames() err := monitor.monitorGames()
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, 1, forecast.calls) require.Equal(t, 1, forecast.calls)
require.Equal(t, 1, bonds.calls) for _, m := range monitors {
require.Equal(t, 1, resolutions.calls) require.Equal(t, 1, m.calls)
require.Equal(t, 1, claims.calls) }
require.Equal(t, 1, withdrawals.calls)
require.Equal(t, 1, l2Challenges.calls)
}) })
t.Run("MonitorsMultipleGames", func(t *testing.T) { t.Run("MonitorsMultipleGames", func(t *testing.T) {
monitor, factory, forecast, bonds, withdrawals, resolutions, claims, l2Challenges := setupMonitorTest(t) monitor, factory, forecast, monitors := setupMonitorTest(t)
factory.games = []*monTypes.EnrichedGameData{{}, {}, {}} factory.games = []*monTypes.EnrichedGameData{{}, {}, {}}
err := monitor.monitorGames() err := monitor.monitorGames()
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, 1, forecast.calls) require.Equal(t, 1, forecast.calls)
require.Equal(t, 1, bonds.calls) for _, m := range monitors {
require.Equal(t, 1, resolutions.calls) require.Equal(t, 1, m.calls)
require.Equal(t, 1, claims.calls) }
require.Equal(t, 1, withdrawals.calls)
require.Equal(t, 1, l2Challenges.calls)
}) })
} }
...@@ -75,7 +71,7 @@ func TestMonitor_StartMonitoring(t *testing.T) { ...@@ -75,7 +71,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, forecaster, _, _, _, _, _ := setupMonitorTest(t) monitor, factory, forecaster, _ := setupMonitorTest(t)
factory.games = []*monTypes.EnrichedGameData{newEnrichedGameData(addr1, 9999), newEnrichedGameData(addr2, 9999)} factory.games = []*monTypes.EnrichedGameData{newEnrichedGameData(addr1, 9999), newEnrichedGameData(addr2, 9999)}
factory.maxSuccess = len(factory.games) // Only allow two successful fetches factory.maxSuccess = len(factory.games) // Only allow two successful fetches
...@@ -88,7 +84,7 @@ func TestMonitor_StartMonitoring(t *testing.T) { ...@@ -88,7 +84,7 @@ func TestMonitor_StartMonitoring(t *testing.T) {
}) })
t.Run("FailsToFetchGames", func(t *testing.T) { t.Run("FailsToFetchGames", func(t *testing.T) {
monitor, factory, forecaster, _, _, _, _, _ := setupMonitorTest(t) monitor, factory, forecaster, _ := setupMonitorTest(t)
factory.fetchErr = errors.New("boom") factory.fetchErr = errors.New("boom")
monitor.StartMonitoring() monitor.StartMonitoring()
...@@ -110,7 +106,7 @@ func newEnrichedGameData(proxy common.Address, timestamp uint64) *monTypes.Enric ...@@ -110,7 +106,7 @@ func newEnrichedGameData(proxy common.Address, timestamp uint64) *monTypes.Enric
} }
} }
func setupMonitorTest(t *testing.T) (*gameMonitor, *mockExtractor, *mockForecast, *mockBonds, *mockMonitor, *mockResolutionMonitor, *mockMonitor, *mockMonitor) { func setupMonitorTest(t *testing.T) (*gameMonitor, *mockExtractor, *mockForecast, []*mockMonitor) {
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
...@@ -123,37 +119,12 @@ func setupMonitorTest(t *testing.T) (*gameMonitor, *mockExtractor, *mockForecast ...@@ -123,37 +119,12 @@ func setupMonitorTest(t *testing.T) (*gameMonitor, *mockExtractor, *mockForecast
cl.Start() cl.Start()
extractor := &mockExtractor{} extractor := &mockExtractor{}
forecast := &mockForecast{} forecast := &mockForecast{}
bonds := &mockBonds{} monitor1 := &mockMonitor{}
resolutions := &mockResolutionMonitor{} monitor2 := &mockMonitor{}
claims := &mockMonitor{} monitor3 := &mockMonitor{}
withdrawals := &mockMonitor{} monitor := newGameMonitor(context.Background(), logger, cl, metrics.NoopMetrics, monitorInterval, 10*time.Second, fetchBlockHash, fetchBlockNum,
l2Challenges := &mockMonitor{} extractor.Extract, forecast.Forecast, monitor1.Check, monitor2.Check, monitor3.Check)
monitor := newGameMonitor( return monitor, extractor, forecast, []*mockMonitor{monitor1, monitor2, monitor3}
context.Background(),
logger,
cl,
metrics.NoopMetrics,
monitorInterval,
10*time.Second,
forecast.Forecast,
bonds.CheckBonds,
resolutions.CheckResolutions,
claims.Check,
withdrawals.Check,
l2Challenges.Check,
extractor.Extract,
fetchBlockNum,
fetchBlockHash,
)
return monitor, extractor, forecast, bonds, withdrawals, resolutions, claims, l2Challenges
}
type mockResolutionMonitor struct {
calls int
}
func (m *mockResolutionMonitor) CheckResolutions(games []*monTypes.EnrichedGameData) {
m.calls++
} }
type mockMonitor struct { type mockMonitor struct {
...@@ -172,14 +143,6 @@ func (m *mockForecast) Forecast(_ []*monTypes.EnrichedGameData, _, _ int) { ...@@ -172,14 +143,6 @@ func (m *mockForecast) Forecast(_ []*monTypes.EnrichedGameData, _, _ int) {
m.calls++ m.calls++
} }
type mockBonds struct {
calls int
}
func (m *mockBonds) CheckBonds(_ []*monTypes.EnrichedGameData) {
m.calls++
}
type mockExtractor struct { type mockExtractor struct {
fetchErr error fetchErr error
calls int calls int
......
...@@ -126,6 +126,7 @@ func (s *Service) initGameCallerCreator() { ...@@ -126,6 +126,7 @@ func (s *Service) initGameCallerCreator() {
func (s *Service) initExtractor(cfg *config.Config) { func (s *Service) initExtractor(cfg *config.Config) {
s.extractor = extract.NewExtractor( s.extractor = extract.NewExtractor(
s.logger, s.logger,
s.cl,
s.game.CreateContract, s.game.CreateContract,
s.factoryContract.GetGamesAtOrAfter, s.factoryContract.GetGamesAtOrAfter,
cfg.IgnoredGames, cfg.IgnoredGames,
...@@ -217,23 +218,17 @@ func (s *Service) initMonitor(ctx context.Context, cfg *config.Config) { ...@@ -217,23 +218,17 @@ func (s *Service) initMonitor(ctx context.Context, cfg *config.Config) {
return block.Hash(), nil return block.Hash(), nil
} }
l2ChallengesMonitor := NewL2ChallengesMonitor(s.logger, s.metrics) l2ChallengesMonitor := NewL2ChallengesMonitor(s.logger, s.metrics)
s.monitor = newGameMonitor( updateTimeMonitor := NewUpdateTimeMonitor(s.cl, s.metrics)
ctx, s.monitor = newGameMonitor(ctx, s.logger, s.cl, s.metrics, cfg.MonitorInterval, cfg.GameWindow, blockHashFetcher,
s.logger, s.l1Client.BlockNumber,
s.cl, s.extractor.Extract,
s.metrics,
cfg.MonitorInterval,
cfg.GameWindow,
s.forecast.Forecast, s.forecast.Forecast,
s.bonds.CheckBonds, s.bonds.CheckBonds,
s.resolutions.CheckResolutions, s.resolutions.CheckResolutions,
s.claims.CheckClaims, s.claims.CheckClaims,
s.withdrawals.CheckWithdrawals, s.withdrawals.CheckWithdrawals,
l2ChallengesMonitor.CheckL2Challenges, l2ChallengesMonitor.CheckL2Challenges,
s.extractor.Extract, updateTimeMonitor.CheckUpdateTimes)
s.l1Client.BlockNumber,
blockHashFetcher,
)
} }
func (s *Service) Start(ctx context.Context) error { func (s *Service) Start(ctx context.Context) error {
......
...@@ -18,6 +18,7 @@ type EnrichedClaim struct { ...@@ -18,6 +18,7 @@ type EnrichedClaim struct {
type EnrichedGameData struct { type EnrichedGameData struct {
types.GameMetadata types.GameMetadata
LastUpdateTime time.Time
L1Head common.Hash L1Head common.Hash
L1HeadNum uint64 L1HeadNum uint64
L2BlockNumber uint64 L2BlockNumber uint64
......
package mon
import (
"time"
"github.com/ethereum-optimism/optimism/op-dispute-mon/mon/types"
"github.com/ethereum-optimism/optimism/op-service/clock"
)
type UpdateTimeMetrics interface {
RecordOldestGameUpdateTime(t time.Time)
}
type UpdateTimeMonitor struct {
metrics UpdateTimeMetrics
clock clock.Clock
}
func NewUpdateTimeMonitor(cl clock.Clock, metrics UpdateTimeMetrics) *UpdateTimeMonitor {
return &UpdateTimeMonitor{clock: cl, metrics: metrics}
}
func (m *UpdateTimeMonitor) CheckUpdateTimes(games []*types.EnrichedGameData) {
// Report the current time if there are no games
// Otherwise the last update time would drop to 0 when there are no games, making it appear there were errors
earliest := m.clock.Now()
for _, game := range games {
if game.LastUpdateTime.Before(earliest) {
earliest = game.LastUpdateTime
}
}
m.metrics.RecordOldestGameUpdateTime(earliest)
}
package mon
import (
"testing"
"time"
"github.com/ethereum-optimism/optimism/op-dispute-mon/mon/types"
"github.com/ethereum-optimism/optimism/op-service/clock"
"github.com/stretchr/testify/require"
)
func TestUpdateTimeMonitor_NoGames(t *testing.T) {
m := &mockUpdateTimeMetrics{}
cl := clock.NewDeterministicClock(time.UnixMilli(45892))
monitor := NewUpdateTimeMonitor(cl, m)
monitor.CheckUpdateTimes(nil)
require.Equal(t, cl.Now(), m.oldestUpdateTime)
cl.AdvanceTime(time.Minute)
monitor.CheckUpdateTimes([]*types.EnrichedGameData{})
require.Equal(t, cl.Now(), m.oldestUpdateTime)
}
func TestUpdateTimeMonitor_ReportsOldestUpdateTime(t *testing.T) {
m := &mockUpdateTimeMetrics{}
cl := clock.NewDeterministicClock(time.UnixMilli(45892))
monitor := NewUpdateTimeMonitor(cl, m)
monitor.CheckUpdateTimes([]*types.EnrichedGameData{
{LastUpdateTime: time.UnixMilli(4)},
{LastUpdateTime: time.UnixMilli(3)},
{LastUpdateTime: time.UnixMilli(7)},
{LastUpdateTime: time.UnixMilli(9)},
})
require.Equal(t, time.UnixMilli(3), m.oldestUpdateTime)
}
type mockUpdateTimeMetrics struct {
oldestUpdateTime time.Time
}
func (m *mockUpdateTimeMetrics) RecordOldestGameUpdateTime(t time.Time) {
m.oldestUpdateTime = t
}
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