Skip to content

Commit

Permalink
op-dispute-mon: Identify unclaimed credits based on the withdrawal re…
Browse files Browse the repository at this point in the history
…quest timestamp (ethereum-optimism#11488)
  • Loading branch information
ajsutton authored Sep 9, 2024
1 parent f46bea7 commit 4428d10
Show file tree
Hide file tree
Showing 5 changed files with 136 additions and 96 deletions.
28 changes: 7 additions & 21 deletions op-dispute-mon/mon/bonds/monitor.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,22 +17,19 @@ 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
honestActors types.HonestActors
logger log.Logger
clock RClock
metrics BondMetrics
}

func NewBonds(logger log.Logger, metrics BondMetrics, honestActors types.HonestActors, clock RClock) *Bonds {
func NewBonds(logger log.Logger, metrics BondMetrics, clock RClock) *Bonds {
return &Bonds{
logger: logger,
clock: clock,
metrics: metrics,
honestActors: honestActors,
logger: logger,
clock: clock,
metrics: metrics,
}
}

Expand All @@ -50,10 +47,6 @@ 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
Expand Down Expand Up @@ -101,12 +94,6 @@ 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")
Expand Down Expand Up @@ -136,5 +123,4 @@ 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)
}
57 changes: 10 additions & 47 deletions op-dispute-mon/mon/bonds/monitor_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,7 @@ import (
)

var (
frozen = time.Unix(int64(time.Hour.Seconds()), 0)
honestActor1 = common.Address{0x11, 0xaa}
honestActor2 = common.Address{0x22, 0xbb}
honestActor3 = common.Address{0x33, 0xcc}
frozen = time.Unix(int64(time.Hour.Seconds()), 0)
)

func TestCheckBonds(t *testing.T) {
Expand Down Expand Up @@ -64,8 +61,8 @@ func TestCheckBonds(t *testing.T) {
}

func TestCheckRecipientCredit(t *testing.T) {
addr1 := honestActor1
addr2 := honestActor2
addr1 := common.Address{0x11, 0xaa}
addr2 := common.Address{0x22, 0xbb}
addr3 := common.Address{0x3c}
addr4 := common.Address{0x4d}
notRootPosition := types.NewPositionFromGIndex(big.NewInt(2))
Expand Down Expand Up @@ -349,14 +346,6 @@ 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
Expand All @@ -382,18 +371,8 @@ 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, 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())))
// addr1 is below expected - no warning as withdrawals may now be possible
// addr2 is correct
// addr3 is above expected - warn
require.NotNil(t, logs.FindLog(
testlog.NewLevelFilter(log.LevelWarn),
Expand Down Expand Up @@ -422,18 +401,8 @@ func TestCheckRecipientCredit(t *testing.T) {
testlog.NewAttributesFilter("withdrawable", "non_withdrawable")))

// Logs from game 4
// 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())))
// addr1 is correct
// addr2 is below expected before max duration, no log because withdrawals may be possible
// addr3 is not involved so no logs
// addr4 is above expected before max duration, so warn
require.NotNil(t, logs.FindLog(
Expand All @@ -450,19 +419,13 @@ func setupBondMetricsTest(t *testing.T) (*Bonds, *stubBondMetrics, *testlog.Capt
credits: make(map[metrics.CreditExpectation]int),
recorded: make(map[common.Address]Collateral),
}
honestActors := monTypes.NewHonestActors([]common.Address{honestActor1, honestActor2, honestActor3})
bonds := NewBonds(logger, metrics, honestActors, clock.NewDeterministicClock(frozen))
bonds := NewBonds(logger, metrics, clock.NewDeterministicClock(frozen))
return bonds, metrics, logs
}

type stubBondMetrics struct {
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
credits map[metrics.CreditExpectation]int
recorded map[common.Address]Collateral
}

func (s *stubBondMetrics) RecordBondCollateral(addr common.Address, required *big.Int, available *big.Int) {
Expand Down
4 changes: 2 additions & 2 deletions op-dispute-mon/mon/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ func (s *Service) initResolutionMonitor() {
}

func (s *Service) initWithdrawalMonitor() {
s.withdrawals = NewWithdrawalMonitor(s.logger, s.metrics)
s.withdrawals = NewWithdrawalMonitor(s.logger, s.cl, s.metrics, s.honestActors)
}

func (s *Service) initGameCallerCreator() {
Expand Down Expand Up @@ -145,7 +145,7 @@ func (s *Service) initForecast(cfg *config.Config) {
}

func (s *Service) initBonds() {
s.bonds = bonds.NewBonds(s.logger, s.metrics, s.honestActors, s.cl)
s.bonds = bonds.NewBonds(s.logger, s.metrics, s.cl)
}

func (s *Service) initOutputRollupClient(ctx context.Context, cfg *config.Config) error {
Expand Down
38 changes: 31 additions & 7 deletions op-dispute-mon/mon/withdrawals.go
Original file line number Diff line number Diff line change
@@ -1,32 +1,45 @@
package mon

import (
"math/big"
"time"

"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)
RecordHonestWithdrawableAmounts(map[common.Address]*big.Int)
}

type WithdrawalMonitor struct {
logger log.Logger
metrics WithdrawalMetrics
logger log.Logger
clock RClock
metrics WithdrawalMetrics
honestActors types.HonestActors
}

func NewWithdrawalMonitor(logger log.Logger, metrics WithdrawalMetrics) *WithdrawalMonitor {
func NewWithdrawalMonitor(logger log.Logger, clock RClock, metrics WithdrawalMetrics, honestActors types.HonestActors) *WithdrawalMonitor {
return &WithdrawalMonitor{
logger: logger,
metrics: metrics,
logger: logger,
clock: clock,
metrics: metrics,
honestActors: honestActors,
}
}

func (w *WithdrawalMonitor) CheckWithdrawals(games []*types.EnrichedGameData) {
now := w.clock.Now() // Use a consistent time for all checks
matching := make(map[common.Address]int)
divergent := make(map[common.Address]int)
honestWithdrawableAmounts := make(map[common.Address]*big.Int)
for address := range w.honestActors {
honestWithdrawableAmounts[address] = big.NewInt(0)
}
for _, game := range games {
matches, diverges := w.validateGameWithdrawals(game)
matches, diverges := w.validateGameWithdrawals(game, now, honestWithdrawableAmounts)
matching[game.WETHContract] += matches
divergent[game.WETHContract] += diverges
}
Expand All @@ -36,9 +49,10 @@ func (w *WithdrawalMonitor) CheckWithdrawals(games []*types.EnrichedGameData) {
for contract, count := range divergent {
w.metrics.RecordWithdrawalRequests(contract, false, count)
}
w.metrics.RecordHonestWithdrawableAmounts(honestWithdrawableAmounts)
}

func (w *WithdrawalMonitor) validateGameWithdrawals(game *types.EnrichedGameData) (int, int) {
func (w *WithdrawalMonitor) validateGameWithdrawals(game *types.EnrichedGameData, now time.Time, honestWithdrawableAmounts map[common.Address]*big.Int) (int, int) {
matching := 0
divergent := 0
for recipient, withdrawalAmount := range game.WithdrawalRequests {
Expand All @@ -48,6 +62,16 @@ func (w *WithdrawalMonitor) validateGameWithdrawals(game *types.EnrichedGameData
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)
}

if withdrawalAmount.Amount.Cmp(big.NewInt(0)) > 0 && w.honestActors.Contains(recipient) {
if time.Unix(withdrawalAmount.Timestamp.Int64(), 0).Add(game.WETHDelay).Before(now) {
// Credits are withdrawable
total := honestWithdrawableAmounts[recipient]
total = new(big.Int).Add(total, withdrawalAmount.Amount)
honestWithdrawableAmounts[recipient] = total
w.logger.Warn("Found unclaimed credit", "recipient", recipient, "game", game.Proxy, "amount", withdrawalAmount.Amount)
}
}
}
return matching, divergent
}
Loading

0 comments on commit 4428d10

Please sign in to comment.