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

op-dispute-mon: Combine the detect and forecast tasks. (#9667)

These update the same metric and were duplicating the request and logic for checking root claim agreement.
parent 8d59c7a1
package mon
import (
"context"
"github.com/ethereum-optimism/optimism/op-challenger/game/types"
"github.com/ethereum-optimism/optimism/op-dispute-mon/metrics"
"github.com/ethereum-optimism/optimism/op-dispute-mon/mon/extract"
monTypes "github.com/ethereum-optimism/optimism/op-dispute-mon/mon/types"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/log"
)
type OutputValidator interface {
CheckRootAgreement(ctx context.Context, blockNum uint64, root common.Hash) (bool, common.Hash, error)
}
type GameCallerCreator interface {
CreateContract(game types.GameMetadata) (extract.GameCaller, error)
}
type DetectorMetrics interface {
RecordGameAgreement(status metrics.GameAgreementStatus, count int)
RecordGamesStatus(inProgress, defenderWon, challengerWon int)
}
type detector struct {
logger log.Logger
metrics DetectorMetrics
validator OutputValidator
}
func newDetector(logger log.Logger, metrics DetectorMetrics, validator OutputValidator) *detector {
return &detector{
logger: logger,
metrics: metrics,
validator: validator,
}
}
func (d *detector) Detect(ctx context.Context, games []*monTypes.EnrichedGameData) {
statBatch := monTypes.StatusBatch{}
detectBatch := monTypes.DetectionBatch{}
for _, game := range games {
statBatch.Add(game.Status)
processed, err := d.checkAgreement(ctx, game.Proxy, game.L2BlockNumber, game.RootClaim, game.Status)
if err != nil {
d.logger.Error("Failed to process game", "err", err)
continue
}
detectBatch.Merge(processed)
}
d.metrics.RecordGamesStatus(statBatch.InProgress, statBatch.DefenderWon, statBatch.ChallengerWon)
d.recordBatch(detectBatch)
d.logger.Info("Completed updating games", "count", len(games))
}
func (d *detector) recordBatch(batch monTypes.DetectionBatch) {
d.metrics.RecordGameAgreement(metrics.AgreeDefenderWins, batch.AgreeDefenderWins)
d.metrics.RecordGameAgreement(metrics.DisagreeDefenderWins, batch.DisagreeDefenderWins)
d.metrics.RecordGameAgreement(metrics.AgreeChallengerWins, batch.AgreeChallengerWins)
d.metrics.RecordGameAgreement(metrics.DisagreeChallengerWins, batch.DisagreeChallengerWins)
}
func (d *detector) checkAgreement(ctx context.Context, addr common.Address, blockNum uint64, rootClaim common.Hash, status types.GameStatus) (monTypes.DetectionBatch, error) {
agree, expectedClaim, err := d.validator.CheckRootAgreement(ctx, blockNum, rootClaim)
if err != nil {
return monTypes.DetectionBatch{}, err
}
batch := monTypes.DetectionBatch{}
batch.Update(status, agree)
if status != types.GameStatusInProgress {
expectedResult := types.GameStatusDefenderWon
if !agree {
expectedResult = types.GameStatusChallengerWon
}
if status != expectedResult {
d.logger.Error("Unexpected game result",
"gameAddr", addr, "blockNum", blockNum,
"expectedResult", expectedResult, "actualResult", status,
"rootClaim", rootClaim, "correctClaim", expectedClaim)
}
}
return batch, nil
}
package mon
import (
"context"
"errors"
"testing"
"github.com/ethereum-optimism/optimism/op-challenger/game/types"
"github.com/ethereum-optimism/optimism/op-dispute-mon/metrics"
monTypes "github.com/ethereum-optimism/optimism/op-dispute-mon/mon/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"
)
func TestDetector_Detect(t *testing.T) {
t.Parallel()
t.Run("NoGames", func(t *testing.T) {
detector, m, _, _ := setupDetectorTest(t)
detector.Detect(context.Background(), []*monTypes.EnrichedGameData{})
m.Equals(t, 0, 0, 0)
m.Mapped(t, map[metrics.GameAgreementStatus]int{})
})
t.Run("CheckAgreementFails", func(t *testing.T) {
detector, m, rollup, _ := setupDetectorTest(t)
rollup.err = errors.New("boom")
detector.Detect(context.Background(), []*monTypes.EnrichedGameData{{}})
m.Equals(t, 1, 0, 0) // Status should still be metriced here!
m.Mapped(t, map[metrics.GameAgreementStatus]int{})
})
t.Run("SingleGame", func(t *testing.T) {
detector, m, _, _ := setupDetectorTest(t)
detector.Detect(context.Background(), []*monTypes.EnrichedGameData{{Status: types.GameStatusChallengerWon}})
m.Equals(t, 0, 0, 1)
m.Mapped(t, map[metrics.GameAgreementStatus]int{metrics.DisagreeChallengerWins: 1})
})
t.Run("MultipleGames", func(t *testing.T) {
detector, m, _, _ := setupDetectorTest(t)
detector.Detect(context.Background(), []*monTypes.EnrichedGameData{
{Status: types.GameStatusChallengerWon},
{Status: types.GameStatusChallengerWon},
{Status: types.GameStatusChallengerWon},
})
m.Equals(t, 0, 0, 3)
m.Mapped(t, map[metrics.GameAgreementStatus]int{metrics.DisagreeChallengerWins: 3})
})
}
func TestDetector_RecordBatch(t *testing.T) {
tests := []struct {
name string
batch monTypes.DetectionBatch
expect func(*testing.T, *mockDetectorMetricer)
}{
{
name: "no games",
batch: monTypes.DetectionBatch{},
expect: func(t *testing.T, metrics *mockDetectorMetricer) {},
},
{
name: "in_progress",
batch: monTypes.DetectionBatch{InProgress: 1},
expect: func(t *testing.T, m *mockDetectorMetricer) {
for status, count := range m.gameAgreement {
require.Zerof(t, count, "incorrectly reported in progress game as %v", status)
}
},
},
{
name: "agree_defender_wins",
batch: monTypes.DetectionBatch{AgreeDefenderWins: 1},
expect: func(t *testing.T, m *mockDetectorMetricer) {
require.Equal(t, 1, m.gameAgreement[metrics.AgreeDefenderWins])
},
},
{
name: "disagree_defender_wins",
batch: monTypes.DetectionBatch{DisagreeDefenderWins: 1},
expect: func(t *testing.T, m *mockDetectorMetricer) {
require.Equal(t, 1, m.gameAgreement[metrics.DisagreeDefenderWins])
},
},
{
name: "agree_challenger_wins",
batch: monTypes.DetectionBatch{AgreeChallengerWins: 1},
expect: func(t *testing.T, m *mockDetectorMetricer) {
require.Equal(t, 1, m.gameAgreement[metrics.AgreeChallengerWins])
},
},
{
name: "disagree_challenger_wins",
batch: monTypes.DetectionBatch{DisagreeChallengerWins: 1},
expect: func(t *testing.T, m *mockDetectorMetricer) {
require.Equal(t, 1, m.gameAgreement[metrics.DisagreeChallengerWins])
},
},
}
for _, test := range tests {
test := test
t.Run(test.name, func(t *testing.T) {
monitor, metrics, _, _ := setupDetectorTest(t)
monitor.recordBatch(test.batch)
test.expect(t, metrics)
})
}
}
func TestDetector_CheckAgreement_Fails(t *testing.T) {
detector, _, rollup, _ := setupDetectorTest(t)
rollup.err = errors.New("boom")
_, err := detector.checkAgreement(context.Background(), common.Address{}, 0, common.Hash{}, types.GameStatusInProgress)
require.ErrorIs(t, err, rollup.err)
}
func TestDetector_CheckAgreement_Succeeds(t *testing.T) {
tests := []struct {
name string
rootClaim common.Hash
status types.GameStatus
expectBatch func(*monTypes.DetectionBatch)
expectErrorLog bool
expectStatus types.GameStatus
err error
}{
{
name: "in_progress",
expectBatch: func(batch *monTypes.DetectionBatch) {
require.Equal(t, 1, batch.InProgress)
},
},
{
name: "agree_defender_wins",
rootClaim: mockRootClaim,
status: types.GameStatusDefenderWon,
expectStatus: types.GameStatusDefenderWon,
expectBatch: func(batch *monTypes.DetectionBatch) {
require.Equal(t, 1, batch.AgreeDefenderWins)
},
},
{
name: "disagree_defender_wins",
status: types.GameStatusDefenderWon,
expectStatus: types.GameStatusChallengerWon,
expectBatch: func(batch *monTypes.DetectionBatch) {
require.Equal(t, 1, batch.DisagreeDefenderWins)
},
expectErrorLog: true,
},
{
name: "agree_challenger_wins",
rootClaim: mockRootClaim,
status: types.GameStatusChallengerWon,
expectStatus: types.GameStatusDefenderWon,
expectBatch: func(batch *monTypes.DetectionBatch) {
require.Equal(t, 1, batch.AgreeChallengerWins)
},
expectErrorLog: true,
},
{
name: "disagree_challenger_wins",
status: types.GameStatusChallengerWon,
expectStatus: types.GameStatusChallengerWon,
expectBatch: func(batch *monTypes.DetectionBatch) {
require.Equal(t, 1, batch.DisagreeChallengerWins)
},
},
}
for _, test := range tests {
test := test
t.Run(test.name, func(t *testing.T) {
detector, _, _, logs := setupDetectorTest(t)
batch, err := detector.checkAgreement(context.Background(), common.Address{}, 0, test.rootClaim, test.status)
require.NoError(t, err)
test.expectBatch(&batch)
levelFilter := testlog.NewLevelFilter(log.LevelError)
if !test.expectErrorLog {
require.Empty(t, logs.FindLogs(levelFilter), "Should not log an error")
} else {
msgFilter := testlog.NewMessageFilter("Unexpected game result")
l := logs.FindLog(levelFilter, msgFilter)
require.NotNil(t, l, "Should have logged an error")
expectedResult := l.AttrValue("expectedResult")
require.Equal(t, test.expectStatus, expectedResult)
actualResult := l.AttrValue("actualResult")
require.Equal(t, test.status, actualResult)
}
})
}
}
func setupDetectorTest(t *testing.T) (*detector, *mockDetectorMetricer, *stubOutputValidator, *testlog.CapturingHandler) {
logger, capturedLogs := testlog.CaptureLogger(t, log.LvlDebug)
metrics := &mockDetectorMetricer{}
validator := &stubOutputValidator{}
detector := newDetector(logger, metrics, validator)
return detector, metrics, 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 mockDetectorMetricer struct {
inProgress int
defenderWon int
challengerWon int
gameAgreement map[metrics.GameAgreementStatus]int
}
func (m *mockDetectorMetricer) Equals(t *testing.T, inProgress, defenderWon, challengerWon int) {
require.Equal(t, inProgress, m.inProgress)
require.Equal(t, defenderWon, m.defenderWon)
require.Equal(t, challengerWon, m.challengerWon)
}
func (m *mockDetectorMetricer) Mapped(t *testing.T, expected map[metrics.GameAgreementStatus]int) {
for k, v := range m.gameAgreement {
require.Equal(t, expected[k], v)
}
}
func (m *mockDetectorMetricer) RecordGamesStatus(inProgress, defenderWon, challengerWon int) {
m.inProgress = inProgress
m.defenderWon = defenderWon
m.challengerWon = challengerWon
}
func (m *mockDetectorMetricer) RecordGameAgreement(status metrics.GameAgreementStatus, count int) {
if m.gameAgreement == nil {
m.gameAgreement = make(map[metrics.GameAgreementStatus]int)
}
m.gameAgreement[status] += count
}
...@@ -10,17 +10,19 @@ import ( ...@@ -10,17 +10,19 @@ import (
"github.com/ethereum-optimism/optimism/op-dispute-mon/mon/resolution" "github.com/ethereum-optimism/optimism/op-dispute-mon/mon/resolution"
"github.com/ethereum-optimism/optimism/op-dispute-mon/mon/transform" "github.com/ethereum-optimism/optimism/op-dispute-mon/mon/transform"
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/go-ethereum/common"
"github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/log"
) )
var ( var (
ErrContractCreation = errors.New("failed to create contract") ErrRootAgreement = errors.New("failed to check root agreement")
ErrMetadataFetch = errors.New("failed to fetch game metadata")
ErrClaimFetch = errors.New("failed to fetch game claims")
ErrRootAgreement = errors.New("failed to check root agreement")
) )
type OutputValidator interface {
CheckRootAgreement(ctx context.Context, blockNum uint64, root common.Hash) (bool, common.Hash, error)
}
type ForecastMetrics interface { type ForecastMetrics interface {
RecordClaimResolutionDelayMax(delay float64) RecordClaimResolutionDelayMax(delay float64)
RecordGameAgreement(status metrics.GameAgreementStatus, count int) RecordGameAgreement(status metrics.GameAgreementStatus, count int)
...@@ -51,6 +53,11 @@ func (f *forecast) Forecast(ctx context.Context, games []*monTypes.EnrichedGameD ...@@ -51,6 +53,11 @@ func (f *forecast) Forecast(ctx context.Context, games []*monTypes.EnrichedGameD
} }
func (f *forecast) recordBatch(batch monTypes.ForecastBatch) { func (f *forecast) recordBatch(batch monTypes.ForecastBatch) {
f.metrics.RecordGameAgreement(metrics.AgreeDefenderWins, batch.AgreeDefenderWins)
f.metrics.RecordGameAgreement(metrics.DisagreeDefenderWins, batch.DisagreeDefenderWins)
f.metrics.RecordGameAgreement(metrics.AgreeChallengerWins, batch.AgreeChallengerWins)
f.metrics.RecordGameAgreement(metrics.DisagreeChallengerWins, batch.DisagreeChallengerWins)
f.metrics.RecordGameAgreement(metrics.AgreeChallengerAhead, batch.AgreeChallengerAhead) f.metrics.RecordGameAgreement(metrics.AgreeChallengerAhead, batch.AgreeChallengerAhead)
f.metrics.RecordGameAgreement(metrics.DisagreeChallengerAhead, batch.DisagreeChallengerAhead) f.metrics.RecordGameAgreement(metrics.DisagreeChallengerAhead, batch.DisagreeChallengerAhead)
f.metrics.RecordGameAgreement(metrics.AgreeDefenderAhead, batch.AgreeDefenderAhead) f.metrics.RecordGameAgreement(metrics.AgreeDefenderAhead, batch.AgreeDefenderAhead)
...@@ -58,8 +65,38 @@ func (f *forecast) recordBatch(batch monTypes.ForecastBatch) { ...@@ -58,8 +65,38 @@ func (f *forecast) recordBatch(batch monTypes.ForecastBatch) {
} }
func (f *forecast) forecastGame(ctx context.Context, game *monTypes.EnrichedGameData, metrics *monTypes.ForecastBatch) error { func (f *forecast) forecastGame(ctx context.Context, game *monTypes.EnrichedGameData, metrics *monTypes.ForecastBatch) error {
// Check the root agreement.
agreement, expected, err := f.validator.CheckRootAgreement(ctx, game.L2BlockNumber, game.RootClaim)
if err != nil {
return fmt.Errorf("%w: %w", ErrRootAgreement, err)
}
expectedResult := types.GameStatusDefenderWon
if !agreement {
expectedResult = types.GameStatusChallengerWon
}
if game.Status != types.GameStatusInProgress { if game.Status != types.GameStatusInProgress {
f.logger.Debug("Game is not in progress, skipping forecast", "game", game.Proxy, "status", game.Status) if game.Status != expectedResult {
f.logger.Error("Unexpected game result",
"game", game.Proxy, "blockNum", game.L2BlockNumber,
"expectedResult", expectedResult, "actualResult", game.Status,
"rootClaim", game.RootClaim, "correctClaim", expected)
}
switch game.Status {
case types.GameStatusDefenderWon:
if agreement {
metrics.AgreeDefenderWins++
} else {
metrics.DisagreeDefenderWins++
}
case types.GameStatusChallengerWon:
if agreement {
metrics.AgreeChallengerWins++
} else {
metrics.DisagreeChallengerWins++
}
}
return nil return nil
} }
...@@ -67,37 +104,31 @@ func (f *forecast) forecastGame(ctx context.Context, game *monTypes.EnrichedGame ...@@ -67,37 +104,31 @@ func (f *forecast) forecastGame(ctx context.Context, game *monTypes.EnrichedGame
tree := transform.CreateBidirectionalTree(game.Claims) tree := transform.CreateBidirectionalTree(game.Claims)
// Compute the resolution status of the game. // Compute the resolution status of the game.
status := resolution.Resolve(tree) forecastStatus := resolution.Resolve(tree)
// Check the root agreement.
agreement, expected, err := f.validator.CheckRootAgreement(ctx, game.L2BlockNumber, game.RootClaim)
if err != nil {
return fmt.Errorf("%w: %w", ErrRootAgreement, err)
}
if agreement { if agreement {
// If we agree with the output root proposal, the Defender should win, defending that claim. // If we agree with the output root proposal, the Defender should win, defending that claim.
if status == types.GameStatusChallengerWon { if forecastStatus == types.GameStatusChallengerWon {
metrics.AgreeChallengerAhead++ metrics.AgreeChallengerAhead++
f.logger.Warn("Forecasting unexpected game result", "status", status, f.logger.Warn("Forecasting unexpected game result", "status", forecastStatus,
"game", game.Proxy, "blockNum", game.L2BlockNumber, "game", game.Proxy, "blockNum", game.L2BlockNumber,
"rootClaim", game.RootClaim, "expected", expected) "rootClaim", game.RootClaim, "expected", expected)
} else { } else {
metrics.AgreeDefenderAhead++ metrics.AgreeDefenderAhead++
f.logger.Debug("Forecasting expected game result", "status", status, f.logger.Debug("Forecasting expected game result", "status", forecastStatus,
"game", game.Proxy, "blockNum", game.L2BlockNumber, "game", game.Proxy, "blockNum", game.L2BlockNumber,
"rootClaim", game.RootClaim, "expected", expected) "rootClaim", game.RootClaim, "expected", expected)
} }
} else { } else {
// If we disagree with the output root proposal, the Challenger should win, challenging that claim. // If we disagree with the output root proposal, the Challenger should win, challenging that claim.
if status == types.GameStatusDefenderWon { if forecastStatus == types.GameStatusDefenderWon {
metrics.DisagreeDefenderAhead++ metrics.DisagreeDefenderAhead++
f.logger.Warn("Forecasting unexpected game result", "status", status, f.logger.Warn("Forecasting unexpected game result", "status", forecastStatus,
"game", game.Proxy, "blockNum", game.L2BlockNumber, "game", game.Proxy, "blockNum", game.L2BlockNumber,
"rootClaim", game.RootClaim, "expected", expected) "rootClaim", game.RootClaim, "expected", expected)
} else { } else {
metrics.DisagreeChallengerAhead++ metrics.DisagreeChallengerAhead++
f.logger.Debug("Forecasting expected game result", "status", status, f.logger.Debug("Forecasting expected game result", "status", forecastStatus,
"game", game.Proxy, "blockNum", game.L2BlockNumber, "game", game.Proxy, "blockNum", game.L2BlockNumber,
"rootClaim", game.RootClaim, "expected", expected) "rootClaim", game.RootClaim, "expected", expected)
} }
......
This diff is collapsed.
...@@ -13,7 +13,6 @@ import ( ...@@ -13,7 +13,6 @@ import (
"github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/log"
) )
type Detect func(ctx context.Context, games []*types.EnrichedGameData)
type Forecast func(ctx context.Context, games []*types.EnrichedGameData) type Forecast func(ctx context.Context, 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)
...@@ -32,7 +31,6 @@ type gameMonitor struct { ...@@ -32,7 +31,6 @@ type gameMonitor struct {
monitorInterval time.Duration monitorInterval time.Duration
delays RecordClaimResolutionDelayMax delays RecordClaimResolutionDelayMax
detect Detect
forecast Forecast forecast Forecast
extract Extract extract Extract
fetchBlockHash BlockHashFetcher fetchBlockHash BlockHashFetcher
...@@ -46,7 +44,6 @@ func newGameMonitor( ...@@ -46,7 +44,6 @@ func newGameMonitor(
monitorInterval time.Duration, monitorInterval time.Duration,
gameWindow time.Duration, gameWindow time.Duration,
delays RecordClaimResolutionDelayMax, delays RecordClaimResolutionDelayMax,
detect Detect,
forecast Forecast, forecast Forecast,
extract Extract, extract Extract,
fetchBlockNumber BlockNumberFetcher, fetchBlockNumber BlockNumberFetcher,
...@@ -60,7 +57,6 @@ func newGameMonitor( ...@@ -60,7 +57,6 @@ func newGameMonitor(
monitorInterval: monitorInterval, monitorInterval: monitorInterval,
gameWindow: gameWindow, gameWindow: gameWindow,
delays: delays, delays: delays,
detect: detect,
forecast: forecast, forecast: forecast,
extract: extract, extract: extract,
fetchBlockNumber: fetchBlockNumber, fetchBlockNumber: fetchBlockNumber,
...@@ -83,19 +79,18 @@ func (m *gameMonitor) minGameTimestamp() uint64 { ...@@ -83,19 +79,18 @@ func (m *gameMonitor) minGameTimestamp() uint64 {
func (m *gameMonitor) monitorGames() error { func (m *gameMonitor) monitorGames() error {
blockNumber, err := m.fetchBlockNumber(m.ctx) blockNumber, err := m.fetchBlockNumber(m.ctx)
if err != nil { if err != nil {
return fmt.Errorf("Failed to fetch block number: %w", err) return fmt.Errorf("failed to fetch block number: %w", err)
} }
m.logger.Debug("Fetched block number", "blockNumber", blockNumber) m.logger.Debug("Fetched block number", "blockNumber", blockNumber)
blockHash, err := m.fetchBlockHash(context.Background(), new(big.Int).SetUint64(blockNumber)) blockHash, err := m.fetchBlockHash(context.Background(), new(big.Int).SetUint64(blockNumber))
if err != nil { if err != nil {
return fmt.Errorf("Failed to fetch block hash: %w", err) return fmt.Errorf("failed to fetch block hash: %w", err)
} }
enrichedGames, err := m.extract(m.ctx, blockHash, m.minGameTimestamp()) enrichedGames, err := m.extract(m.ctx, blockHash, m.minGameTimestamp())
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.delays(enrichedGames) m.delays(enrichedGames)
m.detect(m.ctx, enrichedGames)
m.forecast(m.ctx, enrichedGames) m.forecast(m.ctx, enrichedGames)
return nil return nil
} }
......
...@@ -24,20 +24,20 @@ func TestMonitor_MinGameTimestamp(t *testing.T) { ...@@ -24,20 +24,20 @@ func TestMonitor_MinGameTimestamp(t *testing.T) {
t.Parallel() t.Parallel()
t.Run("ZeroGameWindow", func(t *testing.T) { t.Run("ZeroGameWindow", func(t *testing.T) {
monitor, _, _, _, _ := setupMonitorTest(t) monitor, _, _, _ := setupMonitorTest(t)
monitor.gameWindow = time.Duration(0) monitor.gameWindow = time.Duration(0)
require.Equal(t, monitor.minGameTimestamp(), uint64(0)) require.Equal(t, monitor.minGameTimestamp(), uint64(0))
}) })
t.Run("ZeroClock", func(t *testing.T) { t.Run("ZeroClock", func(t *testing.T) {
monitor, _, _, _, _ := setupMonitorTest(t) monitor, _, _, _ := setupMonitorTest(t)
monitor.gameWindow = time.Minute monitor.gameWindow = time.Minute
monitor.clock = clock.NewDeterministicClock(time.Unix(0, 0)) monitor.clock = clock.NewDeterministicClock(time.Unix(0, 0))
require.Equal(t, uint64(0), monitor.minGameTimestamp()) require.Equal(t, uint64(0), monitor.minGameTimestamp())
}) })
t.Run("ValidArithmetic", func(t *testing.T) { t.Run("ValidArithmetic", func(t *testing.T) {
monitor, _, _, _, _ := setupMonitorTest(t) monitor, _, _, _ := setupMonitorTest(t)
monitor.gameWindow = time.Minute monitor.gameWindow = time.Minute
frozen := time.Unix(int64(time.Hour.Seconds()), 0) frozen := time.Unix(int64(time.Hour.Seconds()), 0)
monitor.clock = clock.NewDeterministicClock(frozen) monitor.clock = clock.NewDeterministicClock(frozen)
...@@ -50,7 +50,7 @@ func TestMonitor_MonitorGames(t *testing.T) { ...@@ -50,7 +50,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
...@@ -60,7 +60,7 @@ func TestMonitor_MonitorGames(t *testing.T) { ...@@ -60,7 +60,7 @@ func TestMonitor_MonitorGames(t *testing.T) {
}) })
t.Run("FailedFetchBlockHash", func(t *testing.T) { t.Run("FailedFetchBlockHash", func(t *testing.T) {
monitor, _, _, _, _ := setupMonitorTest(t) monitor, _, _, _ := setupMonitorTest(t)
boom := errors.New("boom") boom := errors.New("boom")
monitor.fetchBlockHash = func(ctx context.Context, number *big.Int) (common.Hash, error) { monitor.fetchBlockHash = func(ctx context.Context, number *big.Int) (common.Hash, error) {
return common.Hash{}, boom return common.Hash{}, boom
...@@ -69,22 +69,20 @@ func TestMonitor_MonitorGames(t *testing.T) { ...@@ -69,22 +69,20 @@ func TestMonitor_MonitorGames(t *testing.T) {
require.ErrorIs(t, err, boom) require.ErrorIs(t, err, boom)
}) })
t.Run("DetectsWithNoGames", func(t *testing.T) { t.Run("MonitorsWithNoGames", func(t *testing.T) {
monitor, factory, detector, forecast, delays := setupMonitorTest(t) monitor, factory, forecast, delays := 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, detector.calls)
require.Equal(t, 1, forecast.calls) require.Equal(t, 1, forecast.calls)
require.Equal(t, 1, delays.calls) require.Equal(t, 1, delays.calls)
}) })
t.Run("DetectsMultipleGames", func(t *testing.T) { t.Run("MonitorsMultipleGames", func(t *testing.T) {
monitor, factory, detector, forecast, delays := setupMonitorTest(t) monitor, factory, forecast, delays := 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, detector.calls)
require.Equal(t, 1, forecast.calls) require.Equal(t, 1, forecast.calls)
require.Equal(t, 1, delays.calls) require.Equal(t, 1, delays.calls)
}) })
...@@ -94,20 +92,20 @@ func TestMonitor_StartMonitoring(t *testing.T) { ...@@ -94,20 +92,20 @@ func TestMonitor_StartMonitoring(t *testing.T) {
t.Run("MonitorsGames", func(t *testing.T) { t.Run("MonitorsGames", func(t *testing.T) {
addr1 := common.Address{0xaa} addr1 := common.Address{0xaa}
addr2 := common.Address{0xbb} addr2 := common.Address{0xbb}
monitor, factory, detector, _, _ := setupMonitorTest(t) monitor, factory, 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
monitor.StartMonitoring() monitor.StartMonitoring()
require.Eventually(t, func() bool { require.Eventually(t, func() bool {
return detector.calls >= 2 return forecaster.calls >= 2
}, time.Second, 50*time.Millisecond) }, time.Second, 50*time.Millisecond)
monitor.StopMonitoring() monitor.StopMonitoring()
require.Equal(t, len(factory.games), detector.calls) // Each game's status is recorded twice require.Equal(t, len(factory.games), forecaster.calls) // Each game's status is recorded twice
}) })
t.Run("FailsToFetchGames", func(t *testing.T) { t.Run("FailsToFetchGames", func(t *testing.T) {
monitor, factory, detector, _, _ := setupMonitorTest(t) monitor, factory, forecaster, _ := setupMonitorTest(t)
factory.fetchErr = errors.New("boom") factory.fetchErr = errors.New("boom")
monitor.StartMonitoring() monitor.StartMonitoring()
...@@ -115,7 +113,7 @@ func TestMonitor_StartMonitoring(t *testing.T) { ...@@ -115,7 +113,7 @@ func TestMonitor_StartMonitoring(t *testing.T) {
return factory.calls > 0 return factory.calls > 0
}, time.Second, 50*time.Millisecond) }, time.Second, 50*time.Millisecond)
monitor.StopMonitoring() monitor.StopMonitoring()
require.Equal(t, 0, detector.calls) require.Equal(t, 0, forecaster.calls)
}) })
} }
...@@ -129,7 +127,7 @@ func newEnrichedGameData(proxy common.Address, timestamp uint64) *monTypes.Enric ...@@ -129,7 +127,7 @@ func newEnrichedGameData(proxy common.Address, timestamp uint64) *monTypes.Enric
} }
} }
func setupMonitorTest(t *testing.T) (*gameMonitor, *mockExtractor, *mockDetector, *mockForecast, *mockDelayCalculator) { func setupMonitorTest(t *testing.T) (*gameMonitor, *mockExtractor, *mockForecast, *mockDelayCalculator) {
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
...@@ -137,11 +135,10 @@ func setupMonitorTest(t *testing.T) (*gameMonitor, *mockExtractor, *mockDetector ...@@ -137,11 +135,10 @@ func setupMonitorTest(t *testing.T) (*gameMonitor, *mockExtractor, *mockDetector
fetchBlockHash := func(ctx context.Context, number *big.Int) (common.Hash, error) { fetchBlockHash := func(ctx context.Context, number *big.Int) (common.Hash, error) {
return common.Hash{}, nil return common.Hash{}, nil
} }
monitorInterval := time.Duration(100 * time.Millisecond) monitorInterval := 100 * time.Millisecond
cl := clock.NewAdvancingClock(10 * time.Millisecond) cl := clock.NewAdvancingClock(10 * time.Millisecond)
cl.Start() cl.Start()
extractor := &mockExtractor{} extractor := &mockExtractor{}
detect := &mockDetector{}
forecast := &mockForecast{} forecast := &mockForecast{}
delays := &mockDelayCalculator{} delays := &mockDelayCalculator{}
monitor := newGameMonitor( monitor := newGameMonitor(
...@@ -149,15 +146,14 @@ func setupMonitorTest(t *testing.T) (*gameMonitor, *mockExtractor, *mockDetector ...@@ -149,15 +146,14 @@ func setupMonitorTest(t *testing.T) (*gameMonitor, *mockExtractor, *mockDetector
logger, logger,
cl, cl,
monitorInterval, monitorInterval,
time.Duration(10*time.Second), 10*time.Second,
delays.RecordClaimResolutionDelayMax, delays.RecordClaimResolutionDelayMax,
detect.Detect,
forecast.Forecast, forecast.Forecast,
extractor.Extract, extractor.Extract,
fetchBlockNum, fetchBlockNum,
fetchBlockHash, fetchBlockHash,
) )
return monitor, extractor, detect, forecast, delays return monitor, extractor, forecast, delays
} }
type mockDelayCalculator struct { type mockDelayCalculator struct {
...@@ -176,14 +172,6 @@ func (m *mockForecast) Forecast(ctx context.Context, games []*monTypes.EnrichedG ...@@ -176,14 +172,6 @@ func (m *mockForecast) Forecast(ctx context.Context, games []*monTypes.EnrichedG
m.calls++ m.calls++
} }
type mockDetector struct {
calls int
}
func (m *mockDetector) Detect(ctx context.Context, games []*monTypes.EnrichedGameData) {
m.calls++
}
type mockExtractor struct { type mockExtractor struct {
fetchErr error fetchErr error
calls int calls int
......
...@@ -41,7 +41,6 @@ type Service struct { ...@@ -41,7 +41,6 @@ type Service struct {
forecast *forecast forecast *forecast
game *extract.GameCallerCreator game *extract.GameCallerCreator
rollupClient *sources.RollupClient rollupClient *sources.RollupClient
detector *detector
validator *outputValidator validator *outputValidator
l1Client *ethclient.Client l1Client *ethclient.Client
...@@ -91,7 +90,6 @@ func (s *Service) initFromConfig(ctx context.Context, cfg *config.Config) error ...@@ -91,7 +90,6 @@ func (s *Service) initFromConfig(ctx context.Context, cfg *config.Config) error
s.initExtractor() s.initExtractor()
s.initForecast(cfg) s.initForecast(cfg)
s.initDetector()
s.initMonitor(ctx, cfg) // Monitor must be initialized last s.initMonitor(ctx, cfg) // Monitor must be initialized last
...@@ -121,10 +119,6 @@ func (s *Service) initForecast(cfg *config.Config) { ...@@ -121,10 +119,6 @@ func (s *Service) initForecast(cfg *config.Config) {
s.forecast = newForecast(s.logger, s.metrics, s.validator) s.forecast = newForecast(s.logger, s.metrics, s.validator)
} }
func (s *Service) initDetector() {
s.detector = newDetector(s.logger, s.metrics, s.validator)
}
func (s *Service) initOutputRollupClient(ctx context.Context, cfg *config.Config) error { func (s *Service) initOutputRollupClient(ctx context.Context, cfg *config.Config) error {
outputRollupClient, err := dial.DialRollupClientWithTimeout(ctx, dial.DefaultDialTimeout, s.logger, cfg.RollupRpc) outputRollupClient, err := dial.DialRollupClientWithTimeout(ctx, dial.DefaultDialTimeout, s.logger, cfg.RollupRpc)
if err != nil { if err != nil {
...@@ -203,7 +197,6 @@ func (s *Service) initMonitor(ctx context.Context, cfg *config.Config) { ...@@ -203,7 +197,6 @@ func (s *Service) initMonitor(ctx context.Context, cfg *config.Config) {
cfg.MonitorInterval, cfg.MonitorInterval,
cfg.GameWindow, cfg.GameWindow,
s.delays.RecordClaimResolutionDelayMax, s.delays.RecordClaimResolutionDelayMax,
s.detector.Detect,
s.forecast.Forecast, s.forecast.Forecast,
s.extractor.Extract, s.extractor.Extract,
s.l1Client.BlockNumber, s.l1Client.BlockNumber,
......
...@@ -53,39 +53,9 @@ type ForecastBatch struct { ...@@ -53,39 +53,9 @@ type ForecastBatch struct {
DisagreeDefenderAhead int DisagreeDefenderAhead int
AgreeChallengerAhead int AgreeChallengerAhead int
DisagreeChallengerAhead int DisagreeChallengerAhead int
}
type DetectionBatch struct {
InProgress int
AgreeDefenderWins int AgreeDefenderWins int
DisagreeDefenderWins int DisagreeDefenderWins int
AgreeChallengerWins int AgreeChallengerWins int
DisagreeChallengerWins int DisagreeChallengerWins int
} }
func (d *DetectionBatch) Update(status types.GameStatus, agree bool) {
switch status {
case types.GameStatusInProgress:
d.InProgress++
case types.GameStatusDefenderWon:
if agree {
d.AgreeDefenderWins++
} else {
d.DisagreeDefenderWins++
}
case types.GameStatusChallengerWon:
if agree {
d.AgreeChallengerWins++
} else {
d.DisagreeChallengerWins++
}
}
}
func (d *DetectionBatch) Merge(other DetectionBatch) {
d.InProgress += other.InProgress
d.AgreeDefenderWins += other.AgreeDefenderWins
d.DisagreeDefenderWins += other.DisagreeDefenderWins
d.AgreeChallengerWins += other.AgreeChallengerWins
d.DisagreeChallengerWins += other.DisagreeChallengerWins
}
...@@ -67,123 +67,3 @@ func TestStatusBatch_Add(t *testing.T) { ...@@ -67,123 +67,3 @@ func TestStatusBatch_Add(t *testing.T) {
}) })
} }
} }
func TestDetectionBatch_Update(t *testing.T) {
statusExpectations := []struct {
status types.GameStatus
create func(int, bool) DetectionBatch
}{
{
status: types.GameStatusInProgress,
create: func(inProgress int, _ bool) DetectionBatch {
return DetectionBatch{inProgress, 0, 0, 0, 0}
},
},
{
status: types.GameStatusDefenderWon,
create: func(defenderWon int, agree bool) DetectionBatch {
if agree {
return DetectionBatch{0, defenderWon, 0, 0, 0}
}
return DetectionBatch{0, 0, defenderWon, 0, 0}
},
},
{
status: types.GameStatusChallengerWon,
create: func(challengerWon int, agree bool) DetectionBatch {
if agree {
return DetectionBatch{0, 0, 0, challengerWon, 0}
}
return DetectionBatch{0, 0, 0, 0, challengerWon}
},
},
}
type test struct {
name string
status types.GameStatus
agree bool
invocations int
expected DetectionBatch
}
var tests []test
for i := 0; i < 100; i++ {
for _, exp := range statusExpectations {
agree := i%2 == 0
tests = append(tests, test{
name: fmt.Sprintf("Invocation-%d", i),
status: exp.status,
agree: agree,
invocations: i,
expected: exp.create(i, agree),
})
}
}
for _, test := range tests {
test := test
t.Run(test.name, func(t *testing.T) {
d := DetectionBatch{}
for i := 0; i < test.invocations; i++ {
d.Update(test.status, test.agree)
}
require.Equal(t, test.expected, d)
})
}
}
func TestDetectionBatch_Merge(t *testing.T) {
type test struct {
name string
merge DetectionBatch
expected DetectionBatch
}
tests := []test{
{
name: "Empty",
merge: DetectionBatch{},
expected: DetectionBatch{},
},
{
name: "InProgress",
merge: DetectionBatch{1, 0, 0, 0, 0},
expected: DetectionBatch{1, 0, 0, 0, 0},
},
{
name: "AgreeDefenderWins",
merge: DetectionBatch{0, 1, 0, 0, 0},
expected: DetectionBatch{0, 1, 0, 0, 0},
},
{
name: "DisagreeDefenderWins",
merge: DetectionBatch{0, 0, 1, 0, 0},
expected: DetectionBatch{0, 0, 1, 0, 0},
},
{
name: "AgreeChallengerWins",
merge: DetectionBatch{0, 0, 0, 1, 0},
expected: DetectionBatch{0, 0, 0, 1, 0},
},
{
name: "DisagreeChallengerWins",
merge: DetectionBatch{0, 0, 0, 0, 1},
expected: DetectionBatch{0, 0, 0, 0, 1},
},
{
name: "All",
merge: DetectionBatch{1, 1, 1, 1, 1},
expected: DetectionBatch{1, 1, 1, 1, 1},
},
}
for _, test := range tests {
test := test
t.Run(test.name, func(t *testing.T) {
d := DetectionBatch{}
d.Merge(test.merge)
require.Equal(t, test.expected, d)
})
}
}
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