Commit 134f6883 authored by refcell's avatar refcell Committed by GitHub

feat(op-dispute-mon): claim resolution delay max calculation (#9567)

parent 3b8933d3
...@@ -80,25 +80,27 @@ func (c *FaultDisputeGameContract) GetBlockRange(ctx context.Context) (prestateB ...@@ -80,25 +80,27 @@ func (c *FaultDisputeGameContract) GetBlockRange(ctx context.Context) (prestateB
return return
} }
// GetGameMetadata returns the game's L2 block number, root claim, and status. // GetGameMetadata returns the game's L2 block number, root claim, status, and game duration.
func (c *FaultDisputeGameContract) GetGameMetadata(ctx context.Context) (uint64, common.Hash, gameTypes.GameStatus, error) { func (c *FaultDisputeGameContract) GetGameMetadata(ctx context.Context) (uint64, common.Hash, gameTypes.GameStatus, uint64, error) {
results, err := c.multiCaller.Call(ctx, batching.BlockLatest, results, err := c.multiCaller.Call(ctx, batching.BlockLatest,
c.contract.Call(methodL2BlockNumber), c.contract.Call(methodL2BlockNumber),
c.contract.Call(methodRootClaim), c.contract.Call(methodRootClaim),
c.contract.Call(methodStatus)) c.contract.Call(methodStatus),
c.contract.Call(methodGameDuration))
if err != nil { if err != nil {
return 0, common.Hash{}, 0, fmt.Errorf("failed to retrieve game metadata: %w", err) return 0, common.Hash{}, 0, 0, fmt.Errorf("failed to retrieve game metadata: %w", err)
} }
if len(results) != 3 { if len(results) != 4 {
return 0, common.Hash{}, 0, fmt.Errorf("expected 3 results but got %v", len(results)) return 0, common.Hash{}, 0, 0, fmt.Errorf("expected 3 results but got %v", len(results))
} }
l2BlockNumber := results[0].GetBigInt(0).Uint64() l2BlockNumber := results[0].GetBigInt(0).Uint64()
rootClaim := results[1].GetHash(0) rootClaim := results[1].GetHash(0)
duration := results[3].GetUint64(0)
status, err := gameTypes.GameStatusFromUint8(results[2].GetUint8(0)) status, err := gameTypes.GameStatusFromUint8(results[2].GetUint8(0))
if err != nil { if err != nil {
return 0, common.Hash{}, 0, fmt.Errorf("failed to convert game status: %w", err) return 0, common.Hash{}, 0, 0, fmt.Errorf("failed to convert game status: %w", err)
} }
return l2BlockNumber, rootClaim, status, nil return l2BlockNumber, rootClaim, status, duration, nil
} }
func (c *FaultDisputeGameContract) GetGenesisOutputRoot(ctx context.Context) (common.Hash, error) { func (c *FaultDisputeGameContract) GetGenesisOutputRoot(ctx context.Context) (common.Hash, error) {
......
...@@ -319,16 +319,19 @@ func TestGetSplitDepth(t *testing.T) { ...@@ -319,16 +319,19 @@ func TestGetSplitDepth(t *testing.T) {
func TestGetGameMetadata(t *testing.T) { func TestGetGameMetadata(t *testing.T) {
stubRpc, contract := setupFaultDisputeGameTest(t) stubRpc, contract := setupFaultDisputeGameTest(t)
expectedL2BlockNumber := uint64(123) expectedL2BlockNumber := uint64(123)
expectedGameDuration := uint64(456)
expectedRootClaim := common.Hash{0x01, 0x02} expectedRootClaim := common.Hash{0x01, 0x02}
expectedStatus := types.GameStatusChallengerWon expectedStatus := types.GameStatusChallengerWon
stubRpc.SetResponse(fdgAddr, methodL2BlockNumber, batching.BlockLatest, nil, []interface{}{new(big.Int).SetUint64(expectedL2BlockNumber)}) stubRpc.SetResponse(fdgAddr, methodL2BlockNumber, batching.BlockLatest, nil, []interface{}{new(big.Int).SetUint64(expectedL2BlockNumber)})
stubRpc.SetResponse(fdgAddr, methodRootClaim, batching.BlockLatest, nil, []interface{}{expectedRootClaim}) stubRpc.SetResponse(fdgAddr, methodRootClaim, batching.BlockLatest, nil, []interface{}{expectedRootClaim})
stubRpc.SetResponse(fdgAddr, methodStatus, batching.BlockLatest, nil, []interface{}{expectedStatus}) stubRpc.SetResponse(fdgAddr, methodStatus, batching.BlockLatest, nil, []interface{}{expectedStatus})
l2BlockNumber, rootClaim, status, err := contract.GetGameMetadata(context.Background()) stubRpc.SetResponse(fdgAddr, methodGameDuration, batching.BlockLatest, nil, []interface{}{expectedGameDuration})
l2BlockNumber, rootClaim, status, duration, err := contract.GetGameMetadata(context.Background())
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, expectedL2BlockNumber, l2BlockNumber) require.Equal(t, expectedL2BlockNumber, l2BlockNumber)
require.Equal(t, expectedRootClaim, rootClaim) require.Equal(t, expectedRootClaim, rootClaim)
require.Equal(t, expectedStatus, status) require.Equal(t, expectedStatus, status)
require.Equal(t, expectedGameDuration, duration)
} }
func TestGetGenesisOutputRoot(t *testing.T) { func TestGetGenesisOutputRoot(t *testing.T) {
......
...@@ -36,6 +36,8 @@ type Metricer interface { ...@@ -36,6 +36,8 @@ type Metricer interface {
RecordInfo(version string) RecordInfo(version string)
RecordUp() RecordUp()
RecordClaimResolutionDelayMax(delay float64)
RecordGamesStatus(inProgress, defenderWon, challengerWon int) RecordGamesStatus(inProgress, defenderWon, challengerWon int)
RecordGameAgreement(status GameAgreementStatus, count int) RecordGameAgreement(status GameAgreementStatus, count int)
...@@ -55,6 +57,8 @@ type Metrics struct { ...@@ -55,6 +57,8 @@ type Metrics struct {
info prometheus.GaugeVec info prometheus.GaugeVec
up prometheus.Gauge up prometheus.Gauge
claimResolutionDelayMax prometheus.Gauge
trackedGames prometheus.GaugeVec trackedGames prometheus.GaugeVec
gamesAgreement prometheus.GaugeVec gamesAgreement prometheus.GaugeVec
} }
...@@ -88,6 +92,11 @@ func NewMetrics() *Metrics { ...@@ -88,6 +92,11 @@ func NewMetrics() *Metrics {
Name: "up", Name: "up",
Help: "1 if the op-challenger has finished starting up", Help: "1 if the op-challenger has finished starting up",
}), }),
claimResolutionDelayMax: factory.NewGauge(prometheus.GaugeOpts{
Namespace: Namespace,
Name: "claim_resolution_delay_max",
Help: "Maximum claim resolution delay in seconds",
}),
trackedGames: *factory.NewGaugeVec(prometheus.GaugeOpts{ trackedGames: *factory.NewGaugeVec(prometheus.GaugeOpts{
Namespace: Namespace, Namespace: Namespace,
Name: "tracked_games", Name: "tracked_games",
...@@ -132,6 +141,10 @@ func (m *Metrics) RecordUp() { ...@@ -132,6 +141,10 @@ func (m *Metrics) RecordUp() {
m.up.Set(1) m.up.Set(1)
} }
func (m *Metrics) RecordClaimResolutionDelayMax(delay float64) {
m.claimResolutionDelayMax.Set(delay)
}
func (m *Metrics) Document() []opmetrics.DocumentedMetric { func (m *Metrics) Document() []opmetrics.DocumentedMetric {
return m.factory.Document() return m.factory.Document()
} }
......
...@@ -10,5 +10,7 @@ func (*NoopMetricsImpl) RecordUp() {} ...@@ -10,5 +10,7 @@ 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) RecordClaimResolutionDelayMax(delay float64) {}
func (*NoopMetricsImpl) RecordGamesStatus(inProgress, defenderWon, challengerWon int) {} func (*NoopMetricsImpl) RecordGamesStatus(inProgress, defenderWon, challengerWon int) {}
func (*NoopMetricsImpl) RecordGameAgreement(status GameAgreementStatus, count int) {} func (*NoopMetricsImpl) RecordGameAgreement(status GameAgreementStatus, count int) {}
...@@ -6,13 +6,12 @@ import ( ...@@ -6,13 +6,12 @@ import (
"math/big" "math/big"
faultTypes "github.com/ethereum-optimism/optimism/op-challenger/game/fault/types" faultTypes "github.com/ethereum-optimism/optimism/op-challenger/game/fault/types"
monTypes "github.com/ethereum-optimism/optimism/op-dispute-mon/mon/types"
"github.com/ethereum-optimism/optimism/op-service/sources/batching" "github.com/ethereum-optimism/optimism/op-service/sources/batching"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
"golang.org/x/exp/maps" "golang.org/x/exp/maps"
) )
var noBond = new(big.Int).Sub(new(big.Int).Lsh(big.NewInt(1), 128), big.NewInt(1))
type BondContract interface { type BondContract interface {
GetCredits(ctx context.Context, block batching.Block, recipients ...common.Address) ([]*big.Int, error) GetCredits(ctx context.Context, block batching.Block, recipients ...common.Address) ([]*big.Int, error)
} }
...@@ -24,7 +23,7 @@ func CalculateRequiredCollateral(ctx context.Context, contract BondContract, blo ...@@ -24,7 +23,7 @@ func CalculateRequiredCollateral(ctx context.Context, contract BondContract, blo
unpaidBonds := big.NewInt(0) unpaidBonds := big.NewInt(0)
recipients := make(map[common.Address]bool) recipients := make(map[common.Address]bool)
for _, claim := range claims { for _, claim := range claims {
if noBond.Cmp(claim.Bond) != 0 { if monTypes.ResolvedBondAmount.Cmp(claim.Bond) != 0 {
unpaidBonds = new(big.Int).Add(unpaidBonds, claim.Bond) unpaidBonds = new(big.Int).Add(unpaidBonds, claim.Bond)
} }
recipients[claim.Claimant] = true recipients[claim.Claimant] = true
......
...@@ -6,20 +6,17 @@ import ( ...@@ -6,20 +6,17 @@ import (
"testing" "testing"
"github.com/ethereum-optimism/optimism/op-challenger/game/fault/types" "github.com/ethereum-optimism/optimism/op-challenger/game/fault/types"
monTypes "github.com/ethereum-optimism/optimism/op-dispute-mon/mon/types"
"github.com/ethereum-optimism/optimism/op-service/sources/batching" "github.com/ethereum-optimism/optimism/op-service/sources/batching"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
func TestMaxValue(t *testing.T) {
require.Equal(t, noBond.String(), "340282366920938463463374607431768211455")
}
func TestCalculateRequiredCollateral(t *testing.T) { func TestCalculateRequiredCollateral(t *testing.T) {
claims := []types.Claim{ claims := []types.Claim{
{ {
ClaimData: types.ClaimData{ ClaimData: types.ClaimData{
Bond: noBond, Bond: monTypes.ResolvedBondAmount,
}, },
Claimant: common.Address{0x01}, Claimant: common.Address{0x01},
CounteredBy: common.Address{0x02}, CounteredBy: common.Address{0x02},
......
...@@ -16,8 +16,8 @@ import ( ...@@ -16,8 +16,8 @@ import (
const metricsLabel = "game_caller_creator" const metricsLabel = "game_caller_creator"
type GameCaller interface { type GameCaller interface {
GetGameMetadata(context.Context) (uint64, common.Hash, types.GameStatus, error) GetGameMetadata(context.Context) (uint64, common.Hash, types.GameStatus, uint64, error)
GetAllClaims(ctx context.Context) ([]faultTypes.Claim, error) GetAllClaims(context.Context) ([]faultTypes.Claim, error)
} }
type GameCallerCreator struct { type GameCallerCreator struct {
......
...@@ -44,7 +44,7 @@ func (e *Extractor) enrichGames(ctx context.Context, games []gameTypes.GameMetad ...@@ -44,7 +44,7 @@ func (e *Extractor) enrichGames(ctx context.Context, games []gameTypes.GameMetad
e.logger.Error("failed to create game caller", "err", err) e.logger.Error("failed to create game caller", "err", err)
continue continue
} }
l2BlockNum, rootClaim, status, err := caller.GetGameMetadata(ctx) l2BlockNum, rootClaim, status, duration, err := caller.GetGameMetadata(ctx)
if err != nil { if err != nil {
e.logger.Error("failed to fetch game metadata", "err", err) e.logger.Error("failed to fetch game metadata", "err", err)
continue continue
...@@ -59,6 +59,7 @@ func (e *Extractor) enrichGames(ctx context.Context, games []gameTypes.GameMetad ...@@ -59,6 +59,7 @@ func (e *Extractor) enrichGames(ctx context.Context, games []gameTypes.GameMetad
L2BlockNumber: l2BlockNum, L2BlockNumber: l2BlockNum,
RootClaim: rootClaim, RootClaim: rootClaim,
Status: status, Status: status,
Duration: duration,
Claims: claims, Claims: claims,
}) })
} }
......
...@@ -37,7 +37,7 @@ func TestExtractor_Extract(t *testing.T) { ...@@ -37,7 +37,7 @@ func TestExtractor_Extract(t *testing.T) {
require.Equal(t, 1, creator.calls) require.Equal(t, 1, creator.calls)
require.Equal(t, 0, creator.caller.metadataCalls) require.Equal(t, 0, creator.caller.metadataCalls)
require.Equal(t, 0, creator.caller.claimsCalls) require.Equal(t, 0, creator.caller.claimsCalls)
verifyLogs(t, logs, 1, 0, 0) verifyLogs(t, logs, 1, 0, 0, 0)
}) })
t.Run("MetadataFetchErrorLog", func(t *testing.T) { t.Run("MetadataFetchErrorLog", func(t *testing.T) {
...@@ -51,7 +51,7 @@ func TestExtractor_Extract(t *testing.T) { ...@@ -51,7 +51,7 @@ func TestExtractor_Extract(t *testing.T) {
require.Equal(t, 1, creator.calls) require.Equal(t, 1, creator.calls)
require.Equal(t, 1, creator.caller.metadataCalls) require.Equal(t, 1, creator.caller.metadataCalls)
require.Equal(t, 0, creator.caller.claimsCalls) require.Equal(t, 0, creator.caller.claimsCalls)
verifyLogs(t, logs, 0, 1, 0) verifyLogs(t, logs, 0, 1, 0, 0)
}) })
t.Run("ClaimsFetchErrorLog", func(t *testing.T) { t.Run("ClaimsFetchErrorLog", func(t *testing.T) {
...@@ -65,7 +65,7 @@ func TestExtractor_Extract(t *testing.T) { ...@@ -65,7 +65,7 @@ func TestExtractor_Extract(t *testing.T) {
require.Equal(t, 1, creator.calls) require.Equal(t, 1, creator.calls)
require.Equal(t, 1, creator.caller.metadataCalls) require.Equal(t, 1, creator.caller.metadataCalls)
require.Equal(t, 1, creator.caller.claimsCalls) require.Equal(t, 1, creator.caller.claimsCalls)
verifyLogs(t, logs, 0, 0, 1) verifyLogs(t, logs, 0, 0, 1, 0)
}) })
t.Run("Success", func(t *testing.T) { t.Run("Success", func(t *testing.T) {
...@@ -81,7 +81,7 @@ func TestExtractor_Extract(t *testing.T) { ...@@ -81,7 +81,7 @@ func TestExtractor_Extract(t *testing.T) {
}) })
} }
func verifyLogs(t *testing.T, logs *testlog.CapturingHandler, createErr int, metadataErr int, claimsErr int) { func verifyLogs(t *testing.T, logs *testlog.CapturingHandler, createErr int, metadataErr int, claimsErr int, durationErr int) {
errorLevelFilter := testlog.NewLevelFilter(log.LevelError) errorLevelFilter := testlog.NewLevelFilter(log.LevelError)
createMessageFilter := testlog.NewMessageFilter("failed to create game caller") createMessageFilter := testlog.NewMessageFilter("failed to create game caller")
l := logs.FindLogs(errorLevelFilter, createMessageFilter) l := logs.FindLogs(errorLevelFilter, createMessageFilter)
...@@ -92,6 +92,9 @@ func verifyLogs(t *testing.T, logs *testlog.CapturingHandler, createErr int, met ...@@ -92,6 +92,9 @@ func verifyLogs(t *testing.T, logs *testlog.CapturingHandler, createErr int, met
claimsMessageFilter := testlog.NewMessageFilter("failed to fetch game claims") claimsMessageFilter := testlog.NewMessageFilter("failed to fetch game claims")
l = logs.FindLogs(errorLevelFilter, claimsMessageFilter) l = logs.FindLogs(errorLevelFilter, claimsMessageFilter)
require.Len(t, l, claimsErr) require.Len(t, l, claimsErr)
durationMessageFilter := testlog.NewMessageFilter("failed to fetch game duration")
l = logs.FindLogs(errorLevelFilter, durationMessageFilter)
require.Len(t, l, durationErr)
} }
func setupExtractorTest(t *testing.T) (*Extractor, *mockGameCallerCreator, *mockGameFetcher, *testlog.CapturingHandler) { func setupExtractorTest(t *testing.T) (*Extractor, *mockGameCallerCreator, *mockGameFetcher, *testlog.CapturingHandler) {
...@@ -146,12 +149,12 @@ type mockGameCaller struct { ...@@ -146,12 +149,12 @@ type mockGameCaller struct {
claims []faultTypes.Claim claims []faultTypes.Claim
} }
func (m *mockGameCaller) GetGameMetadata(_ context.Context) (uint64, common.Hash, types.GameStatus, error) { func (m *mockGameCaller) GetGameMetadata(_ context.Context) (uint64, common.Hash, types.GameStatus, uint64, error) {
m.metadataCalls++ m.metadataCalls++
if m.metadataErr != nil { if m.metadataErr != nil {
return 0, common.Hash{}, 0, m.metadataErr return 0, common.Hash{}, 0, 0, m.metadataErr
} }
return 0, mockRootClaim, 0, nil return 0, mockRootClaim, 0, 0, nil
} }
func (m *mockGameCaller) GetAllClaims(ctx context.Context) ([]faultTypes.Claim, error) { func (m *mockGameCaller) GetAllClaims(ctx context.Context) ([]faultTypes.Claim, error) {
......
...@@ -7,6 +7,7 @@ import ( ...@@ -7,6 +7,7 @@ import (
"github.com/ethereum-optimism/optimism/op-challenger/game/types" "github.com/ethereum-optimism/optimism/op-challenger/game/types"
"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/resolution"
"github.com/ethereum-optimism/optimism/op-dispute-mon/mon/transform" "github.com/ethereum-optimism/optimism/op-dispute-mon/mon/transform"
monTypes "github.com/ethereum-optimism/optimism/op-dispute-mon/mon/types" monTypes "github.com/ethereum-optimism/optimism/op-dispute-mon/mon/types"
...@@ -21,6 +22,7 @@ var ( ...@@ -21,6 +22,7 @@ var (
) )
type ForecastMetrics interface { type ForecastMetrics interface {
RecordClaimResolutionDelayMax(delay float64)
RecordGameAgreement(status metrics.GameAgreementStatus, count int) RecordGameAgreement(status metrics.GameAgreementStatus, count int)
} }
...@@ -65,7 +67,7 @@ func (f *forecast) forecastGame(ctx context.Context, game *monTypes.EnrichedGame ...@@ -65,7 +67,7 @@ func (f *forecast) forecastGame(ctx context.Context, game *monTypes.EnrichedGame
tree := transform.CreateBidirectionalTree(game.Claims) tree := transform.CreateBidirectionalTree(game.Claims)
// Compute the resolution status of the game. // Compute the resolution status of the game.
status := Resolve(tree) status := resolution.Resolve(tree)
// Check the root agreement. // Check the root agreement.
agreement, expected, err := f.validator.CheckRootAgreement(ctx, game.L2BlockNumber, game.RootClaim) agreement, expected, err := f.validator.CheckRootAgreement(ctx, game.L2BlockNumber, game.RootClaim)
......
...@@ -4,6 +4,8 @@ import ( ...@@ -4,6 +4,8 @@ import (
"context" "context"
"errors" "errors"
"fmt" "fmt"
"math"
"math/big"
"testing" "testing"
faultTypes "github.com/ethereum-optimism/optimism/op-challenger/game/fault/types" faultTypes "github.com/ethereum-optimism/optimism/op-challenger/game/fault/types"
...@@ -267,6 +269,7 @@ type mockForecastMetrics struct { ...@@ -267,6 +269,7 @@ type mockForecastMetrics struct {
disagreeDefenderAhead int disagreeDefenderAhead int
agreeChallengerAhead int agreeChallengerAhead int
disagreeChallengerAhead int disagreeChallengerAhead int
claimResolutionDelayMax float64
} }
func (m *mockForecastMetrics) RecordGameAgreement(status metrics.GameAgreementStatus, count int) { func (m *mockForecastMetrics) RecordGameAgreement(status metrics.GameAgreementStatus, count int) {
...@@ -281,3 +284,38 @@ func (m *mockForecastMetrics) RecordGameAgreement(status metrics.GameAgreementSt ...@@ -281,3 +284,38 @@ func (m *mockForecastMetrics) RecordGameAgreement(status metrics.GameAgreementSt
m.disagreeChallengerAhead = count m.disagreeChallengerAhead = count
} }
} }
func (m *mockForecastMetrics) RecordClaimResolutionDelayMax(delay float64) {
m.claimResolutionDelayMax = delay
}
func createDeepClaimList() []faultTypes.Claim {
return []faultTypes.Claim{
{
ClaimData: faultTypes.ClaimData{
Position: faultTypes.NewPosition(0, big.NewInt(0)),
},
ContractIndex: 0,
CounteredBy: common.HexToAddress("0x222222"),
ParentContractIndex: math.MaxInt64,
Claimant: common.HexToAddress("0x111111"),
},
{
ClaimData: faultTypes.ClaimData{
Position: faultTypes.NewPosition(1, big.NewInt(0)),
},
CounteredBy: common.HexToAddress("0x111111"),
ContractIndex: 1,
ParentContractIndex: 0,
Claimant: common.HexToAddress("0x222222"),
},
{
ClaimData: faultTypes.ClaimData{
Position: faultTypes.NewPosition(2, big.NewInt(0)),
},
ContractIndex: 2,
ParentContractIndex: 1,
Claimant: common.HexToAddress("0x111111"),
},
}
}
...@@ -18,6 +18,7 @@ type Forecast func(ctx context.Context, games []*types.EnrichedGameData) ...@@ -18,6 +18,7 @@ type Forecast func(ctx context.Context, games []*types.EnrichedGameData)
type BlockHashFetcher func(ctx context.Context, number *big.Int) (common.Hash, error) type BlockHashFetcher func(ctx context.Context, number *big.Int) (common.Hash, error)
type BlockNumberFetcher func(ctx context.Context) (uint64, error) type BlockNumberFetcher func(ctx context.Context) (uint64, error)
type Extract func(ctx context.Context, blockHash common.Hash, minTimestamp uint64) ([]*types.EnrichedGameData, error) type Extract func(ctx context.Context, blockHash common.Hash, minTimestamp uint64) ([]*types.EnrichedGameData, error)
type RecordClaimResolutionDelayMax func([]*types.EnrichedGameData)
type gameMonitor struct { type gameMonitor struct {
logger log.Logger logger log.Logger
...@@ -30,6 +31,7 @@ type gameMonitor struct { ...@@ -30,6 +31,7 @@ type gameMonitor struct {
gameWindow time.Duration gameWindow time.Duration
monitorInterval time.Duration monitorInterval time.Duration
delays RecordClaimResolutionDelayMax
detect Detect detect Detect
forecast Forecast forecast Forecast
extract Extract extract Extract
...@@ -43,6 +45,7 @@ func newGameMonitor( ...@@ -43,6 +45,7 @@ func newGameMonitor(
cl clock.Clock, cl clock.Clock,
monitorInterval time.Duration, monitorInterval time.Duration,
gameWindow time.Duration, gameWindow time.Duration,
delays RecordClaimResolutionDelayMax,
detect Detect, detect Detect,
forecast Forecast, forecast Forecast,
extract Extract, extract Extract,
...@@ -56,6 +59,7 @@ func newGameMonitor( ...@@ -56,6 +59,7 @@ func newGameMonitor(
done: make(chan struct{}), done: make(chan struct{}),
monitorInterval: monitorInterval, monitorInterval: monitorInterval,
gameWindow: gameWindow, gameWindow: gameWindow,
delays: delays,
detect: detect, detect: detect,
forecast: forecast, forecast: forecast,
extract: extract, extract: extract,
...@@ -90,6 +94,7 @@ func (m *gameMonitor) monitorGames() error { ...@@ -90,6 +94,7 @@ func (m *gameMonitor) monitorGames() error {
if err != nil { if err != nil {
return fmt.Errorf("failed to load games: %w", err) return fmt.Errorf("failed to load games: %w", err)
} }
m.delays(enrichedGames)
m.detect(m.ctx, enrichedGames) m.detect(m.ctx, enrichedGames)
m.forecast(m.ctx, enrichedGames) m.forecast(m.ctx, enrichedGames)
return nil return nil
......
...@@ -24,20 +24,20 @@ func TestMonitor_MinGameTimestamp(t *testing.T) { ...@@ -24,20 +24,20 @@ func TestMonitor_MinGameTimestamp(t *testing.T) {
t.Parallel() t.Parallel()
t.Run("ZeroGameWindow", func(t *testing.T) { t.Run("ZeroGameWindow", func(t *testing.T) {
monitor, _, _, _ := setupMonitorTest(t) monitor, _, _, _, _ := setupMonitorTest(t)
monitor.gameWindow = time.Duration(0) monitor.gameWindow = time.Duration(0)
require.Equal(t, monitor.minGameTimestamp(), uint64(0)) require.Equal(t, monitor.minGameTimestamp(), uint64(0))
}) })
t.Run("ZeroClock", func(t *testing.T) { t.Run("ZeroClock", func(t *testing.T) {
monitor, _, _, _ := setupMonitorTest(t) monitor, _, _, _, _ := setupMonitorTest(t)
monitor.gameWindow = time.Minute monitor.gameWindow = time.Minute
monitor.clock = clock.NewDeterministicClock(time.Unix(0, 0)) monitor.clock = clock.NewDeterministicClock(time.Unix(0, 0))
require.Equal(t, uint64(0), monitor.minGameTimestamp()) require.Equal(t, uint64(0), monitor.minGameTimestamp())
}) })
t.Run("ValidArithmetic", func(t *testing.T) { t.Run("ValidArithmetic", func(t *testing.T) {
monitor, _, _, _ := setupMonitorTest(t) monitor, _, _, _, _ := setupMonitorTest(t)
monitor.gameWindow = time.Minute monitor.gameWindow = time.Minute
frozen := time.Unix(int64(time.Hour.Seconds()), 0) frozen := time.Unix(int64(time.Hour.Seconds()), 0)
monitor.clock = clock.NewDeterministicClock(frozen) monitor.clock = clock.NewDeterministicClock(frozen)
...@@ -50,7 +50,7 @@ func TestMonitor_MonitorGames(t *testing.T) { ...@@ -50,7 +50,7 @@ func TestMonitor_MonitorGames(t *testing.T) {
t.Parallel() t.Parallel()
t.Run("FailedFetchBlocknumber", func(t *testing.T) { t.Run("FailedFetchBlocknumber", func(t *testing.T) {
monitor, _, _, _ := setupMonitorTest(t) monitor, _, _, _, _ := setupMonitorTest(t)
boom := errors.New("boom") boom := errors.New("boom")
monitor.fetchBlockNumber = func(ctx context.Context) (uint64, error) { monitor.fetchBlockNumber = func(ctx context.Context) (uint64, error) {
return 0, boom return 0, boom
...@@ -60,7 +60,7 @@ func TestMonitor_MonitorGames(t *testing.T) { ...@@ -60,7 +60,7 @@ func TestMonitor_MonitorGames(t *testing.T) {
}) })
t.Run("FailedFetchBlockHash", func(t *testing.T) { t.Run("FailedFetchBlockHash", func(t *testing.T) {
monitor, _, _, _ := setupMonitorTest(t) monitor, _, _, _, _ := setupMonitorTest(t)
boom := errors.New("boom") boom := errors.New("boom")
monitor.fetchBlockHash = func(ctx context.Context, number *big.Int) (common.Hash, error) { monitor.fetchBlockHash = func(ctx context.Context, number *big.Int) (common.Hash, error) {
return common.Hash{}, boom return common.Hash{}, boom
...@@ -70,19 +70,23 @@ func TestMonitor_MonitorGames(t *testing.T) { ...@@ -70,19 +70,23 @@ func TestMonitor_MonitorGames(t *testing.T) {
}) })
t.Run("DetectsWithNoGames", func(t *testing.T) { t.Run("DetectsWithNoGames", func(t *testing.T) {
monitor, factory, detector, _ := setupMonitorTest(t) monitor, factory, detector, forecast, delays := setupMonitorTest(t)
factory.games = []*monTypes.EnrichedGameData{} factory.games = []*monTypes.EnrichedGameData{}
err := monitor.monitorGames() err := monitor.monitorGames()
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, 1, detector.calls) require.Equal(t, 1, detector.calls)
require.Equal(t, 1, forecast.calls)
require.Equal(t, 1, delays.calls)
}) })
t.Run("DetectsMultipleGames", func(t *testing.T) { t.Run("DetectsMultipleGames", func(t *testing.T) {
monitor, factory, detector, _ := setupMonitorTest(t) monitor, factory, detector, forecast, delays := setupMonitorTest(t)
factory.games = []*monTypes.EnrichedGameData{{}, {}, {}} factory.games = []*monTypes.EnrichedGameData{{}, {}, {}}
err := monitor.monitorGames() err := monitor.monitorGames()
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, 1, detector.calls) require.Equal(t, 1, detector.calls)
require.Equal(t, 1, forecast.calls)
require.Equal(t, 1, delays.calls)
}) })
} }
...@@ -90,7 +94,7 @@ func TestMonitor_StartMonitoring(t *testing.T) { ...@@ -90,7 +94,7 @@ func TestMonitor_StartMonitoring(t *testing.T) {
t.Run("MonitorsGames", func(t *testing.T) { t.Run("MonitorsGames", func(t *testing.T) {
addr1 := common.Address{0xaa} addr1 := common.Address{0xaa}
addr2 := common.Address{0xbb} addr2 := common.Address{0xbb}
monitor, factory, detector, _ := setupMonitorTest(t) monitor, factory, detector, _, _ := setupMonitorTest(t)
factory.games = []*monTypes.EnrichedGameData{newEnrichedGameData(addr1, 9999), newEnrichedGameData(addr2, 9999)} factory.games = []*monTypes.EnrichedGameData{newEnrichedGameData(addr1, 9999), newEnrichedGameData(addr2, 9999)}
factory.maxSuccess = len(factory.games) // Only allow two successful fetches factory.maxSuccess = len(factory.games) // Only allow two successful fetches
...@@ -103,7 +107,7 @@ func TestMonitor_StartMonitoring(t *testing.T) { ...@@ -103,7 +107,7 @@ func TestMonitor_StartMonitoring(t *testing.T) {
}) })
t.Run("FailsToFetchGames", func(t *testing.T) { t.Run("FailsToFetchGames", func(t *testing.T) {
monitor, factory, detector, _ := setupMonitorTest(t) monitor, factory, detector, _, _ := setupMonitorTest(t)
factory.fetchErr = errors.New("boom") factory.fetchErr = errors.New("boom")
monitor.StartMonitoring() monitor.StartMonitoring()
...@@ -125,7 +129,7 @@ func newEnrichedGameData(proxy common.Address, timestamp uint64) *monTypes.Enric ...@@ -125,7 +129,7 @@ func newEnrichedGameData(proxy common.Address, timestamp uint64) *monTypes.Enric
} }
} }
func setupMonitorTest(t *testing.T) (*gameMonitor, *mockExtractor, *mockDetector, *mockForecast) { func setupMonitorTest(t *testing.T) (*gameMonitor, *mockExtractor, *mockDetector, *mockForecast, *mockDelayCalculator) {
logger := testlog.Logger(t, log.LvlDebug) logger := testlog.Logger(t, log.LvlDebug)
fetchBlockNum := func(ctx context.Context) (uint64, error) { fetchBlockNum := func(ctx context.Context) (uint64, error) {
return 1, nil return 1, nil
...@@ -139,19 +143,29 @@ func setupMonitorTest(t *testing.T) (*gameMonitor, *mockExtractor, *mockDetector ...@@ -139,19 +143,29 @@ func setupMonitorTest(t *testing.T) (*gameMonitor, *mockExtractor, *mockDetector
extractor := &mockExtractor{} extractor := &mockExtractor{}
detect := &mockDetector{} detect := &mockDetector{}
forecast := &mockForecast{} forecast := &mockForecast{}
delays := &mockDelayCalculator{}
monitor := newGameMonitor( monitor := newGameMonitor(
context.Background(), context.Background(),
logger, logger,
cl, cl,
monitorInterval, monitorInterval,
time.Duration(10*time.Second), time.Duration(10*time.Second),
delays.RecordClaimResolutionDelayMax,
detect.Detect, detect.Detect,
forecast.Forecast, forecast.Forecast,
extractor.Extract, extractor.Extract,
fetchBlockNum, fetchBlockNum,
fetchBlockHash, fetchBlockHash,
) )
return monitor, extractor, detect, forecast return monitor, extractor, detect, forecast, delays
}
type mockDelayCalculator struct {
calls int
}
func (m *mockDelayCalculator) RecordClaimResolutionDelayMax(games []*monTypes.EnrichedGameData) {
m.calls++
} }
type mockForecast struct { type mockForecast struct {
......
package resolution
import (
"github.com/ethereum-optimism/optimism/op-challenger/game/fault/types"
monTypes "github.com/ethereum-optimism/optimism/op-dispute-mon/mon/types"
"github.com/ethereum-optimism/optimism/op-service/clock"
)
type DelayMetrics interface {
RecordClaimResolutionDelayMax(delay float64)
}
type DelayCalculator struct {
metrics DelayMetrics
clock clock.Clock
}
func NewDelayCalculator(metrics DelayMetrics, clock clock.Clock) *DelayCalculator {
return &DelayCalculator{
metrics: metrics,
clock: clock,
}
}
func (d *DelayCalculator) RecordClaimResolutionDelayMax(games []*monTypes.EnrichedGameData) {
var maxDelay uint64 = 0
for _, game := range games {
maxDelay = max(d.getMaxResolutionDelay(game), maxDelay)
}
d.metrics.RecordClaimResolutionDelayMax(float64(maxDelay))
}
func (d *DelayCalculator) getMaxResolutionDelay(game *monTypes.EnrichedGameData) uint64 {
var maxDelay uint64 = 0
for _, claim := range game.Claims {
maxDelay = max(d.getOverflowTime(game.Duration, &claim), maxDelay)
}
return maxDelay
}
func (d *DelayCalculator) getOverflowTime(maxGameDuration uint64, claim *types.Claim) uint64 {
// If the bond amount is the max uint128 value, the claim is resolved.
if monTypes.ResolvedBondAmount.Cmp(claim.ClaimData.Bond) == 0 {
return 0
}
maxChessTime := maxGameDuration / 2
accumulatedTime := uint64(claim.ChessTime(d.clock.Now()))
if accumulatedTime < maxChessTime {
return 0
}
return accumulatedTime - maxChessTime
}
package resolution
import (
"math/big"
"testing"
"time"
"github.com/ethereum-optimism/optimism/op-challenger/game/fault/types"
monTypes "github.com/ethereum-optimism/optimism/op-dispute-mon/mon/types"
"github.com/ethereum-optimism/optimism/op-service/clock"
"github.com/stretchr/testify/require"
)
var (
maxGameDuration = uint64(960)
frozen = time.Unix(int64(time.Hour.Seconds()), 0)
)
func TestDelayCalculator_getOverflowTime(t *testing.T) {
t.Run("NoClock", func(t *testing.T) {
d, metrics, _ := setupDelayCalculatorTest(t)
claim := &types.Claim{
ClaimData: types.ClaimData{
Bond: monTypes.ResolvedBondAmount,
},
}
delay := d.getOverflowTime(maxGameDuration, claim)
require.Equal(t, uint64(0), delay)
require.Equal(t, 0, metrics.calls)
})
t.Run("RemainingTime", func(t *testing.T) {
d, metrics, cl := setupDelayCalculatorTest(t)
duration := uint64(3 * 60)
timestamp := uint64(cl.Now().Add(-time.Minute).Unix())
claim := &types.Claim{
ClaimData: types.ClaimData{
Bond: big.NewInt(5),
},
Clock: types.NewClock(duration, timestamp),
}
delay := d.getOverflowTime(maxGameDuration, claim)
require.Equal(t, uint64(0), delay)
require.Equal(t, 0, metrics.calls)
})
t.Run("OverflowTime", func(t *testing.T) {
d, metrics, cl := setupDelayCalculatorTest(t)
duration := maxGameDuration / 2
timestamp := uint64(cl.Now().Add(4 * -time.Minute).Unix())
claim := &types.Claim{
ClaimData: types.ClaimData{
Bond: big.NewInt(5),
},
Clock: types.NewClock(duration, timestamp),
}
delay := d.getOverflowTime(maxGameDuration, claim)
require.Equal(t, uint64(240), delay)
require.Equal(t, 0, metrics.calls)
})
}
func TestDelayCalculator_getMaxResolutionDelay(t *testing.T) {
tests := []struct {
name string
claims []types.Claim
want uint64
}{
{"NoClaims", []types.Claim{}, 0},
{"SingleClaim", createClaimList()[:1], 180},
{"MultipleClaims", createClaimList()[:2], 300},
{"ClaimsWithMaxUint128", createClaimList(), 300},
}
for _, test := range tests {
test := test
t.Run(test.name, func(t *testing.T) {
d, metrics, _ := setupDelayCalculatorTest(t)
game := &monTypes.EnrichedGameData{
Claims: test.claims,
Duration: maxGameDuration,
}
got := d.getMaxResolutionDelay(game)
require.Equal(t, 0, metrics.calls)
require.Equal(t, test.want, got)
})
}
}
func TestDelayCalculator_RecordClaimResolutionDelayMax(t *testing.T) {
tests := []struct {
name string
games []*monTypes.EnrichedGameData
want float64
}{
{"NoGames", createGameWithClaimsList()[:0], 0},
{"SingleGame", createGameWithClaimsList()[:1], 180},
{"MultipleGames", createGameWithClaimsList()[:2], 300},
{"ClaimsWithMaxUint128", createGameWithClaimsList(), 300},
}
for _, test := range tests {
test := test
t.Run(test.name, func(t *testing.T) {
d, metrics, _ := setupDelayCalculatorTest(t)
d.RecordClaimResolutionDelayMax(test.games)
require.Equal(t, 1, metrics.calls)
require.Equal(t, test.want, metrics.maxDelay)
})
}
}
func setupDelayCalculatorTest(t *testing.T) (*DelayCalculator, *mockDelayMetrics, *clock.DeterministicClock) {
metrics := &mockDelayMetrics{}
cl := clock.NewDeterministicClock(frozen)
return NewDelayCalculator(metrics, cl), metrics, cl
}
func createGameWithClaimsList() []*monTypes.EnrichedGameData {
return []*monTypes.EnrichedGameData{
{
Claims: createClaimList()[:1],
Duration: maxGameDuration,
},
{
Claims: createClaimList()[:2],
Duration: maxGameDuration,
},
{
Claims: createClaimList(),
Duration: maxGameDuration,
},
}
}
func createClaimList() []types.Claim {
newClock := func(multiplier int) *types.Clock {
duration := maxGameDuration / 2
timestamp := uint64(frozen.Add(-time.Minute * time.Duration(multiplier)).Unix())
return types.NewClock(duration, timestamp)
}
return []types.Claim{
{
ClaimData: types.ClaimData{
Bond: big.NewInt(5),
},
Clock: newClock(3),
},
{
ClaimData: types.ClaimData{
Bond: big.NewInt(10),
},
Clock: newClock(5),
},
{
ClaimData: types.ClaimData{
Bond: big.NewInt(100),
},
Clock: newClock(2),
},
{
// This claim should be skipped because it's resolved.
ClaimData: types.ClaimData{
Bond: monTypes.ResolvedBondAmount,
},
Clock: newClock(10),
},
}
}
type mockDelayMetrics struct {
calls int
maxDelay float64
}
func (m *mockDelayMetrics) RecordClaimResolutionDelayMax(delay float64) {
m.calls++
if delay > m.maxDelay {
m.maxDelay = delay
}
}
package mon package resolution
import ( import (
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
......
...@@ -14,6 +14,7 @@ import ( ...@@ -14,6 +14,7 @@ import (
"github.com/ethereum-optimism/optimism/op-dispute-mon/config" "github.com/ethereum-optimism/optimism/op-dispute-mon/config"
"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/extract" "github.com/ethereum-optimism/optimism/op-dispute-mon/mon/extract"
"github.com/ethereum-optimism/optimism/op-dispute-mon/mon/resolution"
"github.com/ethereum-optimism/optimism/op-dispute-mon/version" "github.com/ethereum-optimism/optimism/op-dispute-mon/version"
"github.com/ethereum-optimism/optimism/op-challenger/game/fault/contracts" "github.com/ethereum-optimism/optimism/op-challenger/game/fault/contracts"
...@@ -35,6 +36,7 @@ type Service struct { ...@@ -35,6 +36,7 @@ type Service struct {
cl clock.Clock cl clock.Clock
delays *resolution.DelayCalculator
extractor *extract.Extractor extractor *extract.Extractor
forecast *forecast forecast *forecast
game *extract.GameCallerCreator game *extract.GameCallerCreator
...@@ -85,6 +87,7 @@ func (s *Service) initFromConfig(ctx context.Context, cfg *config.Config) error ...@@ -85,6 +87,7 @@ func (s *Service) initFromConfig(ctx context.Context, cfg *config.Config) error
s.initOutputValidator() // Must be called before initForecast s.initOutputValidator() // Must be called before initForecast
s.initGameCallerCreator() // Must be called before initForecast s.initGameCallerCreator() // Must be called before initForecast
s.initDelayCalculator()
s.initExtractor() s.initExtractor()
s.initForecast(cfg) s.initForecast(cfg)
...@@ -106,6 +109,10 @@ func (s *Service) initGameCallerCreator() { ...@@ -106,6 +109,10 @@ func (s *Service) initGameCallerCreator() {
s.game = extract.NewGameCallerCreator(s.metrics, batching.NewMultiCaller(s.l1Client.Client(), batching.DefaultBatchSize)) s.game = extract.NewGameCallerCreator(s.metrics, batching.NewMultiCaller(s.l1Client.Client(), batching.DefaultBatchSize))
} }
func (s *Service) initDelayCalculator() {
s.delays = resolution.NewDelayCalculator(s.metrics, s.cl)
}
func (s *Service) initExtractor() { func (s *Service) initExtractor() {
s.extractor = extract.NewExtractor(s.logger, s.game.CreateContract, s.factoryContract.GetGamesAtOrAfter) s.extractor = extract.NewExtractor(s.logger, s.game.CreateContract, s.factoryContract.GetGamesAtOrAfter)
} }
...@@ -195,6 +202,7 @@ func (s *Service) initMonitor(ctx context.Context, cfg *config.Config) { ...@@ -195,6 +202,7 @@ func (s *Service) initMonitor(ctx context.Context, cfg *config.Config) {
s.cl, s.cl,
cfg.MonitorInterval, cfg.MonitorInterval,
cfg.GameWindow, cfg.GameWindow,
s.delays.RecordClaimResolutionDelayMax,
s.detector.Detect, s.detector.Detect,
s.forecast.Forecast, s.forecast.Forecast,
s.extractor.Extract, s.extractor.Extract,
......
package types package types
import ( import (
"math/big"
faultTypes "github.com/ethereum-optimism/optimism/op-challenger/game/fault/types" faultTypes "github.com/ethereum-optimism/optimism/op-challenger/game/fault/types"
"github.com/ethereum-optimism/optimism/op-challenger/game/types" "github.com/ethereum-optimism/optimism/op-challenger/game/types"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
) )
// ResolvedBondAmount is the uint128 value where a bond is considered claimed.
var ResolvedBondAmount = new(big.Int).Sub(new(big.Int).Lsh(big.NewInt(1), 128), big.NewInt(1))
type EnrichedGameData struct { type EnrichedGameData struct {
types.GameMetadata types.GameMetadata
L2BlockNumber uint64 L2BlockNumber uint64
RootClaim common.Hash RootClaim common.Hash
Status types.GameStatus Status types.GameStatus
Duration uint64
Claims []faultTypes.Claim Claims []faultTypes.Claim
} }
......
...@@ -8,6 +8,10 @@ import ( ...@@ -8,6 +8,10 @@ import (
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
func TestMaxValue(t *testing.T) {
require.Equal(t, ResolvedBondAmount.String(), "340282366920938463463374607431768211455")
}
func TestStatusBatch_Add(t *testing.T) { func TestStatusBatch_Add(t *testing.T) {
statusExpectations := []struct { statusExpectations := []struct {
status types.GameStatus status types.GameStatus
......
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