Commit 5843583a authored by Adrian Sutton's avatar Adrian Sutton Committed by GitHub

dispute-mon: Add metric to report the total withdrawable ETH for each honest actor (#10838)

parent 3aeb6bdd
...@@ -163,6 +163,8 @@ type Metricer interface { ...@@ -163,6 +163,8 @@ type Metricer interface {
RecordCredit(expectation CreditExpectation, count int) RecordCredit(expectation CreditExpectation, count int)
RecordHonestWithdrawableAmounts(map[common.Address]*big.Int)
RecordClaims(statuses *ClaimStatuses) RecordClaims(statuses *ClaimStatuses)
RecordWithdrawalRequests(delayedWeth common.Address, matches bool, count int) RecordWithdrawalRequests(delayedWeth common.Address, matches bool, count int)
...@@ -208,7 +210,8 @@ type Metrics struct { ...@@ -208,7 +210,8 @@ type Metrics struct {
info prometheus.GaugeVec info prometheus.GaugeVec
up prometheus.Gauge up prometheus.Gauge
credits prometheus.GaugeVec credits prometheus.GaugeVec
honestWithdrawableAmounts prometheus.GaugeVec
lastOutputFetch prometheus.Gauge lastOutputFetch prometheus.Gauge
...@@ -295,6 +298,13 @@ func NewMetrics() *Metrics { ...@@ -295,6 +298,13 @@ func NewMetrics() *Metrics {
"credit", "credit",
"withdrawable", "withdrawable",
}), }),
honestWithdrawableAmounts: *factory.NewGaugeVec(prometheus.GaugeOpts{
Namespace: Namespace,
Name: "honest_actor_pending_withdrawals",
Help: "Current amount of withdrawable ETH for an honest actor",
}, []string{
"actor",
}),
claims: *factory.NewGaugeVec(prometheus.GaugeOpts{ claims: *factory.NewGaugeVec(prometheus.GaugeOpts{
Namespace: Namespace, Namespace: Namespace,
Name: "claims", Name: "claims",
...@@ -453,6 +463,12 @@ func (m *Metrics) RecordCredit(expectation CreditExpectation, count int) { ...@@ -453,6 +463,12 @@ func (m *Metrics) RecordCredit(expectation CreditExpectation, count int) {
m.credits.WithLabelValues(asLabels(expectation)...).Set(float64(count)) m.credits.WithLabelValues(asLabels(expectation)...).Set(float64(count))
} }
func (m *Metrics) RecordHonestWithdrawableAmounts(amounts map[common.Address]*big.Int) {
for addr, amount := range amounts {
m.honestWithdrawableAmounts.WithLabelValues(addr.Hex()).Set(weiToEther(amount))
}
}
func (m *Metrics) RecordClaims(statuses *ClaimStatuses) { func (m *Metrics) RecordClaims(statuses *ClaimStatuses) {
statuses.ForEachStatus(func(status ClaimStatus, count int) { statuses.ForEachStatus(func(status ClaimStatus, count int) {
m.claims.WithLabelValues(status.AsLabels()...).Set(float64(count)) m.claims.WithLabelValues(status.AsLabels()...).Set(float64(count))
......
...@@ -28,6 +28,8 @@ func (*NoopMetricsImpl) RecordGameResolutionStatus(_ ResolutionStatus, _ int) {} ...@@ -28,6 +28,8 @@ func (*NoopMetricsImpl) RecordGameResolutionStatus(_ ResolutionStatus, _ int) {}
func (*NoopMetricsImpl) RecordCredit(_ CreditExpectation, _ int) {} func (*NoopMetricsImpl) RecordCredit(_ CreditExpectation, _ int) {}
func (*NoopMetricsImpl) RecordHonestWithdrawableAmounts(map[common.Address]*big.Int) {}
func (*NoopMetricsImpl) RecordClaims(_ *ClaimStatuses) {} func (*NoopMetricsImpl) RecordClaims(_ *ClaimStatuses) {}
func (*NoopMetricsImpl) RecordWithdrawalRequests(_ common.Address, _ bool, _ int) {} func (*NoopMetricsImpl) RecordWithdrawalRequests(_ common.Address, _ bool, _ int) {}
......
...@@ -17,19 +17,22 @@ type RClock interface { ...@@ -17,19 +17,22 @@ type RClock interface {
type BondMetrics interface { type BondMetrics interface {
RecordCredit(expectation metrics.CreditExpectation, count int) RecordCredit(expectation metrics.CreditExpectation, count int)
RecordBondCollateral(addr common.Address, required *big.Int, available *big.Int) RecordBondCollateral(addr common.Address, required *big.Int, available *big.Int)
RecordHonestWithdrawableAmounts(map[common.Address]*big.Int)
} }
type Bonds struct { type Bonds struct {
logger log.Logger logger log.Logger
clock RClock clock RClock
metrics BondMetrics metrics BondMetrics
honestActors types.HonestActors
} }
func NewBonds(logger log.Logger, metrics BondMetrics, clock RClock) *Bonds { func NewBonds(logger log.Logger, metrics BondMetrics, honestActors types.HonestActors, clock RClock) *Bonds {
return &Bonds{ return &Bonds{
logger: logger, logger: logger,
clock: clock, clock: clock,
metrics: metrics, metrics: metrics,
honestActors: honestActors,
} }
} }
...@@ -47,6 +50,10 @@ func (b *Bonds) CheckBonds(games []*types.EnrichedGameData) { ...@@ -47,6 +50,10 @@ func (b *Bonds) CheckBonds(games []*types.EnrichedGameData) {
func (b *Bonds) checkCredits(games []*types.EnrichedGameData) { func (b *Bonds) checkCredits(games []*types.EnrichedGameData) {
creditMetrics := make(map[metrics.CreditExpectation]int) creditMetrics := make(map[metrics.CreditExpectation]int)
honestWithdrawableAmounts := make(map[common.Address]*big.Int)
for address := range b.honestActors {
honestWithdrawableAmounts[address] = big.NewInt(0)
}
for _, game := range games { for _, game := range games {
// Check if the max duration has been reached for this game // Check if the max duration has been reached for this game
...@@ -94,6 +101,12 @@ func (b *Bonds) checkCredits(games []*types.EnrichedGameData) { ...@@ -94,6 +101,12 @@ func (b *Bonds) checkCredits(games []*types.EnrichedGameData) {
} }
comparison := actual.Cmp(expected) comparison := actual.Cmp(expected)
if maxDurationReached { if maxDurationReached {
if actual.Cmp(big.NewInt(0)) > 0 && b.honestActors.Contains(recipient) {
total := honestWithdrawableAmounts[recipient]
total = new(big.Int).Add(total, actual)
honestWithdrawableAmounts[recipient] = total
b.logger.Warn("Found unclaimed credit", "recipient", recipient, "game", game.Proxy, "amount", actual)
}
if comparison > 0 { if comparison > 0 {
creditMetrics[metrics.CreditAboveWithdrawable] += 1 creditMetrics[metrics.CreditAboveWithdrawable] += 1
b.logger.Warn("Credit above expected amount", "recipient", recipient, "expected", expected, "actual", actual, "game", game.Proxy, "withdrawable", "withdrawable") b.logger.Warn("Credit above expected amount", "recipient", recipient, "expected", expected, "actual", actual, "game", game.Proxy, "withdrawable", "withdrawable")
...@@ -123,4 +136,5 @@ func (b *Bonds) checkCredits(games []*types.EnrichedGameData) { ...@@ -123,4 +136,5 @@ func (b *Bonds) checkCredits(games []*types.EnrichedGameData) {
b.metrics.RecordCredit(metrics.CreditBelowNonWithdrawable, creditMetrics[metrics.CreditBelowNonWithdrawable]) b.metrics.RecordCredit(metrics.CreditBelowNonWithdrawable, creditMetrics[metrics.CreditBelowNonWithdrawable])
b.metrics.RecordCredit(metrics.CreditEqualNonWithdrawable, creditMetrics[metrics.CreditEqualNonWithdrawable]) b.metrics.RecordCredit(metrics.CreditEqualNonWithdrawable, creditMetrics[metrics.CreditEqualNonWithdrawable])
b.metrics.RecordCredit(metrics.CreditAboveNonWithdrawable, creditMetrics[metrics.CreditAboveNonWithdrawable]) b.metrics.RecordCredit(metrics.CreditAboveNonWithdrawable, creditMetrics[metrics.CreditAboveNonWithdrawable])
b.metrics.RecordHonestWithdrawableAmounts(honestWithdrawableAmounts)
} }
...@@ -17,7 +17,10 @@ import ( ...@@ -17,7 +17,10 @@ import (
) )
var ( var (
frozen = time.Unix(int64(time.Hour.Seconds()), 0) frozen = time.Unix(int64(time.Hour.Seconds()), 0)
honestActor1 = common.Address{0x11, 0xaa}
honestActor2 = common.Address{0x22, 0xbb}
honestActor3 = common.Address{0x33, 0xcc}
) )
func TestCheckBonds(t *testing.T) { func TestCheckBonds(t *testing.T) {
...@@ -61,8 +64,8 @@ func TestCheckBonds(t *testing.T) { ...@@ -61,8 +64,8 @@ func TestCheckBonds(t *testing.T) {
} }
func TestCheckRecipientCredit(t *testing.T) { func TestCheckRecipientCredit(t *testing.T) {
addr1 := common.Address{0x1a} addr1 := honestActor1
addr2 := common.Address{0x2b} addr2 := honestActor2
addr3 := common.Address{0x3c} addr3 := common.Address{0x3c}
addr4 := common.Address{0x4d} addr4 := common.Address{0x4d}
notRootPosition := types.NewPositionFromGIndex(big.NewInt(2)) notRootPosition := types.NewPositionFromGIndex(big.NewInt(2))
...@@ -273,7 +276,7 @@ func TestCheckRecipientCredit(t *testing.T) { ...@@ -273,7 +276,7 @@ func TestCheckRecipientCredit(t *testing.T) {
MaxClockDuration: 10, MaxClockDuration: 10,
WETHDelay: 10 * time.Second, WETHDelay: 10 * time.Second,
GameMetadata: gameTypes.GameMetadata{ GameMetadata: gameTypes.GameMetadata{
Proxy: common.Address{44}, Proxy: common.Address{0x44},
Timestamp: uint64(frozen.Unix()) - 22, Timestamp: uint64(frozen.Unix()) - 22,
}, },
BlockNumberChallenged: true, BlockNumberChallenged: true,
...@@ -346,6 +349,14 @@ func TestCheckRecipientCredit(t *testing.T) { ...@@ -346,6 +349,14 @@ func TestCheckRecipientCredit(t *testing.T) {
require.Equal(t, 2, m.credits[metrics.CreditEqualNonWithdrawable], "CreditEqualNonWithdrawable") require.Equal(t, 2, m.credits[metrics.CreditEqualNonWithdrawable], "CreditEqualNonWithdrawable")
require.Equal(t, 2, m.credits[metrics.CreditAboveNonWithdrawable], "CreditAboveNonWithdrawable") require.Equal(t, 2, m.credits[metrics.CreditAboveNonWithdrawable], "CreditAboveNonWithdrawable")
require.Len(t, m.honestWithdrawable, 3)
requireBigInt := func(name string, expected, actual *big.Int) {
require.Truef(t, expected.Cmp(actual) == 0, "Expected %v withdrawable to be %v but was %v", name, expected, actual)
}
requireBigInt("honest addr1", m.honestWithdrawable[addr1], big.NewInt(19))
requireBigInt("honest addr2", m.honestWithdrawable[addr2], big.NewInt(13))
requireBigInt("honest addr3", m.honestWithdrawable[honestActor3], big.NewInt(0))
// Logs from game1 // Logs from game1
// addr1 is correct so has no logs // addr1 is correct so has no logs
// addr2 is below expected before max duration, so warn about early withdrawal // addr2 is below expected before max duration, so warn about early withdrawal
...@@ -371,8 +382,18 @@ func TestCheckRecipientCredit(t *testing.T) { ...@@ -371,8 +382,18 @@ func TestCheckRecipientCredit(t *testing.T) {
testlog.NewAttributesFilter("withdrawable", "non_withdrawable"))) testlog.NewAttributesFilter("withdrawable", "non_withdrawable")))
// Logs from game 2 // Logs from game 2
// addr1 is below expected - no warning as withdrawals may now be possible // addr1 is below expected - no warning as withdrawals may now be possible, but has unclaimed credit
// addr2 is correct require.NotNil(t, logs.FindLog(
testlog.NewLevelFilter(log.LevelWarn),
testlog.NewMessageFilter("Found unclaimed credit"),
testlog.NewAttributesFilter("game", game2.Proxy.Hex()),
testlog.NewAttributesFilter("recipient", addr1.Hex())))
// addr2 is correct but has unclaimed credit
require.NotNil(t, logs.FindLog(
testlog.NewLevelFilter(log.LevelWarn),
testlog.NewMessageFilter("Found unclaimed credit"),
testlog.NewAttributesFilter("game", game2.Proxy.Hex()),
testlog.NewAttributesFilter("recipient", addr2.Hex())))
// addr3 is above expected - warn // addr3 is above expected - warn
require.NotNil(t, logs.FindLog( require.NotNil(t, logs.FindLog(
testlog.NewLevelFilter(log.LevelWarn), testlog.NewLevelFilter(log.LevelWarn),
...@@ -401,8 +422,18 @@ func TestCheckRecipientCredit(t *testing.T) { ...@@ -401,8 +422,18 @@ func TestCheckRecipientCredit(t *testing.T) {
testlog.NewAttributesFilter("withdrawable", "non_withdrawable"))) testlog.NewAttributesFilter("withdrawable", "non_withdrawable")))
// Logs from game 4 // Logs from game 4
// addr1 is correct so has no logs // addr1 is correct but has unclaimed credit
// addr2 is below expected before max duration, no long because withdrawals may be possible require.NotNil(t, logs.FindLog(
testlog.NewLevelFilter(log.LevelWarn),
testlog.NewMessageFilter("Found unclaimed credit"),
testlog.NewAttributesFilter("game", game4.Proxy.Hex()),
testlog.NewAttributesFilter("recipient", addr1.Hex())))
// addr2 is below expected before max duration, no log because withdrawals may be possible but warn about unclaimed
require.NotNil(t, logs.FindLog(
testlog.NewLevelFilter(log.LevelWarn),
testlog.NewMessageFilter("Found unclaimed credit"),
testlog.NewAttributesFilter("game", game4.Proxy.Hex()),
testlog.NewAttributesFilter("recipient", addr2.Hex())))
// addr3 is not involved so no logs // addr3 is not involved so no logs
// addr4 is above expected before max duration, so warn // addr4 is above expected before max duration, so warn
require.NotNil(t, logs.FindLog( require.NotNil(t, logs.FindLog(
...@@ -419,13 +450,19 @@ func setupBondMetricsTest(t *testing.T) (*Bonds, *stubBondMetrics, *testlog.Capt ...@@ -419,13 +450,19 @@ func setupBondMetricsTest(t *testing.T) (*Bonds, *stubBondMetrics, *testlog.Capt
credits: make(map[metrics.CreditExpectation]int), credits: make(map[metrics.CreditExpectation]int),
recorded: make(map[common.Address]Collateral), recorded: make(map[common.Address]Collateral),
} }
bonds := NewBonds(logger, metrics, clock.NewDeterministicClock(frozen)) honestActors := monTypes.NewHonestActors([]common.Address{honestActor1, honestActor2, honestActor3})
bonds := NewBonds(logger, metrics, honestActors, clock.NewDeterministicClock(frozen))
return bonds, metrics, logs return bonds, metrics, logs
} }
type stubBondMetrics struct { type stubBondMetrics struct {
credits map[metrics.CreditExpectation]int credits map[metrics.CreditExpectation]int
recorded map[common.Address]Collateral recorded map[common.Address]Collateral
honestWithdrawable map[common.Address]*big.Int
}
func (s *stubBondMetrics) RecordHonestWithdrawableAmounts(values map[common.Address]*big.Int) {
s.honestWithdrawable = values
} }
func (s *stubBondMetrics) RecordBondCollateral(addr common.Address, required *big.Int, available *big.Int) { func (s *stubBondMetrics) RecordBondCollateral(addr common.Address, required *big.Int, available *big.Int) {
......
...@@ -25,16 +25,12 @@ type ClaimMetrics interface { ...@@ -25,16 +25,12 @@ type ClaimMetrics interface {
type ClaimMonitor struct { type ClaimMonitor struct {
logger log.Logger logger log.Logger
clock RClock clock RClock
honestActors map[common.Address]bool // Map for efficient lookup honestActors types.HonestActors
metrics ClaimMetrics metrics ClaimMetrics
} }
func NewClaimMonitor(logger log.Logger, clock RClock, honestActors []common.Address, metrics ClaimMetrics) *ClaimMonitor { func NewClaimMonitor(logger log.Logger, clock RClock, honestActors types.HonestActors, metrics ClaimMetrics) *ClaimMonitor {
actors := make(map[common.Address]bool) return &ClaimMonitor{logger, clock, honestActors, metrics}
for _, actor := range honestActors {
actors[actor] = true
}
return &ClaimMonitor{logger, clock, actors, metrics}
} }
func (c *ClaimMonitor) CheckClaims(games []*types.EnrichedGameData) { func (c *ClaimMonitor) CheckClaims(games []*types.EnrichedGameData) {
......
...@@ -194,10 +194,10 @@ func newTestClaimMonitor(t *testing.T) (*ClaimMonitor, *clock.DeterministicClock ...@@ -194,10 +194,10 @@ func newTestClaimMonitor(t *testing.T) (*ClaimMonitor, *clock.DeterministicClock
logger, handler := testlog.CaptureLogger(t, log.LvlInfo) logger, handler := testlog.CaptureLogger(t, log.LvlInfo)
cl := clock.NewDeterministicClock(frozen) cl := clock.NewDeterministicClock(frozen)
metrics := &stubClaimMetrics{} metrics := &stubClaimMetrics{}
honestActors := []common.Address{ honestActors := types.NewHonestActors([]common.Address{
{0x01}, {0x01},
{0x02}, {0x02},
} })
monitor := NewClaimMonitor(logger, cl, honestActors, metrics) monitor := NewClaimMonitor(logger, cl, honestActors, metrics)
return monitor, cl, metrics, handler return monitor, cl, metrics, handler
} }
......
...@@ -8,6 +8,7 @@ import ( ...@@ -8,6 +8,7 @@ import (
"sync/atomic" "sync/atomic"
"github.com/ethereum-optimism/optimism/op-dispute-mon/mon/bonds" "github.com/ethereum-optimism/optimism/op-dispute-mon/mon/bonds"
"github.com/ethereum-optimism/optimism/op-dispute-mon/mon/types"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/ethclient" "github.com/ethereum/go-ethereum/ethclient"
"github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/log"
...@@ -28,9 +29,10 @@ import ( ...@@ -28,9 +29,10 @@ import (
) )
type Service struct { type Service struct {
logger log.Logger logger log.Logger
metrics metrics.Metricer metrics metrics.Metricer
monitor *gameMonitor monitor *gameMonitor
honestActors types.HonestActors
factoryContract *contracts.DisputeGameFactoryContract factoryContract *contracts.DisputeGameFactoryContract
...@@ -56,9 +58,10 @@ type Service struct { ...@@ -56,9 +58,10 @@ type Service struct {
// NewService creates a new Service. // NewService creates a new Service.
func NewService(ctx context.Context, logger log.Logger, cfg *config.Config) (*Service, error) { func NewService(ctx context.Context, logger log.Logger, cfg *config.Config) (*Service, error) {
s := &Service{ s := &Service{
cl: clock.SystemClock, cl: clock.SystemClock,
logger: logger, logger: logger,
metrics: metrics.NewMetrics(), metrics: metrics.NewMetrics(),
honestActors: types.NewHonestActors(cfg.HonestActors),
} }
if err := s.initFromConfig(ctx, cfg); err != nil { if err := s.initFromConfig(ctx, cfg); err != nil {
...@@ -105,7 +108,7 @@ func (s *Service) initFromConfig(ctx context.Context, cfg *config.Config) error ...@@ -105,7 +108,7 @@ func (s *Service) initFromConfig(ctx context.Context, cfg *config.Config) error
} }
func (s *Service) initClaimMonitor(cfg *config.Config) { func (s *Service) initClaimMonitor(cfg *config.Config) {
s.claims = NewClaimMonitor(s.logger, s.cl, cfg.HonestActors, s.metrics) s.claims = NewClaimMonitor(s.logger, s.cl, s.honestActors, s.metrics)
} }
func (s *Service) initResolutionMonitor() { func (s *Service) initResolutionMonitor() {
...@@ -142,7 +145,7 @@ func (s *Service) initForecast(cfg *config.Config) { ...@@ -142,7 +145,7 @@ func (s *Service) initForecast(cfg *config.Config) {
} }
func (s *Service) initBonds() { func (s *Service) initBonds() {
s.bonds = bonds.NewBonds(s.logger, s.metrics, s.cl) s.bonds = bonds.NewBonds(s.logger, s.metrics, s.honestActors, s.cl)
} }
func (s *Service) initOutputRollupClient(ctx context.Context, cfg *config.Config) error { func (s *Service) initOutputRollupClient(ctx context.Context, cfg *config.Config) error {
......
package types
import "github.com/ethereum/go-ethereum/common"
type HonestActors map[common.Address]bool // Map for efficient lookup
func NewHonestActors(honestActors []common.Address) HonestActors {
actors := make(map[common.Address]bool)
for _, actor := range honestActors {
actors[actor] = true
}
return actors
}
func (h HonestActors) Contains(addr common.Address) bool {
return h[addr]
}
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