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 {
RecordCredit(expectation CreditExpectation, count int)
RecordHonestWithdrawableAmounts(map[common.Address]*big.Int)
RecordClaims(statuses *ClaimStatuses)
RecordWithdrawalRequests(delayedWeth common.Address, matches bool, count int)
......@@ -208,7 +210,8 @@ type Metrics struct {
info prometheus.GaugeVec
up prometheus.Gauge
credits prometheus.GaugeVec
credits prometheus.GaugeVec
honestWithdrawableAmounts prometheus.GaugeVec
lastOutputFetch prometheus.Gauge
......@@ -295,6 +298,13 @@ func NewMetrics() *Metrics {
"credit",
"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{
Namespace: Namespace,
Name: "claims",
......@@ -453,6 +463,12 @@ func (m *Metrics) RecordCredit(expectation CreditExpectation, count int) {
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) {
statuses.ForEachStatus(func(status ClaimStatus, count int) {
m.claims.WithLabelValues(status.AsLabels()...).Set(float64(count))
......
......@@ -28,6 +28,8 @@ func (*NoopMetricsImpl) RecordGameResolutionStatus(_ ResolutionStatus, _ int) {}
func (*NoopMetricsImpl) RecordCredit(_ CreditExpectation, _ int) {}
func (*NoopMetricsImpl) RecordHonestWithdrawableAmounts(map[common.Address]*big.Int) {}
func (*NoopMetricsImpl) RecordClaims(_ *ClaimStatuses) {}
func (*NoopMetricsImpl) RecordWithdrawalRequests(_ common.Address, _ bool, _ int) {}
......
......@@ -17,19 +17,22 @@ type RClock interface {
type BondMetrics interface {
RecordCredit(expectation metrics.CreditExpectation, count int)
RecordBondCollateral(addr common.Address, required *big.Int, available *big.Int)
RecordHonestWithdrawableAmounts(map[common.Address]*big.Int)
}
type Bonds struct {
logger log.Logger
clock RClock
metrics BondMetrics
logger log.Logger
clock RClock
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{
logger: logger,
clock: clock,
metrics: metrics,
logger: logger,
clock: clock,
metrics: metrics,
honestActors: honestActors,
}
}
......@@ -47,6 +50,10 @@ func (b *Bonds) CheckBonds(games []*types.EnrichedGameData) {
func (b *Bonds) checkCredits(games []*types.EnrichedGameData) {
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 {
// Check if the max duration has been reached for this game
......@@ -94,6 +101,12 @@ func (b *Bonds) checkCredits(games []*types.EnrichedGameData) {
}
comparison := actual.Cmp(expected)
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 {
creditMetrics[metrics.CreditAboveWithdrawable] += 1
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) {
b.metrics.RecordCredit(metrics.CreditBelowNonWithdrawable, creditMetrics[metrics.CreditBelowNonWithdrawable])
b.metrics.RecordCredit(metrics.CreditEqualNonWithdrawable, creditMetrics[metrics.CreditEqualNonWithdrawable])
b.metrics.RecordCredit(metrics.CreditAboveNonWithdrawable, creditMetrics[metrics.CreditAboveNonWithdrawable])
b.metrics.RecordHonestWithdrawableAmounts(honestWithdrawableAmounts)
}
......@@ -17,7 +17,10 @@ import (
)
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) {
......@@ -61,8 +64,8 @@ func TestCheckBonds(t *testing.T) {
}
func TestCheckRecipientCredit(t *testing.T) {
addr1 := common.Address{0x1a}
addr2 := common.Address{0x2b}
addr1 := honestActor1
addr2 := honestActor2
addr3 := common.Address{0x3c}
addr4 := common.Address{0x4d}
notRootPosition := types.NewPositionFromGIndex(big.NewInt(2))
......@@ -273,7 +276,7 @@ func TestCheckRecipientCredit(t *testing.T) {
MaxClockDuration: 10,
WETHDelay: 10 * time.Second,
GameMetadata: gameTypes.GameMetadata{
Proxy: common.Address{44},
Proxy: common.Address{0x44},
Timestamp: uint64(frozen.Unix()) - 22,
},
BlockNumberChallenged: true,
......@@ -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.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
// addr1 is correct so has no logs
// addr2 is below expected before max duration, so warn about early withdrawal
......@@ -371,8 +382,18 @@ func TestCheckRecipientCredit(t *testing.T) {
testlog.NewAttributesFilter("withdrawable", "non_withdrawable")))
// Logs from game 2
// addr1 is below expected - no warning as withdrawals may now be possible
// addr2 is correct
// addr1 is below expected - no warning as withdrawals may now be possible, 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", 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
require.NotNil(t, logs.FindLog(
testlog.NewLevelFilter(log.LevelWarn),
......@@ -401,8 +422,18 @@ func TestCheckRecipientCredit(t *testing.T) {
testlog.NewAttributesFilter("withdrawable", "non_withdrawable")))
// Logs from game 4
// addr1 is correct so has no logs
// addr2 is below expected before max duration, no long because withdrawals may be possible
// addr1 is correct but has unclaimed credit
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
// addr4 is above expected before max duration, so warn
require.NotNil(t, logs.FindLog(
......@@ -419,13 +450,19 @@ func setupBondMetricsTest(t *testing.T) (*Bonds, *stubBondMetrics, *testlog.Capt
credits: make(map[metrics.CreditExpectation]int),
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
}
type stubBondMetrics struct {
credits map[metrics.CreditExpectation]int
recorded map[common.Address]Collateral
credits map[metrics.CreditExpectation]int
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) {
......
......@@ -25,16 +25,12 @@ type ClaimMetrics interface {
type ClaimMonitor struct {
logger log.Logger
clock RClock
honestActors map[common.Address]bool // Map for efficient lookup
honestActors types.HonestActors
metrics ClaimMetrics
}
func NewClaimMonitor(logger log.Logger, clock RClock, honestActors []common.Address, metrics ClaimMetrics) *ClaimMonitor {
actors := make(map[common.Address]bool)
for _, actor := range honestActors {
actors[actor] = true
}
return &ClaimMonitor{logger, clock, actors, metrics}
func NewClaimMonitor(logger log.Logger, clock RClock, honestActors types.HonestActors, metrics ClaimMetrics) *ClaimMonitor {
return &ClaimMonitor{logger, clock, honestActors, metrics}
}
func (c *ClaimMonitor) CheckClaims(games []*types.EnrichedGameData) {
......
......@@ -194,10 +194,10 @@ func newTestClaimMonitor(t *testing.T) (*ClaimMonitor, *clock.DeterministicClock
logger, handler := testlog.CaptureLogger(t, log.LvlInfo)
cl := clock.NewDeterministicClock(frozen)
metrics := &stubClaimMetrics{}
honestActors := []common.Address{
honestActors := types.NewHonestActors([]common.Address{
{0x01},
{0x02},
}
})
monitor := NewClaimMonitor(logger, cl, honestActors, metrics)
return monitor, cl, metrics, handler
}
......
......@@ -8,6 +8,7 @@ import (
"sync/atomic"
"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/ethclient"
"github.com/ethereum/go-ethereum/log"
......@@ -28,9 +29,10 @@ import (
)
type Service struct {
logger log.Logger
metrics metrics.Metricer
monitor *gameMonitor
logger log.Logger
metrics metrics.Metricer
monitor *gameMonitor
honestActors types.HonestActors
factoryContract *contracts.DisputeGameFactoryContract
......@@ -56,9 +58,10 @@ type Service struct {
// NewService creates a new Service.
func NewService(ctx context.Context, logger log.Logger, cfg *config.Config) (*Service, error) {
s := &Service{
cl: clock.SystemClock,
logger: logger,
metrics: metrics.NewMetrics(),
cl: clock.SystemClock,
logger: logger,
metrics: metrics.NewMetrics(),
honestActors: types.NewHonestActors(cfg.HonestActors),
}
if err := s.initFromConfig(ctx, cfg); err != nil {
......@@ -105,7 +108,7 @@ func (s *Service) initFromConfig(ctx context.Context, cfg *config.Config) error
}
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() {
......@@ -142,7 +145,7 @@ func (s *Service) initForecast(cfg *config.Config) {
}
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 {
......
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