Commit 3c7922c8 authored by refcell's avatar refcell Committed by GitHub

feat(op-dispute-mon): WithdrawalRequests Metrics (#9942)

* feat(op-challenger): Delayed Weth Withdrawal Request Caller

* fix(op-challenger): withdrawal request field ordering

* fix(op-challenger): encapsulate delayed weth behind the fault dispute game contract binding

* feat(op-dispute-mon): get withdrawals game caller method

* fix(op-dispute-mon): revert service name changes

* fix(op-dispute-mon): revert extractor test name

* fix(op-dispute-mon): revert extractor changes

* fix(op-dispute-mon): revert newline add

* fix(op-dispute-mon): revert caller field reordering

* feat(op-dispute-mon): weth caller creation and pass it through sub components

* chore(op-dispute-mon): weth caller creation unit test

* fix(op-dispute-mon): extractor test for weth caller creation error

* feat(op-dispute-mon): wire up the withdrawals extractor

* fix(op-dispute-mon): rebases

* fix(op-dispute-mon): revert caller test change

* fix(op-dispute-mon): recipient enricher

* feat(op-dispute-mon): withdrawal request metrics

* Update op-dispute-mon/mon/withdrawals.go
Co-authored-by: default avatarAdrian Sutton <adrian@oplabs.co>

* fix(op-dispute-mon): withdrawal request amounts

---------
Co-authored-by: default avatarAdrian Sutton <adrian@oplabs.co>
parent abcb627f
...@@ -39,6 +39,8 @@ type Metricer interface { ...@@ -39,6 +39,8 @@ type Metricer interface {
RecordInfo(version string) RecordInfo(version string)
RecordUp() RecordUp()
RecordWithdrawalRequests(delayedWeth common.Address, matches bool, count int)
RecordClaimResolutionDelayMax(delay float64) RecordClaimResolutionDelayMax(delay float64)
RecordOutputFetchTime(timestamp float64) RecordOutputFetchTime(timestamp float64)
...@@ -62,6 +64,8 @@ type Metrics struct { ...@@ -62,6 +64,8 @@ type Metrics struct {
*opmetrics.CacheMetrics *opmetrics.CacheMetrics
*contractMetrics.ContractMetrics *contractMetrics.ContractMetrics
withdrawalRequests prometheus.GaugeVec
info prometheus.GaugeVec info prometheus.GaugeVec
up prometheus.Gauge up prometheus.Gauge
...@@ -115,6 +119,14 @@ func NewMetrics() *Metrics { ...@@ -115,6 +119,14 @@ func NewMetrics() *Metrics {
Name: "claim_resolution_delay_max", Name: "claim_resolution_delay_max",
Help: "Maximum claim resolution delay in seconds", Help: "Maximum claim resolution delay in seconds",
}), }),
withdrawalRequests: *factory.NewGaugeVec(prometheus.GaugeOpts{
Namespace: Namespace,
Name: "withdrawal_requests",
Help: "Number of withdrawal requests categorised by the source DelayedWETH contract and whether the withdrawal request amount matches or diverges from its fault dispute game credits",
}, []string{
"delayedWETH",
"credits",
}),
gamesAgreement: *factory.NewGaugeVec(prometheus.GaugeOpts{ gamesAgreement: *factory.NewGaugeVec(prometheus.GaugeOpts{
Namespace: Namespace, Namespace: Namespace,
Name: "games_agreement", Name: "games_agreement",
...@@ -172,6 +184,14 @@ func (m *Metrics) RecordUp() { ...@@ -172,6 +184,14 @@ func (m *Metrics) RecordUp() {
m.up.Set(1) m.up.Set(1)
} }
func (m *Metrics) RecordWithdrawalRequests(delayedWeth common.Address, matches bool, count int) {
credits := "matching"
if !matches {
credits = "divergent"
}
m.withdrawalRequests.WithLabelValues(delayedWeth.Hex(), credits).Set(float64(count))
}
func (m *Metrics) RecordClaimResolutionDelayMax(delay float64) { func (m *Metrics) RecordClaimResolutionDelayMax(delay float64) {
m.claimResolutionDelayMax.Set(delay) m.claimResolutionDelayMax.Set(delay)
} }
......
...@@ -19,6 +19,7 @@ func (*NoopMetricsImpl) RecordUp() {} ...@@ -19,6 +19,7 @@ func (*NoopMetricsImpl) RecordUp() {}
func (*NoopMetricsImpl) CacheAdd(_ string, _ int, _ bool) {} func (*NoopMetricsImpl) CacheAdd(_ string, _ int, _ bool) {}
func (*NoopMetricsImpl) CacheGet(_ string, _ bool) {} func (*NoopMetricsImpl) CacheGet(_ string, _ bool) {}
func (*NoopMetricsImpl) RecordWithdrawalRequests(_ common.Address, _ bool, _ int) {}
func (*NoopMetricsImpl) RecordClaimResolutionDelayMax(delay float64) {} func (*NoopMetricsImpl) RecordClaimResolutionDelayMax(delay float64) {}
func (*NoopMetricsImpl) RecordOutputFetchTime(timestamp float64) {} func (*NoopMetricsImpl) RecordOutputFetchTime(timestamp float64) {}
......
...@@ -15,6 +15,7 @@ import ( ...@@ -15,6 +15,7 @@ import (
type Forecast func(ctx context.Context, games []*types.EnrichedGameData) type Forecast func(ctx context.Context, games []*types.EnrichedGameData)
type Bonds func(games []*types.EnrichedGameData) type Bonds func(games []*types.EnrichedGameData)
type MonitorWithdrawals 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)
type Extract func(ctx context.Context, blockHash common.Hash, minTimestamp uint64) ([]*types.EnrichedGameData, error) type Extract func(ctx context.Context, blockHash common.Hash, minTimestamp uint64) ([]*types.EnrichedGameData, error)
...@@ -34,6 +35,7 @@ type gameMonitor struct { ...@@ -34,6 +35,7 @@ type gameMonitor struct {
delays RecordClaimResolutionDelayMax delays RecordClaimResolutionDelayMax
forecast Forecast forecast Forecast
bonds Bonds bonds Bonds
withdrawals MonitorWithdrawals
extract Extract extract Extract
fetchBlockHash BlockHashFetcher fetchBlockHash BlockHashFetcher
fetchBlockNumber BlockNumberFetcher fetchBlockNumber BlockNumberFetcher
...@@ -48,6 +50,7 @@ func newGameMonitor( ...@@ -48,6 +50,7 @@ func newGameMonitor(
delays RecordClaimResolutionDelayMax, delays RecordClaimResolutionDelayMax,
forecast Forecast, forecast Forecast,
bonds Bonds, bonds Bonds,
withdrawals MonitorWithdrawals,
extract Extract, extract Extract,
fetchBlockNumber BlockNumberFetcher, fetchBlockNumber BlockNumberFetcher,
fetchBlockHash BlockHashFetcher, fetchBlockHash BlockHashFetcher,
...@@ -62,6 +65,7 @@ func newGameMonitor( ...@@ -62,6 +65,7 @@ func newGameMonitor(
delays: delays, delays: delays,
forecast: forecast, forecast: forecast,
bonds: bonds, bonds: bonds,
withdrawals: withdrawals,
extract: extract, extract: extract,
fetchBlockNumber: fetchBlockNumber, fetchBlockNumber: fetchBlockNumber,
fetchBlockHash: fetchBlockHash, fetchBlockHash: fetchBlockHash,
...@@ -86,6 +90,7 @@ func (m *gameMonitor) monitorGames() error { ...@@ -86,6 +90,7 @@ func (m *gameMonitor) monitorGames() error {
m.delays(enrichedGames) m.delays(enrichedGames)
m.forecast(m.ctx, enrichedGames) m.forecast(m.ctx, enrichedGames)
m.bonds(enrichedGames) m.bonds(enrichedGames)
m.withdrawals(enrichedGames)
return nil return nil
} }
......
...@@ -24,7 +24,7 @@ func TestMonitor_MonitorGames(t *testing.T) { ...@@ -24,7 +24,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
...@@ -34,7 +34,7 @@ func TestMonitor_MonitorGames(t *testing.T) { ...@@ -34,7 +34,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
...@@ -44,23 +44,25 @@ func TestMonitor_MonitorGames(t *testing.T) { ...@@ -44,23 +44,25 @@ func TestMonitor_MonitorGames(t *testing.T) {
}) })
t.Run("MonitorsWithNoGames", func(t *testing.T) { t.Run("MonitorsWithNoGames", func(t *testing.T) {
monitor, factory, forecast, delays, bonds := setupMonitorTest(t) monitor, factory, forecast, delays, bonds, withdrawals := 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, delays.calls) require.Equal(t, 1, delays.calls)
require.Equal(t, 1, bonds.calls) require.Equal(t, 1, bonds.calls)
require.Equal(t, 1, withdrawals.calls)
}) })
t.Run("MonitorsMultipleGames", func(t *testing.T) { t.Run("MonitorsMultipleGames", func(t *testing.T) {
monitor, factory, forecast, delays, bonds := setupMonitorTest(t) monitor, factory, forecast, delays, bonds, withdrawals := 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, delays.calls) require.Equal(t, 1, delays.calls)
require.Equal(t, 1, bonds.calls) require.Equal(t, 1, bonds.calls)
require.Equal(t, 1, withdrawals.calls)
}) })
} }
...@@ -68,7 +70,7 @@ func TestMonitor_StartMonitoring(t *testing.T) { ...@@ -68,7 +70,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
...@@ -81,7 +83,7 @@ func TestMonitor_StartMonitoring(t *testing.T) { ...@@ -81,7 +83,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()
...@@ -103,7 +105,7 @@ func newEnrichedGameData(proxy common.Address, timestamp uint64) *monTypes.Enric ...@@ -103,7 +105,7 @@ func newEnrichedGameData(proxy common.Address, timestamp uint64) *monTypes.Enric
} }
} }
func setupMonitorTest(t *testing.T) (*gameMonitor, *mockExtractor, *mockForecast, *mockDelayCalculator, *mockBonds) { func setupMonitorTest(t *testing.T) (*gameMonitor, *mockExtractor, *mockForecast, *mockDelayCalculator, *mockBonds, *mockWithdrawalMonitor) {
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
...@@ -117,6 +119,7 @@ func setupMonitorTest(t *testing.T) (*gameMonitor, *mockExtractor, *mockForecast ...@@ -117,6 +119,7 @@ func setupMonitorTest(t *testing.T) (*gameMonitor, *mockExtractor, *mockForecast
extractor := &mockExtractor{} extractor := &mockExtractor{}
forecast := &mockForecast{} forecast := &mockForecast{}
bonds := &mockBonds{} bonds := &mockBonds{}
withdrawals := &mockWithdrawalMonitor{}
delays := &mockDelayCalculator{} delays := &mockDelayCalculator{}
monitor := newGameMonitor( monitor := newGameMonitor(
context.Background(), context.Background(),
...@@ -127,11 +130,20 @@ func setupMonitorTest(t *testing.T) (*gameMonitor, *mockExtractor, *mockForecast ...@@ -127,11 +130,20 @@ func setupMonitorTest(t *testing.T) (*gameMonitor, *mockExtractor, *mockForecast
delays.RecordClaimResolutionDelayMax, delays.RecordClaimResolutionDelayMax,
forecast.Forecast, forecast.Forecast,
bonds.CheckBonds, bonds.CheckBonds,
withdrawals.CheckWithdrawals,
extractor.Extract, extractor.Extract,
fetchBlockNum, fetchBlockNum,
fetchBlockHash, fetchBlockHash,
) )
return monitor, extractor, forecast, delays, bonds return monitor, extractor, forecast, delays, bonds, withdrawals
}
type mockWithdrawalMonitor struct {
calls int
}
func (m *mockWithdrawalMonitor) CheckWithdrawals(games []*monTypes.EnrichedGameData) {
m.calls++
} }
type mockDelayCalculator struct { type mockDelayCalculator struct {
......
...@@ -42,6 +42,7 @@ type Service struct { ...@@ -42,6 +42,7 @@ type Service struct {
forecast *forecast forecast *forecast
bonds *bonds.Bonds bonds *bonds.Bonds
game *extract.GameCallerCreator game *extract.GameCallerCreator
withdrawals *WithdrawalMonitor
rollupClient *sources.RollupClient rollupClient *sources.RollupClient
validator *outputValidator validator *outputValidator
...@@ -85,6 +86,8 @@ func (s *Service) initFromConfig(ctx context.Context, cfg *config.Config) error ...@@ -85,6 +86,8 @@ func (s *Service) initFromConfig(ctx context.Context, cfg *config.Config) error
return fmt.Errorf("failed to init rollup client: %w", err) return fmt.Errorf("failed to init rollup client: %w", err)
} }
s.initWithdrawalMonitor()
s.initOutputValidator() // Must be called before initForecast s.initOutputValidator() // Must be called before initForecast
s.initGameCallerCreator() // Must be called before initForecast s.initGameCallerCreator() // Must be called before initForecast
...@@ -102,6 +105,10 @@ func (s *Service) initFromConfig(ctx context.Context, cfg *config.Config) error ...@@ -102,6 +105,10 @@ func (s *Service) initFromConfig(ctx context.Context, cfg *config.Config) error
return nil return nil
} }
func (s *Service) initWithdrawalMonitor() {
s.withdrawals = NewWithdrawalMonitor(s.logger, s.metrics)
}
func (s *Service) initOutputValidator() { func (s *Service) initOutputValidator() {
s.validator = newOutputValidator(s.logger, s.metrics, s.rollupClient) s.validator = newOutputValidator(s.logger, s.metrics, s.rollupClient)
} }
...@@ -216,6 +223,7 @@ func (s *Service) initMonitor(ctx context.Context, cfg *config.Config) { ...@@ -216,6 +223,7 @@ func (s *Service) initMonitor(ctx context.Context, cfg *config.Config) {
s.delays.RecordClaimResolutionDelayMax, s.delays.RecordClaimResolutionDelayMax,
s.forecast.Forecast, s.forecast.Forecast,
s.bonds.CheckBonds, s.bonds.CheckBonds,
s.withdrawals.CheckWithdrawals,
s.extractor.Extract, s.extractor.Extract,
s.l1Client.BlockNumber, s.l1Client.BlockNumber,
blockHashFetcher, blockHashFetcher,
......
package mon
import (
"github.com/ethereum-optimism/optimism/op-dispute-mon/mon/types"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/log"
)
type WithdrawalMetrics interface {
RecordWithdrawalRequests(delayedWeth common.Address, matches bool, count int)
}
type WithdrawalMonitor struct {
logger log.Logger
metrics WithdrawalMetrics
}
func NewWithdrawalMonitor(logger log.Logger, metrics WithdrawalMetrics) *WithdrawalMonitor {
return &WithdrawalMonitor{
logger: logger,
metrics: metrics,
}
}
func (w *WithdrawalMonitor) CheckWithdrawals(games []*types.EnrichedGameData) {
matching := make(map[common.Address]int)
divergent := make(map[common.Address]int)
for _, game := range games {
matches, diverges := w.validateGameWithdrawals(game)
matching[game.WETHContract] += matches
divergent[game.WETHContract] += diverges
}
for contract, count := range matching {
w.metrics.RecordWithdrawalRequests(contract, true, count)
}
for contract, count := range divergent {
w.metrics.RecordWithdrawalRequests(contract, false, count)
}
}
func (w *WithdrawalMonitor) validateGameWithdrawals(game *types.EnrichedGameData) (int, int) {
matching := 0
divergent := 0
for recipient, withdrawalAmount := range game.WithdrawalRequests {
if withdrawalAmount.Amount != nil && withdrawalAmount.Amount.Cmp(game.Credits[recipient]) == 0 {
matching++
} else {
divergent++
w.logger.Error("Withdrawal request amount does not match credit", "game", game.Proxy, "recipient", recipient, "credit", game.Credits[recipient], "withdrawal", game.WithdrawalRequests[recipient].Amount)
}
}
return matching, divergent
}
package mon
import (
"math/big"
"testing"
"github.com/ethereum-optimism/optimism/op-challenger/game/fault/contracts"
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"
)
var (
weth1 = common.Address{0x1a}
weth2 = common.Address{0x2b}
)
func makeGames() []*monTypes.EnrichedGameData {
weth1Balance := big.NewInt(4200)
weth2Balance := big.NewInt(6000)
game1 := &monTypes.EnrichedGameData{
Credits: map[common.Address]*big.Int{
common.Address{0x01}: big.NewInt(3),
common.Address{0x02}: big.NewInt(1),
},
WithdrawalRequests: map[common.Address]*contracts.WithdrawalRequest{
common.Address{0x01}: &contracts.WithdrawalRequest{Amount: big.NewInt(3)},
common.Address{0x02}: &contracts.WithdrawalRequest{Amount: big.NewInt(1)},
},
WETHContract: weth1,
ETHCollateral: weth1Balance,
}
game2 := &monTypes.EnrichedGameData{
Credits: map[common.Address]*big.Int{
common.Address{0x01}: big.NewInt(46),
common.Address{0x02}: big.NewInt(1),
},
WithdrawalRequests: map[common.Address]*contracts.WithdrawalRequest{
common.Address{0x01}: &contracts.WithdrawalRequest{Amount: big.NewInt(3)},
common.Address{0x02}: &contracts.WithdrawalRequest{Amount: big.NewInt(1)},
},
WETHContract: weth2,
ETHCollateral: weth2Balance,
}
game3 := &monTypes.EnrichedGameData{
Credits: map[common.Address]*big.Int{
common.Address{0x03}: big.NewInt(2),
common.Address{0x04}: big.NewInt(4),
},
WithdrawalRequests: map[common.Address]*contracts.WithdrawalRequest{
common.Address{0x03}: &contracts.WithdrawalRequest{Amount: big.NewInt(2)},
common.Address{0x04}: &contracts.WithdrawalRequest{Amount: big.NewInt(4)},
},
WETHContract: weth2,
ETHCollateral: weth2Balance,
}
return []*monTypes.EnrichedGameData{game1, game2, game3}
}
func TestCheckWithdrawals(t *testing.T) {
logger := testlog.Logger(t, log.LvlInfo)
metrics := &stubWithdrawalsMetrics{
matching: make(map[common.Address]int),
divergent: make(map[common.Address]int),
}
withdrawals := NewWithdrawalMonitor(logger, metrics)
withdrawals.CheckWithdrawals(makeGames())
require.Equal(t, metrics.matchCalls, 2)
require.Equal(t, metrics.divergeCalls, 2)
require.Len(t, metrics.matching, 2)
require.Len(t, metrics.divergent, 2)
require.Contains(t, metrics.matching, weth1)
require.Contains(t, metrics.matching, weth2)
require.Contains(t, metrics.divergent, weth1)
require.Contains(t, metrics.divergent, weth2)
require.Equal(t, metrics.matching[weth1], 2)
require.Equal(t, metrics.matching[weth2], 3)
require.Equal(t, metrics.divergent[weth1], 0)
require.Equal(t, metrics.divergent[weth2], 1)
}
type stubWithdrawalsMetrics struct {
matchCalls int
divergeCalls int
matching map[common.Address]int
divergent map[common.Address]int
}
func (s *stubWithdrawalsMetrics) RecordWithdrawalRequests(addr common.Address, matches bool, count int) {
if matches {
s.matchCalls++
s.matching[addr] = count
} else {
s.divergeCalls++
s.divergent[addr] = count
}
}
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