From 5447735369619838fa4f19f44f1436a1a5ecb978 Mon Sep 17 00:00:00 2001 From: Andreas Bigger Date: Wed, 14 Feb 2024 16:23:46 -0500 Subject: [PATCH] feat(op-dispute-mon): extractor component --- op-dispute-mon/mon/extract/extractor.go | 60 ++++++++ op-dispute-mon/mon/extract/extractor_test.go | 139 +++++++++++++++++++ op-dispute-mon/mon/types/types.go | 8 ++ 3 files changed, 207 insertions(+) create mode 100644 op-dispute-mon/mon/extract/extractor.go create mode 100644 op-dispute-mon/mon/extract/extractor_test.go diff --git a/op-dispute-mon/mon/extract/extractor.go b/op-dispute-mon/mon/extract/extractor.go new file mode 100644 index 000000000000..12d5d197c052 --- /dev/null +++ b/op-dispute-mon/mon/extract/extractor.go @@ -0,0 +1,60 @@ +package extract + +import ( + "context" + "fmt" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/log" + + gameTypes "github.com/ethereum-optimism/optimism/op-challenger/game/types" + monTypes "github.com/ethereum-optimism/optimism/op-dispute-mon/mon/types" +) + +type CreateGameCaller func(game gameTypes.GameMetadata) (GameCaller, error) +type FactoryGameFetcher func(ctx context.Context, blockHash common.Hash, earliestTimestamp uint64) ([]gameTypes.GameMetadata, error) + +type Extractor struct { + logger log.Logger + createContract CreateGameCaller + fetchGames FactoryGameFetcher +} + +func NewExtractor(logger log.Logger, creator CreateGameCaller, fetchGames FactoryGameFetcher) *Extractor { + return &Extractor{ + logger: logger, + createContract: creator, + fetchGames: fetchGames, + } +} + +func (e *Extractor) Extract(ctx context.Context, blockHash common.Hash, minTimestamp uint64) ([]monTypes.EnrichedGameData, error) { + games, err := e.fetchGames(ctx, blockHash, minTimestamp) + if err != nil { + return nil, fmt.Errorf("failed to load games: %w", err) + } + return e.enrichGameMetadata(ctx, games), nil +} + +func (e *Extractor) enrichGameMetadata(ctx context.Context, games []gameTypes.GameMetadata) []monTypes.EnrichedGameData { + var enrichedGames []monTypes.EnrichedGameData + for _, game := range games { + caller, err := e.createContract(game) + if err != nil { + e.logger.Error("failed to create game caller", "err", err) + continue + } + l2BlockNum, rootClaim, status, err := caller.GetGameMetadata(ctx) + if err != nil { + e.logger.Error("failed to fetch game metadata", "err", err) + continue + } + enrichedGames = append(enrichedGames, monTypes.EnrichedGameData{ + GameMetadata: game, + L2BlockNumber: l2BlockNum, + RootClaim: rootClaim, + Status: status, + }) + } + return enrichedGames +} diff --git a/op-dispute-mon/mon/extract/extractor_test.go b/op-dispute-mon/mon/extract/extractor_test.go new file mode 100644 index 000000000000..9e4af4885f5d --- /dev/null +++ b/op-dispute-mon/mon/extract/extractor_test.go @@ -0,0 +1,139 @@ +package extract + +import ( + "context" + "errors" + "testing" + + "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" + gameTypes "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" +) + +var mockRootClaim = common.HexToHash("0x1234") + +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) + require.ErrorIs(t, err, games.err) + require.Equal(t, 1, games.calls) + }) + + t.Run("CreateGameErrorLog", func(t *testing.T) { + extractor, creator, games, logs := setupExtractorTest(t) + games.games = []gameTypes.GameMetadata{{}} + creator.err = errors.New("boom") + enriched, err := extractor.Extract(context.Background(), common.Hash{}, 0) + require.NoError(t, err) + require.Len(t, enriched, 0) + require.Equal(t, 1, games.calls) + require.Equal(t, 1, creator.calls) + verifyLogs(t, logs, 1, 0) + }) + + t.Run("MetadataFetchErrorLog", func(t *testing.T) { + extractor, creator, games, logs := setupExtractorTest(t) + games.games = []gameTypes.GameMetadata{{}} + creator.caller.err = errors.New("boom") + enriched, err := extractor.Extract(context.Background(), common.Hash{}, 0) + require.NoError(t, err) + require.Len(t, enriched, 0) + require.Equal(t, 1, games.calls) + require.Equal(t, 1, creator.calls) + require.Equal(t, 1, creator.caller.calls) + verifyLogs(t, logs, 0, 1) + }) + + t.Run("Success", func(t *testing.T) { + extractor, creator, games, _ := setupExtractorTest(t) + games.games = []gameTypes.GameMetadata{{}} + enriched, err := extractor.Extract(context.Background(), common.Hash{}, 0) + require.NoError(t, err) + require.Len(t, enriched, 1) + require.Equal(t, 1, games.calls) + require.Equal(t, 1, creator.calls) + require.Equal(t, 1, creator.caller.calls) + }) +} + +func verifyLogs(t *testing.T, logs *testlog.CapturingHandler, createErr int, metadataErr int) { + errorLevelFilter := testlog.NewLevelFilter(log.LevelError) + createMessageFilter := testlog.NewMessageFilter("failed to create game caller") + l := logs.FindLogs(errorLevelFilter, createMessageFilter) + require.Len(t, l, createErr) + fetchMessageFilter := testlog.NewMessageFilter("failed to fetch game metadata") + l = logs.FindLogs(errorLevelFilter, fetchMessageFilter) + require.Len(t, l, metadataErr) +} + +func setupExtractorTest(t *testing.T) (*Extractor, *mockGameCallerCreator, *mockGameFetcher, *testlog.CapturingHandler) { + logger, capturedLogs := testlog.CaptureLogger(t, log.LvlDebug) + games := &mockGameFetcher{} + caller := &mockGameCaller{rootClaim: mockRootClaim} + creator := &mockGameCallerCreator{caller: caller} + return NewExtractor( + logger, + creator.CreateGameCaller, + games.FetchGames, + ), + creator, + games, + capturedLogs +} + +type mockGameFetcher struct { + calls int + err error + games []gameTypes.GameMetadata +} + +func (m *mockGameFetcher) FetchGames(_ context.Context, _ common.Hash, _ uint64) ([]gameTypes.GameMetadata, error) { + m.calls++ + if m.err != nil { + return nil, m.err + } + return m.games, nil +} + +type mockGameCallerCreator struct { + calls int + err error + caller *mockGameCaller +} + +func (m *mockGameCallerCreator) CreateGameCaller(_ gameTypes.GameMetadata) (GameCaller, error) { + m.calls++ + if m.err != nil { + return nil, m.err + } + return m.caller, nil +} + +type mockGameCaller struct { + calls int + err error + rootClaim common.Hash +} + +func (m *mockGameCaller) GetGameMetadata(_ context.Context) (uint64, common.Hash, types.GameStatus, error) { + m.calls++ + if m.err != nil { + return 0, common.Hash{}, 0, m.err + } + return 0, mockRootClaim, 0, nil +} + +func (m *mockGameCaller) GetAllClaims(ctx context.Context) ([]faultTypes.Claim, error) { + m.calls++ + if m.err != nil { + return nil, m.err + } + return []faultTypes.Claim{{}}, nil +} diff --git a/op-dispute-mon/mon/types/types.go b/op-dispute-mon/mon/types/types.go index e183212f3925..020984968884 100644 --- a/op-dispute-mon/mon/types/types.go +++ b/op-dispute-mon/mon/types/types.go @@ -2,8 +2,16 @@ package types import ( "github.com/ethereum-optimism/optimism/op-challenger/game/types" + "github.com/ethereum/go-ethereum/common" ) +type EnrichedGameData struct { + types.GameMetadata + L2BlockNumber uint64 + RootClaim common.Hash + Status types.GameStatus +} + type StatusBatch struct { InProgress int DefenderWon int