Commit 39252fc2 authored by Adrian Sutton's avatar Adrian Sutton Committed by GitHub

op-challenger: Skip attempting to resolve claims when the chess clock hasn't expired (#9946)

* op-challenger: Skip attempting to resolve claims when the chess clock hasn't expired.

* Use <= when comparing chess clock
Co-authored-by: default avatarInphi <mlaw2501@gmail.com>

---------
Co-authored-by: default avatarInphi <mlaw2501@gmail.com>
parent 16bf8454
......@@ -6,6 +6,7 @@ import (
"fmt"
"slices"
"sync"
"time"
"github.com/ethereum-optimism/optimism/op-challenger/game/fault/solver"
"github.com/ethereum-optimism/optimism/op-challenger/game/fault/types"
......@@ -32,22 +33,26 @@ type ClaimLoader interface {
}
type Agent struct {
metrics metrics.Metricer
cl clock.Clock
solver *solver.GameSolver
loader ClaimLoader
responder Responder
selective bool
claimants []common.Address
maxDepth types.Depth
log log.Logger
metrics metrics.Metricer
systemClock clock.Clock
l1Clock types.ClockReader
solver *solver.GameSolver
loader ClaimLoader
responder Responder
selective bool
claimants []common.Address
maxDepth types.Depth
gameDuration time.Duration
log log.Logger
}
func NewAgent(
m metrics.Metricer,
cl clock.Clock,
systemClock clock.Clock,
l1Clock types.ClockReader,
loader ClaimLoader,
maxDepth types.Depth,
gameDuration time.Duration,
trace types.TraceAccessor,
responder Responder,
log log.Logger,
......@@ -55,15 +60,17 @@ func NewAgent(
claimants []common.Address,
) *Agent {
return &Agent{
metrics: m,
cl: cl,
solver: solver.NewGameSolver(maxDepth, trace),
loader: loader,
responder: responder,
selective: selective,
claimants: claimants,
maxDepth: maxDepth,
log: log,
metrics: m,
systemClock: systemClock,
l1Clock: l1Clock,
solver: solver.NewGameSolver(maxDepth, trace),
loader: loader,
responder: responder,
selective: selective,
claimants: claimants,
maxDepth: maxDepth,
gameDuration: gameDuration,
log: log,
}
}
......@@ -73,9 +80,9 @@ func (a *Agent) Act(ctx context.Context) error {
return nil
}
start := a.cl.Now()
start := a.systemClock.Now()
defer func() {
a.metrics.RecordGameActTime(a.cl.Since(start).Seconds())
a.metrics.RecordGameActTime(a.systemClock.Since(start).Seconds())
}()
game, err := a.newGameFromContracts(ctx)
if err != nil {
......@@ -156,9 +163,13 @@ func (a *Agent) tryResolveClaims(ctx context.Context) error {
if len(claims) == 0 {
return errNoResolvableClaims
}
maxChessTime := a.gameDuration / 2
var resolvableClaims []uint64
for _, claim := range claims {
if claim.ChessTime(a.l1Clock.Now()) <= maxChessTime {
continue
}
if a.selective {
a.log.Trace("Selective claim resolution, checking if claim is incentivized", "claimIdx", claim.ContractIndex)
isUncounteredClaim := slices.Contains(a.claimants, claim.Claimant) && claim.CounteredBy == common.Address{}
......@@ -186,9 +197,9 @@ func (a *Agent) tryResolveClaims(ctx context.Context) error {
}
func (a *Agent) resolveClaims(ctx context.Context) error {
start := a.cl.Now()
start := a.systemClock.Now()
defer func() {
a.metrics.RecordClaimResolutionTime(a.cl.Since(start).Seconds())
a.metrics.RecordClaimResolutionTime(a.systemClock.Since(start).Seconds())
}()
for {
err := a.tryResolveClaims(ctx)
......
......@@ -131,6 +131,22 @@ func TestAgent_SelectiveClaimResolution(t *testing.T) {
}
}
func TestSkipAttemptingToResolveClaimsWhenClockNotExpired(t *testing.T) {
agent, claimLoader, responder := setupTestAgent(t)
responder.callResolveErr = errors.New("game is not resolvable")
responder.callResolveClaimErr = errors.New("claim is not resolvable")
depth := types.Depth(4)
claimBuilder := test.NewClaimBuilder(t, depth, alphabet.NewTraceProvider(big.NewInt(0), depth))
claimLoader.claims = []types.Claim{
claimBuilder.CreateRootClaim(test.WithExpiredClock(agent.gameDuration)),
}
require.NoError(t, agent.Act(context.Background()))
require.Zero(t, responder.callResolveClaimCount)
}
func TestLoadClaimsWhenGameNotResolvable(t *testing.T) {
// Checks that if the game isn't resolvable, that the agent continues on to start checking claims
agent, claimLoader, responder := setupTestAgent(t)
......@@ -154,10 +170,12 @@ func setupTestAgent(t *testing.T) (*Agent, *stubClaimLoader, *stubResponder) {
logger := testlog.Logger(t, log.LevelInfo)
claimLoader := &stubClaimLoader{}
depth := types.Depth(4)
gameDuration := 6 * time.Minute
provider := alphabet.NewTraceProvider(big.NewInt(0), depth)
responder := &stubResponder{}
cl := clock.NewDeterministicClock(time.UnixMilli(0))
agent := NewAgent(metrics.NoopMetrics, cl, claimLoader, depth, trace.NewSimpleTraceAccessor(provider), responder, logger, false, []common.Address{})
systemClock := clock.NewDeterministicClock(time.UnixMilli(120200))
l1Clock := clock.NewDeterministicClock(time.UnixMilli(100))
agent := NewAgent(metrics.NoopMetrics, systemClock, l1Clock, claimLoader, depth, gameDuration, trace.NewSimpleTraceAccessor(provider), responder, logger, false, []common.Address{})
return agent, claimLoader, responder
}
......
......@@ -6,6 +6,7 @@ import (
"fmt"
"math"
"math/big"
"time"
"github.com/ethereum-optimism/optimism/op-bindings/bindings"
"github.com/ethereum-optimism/optimism/op-challenger/game/fault/contracts/metrics"
......@@ -239,13 +240,13 @@ func (f *FaultDisputeGameContract) GetOracle(ctx context.Context) (*PreimageOrac
return vm.Oracle(ctx)
}
func (f *FaultDisputeGameContract) GetGameDuration(ctx context.Context) (uint64, error) {
func (f *FaultDisputeGameContract) GetGameDuration(ctx context.Context) (time.Duration, error) {
defer f.metrics.StartContractRequest("GetGameDuration")()
result, err := f.multiCaller.SingleCall(ctx, rpcblock.Latest, f.contract.Call(methodGameDuration))
if err != nil {
return 0, fmt.Errorf("failed to fetch game duration: %w", err)
}
return result.GetUint64(0), nil
return time.Duration(result.GetUint64(0)) * time.Second, nil
}
func (f *FaultDisputeGameContract) GetMaxGameDepth(ctx context.Context) (types.Depth, error) {
......@@ -381,18 +382,18 @@ func (f *FaultDisputeGameContract) resolveCall() *batching.ContractCall {
}
// decodeClock decodes a uint128 into a Clock duration and timestamp.
func decodeClock(clock *big.Int) *types.Clock {
func decodeClock(clock *big.Int) types.Clock {
maxUint64 := new(big.Int).Add(new(big.Int).SetUint64(math.MaxUint64), big.NewInt(1))
remainder := new(big.Int)
quotient, _ := new(big.Int).QuoRem(clock, maxUint64, remainder)
return types.NewClock(quotient.Uint64(), remainder.Uint64())
return types.NewClock(time.Duration(quotient.Int64())*time.Second, time.Unix(remainder.Int64(), 0))
}
// packClock packs the Clock duration and timestamp into a uint128.
func packClock(c *types.Clock) *big.Int {
duration := new(big.Int).SetUint64(c.Duration)
func packClock(c types.Clock) *big.Int {
duration := big.NewInt(int64(c.Duration.Seconds()))
encoded := new(big.Int).Lsh(duration, 64)
return new(big.Int).Or(encoded, new(big.Int).SetUint64(c.Timestamp))
return new(big.Int).Or(encoded, big.NewInt(c.Timestamp.Unix()))
}
func (f *FaultDisputeGameContract) decodeClaim(result *batching.CallResult, contractIndex int) types.Claim {
......
......@@ -6,6 +6,7 @@ import (
"math"
"math/big"
"testing"
"time"
"github.com/ethereum-optimism/optimism/op-bindings/bindings"
contractMetrics "github.com/ethereum-optimism/optimism/op-challenger/game/fault/contracts/metrics"
......@@ -46,6 +47,7 @@ func TestSimpleGetters(t *testing.T) {
methodAlias: "gameDuration",
method: methodGameDuration,
result: uint64(5566),
expected: 5566 * time.Second,
call: func(game *FaultDisputeGameContract) (any, error) {
return game.GetGameDuration(context.Background())
},
......@@ -114,8 +116,8 @@ func TestClock_EncodingDecoding(t *testing.T) {
by := common.Hex2Bytes("00000000000000050000000000000002")
encoded := new(big.Int).SetBytes(by)
clock := decodeClock(encoded)
require.Equal(t, uint64(5), clock.Duration)
require.Equal(t, uint64(2), clock.Timestamp)
require.Equal(t, 5*time.Second, clock.Duration)
require.Equal(t, time.Unix(2, 0), clock.Timestamp)
require.Equal(t, encoded, packClock(clock))
})
......@@ -123,8 +125,8 @@ func TestClock_EncodingDecoding(t *testing.T) {
by := common.Hex2Bytes("00000000000000000000000000000002")
encoded := new(big.Int).SetBytes(by)
clock := decodeClock(encoded)
require.Equal(t, uint64(0), clock.Duration)
require.Equal(t, uint64(2), clock.Timestamp)
require.Equal(t, 0*time.Second, clock.Duration)
require.Equal(t, time.Unix(2, 0), clock.Timestamp)
require.Equal(t, encoded, packClock(clock))
})
......@@ -132,8 +134,8 @@ func TestClock_EncodingDecoding(t *testing.T) {
by := common.Hex2Bytes("00000000000000050000000000000000")
encoded := new(big.Int).SetBytes(by)
clock := decodeClock(encoded)
require.Equal(t, uint64(5), clock.Duration)
require.Equal(t, uint64(0), clock.Timestamp)
require.Equal(t, 5*time.Second, clock.Duration)
require.Equal(t, time.Unix(0, 0), clock.Timestamp)
require.Equal(t, encoded, packClock(clock))
})
......@@ -141,8 +143,8 @@ func TestClock_EncodingDecoding(t *testing.T) {
by := common.Hex2Bytes("00000000000000000000000000000000")
encoded := new(big.Int).SetBytes(by)
clock := decodeClock(encoded)
require.Equal(t, uint64(0), clock.Duration)
require.Equal(t, uint64(0), clock.Timestamp)
require.Equal(t, 0*time.Second, clock.Duration)
require.Equal(t, time.Unix(0, 0), clock.Timestamp)
require.Equal(t, encoded.Uint64(), packClock(clock).Uint64())
})
}
......
......@@ -4,6 +4,7 @@ import (
"context"
"errors"
"fmt"
"time"
"github.com/ethereum-optimism/optimism/op-challenger/game/fault/claims"
"github.com/ethereum-optimism/optimism/op-challenger/game/fault/contracts"
......@@ -58,6 +59,7 @@ type GameContract interface {
ClaimLoader
GetStatus(ctx context.Context) (gameTypes.GameStatus, error)
GetMaxGameDepth(ctx context.Context) (types.Depth, error)
GetGameDuration(ctx context.Context) (time.Duration, error)
GetOracle(ctx context.Context) (*contracts.PreimageOracleContract, error)
GetL1Head(ctx context.Context) (common.Hash, error)
}
......@@ -102,6 +104,11 @@ func NewGamePlayer(
}, nil
}
gameDuration, err := loader.GetGameDuration(ctx)
if err != nil {
return nil, fmt.Errorf("failed to fetch the game duration: %w", err)
}
gameDepth, err := loader.GetMaxGameDepth(ctx)
if err != nil {
return nil, fmt.Errorf("failed to fetch the game depth: %w", err)
......@@ -139,7 +146,7 @@ func NewGamePlayer(
return nil, fmt.Errorf("failed to create the responder: %w", err)
}
agent := NewAgent(m, systemClock, loader, gameDepth, accessor, responder, logger, selective, claimants)
agent := NewAgent(m, systemClock, l1Clock, loader, gameDepth, gameDuration, accessor, responder, logger, selective, claimants)
return &GamePlayer{
act: agent.Act,
loader: loader,
......
......@@ -320,7 +320,6 @@ func applyActions(game types.Game, claimant common.Address, actions []types.Acti
Position: newPosition,
},
Claimant: claimant,
Clock: nil,
ContractIndex: len(claims),
ParentContractIndex: action.ParentIdx,
}
......
......@@ -2,8 +2,10 @@ package test
import (
"context"
"math"
"math/big"
"testing"
"time"
"github.com/ethereum-optimism/optimism/op-challenger/game/fault/types"
"github.com/ethereum/go-ethereum/common"
......@@ -13,10 +15,11 @@ import (
var DefaultClaimant = common.Address{0xba, 0xdb, 0xad, 0xba, 0xdb, 0xad}
type claimCfg struct {
value common.Hash
invalidValue bool
claimant common.Address
parentIdx int
value common.Hash
invalidValue bool
claimant common.Address
parentIdx int
clockDuration time.Duration
}
func newClaimCfg(opts ...ClaimOpt) *claimCfg {
......@@ -60,6 +63,11 @@ func WithParent(claim types.Claim) ClaimOpt {
cfg.parentIdx = claim.ContractIndex
})
}
func WithExpiredClock(gameDuration time.Duration) ClaimOpt {
return claimOptFn(func(cfg *claimCfg) {
cfg.clockDuration = gameDuration / 2
})
}
// ClaimBuilder is a test utility to enable creating claims in a wide range of situations
type ClaimBuilder struct {
......@@ -123,6 +131,10 @@ func (c *ClaimBuilder) claim(pos types.Position, opts ...ClaimOpt) types.Claim {
Position: pos,
},
Claimant: DefaultClaimant,
Clock: types.Clock{
Duration: cfg.clockDuration,
Timestamp: time.Unix(math.MaxInt64-1, 0),
},
}
if cfg.claimant != (common.Address{}) {
claim.Claimant = cfg.claimant
......
......@@ -153,7 +153,7 @@ type Claim struct {
// to be changed/removed to avoid invalid/stale contract state.
CounteredBy common.Address
Claimant common.Address
Clock *Clock
Clock Clock
// Location of the claim & it's parent inside the contract. Does not exist
// for claims that have not made it to the contract.
ContractIndex int
......@@ -169,37 +169,33 @@ func (c Claim) ID() ClaimID {
}
// IsRoot returns true if this claim is the root claim.
func (c *Claim) IsRoot() bool {
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 := int64(0)
if now.Unix() > int64(c.Clock.Timestamp) {
timeSince = now.Unix() - int64(c.Clock.Timestamp)
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 time.Duration(c.Clock.Duration) + time.Duration(timeSince)
return c.Clock.Duration + timeSince
}
// Clock is a packed uint128 with the upper 64 bits being the
// duration and the lower 64 bits being the timestamp.
// ┌────────────┬────────────────┐
// │ Bits │ Value │
// ├────────────┼────────────────┤
// │ [0, 64) │ Duration │
// │ [64, 128) │ Timestamp │
// └────────────┴────────────────┘
// Clock tracks the chess clock for a claim.
type Clock struct {
Duration uint64
Timestamp uint64
// Duration is the time elapsed on the chess clock at the last update.
Duration time.Duration
// Timestamp is the time that the clock was last updated.
Timestamp time.Time
}
// NewClock creates a new Clock instance.
func NewClock(duration uint64, timestamp uint64) *Clock {
return &Clock{
func NewClock(duration time.Duration, timestamp time.Time) Clock {
return Clock{
Duration: duration,
Timestamp: timestamp,
}
......
......@@ -11,8 +11,8 @@ import (
func TestClaim_RemainingDuration(t *testing.T) {
tests := []struct {
name string
duration uint64
timestamp uint64
duration time.Duration
timestamp int64
now int64
expected uint64
}{
......@@ -25,28 +25,28 @@ func TestClaim_RemainingDuration(t *testing.T) {
},
{
name: "ZeroTimestamp",
duration: 5,
duration: 5 * time.Second,
timestamp: 0,
now: 0,
expected: 5,
},
{
name: "ZeroTimestampWithNow",
duration: 5,
duration: 5 * time.Second,
timestamp: 0,
now: 10,
expected: 15,
},
{
name: "ZeroNow",
duration: 5,
duration: 5 * time.Second,
timestamp: 10,
now: 0,
expected: 5,
},
{
name: "ValidTimeSinze",
duration: 20,
duration: 20 * time.Second,
timestamp: 10,
now: 15,
expected: 25,
......@@ -57,9 +57,9 @@ func TestClaim_RemainingDuration(t *testing.T) {
test := test
t.Run(test.name, func(t *testing.T) {
claim := &Claim{
Clock: NewClock(test.duration, test.timestamp),
Clock: NewClock(test.duration, time.Unix(test.timestamp, 0)),
}
require.Equal(t, time.Duration(test.expected), claim.ChessTime(time.Unix(test.now, 0)))
require.Equal(t, time.Duration(test.expected)*time.Second, claim.ChessTime(time.Unix(test.now, 0)))
})
}
}
......
package resolution
import (
"time"
"github.com/ethereum-optimism/optimism/op-dispute-mon/mon/types"
"github.com/ethereum-optimism/optimism/op-service/clock"
)
......@@ -41,10 +43,10 @@ func (d *DelayCalculator) getOverflowTime(maxGameDuration uint64, claim *types.E
if claim.Resolved {
return 0
}
maxChessTime := maxGameDuration / 2
accumulatedTime := uint64(claim.ChessTime(d.clock.Now()))
maxChessTime := time.Duration(maxGameDuration/2) * time.Second
accumulatedTime := claim.ChessTime(d.clock.Now())
if accumulatedTime < maxChessTime {
return 0
}
return accumulatedTime - maxChessTime
return uint64((accumulatedTime - maxChessTime).Seconds())
}
......@@ -29,8 +29,8 @@ func TestDelayCalculator_getOverflowTime(t *testing.T) {
t.Run("RemainingTime", func(t *testing.T) {
d, metrics, cl := setupDelayCalculatorTest(t)
duration := uint64(3 * 60)
timestamp := uint64(cl.Now().Add(-time.Minute).Unix())
duration := 3 * time.Minute
timestamp := cl.Now().Add(-time.Minute)
claim := &monTypes.EnrichedClaim{
Claim: types.Claim{
ClaimData: types.ClaimData{
......@@ -46,8 +46,8 @@ func TestDelayCalculator_getOverflowTime(t *testing.T) {
t.Run("OverflowTime", func(t *testing.T) {
d, metrics, cl := setupDelayCalculatorTest(t)
duration := maxGameDuration / 2
timestamp := uint64(cl.Now().Add(4 * -time.Minute).Unix())
duration := time.Duration(maxGameDuration/2) * time.Second
timestamp := cl.Now().Add(4 * -time.Minute)
claim := &monTypes.EnrichedClaim{
Claim: types.Claim{
ClaimData: types.ClaimData{
......@@ -136,10 +136,10 @@ func createGameWithClaimsList() []*monTypes.EnrichedGameData {
}
func createClaimList() []monTypes.EnrichedClaim {
newClock := func(multiplier int) *types.Clock {
newClock := func(multiplier int) types.Clock {
duration := maxGameDuration / 2
timestamp := uint64(frozen.Add(-time.Minute * time.Duration(multiplier)).Unix())
return types.NewClock(duration, timestamp)
timestamp := frozen.Add(-time.Minute * time.Duration(multiplier))
return types.NewClock(time.Duration(duration)*time.Second, timestamp)
}
return []monTypes.EnrichedClaim{
{
......
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