Commit 813c74c5 authored by Adrian Sutton's avatar Adrian Sutton Committed by GitHub

op-dispute-mon: Report bond collateral metrics (#9791)

* op-dispute-mon: Add extractor for bond data

* op-dispute-mon: Implement transformer for bond collateral data

* op-dispute-mon: Report metrics for bond collateral.
parent 8c3a849d
......@@ -3,11 +3,13 @@ package metrics
import (
"fmt"
"io"
"math/big"
"github.com/ethereum-optimism/optimism/op-service/sources/caching"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/ethclient"
"github.com/ethereum/go-ethereum/log"
"github.com/ethereum/go-ethereum/params"
"github.com/prometheus/client_golang/prometheus"
"github.com/ethereum-optimism/optimism/op-service/httputil"
......@@ -43,6 +45,8 @@ type Metricer interface {
RecordGamesStatus(inProgress, defenderWon, challengerWon int)
RecordGameAgreement(status GameAgreementStatus, count int)
RecordBondCollateral(addr common.Address, required *big.Int, available *big.Int)
caching.Metrics
}
......@@ -65,6 +69,9 @@ type Metrics struct {
trackedGames prometheus.GaugeVec
gamesAgreement prometheus.GaugeVec
requiredCollateral prometheus.GaugeVec
availableCollateral prometheus.GaugeVec
}
func (m *Metrics) Registry() *prometheus.Registry {
......@@ -123,6 +130,26 @@ func NewMetrics() *Metrics {
"result_correctness",
"root_agreement",
}),
requiredCollateral: *factory.NewGaugeVec(prometheus.GaugeOpts{
Namespace: Namespace,
Name: "bond_collateral_required",
Help: "Required collateral (ETH) to cover outstanding bonds and credits",
}, []string{
// Address of the DelayedWETH contract in use. This is a limited set as only permissioned actors can deploy
// additional DelayedWETH contracts to be used by dispute games
"delayedWETH",
"balance",
}),
availableCollateral: *factory.NewGaugeVec(prometheus.GaugeOpts{
Namespace: Namespace,
Name: "bond_collateral_available",
Help: "Available collateral (ETH) to cover outstanding bonds and credits",
}, []string{
// Address of the DelayedWETH contract in use. This is a limited set as only permissioned actors can deploy
// additional DelayedWETH contracts to be used by dispute games
"delayedWETH",
"balance",
}),
}
}
......@@ -172,6 +199,15 @@ func (m *Metrics) RecordGameAgreement(status GameAgreementStatus, count int) {
m.gamesAgreement.WithLabelValues(labelValuesFor(status)...).Set(float64(count))
}
func (m *Metrics) RecordBondCollateral(addr common.Address, required *big.Int, available *big.Int) {
balance := "sufficient"
if required.Cmp(available) > 0 {
balance = "insufficient"
}
m.requiredCollateral.WithLabelValues(addr.Hex(), balance).Set(weiToEther(required))
m.availableCollateral.WithLabelValues(addr.Hex(), balance).Set(weiToEther(available))
}
const (
inProgress = true
correct = true
......@@ -217,3 +253,12 @@ func labelValuesFor(status GameAgreementStatus) []string {
panic(fmt.Errorf("unknown game agreement status: %v", status))
}
}
// weiToEther divides the wei value by 10^18 to get a number in ether as a float64
func weiToEther(wei *big.Int) float64 {
num := new(big.Rat).SetInt(wei)
denom := big.NewRat(params.Ether, 1)
num = num.Quo(num, denom)
f, _ := num.Float64()
return f
}
package metrics
import (
"math/big"
"github.com/ethereum/go-ethereum/common"
)
type NoopMetricsImpl struct{}
var NoopMetrics Metricer = new(NoopMetricsImpl)
......@@ -16,3 +22,5 @@ func (*NoopMetricsImpl) RecordOutputFetchTime(timestamp float64) {}
func (*NoopMetricsImpl) RecordGamesStatus(inProgress, defenderWon, challengerWon int) {}
func (*NoopMetricsImpl) RecordGameAgreement(status GameAgreementStatus, count int) {}
func (i *NoopMetricsImpl) RecordBondCollateral(_ common.Address, _ *big.Int, _ *big.Int) {}
package bonds
import (
"math/big"
"github.com/ethereum-optimism/optimism/op-dispute-mon/mon/transform"
"github.com/ethereum-optimism/optimism/op-dispute-mon/mon/types"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/log"
)
type BondMetrics interface {
RecordBondCollateral(addr common.Address, required *big.Int, available *big.Int)
}
type Bonds struct {
logger log.Logger
metrics BondMetrics
}
func NewBonds(logger log.Logger, metrics BondMetrics) *Bonds {
return &Bonds{
logger: logger,
metrics: metrics,
}
}
func (b *Bonds) CheckBonds(games []*types.EnrichedGameData) {
data := transform.CalculateRequiredCollateral(games)
for addr, collateral := range data {
b.metrics.RecordBondCollateral(addr, collateral.Required, collateral.Actual)
}
}
package bonds
import (
"math/big"
"testing"
"github.com/ethereum-optimism/optimism/op-dispute-mon/mon/transform"
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 TestCheckBonds(t *testing.T) {
weth1 := common.Address{0x1a}
weth1Balance := big.NewInt(4200)
weth2 := common.Address{0x2b}
weth2Balance := big.NewInt(6000)
game1 := &monTypes.EnrichedGameData{
Credits: map[common.Address]*big.Int{
common.Address{0x01}: big.NewInt(2),
},
WETHContract: weth1,
ETHCollateral: weth1Balance,
}
game2 := &monTypes.EnrichedGameData{
Credits: map[common.Address]*big.Int{
common.Address{0x01}: big.NewInt(46),
},
WETHContract: weth2,
ETHCollateral: weth2Balance,
}
logger := testlog.Logger(t, log.LvlInfo)
metrics := &stubBondMetrics{recorded: make(map[common.Address]transform.Collateral)}
bonds := NewBonds(logger, metrics)
bonds.CheckBonds([]*monTypes.EnrichedGameData{game1, game2})
require.Len(t, metrics.recorded, 2)
require.Contains(t, metrics.recorded, weth1)
require.Contains(t, metrics.recorded, weth2)
require.Equal(t, metrics.recorded[weth1].Required.Uint64(), uint64(2))
require.Equal(t, metrics.recorded[weth1].Actual.Uint64(), weth1Balance.Uint64())
require.Equal(t, metrics.recorded[weth2].Required.Uint64(), uint64(46))
require.Equal(t, metrics.recorded[weth2].Actual.Uint64(), weth2Balance.Uint64())
}
type stubBondMetrics struct {
recorded map[common.Address]transform.Collateral
}
func (s *stubBondMetrics) RecordBondCollateral(addr common.Address, required *big.Int, available *big.Int) {
s.recorded[addr] = transform.Collateral{
Required: required,
Actual: available,
}
}
......@@ -14,6 +14,7 @@ import (
)
type Forecast func(ctx context.Context, games []*types.EnrichedGameData)
type Bonds 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, error)
......@@ -32,6 +33,7 @@ type gameMonitor struct {
delays RecordClaimResolutionDelayMax
forecast Forecast
bonds Bonds
extract Extract
fetchBlockHash BlockHashFetcher
fetchBlockNumber BlockNumberFetcher
......@@ -45,6 +47,7 @@ func newGameMonitor(
gameWindow time.Duration,
delays RecordClaimResolutionDelayMax,
forecast Forecast,
bonds Bonds,
extract Extract,
fetchBlockNumber BlockNumberFetcher,
fetchBlockHash BlockHashFetcher,
......@@ -58,6 +61,7 @@ func newGameMonitor(
gameWindow: gameWindow,
delays: delays,
forecast: forecast,
bonds: bonds,
extract: extract,
fetchBlockNumber: fetchBlockNumber,
fetchBlockHash: fetchBlockHash,
......@@ -92,6 +96,7 @@ func (m *gameMonitor) monitorGames() error {
}
m.delays(enrichedGames)
m.forecast(m.ctx, enrichedGames)
m.bonds(enrichedGames)
return nil
}
......
......@@ -24,20 +24,20 @@ func TestMonitor_MinGameTimestamp(t *testing.T) {
t.Parallel()
t.Run("ZeroGameWindow", func(t *testing.T) {
monitor, _, _, _ := setupMonitorTest(t)
monitor, _, _, _, _ := setupMonitorTest(t)
monitor.gameWindow = time.Duration(0)
require.Equal(t, monitor.minGameTimestamp(), uint64(0))
})
t.Run("ZeroClock", func(t *testing.T) {
monitor, _, _, _ := setupMonitorTest(t)
monitor, _, _, _, _ := setupMonitorTest(t)
monitor.gameWindow = time.Minute
monitor.clock = clock.NewDeterministicClock(time.Unix(0, 0))
require.Equal(t, uint64(0), monitor.minGameTimestamp())
})
t.Run("ValidArithmetic", func(t *testing.T) {
monitor, _, _, _ := setupMonitorTest(t)
monitor, _, _, _, _ := setupMonitorTest(t)
monitor.gameWindow = time.Minute
frozen := time.Unix(int64(time.Hour.Seconds()), 0)
monitor.clock = clock.NewDeterministicClock(frozen)
......@@ -50,7 +50,7 @@ func TestMonitor_MonitorGames(t *testing.T) {
t.Parallel()
t.Run("FailedFetchBlocknumber", func(t *testing.T) {
monitor, _, _, _ := setupMonitorTest(t)
monitor, _, _, _, _ := setupMonitorTest(t)
boom := errors.New("boom")
monitor.fetchBlockNumber = func(ctx context.Context) (uint64, error) {
return 0, boom
......@@ -60,7 +60,7 @@ func TestMonitor_MonitorGames(t *testing.T) {
})
t.Run("FailedFetchBlockHash", func(t *testing.T) {
monitor, _, _, _ := setupMonitorTest(t)
monitor, _, _, _, _ := setupMonitorTest(t)
boom := errors.New("boom")
monitor.fetchBlockHash = func(ctx context.Context, number *big.Int) (common.Hash, error) {
return common.Hash{}, boom
......@@ -70,21 +70,23 @@ func TestMonitor_MonitorGames(t *testing.T) {
})
t.Run("MonitorsWithNoGames", func(t *testing.T) {
monitor, factory, forecast, delays := setupMonitorTest(t)
monitor, factory, forecast, delays, bonds := setupMonitorTest(t)
factory.games = []*monTypes.EnrichedGameData{}
err := monitor.monitorGames()
require.NoError(t, err)
require.Equal(t, 1, forecast.calls)
require.Equal(t, 1, delays.calls)
require.Equal(t, 1, bonds.calls)
})
t.Run("MonitorsMultipleGames", func(t *testing.T) {
monitor, factory, forecast, delays := setupMonitorTest(t)
monitor, factory, forecast, delays, bonds := setupMonitorTest(t)
factory.games = []*monTypes.EnrichedGameData{{}, {}, {}}
err := monitor.monitorGames()
require.NoError(t, err)
require.Equal(t, 1, forecast.calls)
require.Equal(t, 1, delays.calls)
require.Equal(t, 1, bonds.calls)
})
}
......@@ -92,7 +94,7 @@ func TestMonitor_StartMonitoring(t *testing.T) {
t.Run("MonitorsGames", func(t *testing.T) {
addr1 := common.Address{0xaa}
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.maxSuccess = len(factory.games) // Only allow two successful fetches
......@@ -105,7 +107,7 @@ func TestMonitor_StartMonitoring(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")
monitor.StartMonitoring()
......@@ -127,7 +129,7 @@ func newEnrichedGameData(proxy common.Address, timestamp uint64) *monTypes.Enric
}
}
func setupMonitorTest(t *testing.T) (*gameMonitor, *mockExtractor, *mockForecast, *mockDelayCalculator) {
func setupMonitorTest(t *testing.T) (*gameMonitor, *mockExtractor, *mockForecast, *mockDelayCalculator, *mockBonds) {
logger := testlog.Logger(t, log.LvlDebug)
fetchBlockNum := func(ctx context.Context) (uint64, error) {
return 1, nil
......@@ -140,6 +142,7 @@ func setupMonitorTest(t *testing.T) (*gameMonitor, *mockExtractor, *mockForecast
cl.Start()
extractor := &mockExtractor{}
forecast := &mockForecast{}
bonds := &mockBonds{}
delays := &mockDelayCalculator{}
monitor := newGameMonitor(
context.Background(),
......@@ -149,11 +152,12 @@ func setupMonitorTest(t *testing.T) (*gameMonitor, *mockExtractor, *mockForecast
10*time.Second,
delays.RecordClaimResolutionDelayMax,
forecast.Forecast,
bonds.CheckBonds,
extractor.Extract,
fetchBlockNum,
fetchBlockHash,
)
return monitor, extractor, forecast, delays
return monitor, extractor, forecast, delays, bonds
}
type mockDelayCalculator struct {
......@@ -172,6 +176,14 @@ func (m *mockForecast) Forecast(ctx context.Context, games []*monTypes.EnrichedG
m.calls++
}
type mockBonds struct {
calls int
}
func (m *mockBonds) CheckBonds(_ []*monTypes.EnrichedGameData) {
m.calls++
}
type mockExtractor struct {
fetchErr error
calls int
......
......@@ -7,6 +7,7 @@ import (
"math/big"
"sync/atomic"
"github.com/ethereum-optimism/optimism/op-dispute-mon/mon/bonds"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/ethclient"
"github.com/ethereum/go-ethereum/log"
......@@ -39,6 +40,7 @@ type Service struct {
delays *resolution.DelayCalculator
extractor *extract.Extractor
forecast *forecast
bonds *bonds.Bonds
game *extract.GameCallerCreator
rollupClient *sources.RollupClient
validator *outputValidator
......@@ -90,6 +92,7 @@ func (s *Service) initFromConfig(ctx context.Context, cfg *config.Config) error
s.initExtractor()
s.initForecast(cfg)
s.initBonds()
s.initMonitor(ctx, cfg) // Monitor must be initialized last
......@@ -122,6 +125,10 @@ func (s *Service) initForecast(cfg *config.Config) {
s.forecast = newForecast(s.logger, s.metrics, s.validator)
}
func (s *Service) initBonds() {
s.bonds = bonds.NewBonds(s.logger, s.metrics)
}
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 {
......@@ -201,6 +208,7 @@ func (s *Service) initMonitor(ctx context.Context, cfg *config.Config) {
cfg.GameWindow,
s.delays.RecordClaimResolutionDelayMax,
s.forecast.Forecast,
s.bonds.CheckBonds,
s.extractor.Extract,
s.l1Client.BlockNumber,
blockHashFetcher,
......
package transform
import (
"math/big"
monTypes "github.com/ethereum-optimism/optimism/op-dispute-mon/mon/types"
"github.com/ethereum/go-ethereum/common"
)
type Collateral struct {
// Required is the amount of collateral required to pay out bonds.
Required *big.Int
// Actual is the amount of collateral actually head by the DelayedWETH contract
Actual *big.Int
}
// CalculateRequiredCollateral determines the minimum balance required for each DelayedWETH contract used by a set
// of dispute games.
// Returns a map of DelayedWETH contract address to collateral data (required and actual amounts)
func CalculateRequiredCollateral(games []*monTypes.EnrichedGameData) map[common.Address]Collateral {
result := make(map[common.Address]Collateral)
for _, game := range games {
collateral, ok := result[game.WETHContract]
if !ok {
collateral = Collateral{
Required: big.NewInt(0),
Actual: game.ETHCollateral,
}
}
gameRequired := requiredCollateralForGame(game)
collateral.Required = new(big.Int).Add(collateral.Required, gameRequired)
result[game.WETHContract] = collateral
}
return result
}
func requiredCollateralForGame(game *monTypes.EnrichedGameData) *big.Int {
required := big.NewInt(0)
for _, claim := range game.Claims {
if monTypes.ResolvedBondAmount.Cmp(claim.Bond) != 0 {
required = new(big.Int).Add(required, claim.Bond)
}
}
for _, unclaimedCredit := range game.Credits {
required = new(big.Int).Add(required, unclaimedCredit)
}
return required
}
package transform
import (
"math/big"
"testing"
"github.com/ethereum-optimism/optimism/op-challenger/game/fault/types"
monTypes "github.com/ethereum-optimism/optimism/op-dispute-mon/mon/types"
"github.com/ethereum/go-ethereum/common"
"github.com/stretchr/testify/require"
)
func TestCalculateRequiredCollateral(t *testing.T) {
weth1 := common.Address{0x1a}
weth1Balance := big.NewInt(4200)
weth2 := common.Address{0x2b}
weth2Balance := big.NewInt(6000)
game1 := &monTypes.EnrichedGameData{
Claims: []types.Claim{
{
ClaimData: types.ClaimData{
Bond: monTypes.ResolvedBondAmount,
},
Claimant: common.Address{0x01},
CounteredBy: common.Address{0x02},
},
{
ClaimData: types.ClaimData{
Bond: big.NewInt(5),
},
Claimant: common.Address{0x03},
CounteredBy: common.Address{},
},
{
ClaimData: types.ClaimData{
Bond: big.NewInt(7),
},
Claimant: common.Address{0x03},
CounteredBy: common.Address{},
},
},
Credits: map[common.Address]*big.Int{
common.Address{0x01}: big.NewInt(2),
common.Address{0x04}: big.NewInt(3),
},
WETHContract: weth1,
ETHCollateral: weth1Balance,
}
game2 := &monTypes.EnrichedGameData{
Claims: []types.Claim{
{
ClaimData: types.ClaimData{
Bond: monTypes.ResolvedBondAmount,
},
Claimant: common.Address{0x01},
CounteredBy: common.Address{0x02},
},
{
ClaimData: types.ClaimData{
Bond: big.NewInt(6),
},
Claimant: common.Address{0x03},
CounteredBy: common.Address{},
},
{
ClaimData: types.ClaimData{
Bond: big.NewInt(9),
},
Claimant: common.Address{0x03},
CounteredBy: common.Address{},
},
},
Credits: map[common.Address]*big.Int{
common.Address{0x01}: big.NewInt(4),
common.Address{0x04}: big.NewInt(1),
},
WETHContract: weth1,
ETHCollateral: weth1Balance,
}
game3 := &monTypes.EnrichedGameData{
Claims: []types.Claim{
{
ClaimData: types.ClaimData{
Bond: big.NewInt(23),
},
Claimant: common.Address{0x03},
CounteredBy: common.Address{},
},
},
Credits: map[common.Address]*big.Int{
common.Address{0x01}: big.NewInt(46),
},
WETHContract: weth2,
ETHCollateral: weth2Balance,
}
actual := CalculateRequiredCollateral([]*monTypes.EnrichedGameData{game1, game2, game3})
require.Len(t, actual, 2)
require.Contains(t, actual, weth1)
require.Contains(t, actual, weth2)
require.Equal(t, actual[weth1].Required.Uint64(), uint64(5+7+2+3+6+9+4+1))
require.Equal(t, actual[weth1].Actual.Uint64(), weth1Balance.Uint64())
require.Equal(t, actual[weth2].Required.Uint64(), uint64(23+46))
require.Equal(t, actual[weth2].Actual.Uint64(), weth2Balance.Uint64())
}
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