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

feat(op-dispute-mon): rollup output root checking (#9422)

Co-authored-by: default avatarclabby <ben@clab.by>
parent c9340197
...@@ -14,6 +14,7 @@ import ( ...@@ -14,6 +14,7 @@ import (
var ( var (
ErrMissingL1EthRPC = errors.New("missing l1 eth rpc url") ErrMissingL1EthRPC = errors.New("missing l1 eth rpc url")
ErrMissingGameFactoryAddress = errors.New("missing game factory address") ErrMissingGameFactoryAddress = errors.New("missing game factory address")
ErrMissingRollupRpc = errors.New("missing rollup rpc url")
) )
const ( const (
...@@ -31,8 +32,10 @@ const ( ...@@ -31,8 +32,10 @@ const (
type Config struct { type Config struct {
L1EthRpc string // L1 RPC Url L1EthRpc string // L1 RPC Url
GameFactoryAddress common.Address // Address of the dispute game factory GameFactoryAddress common.Address // Address of the dispute game factory
MonitorInterval time.Duration // Frequency to check for new games to monitor. RollupRpc string // The rollup node RPC URL.
GameWindow time.Duration // Maximum window to look for games to monitor.
MonitorInterval time.Duration // Frequency to check for new games to monitor.
GameWindow time.Duration // Maximum window to look for games to monitor.
MetricsConfig opmetrics.CLIConfig MetricsConfig opmetrics.CLIConfig
PprofConfig oppprof.CLIConfig PprofConfig oppprof.CLIConfig
...@@ -55,6 +58,9 @@ func (c Config) Check() error { ...@@ -55,6 +58,9 @@ func (c Config) Check() error {
if c.L1EthRpc == "" { if c.L1EthRpc == "" {
return ErrMissingL1EthRPC return ErrMissingL1EthRPC
} }
if c.RollupRpc == "" {
return ErrMissingRollupRpc
}
if c.GameFactoryAddress == (common.Address{}) { if c.GameFactoryAddress == (common.Address{}) {
return ErrMissingGameFactoryAddress return ErrMissingGameFactoryAddress
} }
......
...@@ -11,10 +11,13 @@ import ( ...@@ -11,10 +11,13 @@ import (
var ( var (
validL1EthRpc = "http://localhost:8545" validL1EthRpc = "http://localhost:8545"
validGameFactoryAddress = common.Address{0x23} validGameFactoryAddress = common.Address{0x23}
validRollupRpc = "http://localhost:8555"
) )
func validConfig() Config { func validConfig() Config {
return NewConfig(validGameFactoryAddress, validL1EthRpc) cfg := NewConfig(validGameFactoryAddress, validL1EthRpc)
cfg.RollupRpc = validRollupRpc
return cfg
} }
func TestValidConfigIsValid(t *testing.T) { func TestValidConfigIsValid(t *testing.T) {
...@@ -32,3 +35,9 @@ func TestGameFactoryAddressRequired(t *testing.T) { ...@@ -32,3 +35,9 @@ func TestGameFactoryAddressRequired(t *testing.T) {
config.GameFactoryAddress = common.Address{} config.GameFactoryAddress = common.Address{}
require.ErrorIs(t, config.Check(), ErrMissingGameFactoryAddress) require.ErrorIs(t, config.Check(), ErrMissingGameFactoryAddress)
} }
func TestRollupRpcRequired(t *testing.T) {
config := validConfig()
config.RollupRpc = ""
require.ErrorIs(t, config.Check(), ErrMissingRollupRpc)
}
...@@ -33,6 +33,11 @@ var ( ...@@ -33,6 +33,11 @@ var (
EnvVars: prefixEnvVars("GAME_FACTORY_ADDRESS"), EnvVars: prefixEnvVars("GAME_FACTORY_ADDRESS"),
} }
// Optional Flags // Optional Flags
RollupRpcFlag = &cli.StringFlag{
Name: "rollup-rpc",
Usage: "HTTP provider URL for the rollup node",
EnvVars: prefixEnvVars("ROLLUP_RPC"),
}
MonitorIntervalFlag = &cli.DurationFlag{ MonitorIntervalFlag = &cli.DurationFlag{
Name: "monitor-interval", Name: "monitor-interval",
Usage: "The interval at which the dispute monitor will check for new games to monitor.", Usage: "The interval at which the dispute monitor will check for new games to monitor.",
...@@ -56,6 +61,7 @@ var requiredFlags = []cli.Flag{ ...@@ -56,6 +61,7 @@ var requiredFlags = []cli.Flag{
// optionalFlags is a list of unchecked cli flags // optionalFlags is a list of unchecked cli flags
var optionalFlags = []cli.Flag{ var optionalFlags = []cli.Flag{
RollupRpcFlag,
MonitorIntervalFlag, MonitorIntervalFlag,
GameWindowFlag, GameWindowFlag,
} }
...@@ -97,6 +103,7 @@ func NewConfigFromCLI(ctx *cli.Context) (*config.Config, error) { ...@@ -97,6 +103,7 @@ func NewConfigFromCLI(ctx *cli.Context) (*config.Config, error) {
L1EthRpc: ctx.String(L1EthRpcFlag.Name), L1EthRpc: ctx.String(L1EthRpcFlag.Name),
GameFactoryAddress: gameFactoryAddress, GameFactoryAddress: gameFactoryAddress,
RollupRpc: ctx.String(RollupRpcFlag.Name),
MonitorInterval: ctx.Duration(MonitorIntervalFlag.Name), MonitorInterval: ctx.Duration(MonitorIntervalFlag.Name),
GameWindow: ctx.Duration(GameWindowFlag.Name), GameWindow: ctx.Duration(GameWindowFlag.Name),
......
...@@ -20,6 +20,7 @@ type Metricer interface { ...@@ -20,6 +20,7 @@ type Metricer interface {
RecordUp() RecordUp()
RecordGamesStatus(inProgress, defenderWon, challengerWon int) RecordGamesStatus(inProgress, defenderWon, challengerWon int)
RecordGameAgreement(status string, count int)
caching.Metrics caching.Metrics
} }
...@@ -37,7 +38,8 @@ type Metrics struct { ...@@ -37,7 +38,8 @@ type Metrics struct {
info prometheus.GaugeVec info prometheus.GaugeVec
up prometheus.Gauge up prometheus.Gauge
trackedGames prometheus.GaugeVec trackedGames prometheus.GaugeVec
gamesAgreement prometheus.GaugeVec
} }
func (m *Metrics) Registry() *prometheus.Registry { func (m *Metrics) Registry() *prometheus.Registry {
...@@ -76,6 +78,13 @@ func NewMetrics() *Metrics { ...@@ -76,6 +78,13 @@ func NewMetrics() *Metrics {
}, []string{ }, []string{
"status", "status",
}), }),
gamesAgreement: *factory.NewGaugeVec(prometheus.GaugeOpts{
Namespace: Namespace,
Name: "games_agreement",
Help: "Number of games broken down by whether the result agrees with the reference node",
}, []string{
"status",
}),
} }
} }
...@@ -112,3 +121,7 @@ func (m *Metrics) RecordGamesStatus(inProgress, defenderWon, challengerWon int) ...@@ -112,3 +121,7 @@ func (m *Metrics) RecordGamesStatus(inProgress, defenderWon, challengerWon int)
m.trackedGames.WithLabelValues("defender_won").Set(float64(defenderWon)) m.trackedGames.WithLabelValues("defender_won").Set(float64(defenderWon))
m.trackedGames.WithLabelValues("challenger_won").Set(float64(challengerWon)) m.trackedGames.WithLabelValues("challenger_won").Set(float64(challengerWon))
} }
func (m *Metrics) RecordGameAgreement(status string, count int) {
m.gamesAgreement.WithLabelValues(status).Set(float64(count))
}
...@@ -11,3 +11,4 @@ func (*NoopMetricsImpl) CacheAdd(_ string, _ int, _ bool) {} ...@@ -11,3 +11,4 @@ func (*NoopMetricsImpl) CacheAdd(_ string, _ int, _ bool) {}
func (*NoopMetricsImpl) CacheGet(_ string, _ bool) {} func (*NoopMetricsImpl) CacheGet(_ string, _ bool) {}
func (*NoopMetricsImpl) RecordGamesStatus(inProgress, defenderWon, challengerWon int) {} func (*NoopMetricsImpl) RecordGamesStatus(inProgress, defenderWon, challengerWon int) {}
func (*NoopMetricsImpl) RecordGameAgreement(status string, count int) {}
package mon
import (
"context"
"fmt"
"github.com/ethereum-optimism/optimism/op-challenger/game/types"
"github.com/ethereum-optimism/optimism/op-service/eth"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/log"
)
type statusBatch struct {
inProgress, defenderWon, challengerWon int
}
func (s *statusBatch) Add(status types.GameStatus) {
switch status {
case types.GameStatusInProgress:
s.inProgress++
case types.GameStatusDefenderWon:
s.defenderWon++
case types.GameStatusChallengerWon:
s.challengerWon++
}
}
type detectionBatch struct {
inProgress int
agreeDefenderWins int
disagreeDefenderWins int
agreeChallengerWins int
disagreeChallengerWins int
}
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
}
type OutputRollupClient interface {
OutputAtBlock(ctx context.Context, blockNum uint64) (*eth.OutputResponse, error)
}
type MetadataCreator interface {
CreateContract(game types.GameMetadata) (MetadataLoader, error)
}
type DetectorMetrics interface {
RecordGameAgreement(status string, count int)
RecordGamesStatus(inProgress, defenderWon, challengerWon int)
}
type detector struct {
logger log.Logger
metrics DetectorMetrics
creator MetadataCreator
outputClient OutputRollupClient
}
func newDetector(logger log.Logger, metrics DetectorMetrics, creator MetadataCreator, outputClient OutputRollupClient) *detector {
return &detector{
logger: logger,
metrics: metrics,
creator: creator,
outputClient: outputClient,
}
}
func (d *detector) Detect(ctx context.Context, games []types.GameMetadata) {
statBatch := statusBatch{}
detectBatch := detectionBatch{}
for _, game := range games {
// Fetch the game metadata to ensure the game status is recorded
// regardless of whether the game agreement is checked.
l2BlockNum, rootClaim, status, err := d.fetchGameMetadata(ctx, game)
if err != nil {
d.logger.Error("Failed to fetch game metadata", "err", err)
continue
}
statBatch.Add(status)
processed, err := d.checkAgreement(ctx, game.Proxy, l2BlockNum, rootClaim, 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)
}
func (d *detector) recordBatch(batch detectionBatch) {
d.metrics.RecordGameAgreement("in_progress", batch.inProgress)
d.metrics.RecordGameAgreement("agree_defender_wins", batch.agreeDefenderWins)
d.metrics.RecordGameAgreement("disagree_defender_wins", batch.disagreeDefenderWins)
d.metrics.RecordGameAgreement("agree_challenger_wins", batch.agreeChallengerWins)
d.metrics.RecordGameAgreement("disagree_challenger_wins", batch.disagreeChallengerWins)
}
func (d *detector) fetchGameMetadata(ctx context.Context, game types.GameMetadata) (uint64, common.Hash, types.GameStatus, error) {
loader, err := d.creator.CreateContract(game)
if err != nil {
return 0, common.Hash{}, 0, fmt.Errorf("failed to create contract: %w", err)
}
blockNum, rootClaim, status, err := loader.GetGameMetadata(ctx)
if err != nil {
return 0, common.Hash{}, 0, fmt.Errorf("failed to fetch game metadata: %w", err)
}
return blockNum, rootClaim, status, nil
}
func (d *detector) checkAgreement(ctx context.Context, addr common.Address, blockNum uint64, rootClaim common.Hash, status types.GameStatus) (detectionBatch, error) {
agree, err := d.checkRootAgreement(ctx, blockNum, rootClaim)
if err != nil {
return detectionBatch{}, err
}
batch := detectionBatch{}
switch status {
case types.GameStatusInProgress:
batch.inProgress++
case types.GameStatusDefenderWon:
if agree {
batch.agreeDefenderWins++
} else {
batch.disagreeDefenderWins++
d.logger.Error("Defender won but root claim does not match", "gameAddr", addr, "rootClaim", rootClaim)
}
case types.GameStatusChallengerWon:
if agree {
batch.agreeChallengerWins++
} else {
batch.disagreeChallengerWins++
d.logger.Error("Challenger won but root claim does not match", "gameAddr", addr, "rootClaim", rootClaim)
}
}
return batch, nil
}
func (d *detector) checkRootAgreement(ctx context.Context, blockNum uint64, rootClaim common.Hash) (bool, error) {
output, err := d.outputClient.OutputAtBlock(ctx, blockNum)
if err != nil {
return false, fmt.Errorf("failed to get output at block: %w", err)
}
return rootClaim == common.Hash(output.OutputRoot), nil
}
package mon
import (
"context"
"errors"
"testing"
"github.com/ethereum-optimism/optimism/op-challenger/game/types"
"github.com/ethereum-optimism/optimism/op-service/eth"
"github.com/ethereum-optimism/optimism/op-service/testlog"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/log"
"github.com/stretchr/testify/require"
)
var (
mockRootClaim = common.HexToHash("0x10")
)
func TestDetector_Detect(t *testing.T) {
t.Parallel()
t.Run("NoGames", func(t *testing.T) {
detector, metrics, _, _ := setupDetectorTest(t)
detector.Detect(context.Background(), []types.GameMetadata{})
metrics.Equals(t, 0, 0, 0)
metrics.Mapped(t, map[string]int{})
})
t.Run("MetadataFetchFails", func(t *testing.T) {
detector, metrics, creator, _ := setupDetectorTest(t)
creator.err = errors.New("boom")
detector.Detect(context.Background(), []types.GameMetadata{{}})
metrics.Equals(t, 0, 0, 0)
metrics.Mapped(t, map[string]int{})
})
t.Run("CheckAgreementFails", func(t *testing.T) {
detector, metrics, creator, rollup := setupDetectorTest(t)
rollup.err = errors.New("boom")
creator.loader = &mockMetadataLoader{status: types.GameStatusInProgress}
detector.Detect(context.Background(), []types.GameMetadata{{}})
metrics.Equals(t, 1, 0, 0) // Status should still be metriced here!
metrics.Mapped(t, map[string]int{})
})
t.Run("SingleGame", func(t *testing.T) {
detector, metrics, creator, _ := setupDetectorTest(t)
loader := &mockMetadataLoader{status: types.GameStatusInProgress}
creator.loader = loader
detector.Detect(context.Background(), []types.GameMetadata{{}})
metrics.Equals(t, 1, 0, 0)
metrics.Mapped(t, map[string]int{"in_progress": 1})
})
t.Run("MultipleGames", func(t *testing.T) {
detector, metrics, creator, _ := setupDetectorTest(t)
loader := &mockMetadataLoader{status: types.GameStatusInProgress}
creator.loader = loader
detector.Detect(context.Background(), []types.GameMetadata{{}, {}, {}})
metrics.Equals(t, 3, 0, 0)
metrics.Mapped(t, map[string]int{"in_progress": 3})
})
}
func TestDetector_RecordBatch(t *testing.T) {
tests := []struct {
name string
batch detectionBatch
expect func(*testing.T, *mockDetectorMetricer)
}{
{
name: "no games",
batch: detectionBatch{},
expect: func(t *testing.T, metrics *mockDetectorMetricer) {},
},
{
name: "in_progress",
batch: detectionBatch{inProgress: 1},
expect: func(t *testing.T, metrics *mockDetectorMetricer) {
require.Equal(t, 1, metrics.gameAgreement["in_progress"])
},
},
{
name: "agree_defender_wins",
batch: detectionBatch{agreeDefenderWins: 1},
expect: func(t *testing.T, metrics *mockDetectorMetricer) {
require.Equal(t, 1, metrics.gameAgreement["agree_defender_wins"])
},
},
{
name: "disagree_defender_wins",
batch: detectionBatch{disagreeDefenderWins: 1},
expect: func(t *testing.T, metrics *mockDetectorMetricer) {
require.Equal(t, 1, metrics.gameAgreement["disagree_defender_wins"])
},
},
{
name: "agree_challenger_wins",
batch: detectionBatch{agreeChallengerWins: 1},
expect: func(t *testing.T, metrics *mockDetectorMetricer) {
require.Equal(t, 1, metrics.gameAgreement["agree_challenger_wins"])
},
},
{
name: "disagree_challenger_wins",
batch: detectionBatch{disagreeChallengerWins: 1},
expect: func(t *testing.T, metrics *mockDetectorMetricer) {
require.Equal(t, 1, metrics.gameAgreement["disagree_challenger_wins"])
},
},
}
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_FetchGameMetadata(t *testing.T) {
t.Parallel()
t.Run("CreateContractFails", func(t *testing.T) {
detector, _, creator, _ := setupDetectorTest(t)
creator.err = errors.New("boom")
_, _, _, err := detector.fetchGameMetadata(context.Background(), types.GameMetadata{})
require.ErrorIs(t, err, creator.err)
})
t.Run("GetGameMetadataFails", func(t *testing.T) {
detector, _, creator, _ := setupDetectorTest(t)
loader := &mockMetadataLoader{err: errors.New("boom")}
creator.loader = loader
_, _, _, err := detector.fetchGameMetadata(context.Background(), types.GameMetadata{})
require.Error(t, err)
})
t.Run("Success", func(t *testing.T) {
detector, _, creator, _ := setupDetectorTest(t)
loader := &mockMetadataLoader{status: types.GameStatusInProgress}
creator.loader = loader
_, _, status, err := detector.fetchGameMetadata(context.Background(), types.GameMetadata{})
require.NoError(t, err)
require.Equal(t, types.GameStatusInProgress, status)
})
}
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(*detectionBatch)
err error
}{
{
name: "in_progress",
expectBatch: func(batch *detectionBatch) {
require.Equal(t, 1, batch.inProgress)
},
},
{
name: "agree_defender_wins",
rootClaim: mockRootClaim,
status: types.GameStatusDefenderWon,
expectBatch: func(batch *detectionBatch) {
require.Equal(t, 1, batch.agreeDefenderWins)
},
},
{
name: "disagree_defender_wins",
status: types.GameStatusDefenderWon,
expectBatch: func(batch *detectionBatch) {
require.Equal(t, 1, batch.disagreeDefenderWins)
},
},
{
name: "agree_challenger_wins",
rootClaim: mockRootClaim,
status: types.GameStatusChallengerWon,
expectBatch: func(batch *detectionBatch) {
require.Equal(t, 1, batch.agreeChallengerWins)
},
},
{
name: "disagree_challenger_wins",
status: types.GameStatusChallengerWon,
expectBatch: func(batch *detectionBatch) {
require.Equal(t, 1, batch.disagreeChallengerWins)
},
},
}
for _, test := range tests {
test := test
t.Run(test.name, func(t *testing.T) {
detector, _, _, _ := setupDetectorTest(t)
batch, err := detector.checkAgreement(context.Background(), common.Address{}, 0, test.rootClaim, test.status)
require.NoError(t, err)
test.expectBatch(&batch)
})
}
}
func TestDetector_CheckRootAgreement(t *testing.T) {
t.Parallel()
t.Run("OutputFetchFails", func(t *testing.T) {
detector, _, _, rollup := setupDetectorTest(t)
rollup.err = errors.New("boom")
agree, err := detector.checkRootAgreement(context.Background(), 0, mockRootClaim)
require.ErrorIs(t, err, rollup.err)
require.False(t, agree)
})
t.Run("OutputMismatch", func(t *testing.T) {
detector, _, _, _ := setupDetectorTest(t)
agree, err := detector.checkRootAgreement(context.Background(), 0, common.Hash{})
require.NoError(t, err)
require.False(t, agree)
})
t.Run("OutputMatches", func(t *testing.T) {
detector, _, _, _ := setupDetectorTest(t)
agree, err := detector.checkRootAgreement(context.Background(), 0, mockRootClaim)
require.NoError(t, err)
require.True(t, agree)
})
}
func setupDetectorTest(t *testing.T) (*detector, *mockDetectorMetricer, *mockMetadataCreator, *stubRollupClient) {
logger := testlog.Logger(t, log.LvlDebug)
metrics := &mockDetectorMetricer{}
loader := &mockMetadataLoader{}
creator := &mockMetadataCreator{loader: loader}
rollupClient := &stubRollupClient{}
detector := newDetector(logger, metrics, creator, rollupClient)
return detector, metrics, creator, rollupClient
}
type stubRollupClient struct {
blockNum uint64
err error
}
func (s *stubRollupClient) OutputAtBlock(ctx context.Context, blockNum uint64) (*eth.OutputResponse, error) {
s.blockNum = blockNum
return &eth.OutputResponse{OutputRoot: eth.Bytes32(mockRootClaim)}, s.err
}
type mockMetadataCreator struct {
calls int
err error
loader *mockMetadataLoader
}
func (m *mockMetadataCreator) CreateContract(game types.GameMetadata) (MetadataLoader, error) {
m.calls++
if m.err != nil {
return nil, m.err
}
return m.loader, nil
}
type mockMetadataLoader struct {
calls int
status types.GameStatus
err error
}
func (m *mockMetadataLoader) GetGameMetadata(ctx context.Context) (uint64, common.Hash, types.GameStatus, error) {
m.calls++
if m.err != nil {
return 0, common.Hash{}, m.status, m.err
}
return 0, common.Hash{}, m.status, nil
}
type mockDetectorMetricer struct {
inProgress int
defenderWon int
challengerWon int
gameAgreement map[string]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[string]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 string, count int) {
if m.gameAgreement == nil {
m.gameAgreement = make(map[string]int)
}
m.gameAgreement[status] += count
}
...@@ -13,61 +13,48 @@ import ( ...@@ -13,61 +13,48 @@ import (
"github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/log"
) )
type blockNumberFetcher func(ctx context.Context) (uint64, error) type Detect func(ctx context.Context, games []types.GameMetadata)
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)
// gameSource loads information about the games available to play type FactoryGameFetcher func(ctx context.Context, blockHash common.Hash, earliestTimestamp uint64) ([]types.GameMetadata, error)
type gameSource interface {
GetGamesAtOrAfter(ctx context.Context, blockHash common.Hash, earliestTimestamp uint64) ([]types.GameMetadata, error)
}
type MonitorMetricer interface {
RecordGamesStatus(inProgress, defenderWon, challengerWon int)
}
type MetadataCreator interface {
CreateContract(game types.GameMetadata) (MetadataLoader, error)
}
type gameMonitor struct { type gameMonitor struct {
logger log.Logger logger log.Logger
metrics MonitorMetricer clock clock.Clock
done chan struct{}
ctx context.Context ctx context.Context
cancel context.CancelFunc cancel context.CancelFunc
clock clock.Clock gameWindow time.Duration
monitorInterval time.Duration monitorInterval time.Duration
done chan struct{}
source gameSource detect Detect
metadata MetadataCreator fetchGames FactoryGameFetcher
gameWindow time.Duration fetchBlockHash BlockHashFetcher
fetchBlockNumber blockNumberFetcher fetchBlockNumber BlockNumberFetcher
fetchBlockHash blockHashFetcher
} }
func newGameMonitor( func newGameMonitor(
ctx context.Context, ctx context.Context,
logger log.Logger, logger log.Logger,
metrics MonitorMetricer,
cl clock.Clock, cl clock.Clock,
monitorInterval time.Duration, monitorInterval time.Duration,
source gameSource,
metadata MetadataCreator,
gameWindow time.Duration, gameWindow time.Duration,
fetchBlockNumber blockNumberFetcher, detect Detect,
fetchBlockHash blockHashFetcher, factory FactoryGameFetcher,
fetchBlockNumber BlockNumberFetcher,
fetchBlockHash BlockHashFetcher,
) *gameMonitor { ) *gameMonitor {
return &gameMonitor{ return &gameMonitor{
logger: logger, logger: logger,
metrics: metrics,
ctx: ctx,
clock: cl, clock: cl,
ctx: ctx,
done: make(chan struct{}), done: make(chan struct{}),
monitorInterval: monitorInterval, monitorInterval: monitorInterval,
source: source,
metadata: metadata,
gameWindow: gameWindow, gameWindow: gameWindow,
detect: detect,
fetchGames: factory,
fetchBlockNumber: fetchBlockNumber, fetchBlockNumber: fetchBlockNumber,
fetchBlockHash: fetchBlockHash, fetchBlockHash: fetchBlockHash,
} }
...@@ -95,36 +82,11 @@ func (m *gameMonitor) monitorGames() error { ...@@ -95,36 +82,11 @@ func (m *gameMonitor) monitorGames() error {
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)
} }
games, err := m.source.GetGamesAtOrAfter(m.ctx, blockHash, m.minGameTimestamp()) games, err := m.fetchGames(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)
} }
return m.recordGamesStatus(m.ctx, games) m.detect(m.ctx, games)
}
func (m *gameMonitor) recordGamesStatus(ctx context.Context, games []types.GameMetadata) error {
inProgress, defenderWon, challengerWon := 0, 0, 0
for _, game := range games {
loader, err := m.metadata.CreateContract(game)
if err != nil {
m.logger.Error("Failed to create contract", "err", err)
continue
}
_, _, status, err := loader.GetGameMetadata(ctx)
if err != nil {
m.logger.Error("Failed to get game metadata", "err", err)
continue
}
switch status {
case types.GameStatusInProgress:
inProgress++
case types.GameStatusDefenderWon:
defenderWon++
case types.GameStatusChallengerWon:
challengerWon++
}
}
m.metrics.RecordGamesStatus(inProgress, defenderWon, challengerWon)
return nil return nil
} }
......
This diff is collapsed.
...@@ -21,6 +21,7 @@ import ( ...@@ -21,6 +21,7 @@ import (
"github.com/ethereum-optimism/optimism/op-service/httputil" "github.com/ethereum-optimism/optimism/op-service/httputil"
opmetrics "github.com/ethereum-optimism/optimism/op-service/metrics" opmetrics "github.com/ethereum-optimism/optimism/op-service/metrics"
"github.com/ethereum-optimism/optimism/op-service/oppprof" "github.com/ethereum-optimism/optimism/op-service/oppprof"
"github.com/ethereum-optimism/optimism/op-service/sources"
"github.com/ethereum-optimism/optimism/op-service/sources/batching" "github.com/ethereum-optimism/optimism/op-service/sources/batching"
) )
...@@ -33,7 +34,9 @@ type Service struct { ...@@ -33,7 +34,9 @@ type Service struct {
cl clock.Clock cl clock.Clock
metadata *metadataCreator metadata *metadataCreator
rollupClient *sources.RollupClient
detector *detector
l1Client *ethclient.Client l1Client *ethclient.Client
...@@ -71,6 +74,10 @@ func (s *Service) initFromConfig(ctx context.Context, cfg *config.Config) error ...@@ -71,6 +74,10 @@ func (s *Service) initFromConfig(ctx context.Context, cfg *config.Config) error
if err := s.initFactoryContract(cfg); err != nil { if err := s.initFactoryContract(cfg); err != nil {
return fmt.Errorf("failed to create factory contract bindings: %w", err) return fmt.Errorf("failed to create factory contract bindings: %w", err)
} }
if err := s.initOutputRollupClient(ctx, cfg); err != nil {
return fmt.Errorf("failed to init rollup client: %w", err)
}
s.initDetector()
s.initMetadataCreator() s.initMetadataCreator()
s.initMonitor(ctx, cfg) s.initMonitor(ctx, cfg)
...@@ -80,6 +87,19 @@ func (s *Service) initFromConfig(ctx context.Context, cfg *config.Config) error ...@@ -80,6 +87,19 @@ func (s *Service) initFromConfig(ctx context.Context, cfg *config.Config) error
return nil return nil
} }
func (s *Service) initDetector() {
s.detector = newDetector(s.logger, s.metrics, s.metadata, s.rollupClient)
}
func (s *Service) initOutputRollupClient(ctx context.Context, cfg *config.Config) error {
outputRollupClient, err := dial.DialRollupClientWithTimeout(ctx, dial.DefaultDialTimeout, s.logger, cfg.RollupRpc)
if err != nil {
return fmt.Errorf("failed to dial rollup client: %w", err)
}
s.rollupClient = outputRollupClient
return nil
}
func (s *Service) initMetadataCreator() { func (s *Service) initMetadataCreator() {
s.metadata = NewMetadataCreator(s.metrics, batching.NewMultiCaller(s.l1Client.Client(), batching.DefaultBatchSize)) s.metadata = NewMetadataCreator(s.metrics, batching.NewMultiCaller(s.l1Client.Client(), batching.DefaultBatchSize))
} }
...@@ -149,12 +169,11 @@ func (s *Service) initMonitor(ctx context.Context, cfg *config.Config) { ...@@ -149,12 +169,11 @@ func (s *Service) initMonitor(ctx context.Context, cfg *config.Config) {
s.monitor = newGameMonitor( s.monitor = newGameMonitor(
ctx, ctx,
s.logger, s.logger,
s.metrics,
s.cl, s.cl,
cfg.MonitorInterval, cfg.MonitorInterval,
s.factoryContract,
s.metadata,
cfg.GameWindow, cfg.GameWindow,
s.detector.Detect,
s.factoryContract.GetGamesAtOrAfter,
s.l1Client.BlockNumber, s.l1Client.BlockNumber,
blockHashFetcher, blockHashFetcher,
) )
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment