Skip to content

Commit

Permalink
feat(op-dispute-mon): Unexpected Claim Resolution (ethereum-optimism#…
Browse files Browse the repository at this point in the history
…10031)

* feat(op-dispute-mon): track claims resolved against honest actors

* fix(op-dispute-mon): break after honest actor error is logged

* fix(op-dispute-mon): lints
  • Loading branch information
refcell authored Apr 2, 2024
1 parent 93b96df commit 2207569
Show file tree
Hide file tree
Showing 7 changed files with 139 additions and 32 deletions.
8 changes: 5 additions & 3 deletions op-dispute-mon/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,11 @@ const (
type Config struct {
L1EthRpc string // L1 RPC Url
GameFactoryAddress common.Address // Address of the dispute game factory
RollupRpc string // The rollup node RPC URL.

MonitorInterval time.Duration // Frequency to check for new games to monitor.
GameWindow time.Duration // Maximum window to look for games to monitor.
HonestActors []common.Address // List of honest actors to monitor claims for.
RollupRpc string // The rollup node RPC URL.
MonitorInterval time.Duration // Frequency to check for new games to monitor.
GameWindow time.Duration // Maximum window to look for games to monitor.

MetricsConfig opmetrics.CLIConfig
PprofConfig oppprof.CLIConfig
Expand All @@ -46,6 +47,7 @@ func NewConfig(gameFactoryAddress common.Address, l1EthRpc string) Config {
L1EthRpc: l1EthRpc,
GameFactoryAddress: gameFactoryAddress,

HonestActors: []common.Address{},
MonitorInterval: DefaultMonitorInterval,
GameWindow: DefaultGameWindow,

Expand Down
19 changes: 19 additions & 0 deletions op-dispute-mon/flags/flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
oplog "github.com/ethereum-optimism/optimism/op-service/log"
opmetrics "github.com/ethereum-optimism/optimism/op-service/metrics"
"github.com/ethereum-optimism/optimism/op-service/oppprof"
"github.com/ethereum/go-ethereum/common"
)

const (
Expand All @@ -33,6 +34,11 @@ var (
EnvVars: prefixEnvVars("GAME_FACTORY_ADDRESS"),
}
// Optional Flags
HonestActorsFlag = &cli.StringSliceFlag{
Name: "honest-actors",
Usage: "List of honest actors that are monitored for any claims that are resolved against them.",
EnvVars: prefixEnvVars("HONEST_ACTORS"),
}
RollupRpcFlag = &cli.StringFlag{
Name: "rollup-rpc",
Usage: "HTTP provider URL for the rollup node",
Expand Down Expand Up @@ -62,6 +68,7 @@ var requiredFlags = []cli.Flag{
// optionalFlags is a list of unchecked cli flags
var optionalFlags = []cli.Flag{
RollupRpcFlag,
HonestActorsFlag,
MonitorIntervalFlag,
GameWindowFlag,
}
Expand Down Expand Up @@ -96,13 +103,25 @@ func NewConfigFromCLI(ctx *cli.Context) (*config.Config, error) {
return nil, err
}

var actors []common.Address
if ctx.IsSet(HonestActorsFlag.Name) {
for _, addrStr := range ctx.StringSlice(HonestActorsFlag.Name) {
actor, err := opservice.ParseAddress(addrStr)
if err != nil {
return nil, fmt.Errorf("invalid honest actor address: %w", err)
}
actors = append(actors, actor)
}
}

metricsConfig := opmetrics.ReadCLIConfig(ctx)
pprofConfig := oppprof.ReadCLIConfig(ctx)

return &config.Config{
L1EthRpc: ctx.String(L1EthRpcFlag.Name),
GameFactoryAddress: gameFactoryAddress,

HonestActors: actors,
RollupRpc: ctx.String(RollupRpcFlag.Name),
MonitorInterval: ctx.Duration(MonitorIntervalFlag.Name),
GameWindow: ctx.Duration(GameWindowFlag.Name),
Expand Down
15 changes: 15 additions & 0 deletions op-dispute-mon/metrics/metrics.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,8 @@ type Metricer interface {
RecordInfo(version string)
RecordUp()

RecordUnexpectedClaimResolution(address common.Address, count int)

RecordGameResolutionStatus(complete bool, maxDurationReached bool, count int)

RecordCredit(expectation CreditExpectation, count int)
Expand Down Expand Up @@ -104,6 +106,8 @@ type Metrics struct {

claims prometheus.GaugeVec

unexpectedClaimResolutions prometheus.GaugeVec

withdrawalRequests prometheus.GaugeVec

info prometheus.GaugeVec
Expand Down Expand Up @@ -161,6 +165,13 @@ func NewMetrics() *Metrics {
Name: "claim_resolution_delay_max",
Help: "Maximum claim resolution delay in seconds",
}),
unexpectedClaimResolutions: *factory.NewGaugeVec(prometheus.GaugeOpts{
Namespace: Namespace,
Name: "unexpected_claim_resolutions",
Help: "Total number of unexpected claim resolutions against an honest actor",
}, []string{
"honest_actor_address",
}),
resolutionStatus: *factory.NewGaugeVec(prometheus.GaugeOpts{
Namespace: Namespace,
Name: "resolution_status",
Expand Down Expand Up @@ -251,6 +262,10 @@ func (m *Metrics) RecordUp() {
m.up.Set(1)
}

func (m *Metrics) RecordUnexpectedClaimResolution(address common.Address, count int) {
m.unexpectedClaimResolutions.WithLabelValues(address.Hex()).Set(float64(count))
}

func (m *Metrics) RecordGameResolutionStatus(complete bool, maxDurationReached bool, count int) {
completion := "complete"
if !complete {
Expand Down
2 changes: 2 additions & 0 deletions op-dispute-mon/metrics/noop.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ func (*NoopMetricsImpl) RecordUp() {}
func (*NoopMetricsImpl) CacheAdd(_ string, _ int, _ bool) {}
func (*NoopMetricsImpl) CacheGet(_ string, _ bool) {}

func (*NoopMetricsImpl) RecordUnexpectedClaimResolution(_ common.Address, _ int) {}

func (*NoopMetricsImpl) RecordGameResolutionStatus(_ bool, _ bool, _ int) {}

func (*NoopMetricsImpl) RecordCredit(_ CreditExpectation, _ int) {}
Expand Down
40 changes: 33 additions & 7 deletions op-dispute-mon/mon/claims.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (

"github.com/ethereum-optimism/optimism/op-dispute-mon/metrics"
"github.com/ethereum-optimism/optimism/op-dispute-mon/mon/types"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/log"
)

Expand All @@ -14,35 +15,60 @@ type RClock interface {

type ClaimMetrics interface {
RecordClaims(status metrics.ClaimStatus, count int)
RecordUnexpectedClaimResolution(address common.Address, count int)
}

type ClaimMonitor struct {
logger log.Logger
clock RClock
metrics ClaimMetrics
logger log.Logger
clock RClock
honestActors []common.Address
metrics ClaimMetrics
}

func NewClaimMonitor(logger log.Logger, clock RClock, metrics ClaimMetrics) *ClaimMonitor {
return &ClaimMonitor{logger, clock, metrics}
func NewClaimMonitor(logger log.Logger, clock RClock, honestActors []common.Address, metrics ClaimMetrics) *ClaimMonitor {
return &ClaimMonitor{logger, clock, honestActors, metrics}
}

func (c *ClaimMonitor) CheckClaims(games []*types.EnrichedGameData) {
claimStatus := make(map[metrics.ClaimStatus]int)
unexpected := make(map[common.Address]int)
for _, game := range games {
c.checkGameClaims(game, claimStatus)
c.checkGameClaims(game, claimStatus, unexpected)
}
for status, count := range claimStatus {
c.metrics.RecordClaims(status, count)
}
for address, count := range unexpected {
c.metrics.RecordUnexpectedClaimResolution(address, count)
}
}

func (c *ClaimMonitor) checkResolvedAgainstHonestActor(proxy common.Address, claim *types.EnrichedClaim, unexpected map[common.Address]int) {
for _, actor := range c.honestActors {
if claim.Claimant == actor && claim.CounteredBy != (common.Address{}) {
unexpected[actor]++
c.logger.Error("Claim resolved against honest actor", "game", proxy, "honest_actor", actor, "countered_by", claim.CounteredBy, "claim_contract_index", claim.ContractIndex)
break
}
}
}

func (c *ClaimMonitor) checkGameClaims(game *types.EnrichedGameData, claimStatus map[metrics.ClaimStatus]int) {
func (c *ClaimMonitor) checkGameClaims(
game *types.EnrichedGameData,
claimStatus map[metrics.ClaimStatus]int,
unexpected map[common.Address]int,
) {
// Check if the game is in the first half
duration := uint64(c.clock.Now().Unix()) - game.Timestamp
firstHalf := duration <= (game.Duration / 2)

// Iterate over the game's claims
for _, claim := range game.Claims {
// Check if the claim has resolved against an honest actor
if claim.Resolved {
c.checkResolvedAgainstHonestActor(game.Proxy, &claim, unexpected)
}

// Check if the clock has expired
if firstHalf && claim.Resolved {
c.logger.Error("Claim resolved in the first half of the game duration", "game", game.Proxy, "claimContractIndex", claim.ContractIndex)
Expand Down
77 changes: 60 additions & 17 deletions op-dispute-mon/mon/claims_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,30 +17,51 @@ import (
var frozen = time.Unix(int64(time.Hour.Seconds()), 0)

func TestClaimMonitor_CheckClaims(t *testing.T) {
cm, cl, cMetrics := newTestClaimMonitor(t)
games := makeMultipleTestGames(uint64(cl.Now().Unix()))
cm.CheckClaims(games)
t.Run("RecordsClaims", func(t *testing.T) {
monitor, cl, cMetrics := newTestClaimMonitor(t)
games := makeMultipleTestGames(uint64(cl.Now().Unix()))
monitor.CheckClaims(games)

require.Equal(t, 1, cMetrics.calls[metrics.FirstHalfExpiredResolved])
require.Equal(t, 1, cMetrics.calls[metrics.FirstHalfExpiredUnresolved])
require.Equal(t, 1, cMetrics.calls[metrics.FirstHalfNotExpiredResolved])
require.Equal(t, 1, cMetrics.calls[metrics.FirstHalfNotExpiredUnresolved])
require.Equal(t, 2, cMetrics.calls[metrics.FirstHalfExpiredResolved])
require.Equal(t, 1, cMetrics.calls[metrics.FirstHalfExpiredUnresolved])
require.Equal(t, 1, cMetrics.calls[metrics.FirstHalfNotExpiredResolved])
require.Equal(t, 1, cMetrics.calls[metrics.FirstHalfNotExpiredUnresolved])

require.Equal(t, 1, cMetrics.calls[metrics.SecondHalfExpiredResolved])
require.Equal(t, 1, cMetrics.calls[metrics.SecondHalfExpiredUnresolved])
require.Equal(t, 1, cMetrics.calls[metrics.SecondHalfNotExpiredResolved])
require.Equal(t, 1, cMetrics.calls[metrics.SecondHalfNotExpiredUnresolved])
require.Equal(t, 2, cMetrics.calls[metrics.SecondHalfExpiredResolved])
require.Equal(t, 1, cMetrics.calls[metrics.SecondHalfExpiredUnresolved])
require.Equal(t, 1, cMetrics.calls[metrics.SecondHalfNotExpiredResolved])
require.Equal(t, 1, cMetrics.calls[metrics.SecondHalfNotExpiredUnresolved])
})

t.Run("RecordsUnexpectedClaimResolution", func(t *testing.T) {
monitor, cl, cMetrics := newTestClaimMonitor(t)
games := makeMultipleTestGames(uint64(cl.Now().Unix()))
monitor.CheckClaims(games)

// Our honest actors 0x01 has claims resolved against them (1 per game)
require.Equal(t, 2, cMetrics.unexpected[common.Address{0x01}])
require.Equal(t, 0, cMetrics.unexpected[common.Address{0x02}])

// The other actors should not be metriced
require.Equal(t, 0, cMetrics.unexpected[common.Address{0x03}])
require.Equal(t, 0, cMetrics.unexpected[common.Address{0x04}])
})
}

func newTestClaimMonitor(t *testing.T) (*ClaimMonitor, *clock.DeterministicClock, *stubClaimMetrics) {
logger := testlog.Logger(t, log.LvlInfo)
cl := clock.NewDeterministicClock(frozen)
metrics := &stubClaimMetrics{}
return NewClaimMonitor(logger, cl, metrics), cl, metrics
honestActors := []common.Address{
common.Address{0x01},
common.Address{0x02},
}
return NewClaimMonitor(logger, cl, honestActors, metrics), cl, metrics
}

type stubClaimMetrics struct {
calls map[metrics.ClaimStatus]int
calls map[metrics.ClaimStatus]int
unexpected map[common.Address]int
}

func (s *stubClaimMetrics) RecordClaims(status metrics.ClaimStatus, count int) {
Expand All @@ -50,6 +71,13 @@ func (s *stubClaimMetrics) RecordClaims(status metrics.ClaimStatus, count int) {
s.calls[status] += count
}

func (s *stubClaimMetrics) RecordUnexpectedClaimResolution(address common.Address, count int) {
if s.unexpected == nil {
s.unexpected = make(map[common.Address]int)
}
s.unexpected[address] += count
}

func makeMultipleTestGames(duration uint64) []*types.EnrichedGameData {
return []*types.EnrichedGameData{
makeTestGame(duration), // first half
Expand All @@ -68,21 +96,36 @@ func makeTestGame(duration uint64) *types.EnrichedGameData {
Claims: []types.EnrichedClaim{
{
Claim: faultTypes.Claim{
Clock: faultTypes.NewClock(time.Duration(0), frozen),
Clock: faultTypes.NewClock(time.Duration(0), frozen),
Claimant: common.Address{0x02},
},
Resolved: true,
},
{
Claim: faultTypes.Claim{
Claimant: common.Address{0x01},
CounteredBy: common.Address{0x03},
},
Resolved: true,
},
{
Claim: faultTypes.Claim{},
Claim: faultTypes.Claim{
Claimant: common.Address{0x04},
CounteredBy: common.Address{0x02},
},
Resolved: true,
},
{
Claim: faultTypes.Claim{
Clock: faultTypes.NewClock(time.Duration(0), frozen),
Claimant: common.Address{0x04},
CounteredBy: common.Address{0x02},
Clock: faultTypes.NewClock(time.Duration(0), frozen),
},
},
{
Claim: faultTypes.Claim{},
Claim: faultTypes.Claim{
Claimant: common.Address{0x01},
},
},
},
}
Expand Down
10 changes: 5 additions & 5 deletions op-dispute-mon/mon/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -88,8 +88,8 @@ func (s *Service) initFromConfig(ctx context.Context, cfg *config.Config) error
return fmt.Errorf("failed to init rollup client: %w", err)
}

s.initClaimMonitor(cfg)
s.initResolutionMonitor()
s.initClaimMonitor()
s.initWithdrawalMonitor()

s.initOutputValidator() // Must be called before initForecast
Expand All @@ -109,12 +109,12 @@ func (s *Service) initFromConfig(ctx context.Context, cfg *config.Config) error
return nil
}

func (s *Service) initResolutionMonitor() {
s.resolutions = NewResolutionMonitor(s.logger, s.metrics, s.cl)
func (s *Service) initClaimMonitor(cfg *config.Config) {
s.claims = NewClaimMonitor(s.logger, s.cl, cfg.HonestActors, s.metrics)
}

func (s *Service) initClaimMonitor() {
s.claims = NewClaimMonitor(s.logger, s.cl, s.metrics)
func (s *Service) initResolutionMonitor() {
s.resolutions = NewResolutionMonitor(s.logger, s.metrics, s.cl)
}

func (s *Service) initWithdrawalMonitor() {
Expand Down

0 comments on commit 2207569

Please sign in to comment.