Commit 2207569e authored by refcell's avatar refcell Committed by GitHub

feat(op-dispute-mon): Unexpected Claim Resolution (#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
parent 93b96dff
...@@ -32,8 +32,9 @@ const ( ...@@ -32,8 +32,9 @@ const (
type Config struct { type Config struct {
L1EthRpc string // L1 RPC Url L1EthRpc string // L1 RPC Url
GameFactoryAddress common.Address // Address of the dispute game factory GameFactoryAddress common.Address // Address of the dispute game factory
RollupRpc string // The rollup node RPC URL.
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. MonitorInterval time.Duration // Frequency to check for new games to monitor.
GameWindow time.Duration // Maximum window to look for games to monitor. GameWindow time.Duration // Maximum window to look for games to monitor.
...@@ -46,6 +47,7 @@ func NewConfig(gameFactoryAddress common.Address, l1EthRpc string) Config { ...@@ -46,6 +47,7 @@ func NewConfig(gameFactoryAddress common.Address, l1EthRpc string) Config {
L1EthRpc: l1EthRpc, L1EthRpc: l1EthRpc,
GameFactoryAddress: gameFactoryAddress, GameFactoryAddress: gameFactoryAddress,
HonestActors: []common.Address{},
MonitorInterval: DefaultMonitorInterval, MonitorInterval: DefaultMonitorInterval,
GameWindow: DefaultGameWindow, GameWindow: DefaultGameWindow,
......
...@@ -10,6 +10,7 @@ import ( ...@@ -10,6 +10,7 @@ import (
oplog "github.com/ethereum-optimism/optimism/op-service/log" oplog "github.com/ethereum-optimism/optimism/op-service/log"
opmetrics "github.com/ethereum-optimism/optimism/op-service/metrics" opmetrics "github.com/ethereum-optimism/optimism/op-service/metrics"
"github.com/ethereum-optimism/optimism/op-service/oppprof" "github.com/ethereum-optimism/optimism/op-service/oppprof"
"github.com/ethereum/go-ethereum/common"
) )
const ( const (
...@@ -33,6 +34,11 @@ var ( ...@@ -33,6 +34,11 @@ var (
EnvVars: prefixEnvVars("GAME_FACTORY_ADDRESS"), EnvVars: prefixEnvVars("GAME_FACTORY_ADDRESS"),
} }
// Optional Flags // 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{ RollupRpcFlag = &cli.StringFlag{
Name: "rollup-rpc", Name: "rollup-rpc",
Usage: "HTTP provider URL for the rollup node", Usage: "HTTP provider URL for the rollup node",
...@@ -62,6 +68,7 @@ var requiredFlags = []cli.Flag{ ...@@ -62,6 +68,7 @@ var requiredFlags = []cli.Flag{
// optionalFlags is a list of unchecked cli flags // optionalFlags is a list of unchecked cli flags
var optionalFlags = []cli.Flag{ var optionalFlags = []cli.Flag{
RollupRpcFlag, RollupRpcFlag,
HonestActorsFlag,
MonitorIntervalFlag, MonitorIntervalFlag,
GameWindowFlag, GameWindowFlag,
} }
...@@ -96,6 +103,17 @@ func NewConfigFromCLI(ctx *cli.Context) (*config.Config, error) { ...@@ -96,6 +103,17 @@ func NewConfigFromCLI(ctx *cli.Context) (*config.Config, error) {
return nil, err 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) metricsConfig := opmetrics.ReadCLIConfig(ctx)
pprofConfig := oppprof.ReadCLIConfig(ctx) pprofConfig := oppprof.ReadCLIConfig(ctx)
...@@ -103,6 +121,7 @@ func NewConfigFromCLI(ctx *cli.Context) (*config.Config, error) { ...@@ -103,6 +121,7 @@ func NewConfigFromCLI(ctx *cli.Context) (*config.Config, error) {
L1EthRpc: ctx.String(L1EthRpcFlag.Name), L1EthRpc: ctx.String(L1EthRpcFlag.Name),
GameFactoryAddress: gameFactoryAddress, GameFactoryAddress: gameFactoryAddress,
HonestActors: actors,
RollupRpc: ctx.String(RollupRpcFlag.Name), RollupRpc: ctx.String(RollupRpcFlag.Name),
MonitorInterval: ctx.Duration(MonitorIntervalFlag.Name), MonitorInterval: ctx.Duration(MonitorIntervalFlag.Name),
GameWindow: ctx.Duration(GameWindowFlag.Name), GameWindow: ctx.Duration(GameWindowFlag.Name),
......
...@@ -69,6 +69,8 @@ type Metricer interface { ...@@ -69,6 +69,8 @@ type Metricer interface {
RecordInfo(version string) RecordInfo(version string)
RecordUp() RecordUp()
RecordUnexpectedClaimResolution(address common.Address, count int)
RecordGameResolutionStatus(complete bool, maxDurationReached bool, count int) RecordGameResolutionStatus(complete bool, maxDurationReached bool, count int)
RecordCredit(expectation CreditExpectation, count int) RecordCredit(expectation CreditExpectation, count int)
...@@ -104,6 +106,8 @@ type Metrics struct { ...@@ -104,6 +106,8 @@ type Metrics struct {
claims prometheus.GaugeVec claims prometheus.GaugeVec
unexpectedClaimResolutions prometheus.GaugeVec
withdrawalRequests prometheus.GaugeVec withdrawalRequests prometheus.GaugeVec
info prometheus.GaugeVec info prometheus.GaugeVec
...@@ -161,6 +165,13 @@ func NewMetrics() *Metrics { ...@@ -161,6 +165,13 @@ func NewMetrics() *Metrics {
Name: "claim_resolution_delay_max", Name: "claim_resolution_delay_max",
Help: "Maximum claim resolution delay in seconds", 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{ resolutionStatus: *factory.NewGaugeVec(prometheus.GaugeOpts{
Namespace: Namespace, Namespace: Namespace,
Name: "resolution_status", Name: "resolution_status",
...@@ -251,6 +262,10 @@ func (m *Metrics) RecordUp() { ...@@ -251,6 +262,10 @@ func (m *Metrics) RecordUp() {
m.up.Set(1) 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) { func (m *Metrics) RecordGameResolutionStatus(complete bool, maxDurationReached bool, count int) {
completion := "complete" completion := "complete"
if !complete { if !complete {
......
...@@ -19,6 +19,8 @@ func (*NoopMetricsImpl) RecordUp() {} ...@@ -19,6 +19,8 @@ func (*NoopMetricsImpl) RecordUp() {}
func (*NoopMetricsImpl) CacheAdd(_ string, _ int, _ bool) {} func (*NoopMetricsImpl) CacheAdd(_ string, _ int, _ bool) {}
func (*NoopMetricsImpl) CacheGet(_ string, _ bool) {} func (*NoopMetricsImpl) CacheGet(_ string, _ bool) {}
func (*NoopMetricsImpl) RecordUnexpectedClaimResolution(_ common.Address, _ int) {}
func (*NoopMetricsImpl) RecordGameResolutionStatus(_ bool, _ bool, _ int) {} func (*NoopMetricsImpl) RecordGameResolutionStatus(_ bool, _ bool, _ int) {}
func (*NoopMetricsImpl) RecordCredit(_ CreditExpectation, _ int) {} func (*NoopMetricsImpl) RecordCredit(_ CreditExpectation, _ int) {}
......
...@@ -5,6 +5,7 @@ import ( ...@@ -5,6 +5,7 @@ import (
"github.com/ethereum-optimism/optimism/op-dispute-mon/metrics" "github.com/ethereum-optimism/optimism/op-dispute-mon/metrics"
"github.com/ethereum-optimism/optimism/op-dispute-mon/mon/types" "github.com/ethereum-optimism/optimism/op-dispute-mon/mon/types"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/log"
) )
...@@ -14,35 +15,60 @@ type RClock interface { ...@@ -14,35 +15,60 @@ type RClock interface {
type ClaimMetrics interface { type ClaimMetrics interface {
RecordClaims(status metrics.ClaimStatus, count int) RecordClaims(status metrics.ClaimStatus, count int)
RecordUnexpectedClaimResolution(address common.Address, count int)
} }
type ClaimMonitor struct { type ClaimMonitor struct {
logger log.Logger logger log.Logger
clock RClock clock RClock
honestActors []common.Address
metrics ClaimMetrics metrics ClaimMetrics
} }
func NewClaimMonitor(logger log.Logger, clock RClock, metrics ClaimMetrics) *ClaimMonitor { func NewClaimMonitor(logger log.Logger, clock RClock, honestActors []common.Address, metrics ClaimMetrics) *ClaimMonitor {
return &ClaimMonitor{logger, clock, metrics} return &ClaimMonitor{logger, clock, honestActors, metrics}
} }
func (c *ClaimMonitor) CheckClaims(games []*types.EnrichedGameData) { func (c *ClaimMonitor) CheckClaims(games []*types.EnrichedGameData) {
claimStatus := make(map[metrics.ClaimStatus]int) claimStatus := make(map[metrics.ClaimStatus]int)
unexpected := make(map[common.Address]int)
for _, game := range games { for _, game := range games {
c.checkGameClaims(game, claimStatus) c.checkGameClaims(game, claimStatus, unexpected)
} }
for status, count := range claimStatus { for status, count := range claimStatus {
c.metrics.RecordClaims(status, count) 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 // Check if the game is in the first half
duration := uint64(c.clock.Now().Unix()) - game.Timestamp duration := uint64(c.clock.Now().Unix()) - game.Timestamp
firstHalf := duration <= (game.Duration / 2) firstHalf := duration <= (game.Duration / 2)
// Iterate over the game's claims // Iterate over the game's claims
for _, claim := range game.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 // Check if the clock has expired
if firstHalf && claim.Resolved { if firstHalf && claim.Resolved {
c.logger.Error("Claim resolved in the first half of the game duration", "game", game.Proxy, "claimContractIndex", claim.ContractIndex) c.logger.Error("Claim resolved in the first half of the game duration", "game", game.Proxy, "claimContractIndex", claim.ContractIndex)
......
...@@ -17,30 +17,51 @@ import ( ...@@ -17,30 +17,51 @@ import (
var frozen = time.Unix(int64(time.Hour.Seconds()), 0) var frozen = time.Unix(int64(time.Hour.Seconds()), 0)
func TestClaimMonitor_CheckClaims(t *testing.T) { func TestClaimMonitor_CheckClaims(t *testing.T) {
cm, cl, cMetrics := newTestClaimMonitor(t) t.Run("RecordsClaims", func(t *testing.T) {
monitor, cl, cMetrics := newTestClaimMonitor(t)
games := makeMultipleTestGames(uint64(cl.Now().Unix())) games := makeMultipleTestGames(uint64(cl.Now().Unix()))
cm.CheckClaims(games) monitor.CheckClaims(games)
require.Equal(t, 1, cMetrics.calls[metrics.FirstHalfExpiredResolved]) require.Equal(t, 2, cMetrics.calls[metrics.FirstHalfExpiredResolved])
require.Equal(t, 1, cMetrics.calls[metrics.FirstHalfExpiredUnresolved]) require.Equal(t, 1, cMetrics.calls[metrics.FirstHalfExpiredUnresolved])
require.Equal(t, 1, cMetrics.calls[metrics.FirstHalfNotExpiredResolved]) require.Equal(t, 1, cMetrics.calls[metrics.FirstHalfNotExpiredResolved])
require.Equal(t, 1, cMetrics.calls[metrics.FirstHalfNotExpiredUnresolved]) require.Equal(t, 1, cMetrics.calls[metrics.FirstHalfNotExpiredUnresolved])
require.Equal(t, 1, cMetrics.calls[metrics.SecondHalfExpiredResolved]) require.Equal(t, 2, cMetrics.calls[metrics.SecondHalfExpiredResolved])
require.Equal(t, 1, cMetrics.calls[metrics.SecondHalfExpiredUnresolved]) require.Equal(t, 1, cMetrics.calls[metrics.SecondHalfExpiredUnresolved])
require.Equal(t, 1, cMetrics.calls[metrics.SecondHalfNotExpiredResolved]) require.Equal(t, 1, cMetrics.calls[metrics.SecondHalfNotExpiredResolved])
require.Equal(t, 1, cMetrics.calls[metrics.SecondHalfNotExpiredUnresolved]) 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) { func newTestClaimMonitor(t *testing.T) (*ClaimMonitor, *clock.DeterministicClock, *stubClaimMetrics) {
logger := testlog.Logger(t, log.LvlInfo) logger := testlog.Logger(t, log.LvlInfo)
cl := clock.NewDeterministicClock(frozen) cl := clock.NewDeterministicClock(frozen)
metrics := &stubClaimMetrics{} 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 { 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) { func (s *stubClaimMetrics) RecordClaims(status metrics.ClaimStatus, count int) {
...@@ -50,6 +71,13 @@ func (s *stubClaimMetrics) RecordClaims(status metrics.ClaimStatus, count int) { ...@@ -50,6 +71,13 @@ func (s *stubClaimMetrics) RecordClaims(status metrics.ClaimStatus, count int) {
s.calls[status] += count 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 { func makeMultipleTestGames(duration uint64) []*types.EnrichedGameData {
return []*types.EnrichedGameData{ return []*types.EnrichedGameData{
makeTestGame(duration), // first half makeTestGame(duration), // first half
...@@ -69,20 +97,35 @@ func makeTestGame(duration uint64) *types.EnrichedGameData { ...@@ -69,20 +97,35 @@ func makeTestGame(duration uint64) *types.EnrichedGameData {
{ {
Claim: faultTypes.Claim{ Claim: faultTypes.Claim{
Clock: faultTypes.NewClock(time.Duration(0), frozen), Clock: faultTypes.NewClock(time.Duration(0), frozen),
Claimant: common.Address{0x02},
}, },
Resolved: true, Resolved: true,
}, },
{ {
Claim: faultTypes.Claim{}, Claim: faultTypes.Claim{
Claimant: common.Address{0x01},
CounteredBy: common.Address{0x03},
},
Resolved: true,
},
{
Claim: faultTypes.Claim{
Claimant: common.Address{0x04},
CounteredBy: common.Address{0x02},
},
Resolved: true, Resolved: true,
}, },
{ {
Claim: faultTypes.Claim{ Claim: faultTypes.Claim{
Claimant: common.Address{0x04},
CounteredBy: common.Address{0x02},
Clock: faultTypes.NewClock(time.Duration(0), frozen), Clock: faultTypes.NewClock(time.Duration(0), frozen),
}, },
}, },
{ {
Claim: faultTypes.Claim{}, Claim: faultTypes.Claim{
Claimant: common.Address{0x01},
},
}, },
}, },
} }
......
...@@ -88,8 +88,8 @@ func (s *Service) initFromConfig(ctx context.Context, cfg *config.Config) error ...@@ -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) return fmt.Errorf("failed to init rollup client: %w", err)
} }
s.initClaimMonitor(cfg)
s.initResolutionMonitor() s.initResolutionMonitor()
s.initClaimMonitor()
s.initWithdrawalMonitor() s.initWithdrawalMonitor()
s.initOutputValidator() // Must be called before initForecast s.initOutputValidator() // Must be called before initForecast
...@@ -109,12 +109,12 @@ func (s *Service) initFromConfig(ctx context.Context, cfg *config.Config) error ...@@ -109,12 +109,12 @@ func (s *Service) initFromConfig(ctx context.Context, cfg *config.Config) error
return nil return nil
} }
func (s *Service) initResolutionMonitor() { func (s *Service) initClaimMonitor(cfg *config.Config) {
s.resolutions = NewResolutionMonitor(s.logger, s.metrics, s.cl) s.claims = NewClaimMonitor(s.logger, s.cl, cfg.HonestActors, s.metrics)
} }
func (s *Service) initClaimMonitor() { func (s *Service) initResolutionMonitor() {
s.claims = NewClaimMonitor(s.logger, s.cl, s.metrics) s.resolutions = NewResolutionMonitor(s.logger, s.metrics, s.cl)
} }
func (s *Service) initWithdrawalMonitor() { func (s *Service) initWithdrawalMonitor() {
......
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