Commit 97a4ca87 authored by Adrian Sutton's avatar Adrian Sutton Committed by GitHub

op-challenger, op-dispute-mon: Fix chess clock calculations to match solidity...

op-challenger, op-dispute-mon: Fix chess clock calculations to match solidity implementation (#10509)

* op-challenger: Show resolvable time in claims list.

Don't require a sort param in list-games.

* op-challenger, op-dispute-mon: Fix chess clock calculations to match the solidity implementation.

Adds clock and L2 block challenge information to list-claims subcommand.
parent 92a78de4
......@@ -5,6 +5,7 @@ import (
"fmt"
"math/big"
"strconv"
"time"
"github.com/ethereum-optimism/optimism/op-challenger/flags"
"github.com/ethereum-optimism/optimism/op-challenger/game/fault/contracts"
......@@ -25,6 +26,12 @@ var (
Usage: "Address of the fault game contract.",
EnvVars: opservice.PrefixEnvVar(flags.EnvVarPrefix, "GAME_ADDRESS"),
}
VerboseFlag = &cli.BoolFlag{
Name: "verbose",
Aliases: []string{"v"},
Usage: "Verbose output",
EnvVars: opservice.PrefixEnvVar(flags.EnvVarPrefix, "VERBOSE"),
}
)
func ListClaims(ctx *cli.Context) error {
......@@ -52,22 +59,27 @@ func ListClaims(ctx *cli.Context) error {
if err != nil {
return err
}
return listClaims(ctx.Context, contract)
return listClaims(ctx.Context, contract, ctx.Bool(VerboseFlag.Name))
}
func listClaims(ctx context.Context, game contracts.FaultDisputeGameContract) error {
func listClaims(ctx context.Context, game contracts.FaultDisputeGameContract, verbose bool) error {
metadata, err := game.GetGameMetadata(ctx, rpcblock.Latest)
if err != nil {
return fmt.Errorf("failed to retrieve metadata: %w", err)
}
maxDepth, err := game.GetMaxGameDepth(ctx)
if err != nil {
return fmt.Errorf("failed to retrieve max depth: %w", err)
}
splitDepth, err := game.GetSplitDepth(ctx)
maxClockDuration, err := game.GetMaxClockDuration(ctx)
if err != nil {
return fmt.Errorf("failed to retrieve split depth: %w", err)
return fmt.Errorf("failed to retrieve max clock duration: %w", err)
}
status, err := game.GetStatus(ctx)
splitDepth, err := game.GetSplitDepth(ctx)
if err != nil {
return fmt.Errorf("failed to retrieve status: %w", err)
return fmt.Errorf("failed to retrieve split depth: %w", err)
}
status := metadata.Status
l2StartBlockNum, l2BlockNum, err := game.GetBlockRange(ctx)
if err != nil {
return fmt.Errorf("failed to retrieve status: %w", err)
......@@ -88,17 +100,34 @@ func listClaims(ctx context.Context, game contracts.FaultDisputeGameContract) er
}
gameState := types.NewGameState(claims, maxDepth)
lineFormat := "%3v %-7v %6v %5v %14v %-66v %-42v %-44v\n"
info := fmt.Sprintf(lineFormat, "Idx", "Move", "Parent", "Depth", "Index", "Value", "Claimant", "Resolution")
valueFormat := "%-14v"
if verbose {
valueFormat = "%-66v"
}
now := time.Now()
lineFormat := "%3v %-7v %6v %5v %14v " + valueFormat + " %-42v %-19v %10v %v\n"
info := fmt.Sprintf(lineFormat, "Idx", "Move", "Parent", "Depth", "Index", "Value", "Claimant", "Time", "Clock Used", "Resolution")
for i, claim := range claims {
pos := claim.Position
parent := strconv.Itoa(claim.ParentContractIndex)
var elapsed time.Duration // Root claim does not accumulate any time on its team's chess clock
if claim.IsRoot() {
parent = ""
} else {
parentClaim, err := gameState.GetParent(claim)
if err != nil {
return fmt.Errorf("failed to retrieve parent claim: %w", err)
}
// Get the total chess clock time accumulated by the team that posted this claim at the time of the claim.
elapsed = gameState.ChessClock(claim.Clock.Timestamp, parentClaim)
}
var countered string
if !resolved[i] {
countered = "-"
clock := gameState.ChessClock(now, claim)
resolvableAt := now.Add(maxClockDuration - clock).Format(time.DateTime)
countered = fmt.Sprintf("⏱️ %v", resolvableAt)
} else if claim.IsRoot() && metadata.L2BlockNumberChallenged {
countered = "❌ " + metadata.L2BlockNumberChallenger.Hex()
} else if claim.CounteredBy != (common.Address{}) {
countered = "❌ " + claim.CounteredBy.Hex()
} else {
......@@ -120,11 +149,20 @@ func listClaims(ctx context.Context, game contracts.FaultDisputeGameContract) er
traceIdx = relativePos.TraceIndex(bottomDepth)
}
}
value := claim.Value.TerminalString()
if verbose {
value = claim.Value.Hex()
}
timestamp := claim.Clock.Timestamp.Format(time.DateTime)
info = info + fmt.Sprintf(lineFormat,
i, move, parent, pos.Depth(), traceIdx, claim.Value.Hex(), claim.Claimant, countered)
i, move, parent, pos.Depth(), traceIdx, value, claim.Claimant, timestamp, elapsed, countered)
}
blockNumChallenger := "L2 Block: Unchallenged"
if metadata.L2BlockNumberChallenged {
blockNumChallenger = "L2 Block: ❌ " + metadata.L2BlockNumberChallenger.Hex()
}
fmt.Printf("Status: %v • L2 Blocks: %v to %v • Split Depth: %v • Max Depth: %v • Claim Count: %v\n%v\n",
status, l2StartBlockNum, l2BlockNum, splitDepth, maxDepth, len(claims), info)
fmt.Printf("Status: %v • L2 Blocks: %v to %v • Split Depth: %v • Max Depth: %v • %v • Claim Count: %v\n%v\n",
status, l2StartBlockNum, l2BlockNum, splitDepth, maxDepth, blockNumChallenger, len(claims), info)
return nil
}
......@@ -132,6 +170,7 @@ func listClaimsFlags() []cli.Flag {
cliFlags := []cli.Flag{
flags.L1EthRpcFlag,
GameAddressFlag,
VerboseFlag,
}
cliFlags = append(cliFlags, oplog.CLIFlags(flags.EnvVarPrefix)...)
return cliFlags
......
......@@ -29,6 +29,7 @@ var (
SortByFlag = &cli.StringFlag{
Name: "sort-by",
Usage: "Sort games by column. Valid options: " + openum.EnumString(ColumnTypes),
Value: "time",
EnvVars: opservice.PrefixEnvVar(flags.EnvVarPrefix, "SORT_BY"),
}
SortOrderFlag = &cli.StringFlag{
......@@ -53,7 +54,7 @@ func ListGames(ctx *cli.Context) error {
return err
}
sortBy := ctx.String(SortByFlag.Name)
if !slices.Contains(ColumnTypes, sortBy) {
if sortBy != "" && !slices.Contains(ColumnTypes, sortBy) {
return fmt.Errorf("invalid sort-by value: %v", sortBy)
}
sortOrder := ctx.String(SortOrderFlag.Name)
......
......@@ -179,7 +179,11 @@ func (a *Agent) tryResolveClaims(ctx context.Context) error {
var resolvableClaims []uint64
for _, claim := range claims {
if claim.ChessTime(a.l1Clock.Now()) <= a.maxClockDuration {
var parent types.Claim
if !claim.IsRootPosition() {
parent = claims[claim.ParentContractIndex]
}
if types.ChessClock(a.l1Clock.Now(), claim, parent) <= a.maxClockDuration {
continue
}
if a.selective {
......
......@@ -24,6 +24,8 @@ import (
"github.com/ethereum-optimism/optimism/op-service/testlog"
)
var l1Time = time.UnixMilli(100)
func TestDoNotMakeMovesWhenGameIsResolvable(t *testing.T) {
ctx := context.Background()
......@@ -150,13 +152,19 @@ func TestSkipAttemptingToResolveClaimsWhenClockNotExpired(t *testing.T) {
depth := types.Depth(4)
claimBuilder := test.NewClaimBuilder(t, depth, alphabet.NewTraceProvider(big.NewInt(0), depth))
claimLoader.claims = []types.Claim{
claimBuilder.CreateRootClaim(test.WithExpiredClock(agent.maxClockDuration)),
}
rootTime := l1Time.Add(-agent.maxClockDuration - 5*time.Minute)
gameBuilder := claimBuilder.GameBuilder(test.WithClock(rootTime, 0))
gameBuilder.Seq().
Attack(test.WithClock(rootTime.Add(5*time.Minute), 5*time.Minute)).
Defend(test.WithClock(rootTime.Add(7*time.Minute), 2*time.Minute)).
Attack(test.WithClock(rootTime.Add(11*time.Minute), 4*time.Minute))
claimLoader.claims = gameBuilder.Game.Claims()
require.NoError(t, agent.Act(context.Background()))
require.Zero(t, responder.callResolveClaimCount)
// Currently tries to resolve the first two claims because their clock's have expired, but doesn't detect that
// they have unresolvable children.
require.Equal(t, 2, responder.callResolveClaimCount)
}
func TestLoadClaimsWhenGameNotResolvable(t *testing.T) {
......@@ -186,7 +194,7 @@ func setupTestAgent(t *testing.T) (*Agent, *stubClaimLoader, *stubResponder) {
provider := alphabet.NewTraceProvider(big.NewInt(0), depth)
responder := &stubResponder{}
systemClock := clock.NewDeterministicClock(time.UnixMilli(120200))
l1Clock := clock.NewDeterministicClock(time.UnixMilli(100))
l1Clock := clock.NewDeterministicClock(l1Time)
agent := NewAgent(metrics.NoopMetrics, systemClock, l1Clock, claimLoader, depth, gameDuration, trace.NewSimpleTraceAccessor(provider), responder, logger, false, []common.Address{})
return agent, claimLoader, responder
}
......
......@@ -15,15 +15,18 @@ import (
var DefaultClaimant = common.Address{0xba, 0xdb, 0xad, 0xba, 0xdb, 0xad}
type claimCfg struct {
value common.Hash
invalidValue bool
claimant common.Address
parentIdx int
clockDuration time.Duration
value common.Hash
invalidValue bool
claimant common.Address
parentIdx int
clockTimestamp time.Time
clockDuration time.Duration
}
func newClaimCfg(opts ...ClaimOpt) *claimCfg {
cfg := &claimCfg{}
cfg := &claimCfg{
clockTimestamp: time.Unix(math.MaxInt64-1, 0),
}
for _, opt := range opts {
opt.Apply(cfg)
}
......@@ -64,9 +67,10 @@ func WithParent(claim types.Claim) ClaimOpt {
})
}
func WithExpiredClock(maxClockDuration time.Duration) ClaimOpt {
func WithClock(timestamp time.Time, duration time.Duration) ClaimOpt {
return claimOptFn(func(cfg *claimCfg) {
cfg.clockDuration = maxClockDuration
cfg.clockTimestamp = timestamp
cfg.clockDuration = duration
})
}
......@@ -134,7 +138,7 @@ func (c *ClaimBuilder) claim(pos types.Position, opts ...ClaimOpt) types.Claim {
Claimant: DefaultClaimant,
Clock: types.Clock{
Duration: cfg.clockDuration,
Timestamp: time.Unix(math.MaxInt64-1, 0),
Timestamp: cfg.clockTimestamp,
},
}
if cfg.claimant != (common.Address{}) {
......
......@@ -3,6 +3,7 @@ package types
import (
"errors"
"math/big"
"time"
)
var (
......@@ -22,6 +23,10 @@ type Game interface {
// its parent.
DefendsParent(claim Claim) bool
// ChessClock returns the amount of time elapsed on the chess clock of the potential challenger to the supplied claim.
// Specifically, this returns the chess clock of the team that *disagrees* with the supplied claim.
ChessClock(now time.Time, claim Claim) time.Duration
// IsDuplicate returns true if the provided [Claim] already exists in the game state
// referencing the same parent claim
IsDuplicate(claim Claim) bool
......@@ -100,6 +105,27 @@ func (g *gameState) DefendsParent(claim Claim) bool {
return claim.RightOf(parent.Position)
}
// ChessClock returns the amount of time elapsed on the chess clock of the potential challenger to the supplied claim.
// Specifically, this returns the chess clock of the team that *disagrees* with the supplied claim.
func (g *gameState) ChessClock(now time.Time, claim Claim) time.Duration {
parentRef := g.getParent(claim)
var parent Claim
if parentRef != nil {
parent = *parentRef
}
return ChessClock(now, claim, parent)
}
func ChessClock(now time.Time, claim Claim, parent Claim) time.Duration {
// Calculate the time elapsed since the claim was created
duration := now.Sub(claim.Clock.Timestamp)
if parent != (Claim{}) {
// Add total time elapsed from previous turns
duration = parent.Clock.Duration + duration
}
return duration
}
func (g *gameState) getParent(claim Claim) *Claim {
if claim.IsRoot() {
return nil
......
......@@ -3,6 +3,7 @@ package types
import (
"math/big"
"testing"
"time"
"github.com/ethereum/go-ethereum/common"
"github.com/stretchr/testify/require"
......@@ -217,6 +218,43 @@ func TestAncestorWithTraceIndex(t *testing.T) {
require.Equal(t, claims[3], actual)
}
func TestChessClock(t *testing.T) {
rootTime := time.UnixMilli(42978249)
defenderRootClaim, challengerFirstClaim, defenderSecondClaim, challengerSecondClaim := createTestClaims()
defenderRootClaim.Clock = Clock{Timestamp: rootTime, Duration: 0}
challengerFirstClaim.Clock = Clock{Timestamp: rootTime.Add(5 * time.Minute), Duration: 5 * time.Minute}
defenderSecondClaim.Clock = Clock{Timestamp: challengerFirstClaim.Clock.Timestamp.Add(2 * time.Minute), Duration: 2 * time.Minute}
challengerSecondClaim.Clock = Clock{Timestamp: defenderSecondClaim.Clock.Timestamp.Add(3 * time.Minute), Duration: 8 * time.Minute}
claims := []Claim{defenderRootClaim, challengerFirstClaim, defenderSecondClaim, challengerSecondClaim}
game := NewGameState(claims, 10)
// At the time the root claim is posted, both defender and challenger have no time on their chess clock
// The root claim starts the chess clock for the challenger
require.Equal(t, time.Duration(0), game.ChessClock(rootTime, game.Claims()[0]))
// As time progresses, the challenger's chess clock increases
require.Equal(t, 2*time.Minute, game.ChessClock(rootTime.Add(2*time.Minute), game.Claims()[0]))
// The challenger's first claim arrives 5 minutes after the root claim and starts the clock for the defender
// This is the defender's first turn so at the time the claim is posted, the defender's chess clock is 0
require.Equal(t, time.Duration(0), game.ChessClock(challengerFirstClaim.Clock.Timestamp, challengerFirstClaim))
// As time progresses, the defender's chess clock increases
require.Equal(t, 3*time.Minute, game.ChessClock(challengerFirstClaim.Clock.Timestamp.Add(3*time.Minute), challengerFirstClaim))
// The defender's second claim arrives 2 minutes after the challenger's first claim.
// This starts the challenger's clock again. At the time of the claim it already has 5 minutes on the clock
// from the challenger's previous turn
require.Equal(t, 5*time.Minute, game.ChessClock(defenderSecondClaim.Clock.Timestamp, defenderSecondClaim))
// As time progresses the challenger's chess clock increases
require.Equal(t, 5*time.Minute+30*time.Second, game.ChessClock(defenderSecondClaim.Clock.Timestamp.Add(30*time.Second), defenderSecondClaim))
// The challenger's second claim arrives 3 minutes after the defender's second claim.
// This starts the defender's clock again. At the time of the claim it already has 2 minutes on the clock
// from the defenders previous turn
require.Equal(t, 2*time.Minute, game.ChessClock(challengerSecondClaim.Clock.Timestamp, challengerSecondClaim))
// As time progresses, the defender's chess clock increases
require.Equal(t, 2*time.Minute+45*time.Minute, game.ChessClock(challengerSecondClaim.Clock.Timestamp.Add(45*time.Minute), challengerSecondClaim))
}
func buildGameWithClaim(claimGIndex *big.Int, parentGIndex *big.Int) *gameState {
parentClaim := Claim{
ClaimData: ClaimData{
......
......@@ -41,13 +41,13 @@ func NewPositionFromGIndex(x *big.Int) Position {
}
func (p Position) String() string {
return fmt.Sprintf("Position(depth: %v, indexAtDepth: %v)", p.depth, p.indexAtDepth)
return fmt.Sprintf("Position(depth: %v, indexAtDepth: %v)", p.depth, p.IndexAtDepth())
}
func (p Position) MoveRight() Position {
return Position{
depth: p.depth,
indexAtDepth: new(big.Int).Add(p.indexAtDepth, big.NewInt(1)),
indexAtDepth: new(big.Int).Add(p.IndexAtDepth(), big.NewInt(1)),
}
}
......@@ -59,7 +59,7 @@ func (p Position) RelativeToAncestorAtDepth(ancestor Depth) (Position, error) {
}
newPosDepth := p.depth - ancestor
nodesAtDepth := 1 << newPosDepth
newIndexAtDepth := new(big.Int).Mod(p.indexAtDepth, big.NewInt(int64(nodesAtDepth)))
newIndexAtDepth := new(big.Int).Mod(p.IndexAtDepth(), big.NewInt(int64(nodesAtDepth)))
return NewPosition(newPosDepth, newIndexAtDepth), nil
}
......@@ -75,7 +75,7 @@ func (p Position) IndexAtDepth() *big.Int {
}
func (p Position) IsRootPosition() bool {
return p.depth == 0 && common.Big0.Cmp(p.indexAtDepth) == 0
return p.depth == 0 && common.Big0.Cmp(p.IndexAtDepth()) == 0
}
func (p Position) lshIndex(amount Depth) *big.Int {
......@@ -140,7 +140,7 @@ func (p Position) Defend() Position {
}
func (p Position) Print(maxDepth Depth) {
fmt.Printf("GIN: %4b\tTrace Position is %4b\tTrace Depth is: %d\tTrace Index is: %d\n", p.ToGIndex(), p.indexAtDepth, p.depth, p.TraceIndex(maxDepth))
fmt.Printf("GIN: %4b\tTrace Position is %4b\tTrace Depth is: %d\tTrace Index is: %d\n", p.ToGIndex(), p.IndexAtDepth(), p.depth, p.TraceIndex(maxDepth))
}
func (p Position) ToGIndex() *big.Int {
......
......@@ -185,17 +185,6 @@ func (c Claim) IsRoot() bool {
return c.Position.IsRootPosition()
}
// ChessTime returns the amount of time accumulated in the chess clock.
// Does not assume the claim is countered and uses the specified time
// to calculate the time since the claim was posted.
func (c Claim) ChessTime(now time.Time) time.Duration {
timeSince := time.Duration(0)
if now.Compare(c.Clock.Timestamp) > 0 {
timeSince = now.Sub(c.Clock.Timestamp)
}
return c.Clock.Duration + timeSince
}
// Clock tracks the chess clock for a claim.
type Clock struct {
// Duration is the time elapsed on the chess clock at the last update.
......
......@@ -3,67 +3,10 @@ package types
import (
"math/big"
"testing"
"time"
"github.com/stretchr/testify/require"
)
func TestClaim_RemainingDuration(t *testing.T) {
tests := []struct {
name string
duration time.Duration
timestamp int64
now int64
expected uint64
}{
{
name: "AllZeros",
duration: 0,
timestamp: 0,
now: 0,
expected: 0,
},
{
name: "ZeroTimestamp",
duration: 5 * time.Second,
timestamp: 0,
now: 0,
expected: 5,
},
{
name: "ZeroTimestampWithNow",
duration: 5 * time.Second,
timestamp: 0,
now: 10,
expected: 15,
},
{
name: "ZeroNow",
duration: 5 * time.Second,
timestamp: 10,
now: 0,
expected: 5,
},
{
name: "ValidTimeSinze",
duration: 20 * time.Second,
timestamp: 10,
now: 15,
expected: 25,
},
}
for _, test := range tests {
test := test
t.Run(test.name, func(t *testing.T) {
claim := &Claim{
Clock: NewClock(test.duration, time.Unix(test.timestamp, 0)),
}
require.Equal(t, time.Duration(test.expected)*time.Second, claim.ChessTime(time.Unix(test.now, 0)))
})
}
}
func TestNewPreimageOracleData(t *testing.T) {
t.Run("LocalData", func(t *testing.T) {
data := NewPreimageOracleData([]byte{1, 2, 3}, []byte{4, 5, 6}, 7)
......@@ -103,6 +46,12 @@ func TestIsRootPosition(t *testing.T) {
position: NewPositionFromGIndex(big.NewInt(2)),
expected: false,
},
{
// Mostly to avoid nil dereferences in tests which may not set a real Position
name: "DefaultValue",
position: Position{},
expected: true,
},
}
for _, test := range tests {
......
......@@ -4,6 +4,7 @@ import (
"math/big"
"time"
faultTypes "github.com/ethereum-optimism/optimism/op-challenger/game/fault/types"
"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"
......@@ -100,7 +101,11 @@ func (c *ClaimMonitor) checkGameClaims(
}
maxChessTime := time.Duration(game.MaxClockDuration) * time.Second
accumulatedTime := claim.ChessTime(c.clock.Now())
var parent faultTypes.Claim
if !claim.IsRoot() {
parent = game.Claims[claim.ParentContractIndex].Claim
}
accumulatedTime := faultTypes.ChessClock(c.clock.Now(), claim.Claim, parent)
clockExpired := accumulatedTime >= maxChessTime
if claim.Resolved {
......
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