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
return
}
// GetGameMetadata returns the game's L2 block number, root claim, and status.
func (c *FaultDisputeGameContract) GetGameMetadata(ctx context.Context) (uint64, common.Hash, gameTypes.GameStatus, error) {
// 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, uint64, error) {
results, err := c.multiCaller.Call(ctx, batching.BlockLatest,
c.contract.Call(methodL2BlockNumber),
c.contract.Call(methodRootClaim),
c.contract.Call(methodStatus))
c.contract.Call(methodStatus),
c.contract.Call(methodGameDuration))
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 {
return 0, common.Hash{}, 0, fmt.Errorf("expected 3 results but got %v", len(results))
if len(results) != 4 {
return 0, common.Hash{}, 0, 0, fmt.Errorf("expected 3 results but got %v", len(results))
}
l2BlockNumber := results[0].GetBigInt(0).Uint64()
rootClaim := results[1].GetHash(0)
duration := results[3].GetUint64(0)
status, err := gameTypes.GameStatusFromUint8(results[2].GetUint8(0))
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) {
......
......@@ -319,16 +319,19 @@ func TestGetSplitDepth(t *testing.T) {
func TestGetGameMetadata(t *testing.T) {
stubRpc, contract := setupFaultDisputeGameTest(t)
expectedL2BlockNumber := uint64(123)
expectedGameDuration := uint64(456)
expectedRootClaim := common.Hash{0x01, 0x02}
expectedStatus := types.GameStatusChallengerWon
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, 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.Equal(t, expectedL2BlockNumber, l2BlockNumber)
require.Equal(t, expectedRootClaim, rootClaim)
require.Equal(t, expectedStatus, status)
require.Equal(t, expectedGameDuration, duration)
}
func TestGetGenesisOutputRoot(t *testing.T) {
......
......@@ -36,6 +36,8 @@ type Metricer interface {
RecordInfo(version string)
RecordUp()
RecordClaimResolutionDelayMax(delay float64)
RecordGamesStatus(inProgress, defenderWon, challengerWon int)
RecordGameAgreement(status GameAgreementStatus, count int)
......@@ -55,6 +57,8 @@ type Metrics struct {
info prometheus.GaugeVec
up prometheus.Gauge
claimResolutionDelayMax prometheus.Gauge
trackedGames prometheus.GaugeVec
gamesAgreement prometheus.GaugeVec
}
......@@ -88,6 +92,11 @@ func NewMetrics() *Metrics {
Name: "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{
Namespace: Namespace,
Name: "tracked_games",
......@@ -132,6 +141,10 @@ func (m *Metrics) RecordUp() {
m.up.Set(1)
}
func (m *Metrics) RecordClaimResolutionDelayMax(delay float64) {
m.claimResolutionDelayMax.Set(delay)
}
func (m *Metrics) Document() []opmetrics.DocumentedMetric {
return m.factory.Document()
}
......
......@@ -10,5 +10,7 @@ func (*NoopMetricsImpl) RecordUp() {}
func (*NoopMetricsImpl) CacheAdd(_ string, _ int, _ bool) {}
func (*NoopMetricsImpl) CacheGet(_ string, _ bool) {}
func (*NoopMetricsImpl) RecordClaimResolutionDelayMax(delay float64) {}
func (*NoopMetricsImpl) RecordGamesStatus(inProgress, defenderWon, challengerWon int) {}
func (*NoopMetricsImpl) RecordGameAgreement(status GameAgreementStatus, count int) {}
......@@ -6,13 +6,12 @@ import (
"math/big"
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/go-ethereum/common"
"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 {
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
unpaidBonds := big.NewInt(0)
recipients := make(map[common.Address]bool)
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)
}
recipients[claim.Claimant] = true
......
......@@ -6,20 +6,17 @@ import (
"testing"
"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/go-ethereum/common"
"github.com/stretchr/testify/require"
)
func TestMaxValue(t *testing.T) {
require.Equal(t, noBond.String(), "340282366920938463463374607431768211455")
}
func TestCalculateRequiredCollateral(t *testing.T) {
claims := []types.Claim{
{
ClaimData: types.ClaimData{
Bond: noBond,
Bond: monTypes.ResolvedBondAmount,
},
Claimant: common.Address{0x01},
CounteredBy: common.Address{0x02},
......
......@@ -16,8 +16,8 @@ import (
const metricsLabel = "game_caller_creator"
type GameCaller interface {
GetGameMetadata(context.Context) (uint64, common.Hash, types.GameStatus, error)
GetAllClaims(ctx context.Context) ([]faultTypes.Claim, error)
GetGameMetadata(context.Context) (uint64, common.Hash, types.GameStatus, uint64, error)
GetAllClaims(context.Context) ([]faultTypes.Claim, error)
}
type GameCallerCreator struct {
......
......@@ -44,7 +44,7 @@ func (e *Extractor) enrichGames(ctx context.Context, games []gameTypes.GameMetad
e.logger.Error("failed to create game caller", "err", err)
continue
}
l2BlockNum, rootClaim, status, err := caller.GetGameMetadata(ctx)
l2BlockNum, rootClaim, status, duration, err := caller.GetGameMetadata(ctx)
if err != nil {
e.logger.Error("failed to fetch game metadata", "err", err)
continue
......@@ -59,6 +59,7 @@ func (e *Extractor) enrichGames(ctx context.Context, games []gameTypes.GameMetad
L2BlockNumber: l2BlockNum,
RootClaim: rootClaim,
Status: status,
Duration: duration,
Claims: claims,
})
}
......
......@@ -37,7 +37,7 @@ func TestExtractor_Extract(t *testing.T) {
require.Equal(t, 1, creator.calls)
require.Equal(t, 0, creator.caller.metadataCalls)
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) {
......@@ -51,7 +51,7 @@ func TestExtractor_Extract(t *testing.T) {
require.Equal(t, 1, creator.calls)
require.Equal(t, 1, creator.caller.metadataCalls)
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) {
......@@ -65,7 +65,7 @@ func TestExtractor_Extract(t *testing.T) {
require.Equal(t, 1, creator.calls)
require.Equal(t, 1, creator.caller.metadataCalls)
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) {
......@@ -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)
createMessageFilter := testlog.NewMessageFilter("failed to create game caller")
l := logs.FindLogs(errorLevelFilter, createMessageFilter)
......@@ -92,6 +92,9 @@ func verifyLogs(t *testing.T, logs *testlog.CapturingHandler, createErr int, met
claimsMessageFilter := testlog.NewMessageFilter("failed to fetch game claims")
l = logs.FindLogs(errorLevelFilter, claimsMessageFilter)
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) {
......@@ -146,12 +149,12 @@ type mockGameCaller struct {
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++
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) {
......
......@@ -7,6 +7,7 @@ import (
"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/mon/resolution"
"github.com/ethereum-optimism/optimism/op-dispute-mon/mon/transform"
monTypes "github.com/ethereum-optimism/optimism/op-dispute-mon/mon/types"
......@@ -21,6 +22,7 @@ var (
)
type ForecastMetrics interface {
RecordClaimResolutionDelayMax(delay float64)
RecordGameAgreement(status metrics.GameAgreementStatus, count int)
}
......@@ -65,7 +67,7 @@ func (f *forecast) forecastGame(ctx context.Context, game *monTypes.EnrichedGame
tree := transform.CreateBidirectionalTree(game.Claims)
// Compute the resolution status of the game.
status := Resolve(tree)
status := resolution.Resolve(tree)
// Check the root agreement.
agreement, expected, err := f.validator.CheckRootAgreement(ctx, game.L2BlockNumber, game.RootClaim)
......
......@@ -4,6 +4,8 @@ import (
"context"
"errors"
"fmt"
"math"
"math/big"
"testing"
faultTypes "github.com/ethereum-optimism/optimism/op-challenger/game/fault/types"
......@@ -267,6 +269,7 @@ type mockForecastMetrics struct {
disagreeDefenderAhead int
agreeChallengerAhead int
disagreeChallengerAhead int
claimResolutionDelayMax float64
}
func (m *mockForecastMetrics) RecordGameAgreement(status metrics.GameAgreementStatus, count int) {
......@@ -281,3 +284,38 @@ func (m *mockForecastMetrics) RecordGameAgreement(status metrics.GameAgreementSt
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)
type BlockHashFetcher func(ctx context.Context, number *big.Int) (common.Hash, error)
type BlockNumberFetcher func(ctx context.Context) (uint64, error)
type Extract func(ctx context.Context, blockHash common.Hash, minTimestamp uint64) ([]*types.EnrichedGameData, error)
type RecordClaimResolutionDelayMax func([]*types.EnrichedGameData)
type gameMonitor struct {
logger log.Logger
......@@ -30,6 +31,7 @@ type gameMonitor struct {
gameWindow time.Duration
monitorInterval time.Duration
delays RecordClaimResolutionDelayMax
detect Detect
forecast Forecast
extract Extract
......@@ -43,6 +45,7 @@ func newGameMonitor(
cl clock.Clock,
monitorInterval time.Duration,
gameWindow time.Duration,
delays RecordClaimResolutionDelayMax,
detect Detect,
forecast Forecast,
extract Extract,
......@@ -56,6 +59,7 @@ func newGameMonitor(
done: make(chan struct{}),
monitorInterval: monitorInterval,
gameWindow: gameWindow,
delays: delays,
detect: detect,
forecast: forecast,
extract: extract,
......@@ -90,6 +94,7 @@ func (m *gameMonitor) monitorGames() error {
if err != nil {
return fmt.Errorf("failed to load games: %w", err)
}
m.delays(enrichedGames)
m.detect(m.ctx, enrichedGames)
m.forecast(m.ctx, enrichedGames)
return nil
......
......@@ -24,20 +24,20 @@ func TestMonitor_MinGameTimestamp(t *testing.T) {
t.Parallel()
t.Run("ZeroGameWindow", func(t *testing.T) {
monitor, _, _, _ := setupMonitorTest(t)
monitor, _, _, _, _ := setupMonitorTest(t)
monitor.gameWindow = time.Duration(0)
require.Equal(t, monitor.minGameTimestamp(), uint64(0))
})
t.Run("ZeroClock", func(t *testing.T) {
monitor, _, _, _ := setupMonitorTest(t)
monitor, _, _, _, _ := setupMonitorTest(t)
monitor.gameWindow = time.Minute
monitor.clock = clock.NewDeterministicClock(time.Unix(0, 0))
require.Equal(t, uint64(0), monitor.minGameTimestamp())
})
t.Run("ValidArithmetic", func(t *testing.T) {
monitor, _, _, _ := setupMonitorTest(t)
monitor, _, _, _, _ := setupMonitorTest(t)
monitor.gameWindow = time.Minute
frozen := time.Unix(int64(time.Hour.Seconds()), 0)
monitor.clock = clock.NewDeterministicClock(frozen)
......@@ -50,7 +50,7 @@ func TestMonitor_MonitorGames(t *testing.T) {
t.Parallel()
t.Run("FailedFetchBlocknumber", func(t *testing.T) {
monitor, _, _, _ := setupMonitorTest(t)
monitor, _, _, _, _ := setupMonitorTest(t)
boom := errors.New("boom")
monitor.fetchBlockNumber = func(ctx context.Context) (uint64, error) {
return 0, boom
......@@ -60,7 +60,7 @@ func TestMonitor_MonitorGames(t *testing.T) {
})
t.Run("FailedFetchBlockHash", func(t *testing.T) {
monitor, _, _, _ := setupMonitorTest(t)
monitor, _, _, _, _ := setupMonitorTest(t)
boom := errors.New("boom")
monitor.fetchBlockHash = func(ctx context.Context, number *big.Int) (common.Hash, error) {
return common.Hash{}, boom
......@@ -70,19 +70,23 @@ func TestMonitor_MonitorGames(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{}
err := monitor.monitorGames()
require.NoError(t, err)
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) {
monitor, factory, detector, _ := setupMonitorTest(t)
monitor, factory, detector, forecast, delays := setupMonitorTest(t)
factory.games = []*monTypes.EnrichedGameData{{}, {}, {}}
err := monitor.monitorGames()
require.NoError(t, err)
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) {
t.Run("MonitorsGames", func(t *testing.T) {
addr1 := common.Address{0xaa}
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.maxSuccess = len(factory.games) // Only allow two successful fetches
......@@ -103,7 +107,7 @@ func TestMonitor_StartMonitoring(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")
monitor.StartMonitoring()
......@@ -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)
fetchBlockNum := func(ctx context.Context) (uint64, error) {
return 1, nil
......@@ -139,19 +143,29 @@ func setupMonitorTest(t *testing.T) (*gameMonitor, *mockExtractor, *mockDetector
extractor := &mockExtractor{}
detect := &mockDetector{}
forecast := &mockForecast{}
delays := &mockDelayCalculator{}
monitor := newGameMonitor(
context.Background(),
logger,
cl,
monitorInterval,
time.Duration(10*time.Second),
delays.RecordClaimResolutionDelayMax,
detect.Detect,
forecast.Forecast,
extractor.Extract,
fetchBlockNum,
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 {
......
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 (
"github.com/ethereum/go-ethereum/common"
......
......@@ -14,6 +14,7 @@ import (
"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/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-challenger/game/fault/contracts"
......@@ -35,6 +36,7 @@ type Service struct {
cl clock.Clock
delays *resolution.DelayCalculator
extractor *extract.Extractor
forecast *forecast
game *extract.GameCallerCreator
......@@ -85,6 +87,7 @@ func (s *Service) initFromConfig(ctx context.Context, cfg *config.Config) error
s.initOutputValidator() // Must be called before initForecast
s.initGameCallerCreator() // Must be called before initForecast
s.initDelayCalculator()
s.initExtractor()
s.initForecast(cfg)
......@@ -106,6 +109,10 @@ func (s *Service) initGameCallerCreator() {
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() {
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) {
s.cl,
cfg.MonitorInterval,
cfg.GameWindow,
s.delays.RecordClaimResolutionDelayMax,
s.detector.Detect,
s.forecast.Forecast,
s.extractor.Extract,
......
package types
import (
"math/big"
faultTypes "github.com/ethereum-optimism/optimism/op-challenger/game/fault/types"
"github.com/ethereum-optimism/optimism/op-challenger/game/types"
"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 {
types.GameMetadata
L2BlockNumber uint64
RootClaim common.Hash
Status types.GameStatus
Duration uint64
Claims []faultTypes.Claim
}
......
......@@ -8,6 +8,10 @@ import (
"github.com/stretchr/testify/require"
)
func TestMaxValue(t *testing.T) {
require.Equal(t, ResolvedBondAmount.String(), "340282366920938463463374607431768211455")
}
func TestStatusBatch_Add(t *testing.T) {
statusExpectations := []struct {
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