Commit 84723638 authored by refcell's avatar refcell Committed by GitHub

feat(op-dispute-mon): Contract Creation Failure Metric (#10447)

* feat(op-dispute-mon): contract creation failure metric

* fix(op-dispute-mon): log the game address for easier investigation
parent 700a1dbc
......@@ -107,6 +107,8 @@ type Metricer interface {
RecordInfo(version string)
RecordUp()
RecordFailedGames(count int)
RecordHonestActorClaims(address common.Address, stats *HonestActorData)
RecordGameResolutionStatus(status ResolutionStatus, count int)
......@@ -161,6 +163,7 @@ type Metrics struct {
gamesAgreement prometheus.GaugeVec
latestInvalidProposal prometheus.Gauge
ignoredGames prometheus.Gauge
failedGames prometheus.Gauge
requiredCollateral prometheus.GaugeVec
availableCollateral prometheus.GaugeVec
......@@ -280,6 +283,11 @@ func NewMetrics() *Metrics {
"delayedWETH",
"balance",
}),
failedGames: factory.NewGauge(prometheus.GaugeOpts{
Namespace: Namespace,
Name: "failed_games",
Help: "Number of games present in the game window but failed to be monitored",
}),
availableCollateral: *factory.NewGaugeVec(prometheus.GaugeOpts{
Namespace: Namespace,
Name: "bond_collateral_available",
......@@ -425,6 +433,10 @@ func (m *Metrics) RecordIgnoredGames(count int) {
m.ignoredGames.Set(float64(count))
}
func (m *Metrics) RecordFailedGames(count int) {
m.failedGames.Set(float64(count))
}
func (m *Metrics) RecordBondCollateral(addr common.Address, required *big.Int, available *big.Int) {
balance := "sufficient"
if required.Cmp(available) > 0 {
......
......@@ -37,4 +37,6 @@ func (*NoopMetricsImpl) RecordLatestInvalidProposal(_ uint64) {}
func (*NoopMetricsImpl) RecordIgnoredGames(_ int) {}
func (i *NoopMetricsImpl) RecordBondCollateral(_ common.Address, _ *big.Int, _ *big.Int) {}
func (*NoopMetricsImpl) RecordFailedGames(_ int) {}
func (*NoopMetricsImpl) RecordBondCollateral(_ common.Address, _ *big.Int, _ *big.Int) {}
......@@ -51,7 +51,7 @@ func (g *GameCallerCreator) CreateContract(ctx context.Context, game gameTypes.G
return fdg, nil
}
switch game.GameType {
case faultTypes.CannonGameType, faultTypes.AsteriscGameType, faultTypes.AlphabetGameType:
case faultTypes.CannonGameType, faultTypes.PermissionedGameType, faultTypes.AsteriscGameType, faultTypes.AlphabetGameType:
fdg, err := contracts.NewFaultDisputeGameContract(ctx, g.m, game.Proxy, g.caller)
if err != nil {
return nil, fmt.Errorf("failed to create fault dispute game contract: %w", err)
......
......@@ -43,18 +43,19 @@ func NewExtractor(logger log.Logger, creator CreateGameCaller, fetchGames Factor
}
}
func (e *Extractor) Extract(ctx context.Context, blockHash common.Hash, minTimestamp uint64) ([]*monTypes.EnrichedGameData, int, error) {
func (e *Extractor) Extract(ctx context.Context, blockHash common.Hash, minTimestamp uint64) ([]*monTypes.EnrichedGameData, int, int, error) {
games, err := e.fetchGames(ctx, blockHash, minTimestamp)
if err != nil {
return nil, 0, fmt.Errorf("failed to load games: %w", err)
return nil, 0, 0, fmt.Errorf("failed to load games: %w", err)
}
enriched, ignored := e.enrichGames(ctx, blockHash, games)
return enriched, ignored, nil
enriched, ignored, failed := e.enrichGames(ctx, blockHash, games)
return enriched, ignored, failed, nil
}
func (e *Extractor) enrichGames(ctx context.Context, blockHash common.Hash, games []gameTypes.GameMetadata) ([]*monTypes.EnrichedGameData, int) {
func (e *Extractor) enrichGames(ctx context.Context, blockHash common.Hash, games []gameTypes.GameMetadata) ([]*monTypes.EnrichedGameData, int, int) {
var enrichedGames []*monTypes.EnrichedGameData
ignored := 0
failed := 0
for _, game := range games {
if e.ignoredGames[game.Proxy] {
ignored++
......@@ -63,7 +64,8 @@ func (e *Extractor) enrichGames(ctx context.Context, blockHash common.Hash, game
}
caller, err := e.createContract(ctx, game)
if err != nil {
e.logger.Error("Failed to create game caller", "err", err)
e.logger.Error("Failed to create game caller", "game", game.Proxy, "err", err)
failed++
continue
}
l1Head, l2BlockNum, rootClaim, status, duration, err := caller.GetGameMetadata(ctx, rpcblock.ByHash(blockHash))
......@@ -95,7 +97,7 @@ func (e *Extractor) enrichGames(ctx context.Context, blockHash common.Hash, game
}
enrichedGames = append(enrichedGames, enrichedGame)
}
return enrichedGames, ignored
return enrichedGames, ignored, failed
}
func (e *Extractor) applyEnrichers(ctx context.Context, blockHash common.Hash, caller GameCaller, game *monTypes.EnrichedGameData) error {
......
......@@ -27,7 +27,7 @@ func TestExtractor_Extract(t *testing.T) {
t.Run("FetchGamesError", func(t *testing.T) {
extractor, _, games, _ := setupExtractorTest(t)
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.Equal(t, 1, games.calls)
})
......@@ -36,8 +36,9 @@ func TestExtractor_Extract(t *testing.T) {
extractor, creator, games, logs := setupExtractorTest(t)
games.games = []gameTypes.GameMetadata{{}}
creator.err = errors.New("boom")
enriched, ignored, err := extractor.Extract(context.Background(), common.Hash{}, 0)
enriched, ignored, failed, err := extractor.Extract(context.Background(), common.Hash{}, 0)
require.NoError(t, err)
require.Equal(t, 1, failed)
require.Zero(t, ignored)
require.Len(t, enriched, 0)
require.Equal(t, 1, games.calls)
......@@ -51,9 +52,10 @@ func TestExtractor_Extract(t *testing.T) {
extractor, creator, games, logs := setupExtractorTest(t)
games.games = []gameTypes.GameMetadata{{}}
creator.caller.metadataErr = errors.New("boom")
enriched, ignored, err := extractor.Extract(context.Background(), common.Hash{}, 0)
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, 0)
require.Equal(t, 1, games.calls)
require.Equal(t, 1, creator.calls)
......@@ -66,9 +68,10 @@ func TestExtractor_Extract(t *testing.T) {
extractor, creator, games, logs := setupExtractorTest(t)
games.games = []gameTypes.GameMetadata{{}}
creator.caller.claimsErr = errors.New("boom")
enriched, ignored, err := extractor.Extract(context.Background(), common.Hash{}, 0)
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, 0)
require.Equal(t, 1, games.calls)
require.Equal(t, 1, creator.calls)
......@@ -80,9 +83,10 @@ func TestExtractor_Extract(t *testing.T) {
t.Run("Success", func(t *testing.T) {
extractor, creator, games, _ := setupExtractorTest(t)
games.games = []gameTypes.GameMetadata{{}}
enriched, ignored, err := extractor.Extract(context.Background(), common.Hash{}, 0)
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, 1)
require.Equal(t, 1, games.calls)
require.Equal(t, 1, creator.calls)
......@@ -94,9 +98,10 @@ func TestExtractor_Extract(t *testing.T) {
enricher := &mockEnricher{err: errors.New("whoops")}
extractor, _, games, logs := setupExtractorTest(t, enricher)
games.games = []gameTypes.GameMetadata{{}}
enriched, ignored, err := extractor.Extract(context.Background(), common.Hash{}, 0)
enriched, ignored, failed, err := extractor.Extract(context.Background(), common.Hash{}, 0)
require.NoError(t, err)
require.Zero(t, ignored)
require.Zero(t, failed)
l := logs.FindLogs(testlog.NewMessageFilter("Failed to enrich game"))
require.Len(t, l, 1, "Should have logged error")
require.Len(t, enriched, 0, "Should not return games that failed to enrich")
......@@ -106,9 +111,10 @@ func TestExtractor_Extract(t *testing.T) {
enricher := &mockEnricher{}
extractor, _, games, _ := setupExtractorTest(t, enricher)
games.games = []gameTypes.GameMetadata{{}}
enriched, ignored, err := extractor.Extract(context.Background(), common.Hash{}, 0)
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, 1)
require.Equal(t, 1, enricher.calls)
})
......@@ -118,9 +124,10 @@ func TestExtractor_Extract(t *testing.T) {
enricher2 := &mockEnricher{}
extractor, _, games, _ := setupExtractorTest(t, enricher1, enricher2)
games.games = []gameTypes.GameMetadata{{}, {}}
enriched, ignored, err := extractor.Extract(context.Background(), common.Hash{}, 0)
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, enricher1.calls)
require.Equal(t, 2, enricher2.calls)
......@@ -131,10 +138,11 @@ func TestExtractor_Extract(t *testing.T) {
extractor, _, games, logs := setupExtractorTest(t, enricher1)
// Two games, one of which is ignored
games.games = []gameTypes.GameMetadata{{Proxy: ignoredGames[0]}, {Proxy: common.Address{0xaa}}}
enriched, ignored, err := extractor.Extract(context.Background(), common.Hash{}, 0)
enriched, ignored, failed, err := extractor.Extract(context.Background(), common.Hash{}, 0)
require.NoError(t, err)
// Should ignore one and enrich the other
require.Equal(t, 1, ignored)
require.Zero(t, failed)
require.Len(t, enriched, 1)
require.Equal(t, 1, enricher1.calls)
require.Equal(t, enriched[0].Proxy, common.Address{0xaa})
......
......@@ -26,6 +26,7 @@ type ForecastMetrics interface {
RecordGameAgreement(status metrics.GameAgreementStatus, count int)
RecordLatestInvalidProposal(timestamp uint64)
RecordIgnoredGames(count int)
RecordFailedGames(count int)
}
type forecastBatch struct {
......@@ -56,17 +57,17 @@ func NewForecast(logger log.Logger, metrics ForecastMetrics, validator OutputVal
}
}
func (f *Forecast) Forecast(ctx context.Context, games []*monTypes.EnrichedGameData, ignoredCount int) {
func (f *Forecast) Forecast(ctx context.Context, games []*monTypes.EnrichedGameData, ignoredCount, failedCount int) {
batch := forecastBatch{}
for _, game := range games {
if err := f.forecastGame(ctx, game, &batch); err != nil {
f.logger.Error("Failed to forecast game", "err", err)
}
}
f.recordBatch(batch, ignoredCount)
f.recordBatch(batch, ignoredCount, failedCount)
}
func (f *Forecast) recordBatch(batch forecastBatch, ignoredCount int) {
func (f *Forecast) recordBatch(batch forecastBatch, ignoredCount, failedCount int) {
f.metrics.RecordGameAgreement(metrics.AgreeDefenderWins, batch.AgreeDefenderWins)
f.metrics.RecordGameAgreement(metrics.DisagreeDefenderWins, batch.DisagreeDefenderWins)
f.metrics.RecordGameAgreement(metrics.AgreeChallengerWins, batch.AgreeChallengerWins)
......@@ -80,6 +81,7 @@ func (f *Forecast) recordBatch(batch forecastBatch, ignoredCount int) {
f.metrics.RecordLatestInvalidProposal(batch.LatestInvalidProposal)
f.metrics.RecordIgnoredGames(ignoredCount)
f.metrics.RecordFailedGames(failedCount)
}
func (f *Forecast) forecastGame(ctx context.Context, game *monTypes.EnrichedGameData, metrics *forecastBatch) error {
......
......@@ -30,7 +30,7 @@ func TestForecast_Forecast_BasicTests(t *testing.T) {
t.Run("NoGames", func(t *testing.T) {
forecast, _, rollup, logs := setupForecastTest(t)
forecast.Forecast(context.Background(), []*monTypes.EnrichedGameData{}, 0)
forecast.Forecast(context.Background(), []*monTypes.EnrichedGameData{}, 0, 0)
require.Equal(t, 0, rollup.calls)
levelFilter := testlog.NewLevelFilter(log.LevelError)
messageFilter := testlog.NewMessageFilter(failedForecastLog)
......@@ -40,7 +40,7 @@ func TestForecast_Forecast_BasicTests(t *testing.T) {
t.Run("RollupFetchFails", func(t *testing.T) {
forecast, _, rollup, logs := setupForecastTest(t)
rollup.err = errors.New("boom")
forecast.Forecast(context.Background(), []*monTypes.EnrichedGameData{{}}, 0)
forecast.Forecast(context.Background(), []*monTypes.EnrichedGameData{{}}, 0, 0)
require.Equal(t, 1, rollup.calls)
levelFilter := testlog.NewLevelFilter(log.LevelError)
messageFilter := testlog.NewMessageFilter(failedForecastLog)
......@@ -54,7 +54,7 @@ func TestForecast_Forecast_BasicTests(t *testing.T) {
t.Run("ChallengerWonGame_Agree", func(t *testing.T) {
forecast, m, _, logs := setupForecastTest(t)
expectedGame := monTypes.EnrichedGameData{Status: types.GameStatusChallengerWon, RootClaim: mockRootClaim}
forecast.Forecast(context.Background(), []*monTypes.EnrichedGameData{&expectedGame}, 0)
forecast.Forecast(context.Background(), []*monTypes.EnrichedGameData{&expectedGame}, 0, 0)
l := logs.FindLog(testlog.NewLevelFilter(log.LevelError), testlog.NewMessageFilter(lostGameLog))
require.NotNil(t, l)
require.Equal(t, expectedGame.Proxy, l.AttrValue("game"))
......@@ -69,7 +69,7 @@ func TestForecast_Forecast_BasicTests(t *testing.T) {
t.Run("ChallengerWonGame_Disagree", func(t *testing.T) {
forecast, m, _, logs := setupForecastTest(t)
expectedGame := monTypes.EnrichedGameData{Status: types.GameStatusChallengerWon, RootClaim: common.Hash{0xbb}}
forecast.Forecast(context.Background(), []*monTypes.EnrichedGameData{&expectedGame}, 0)
forecast.Forecast(context.Background(), []*monTypes.EnrichedGameData{&expectedGame}, 0, 0)
l := logs.FindLog(testlog.NewLevelFilter(log.LevelError), testlog.NewMessageFilter(lostGameLog))
require.Nil(t, l)
......@@ -81,7 +81,7 @@ func TestForecast_Forecast_BasicTests(t *testing.T) {
t.Run("DefenderWonGame_Agree", func(t *testing.T) {
forecast, m, _, logs := setupForecastTest(t)
expectedGame := monTypes.EnrichedGameData{Status: types.GameStatusDefenderWon, RootClaim: mockRootClaim}
forecast.Forecast(context.Background(), []*monTypes.EnrichedGameData{&expectedGame}, 0)
forecast.Forecast(context.Background(), []*monTypes.EnrichedGameData{&expectedGame}, 0, 0)
l := logs.FindLog(testlog.NewLevelFilter(log.LevelError), testlog.NewMessageFilter(lostGameLog))
require.Nil(t, l)
......@@ -93,7 +93,7 @@ func TestForecast_Forecast_BasicTests(t *testing.T) {
t.Run("DefenderWonGame_Disagree", func(t *testing.T) {
forecast, m, _, logs := setupForecastTest(t)
expectedGame := monTypes.EnrichedGameData{Status: types.GameStatusDefenderWon, RootClaim: common.Hash{0xbb}}
forecast.Forecast(context.Background(), []*monTypes.EnrichedGameData{&expectedGame}, 0)
forecast.Forecast(context.Background(), []*monTypes.EnrichedGameData{&expectedGame}, 0, 0)
l := logs.FindLog(testlog.NewLevelFilter(log.LevelError), testlog.NewMessageFilter(lostGameLog))
require.NotNil(t, l)
require.Equal(t, expectedGame.Proxy, l.AttrValue("game"))
......@@ -107,14 +107,14 @@ func TestForecast_Forecast_BasicTests(t *testing.T) {
t.Run("SingleGame", func(t *testing.T) {
forecast, _, rollup, logs := setupForecastTest(t)
forecast.Forecast(context.Background(), []*monTypes.EnrichedGameData{{}}, 0)
forecast.Forecast(context.Background(), []*monTypes.EnrichedGameData{{}}, 0, 0)
require.Equal(t, 1, rollup.calls)
require.Nil(t, logs.FindLog(testlog.NewLevelFilter(log.LevelError), testlog.NewMessageFilter(failedForecastLog)))
})
t.Run("MultipleGames", func(t *testing.T) {
forecast, _, rollup, logs := setupForecastTest(t)
forecast.Forecast(context.Background(), []*monTypes.EnrichedGameData{{}, {}, {}}, 0)
forecast.Forecast(context.Background(), []*monTypes.EnrichedGameData{{}, {}, {}}, 0, 0)
require.Equal(t, 3, rollup.calls)
require.Nil(t, logs.FindLog(testlog.NewLevelFilter(log.LevelError), testlog.NewMessageFilter(failedForecastLog)))
})
......@@ -130,7 +130,7 @@ func TestForecast_Forecast_EndLogs(t *testing.T) {
RootClaim: mockRootClaim,
Claims: createDeepClaimList()[:1],
}}
forecast.Forecast(context.Background(), games, 0)
forecast.Forecast(context.Background(), games, 0, 0)
require.Equal(t, 1, rollup.calls)
levelFilter := testlog.NewLevelFilter(log.LevelError)
messageFilter := testlog.NewMessageFilter(failedForecastLog)
......@@ -151,7 +151,7 @@ func TestForecast_Forecast_EndLogs(t *testing.T) {
RootClaim: mockRootClaim,
Claims: createDeepClaimList()[:2],
}}
forecast.Forecast(context.Background(), games, 0)
forecast.Forecast(context.Background(), games, 0, 0)
require.Equal(t, 1, rollup.calls)
levelFilter := testlog.NewLevelFilter(log.LevelError)
messageFilter := testlog.NewMessageFilter(failedForecastLog)
......@@ -170,7 +170,7 @@ func TestForecast_Forecast_EndLogs(t *testing.T) {
forecast.Forecast(context.Background(), []*monTypes.EnrichedGameData{{
Status: types.GameStatusInProgress,
Claims: createDeepClaimList()[:2],
}}, 0)
}}, 0, 0)
require.Equal(t, 1, rollup.calls)
levelFilter := testlog.NewLevelFilter(log.LevelError)
messageFilter := testlog.NewMessageFilter(failedForecastLog)
......@@ -189,7 +189,7 @@ func TestForecast_Forecast_EndLogs(t *testing.T) {
forecast.Forecast(context.Background(), []*monTypes.EnrichedGameData{{
Status: types.GameStatusInProgress,
Claims: createDeepClaimList()[:1],
}}, 0)
}}, 0, 0)
require.Equal(t, 1, rollup.calls)
levelFilter := testlog.NewLevelFilter(log.LevelError)
messageFilter := testlog.NewMessageFilter(failedForecastLog)
......@@ -250,7 +250,7 @@ func TestForecast_Forecast_MultipleGames(t *testing.T) {
},
}
}
forecast.Forecast(context.Background(), games, 3)
forecast.Forecast(context.Background(), games, 3, 4)
require.Equal(t, len(games), rollup.calls)
require.Nil(t, logs.FindLog(testlog.NewLevelFilter(log.LevelError), testlog.NewMessageFilter(failedForecastLog)))
expectedMetrics := zeroGameAgreement()
......@@ -263,6 +263,7 @@ func TestForecast_Forecast_MultipleGames(t *testing.T) {
expectedMetrics[metrics.DisagreeChallengerWins] = 2
require.Equal(t, expectedMetrics, m.gameAgreement)
require.Equal(t, 3, m.ignoredGames)
require.Equal(t, 4, m.contractCreationFails)
require.EqualValues(t, 7, m.latestInvalidProposal)
}
......@@ -292,6 +293,11 @@ type mockForecastMetrics struct {
gameAgreement map[metrics.GameAgreementStatus]int
ignoredGames int
latestInvalidProposal uint64
contractCreationFails int
}
func (m *mockForecastMetrics) RecordFailedGames(count int) {
m.contractCreationFails = count
}
func (m *mockForecastMetrics) RecordGameAgreement(status metrics.GameAgreementStatus, count int) {
......
......@@ -13,14 +13,14 @@ import (
"github.com/ethereum/go-ethereum/log"
)
type ForecastResolution func(ctx context.Context, games []*types.EnrichedGameData, ignoredCount int)
type ForecastResolution func(ctx context.Context, games []*types.EnrichedGameData, ignoredCount, failedCount int)
type Bonds func(games []*types.EnrichedGameData)
type Resolutions func(games []*types.EnrichedGameData)
type MonitorClaims func(games []*types.EnrichedGameData)
type MonitorWithdrawals func(games []*types.EnrichedGameData)
type BlockHashFetcher func(ctx context.Context, number *big.Int) (common.Hash, error)
type BlockNumberFetcher func(ctx context.Context) (uint64, error)
type Extract func(ctx context.Context, blockHash common.Hash, minTimestamp uint64) ([]*types.EnrichedGameData, int, error)
type Extract func(ctx context.Context, blockHash common.Hash, minTimestamp uint64) ([]*types.EnrichedGameData, int, int, error)
type gameMonitor struct {
logger log.Logger
......@@ -87,12 +87,12 @@ func (m *gameMonitor) monitorGames() error {
return fmt.Errorf("failed to fetch block hash: %w", err)
}
minGameTimestamp := clock.MinCheckedTimestamp(m.clock, m.gameWindow)
enrichedGames, ignored, err := m.extract(m.ctx, blockHash, minGameTimestamp)
enrichedGames, ignored, failed, err := m.extract(m.ctx, blockHash, minGameTimestamp)
if err != nil {
return fmt.Errorf("failed to load games: %w", err)
}
m.resolutions(enrichedGames)
m.forecast(m.ctx, enrichedGames, ignored)
m.forecast(m.ctx, enrichedGames, ignored, failed)
m.bonds(enrichedGames)
m.claims(enrichedGames)
m.withdrawals(enrichedGames)
......
......@@ -170,7 +170,7 @@ type mockForecast struct {
calls int
}
func (m *mockForecast) Forecast(_ context.Context, _ []*monTypes.EnrichedGameData, _ int) {
func (m *mockForecast) Forecast(_ context.Context, _ []*monTypes.EnrichedGameData, _, _ int) {
m.calls++
}
......@@ -188,19 +188,20 @@ type mockExtractor struct {
maxSuccess int
games []*monTypes.EnrichedGameData
ignoredCount int
failedCount int
}
func (m *mockExtractor) Extract(
_ context.Context,
_ common.Hash,
_ uint64,
) ([]*monTypes.EnrichedGameData, int, error) {
) ([]*monTypes.EnrichedGameData, int, int, error) {
m.calls++
if m.fetchErr != nil {
return nil, 0, m.fetchErr
return nil, 0, 0, m.fetchErr
}
if m.calls > m.maxSuccess && m.maxSuccess != 0 {
return nil, 0, mockErr
return nil, 0, 0, mockErr
}
return m.games, m.ignoredCount, nil
return m.games, m.ignoredCount, m.failedCount, nil
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment