Commit 7fefff6e authored by Inphi's avatar Inphi Committed by GitHub

op-challenger: Updated for the revised resolution (#7155)

* op-challenger: Update to the reworked fdg resolution

* uint64 resolve claim index

* Update op-e2e/e2eutils/disputegame/game_helper.go
Co-authored-by: default avatarrefcell.eth <abigger87@gmail.com>

* Update op-e2e/e2eutils/disputegame/game_helper.go
Co-authored-by: default avatarrefcell.eth <abigger87@gmail.com>

* Retry claims resolution until done

* short-circuit tryResolveClaims for empty games

* bad switch case ordering

---------
Co-authored-by: default avatarrefcell.eth <abigger87@gmail.com>
parent 802b3a2f
...@@ -4,6 +4,7 @@ import ( ...@@ -4,6 +4,7 @@ import (
"context" "context"
"errors" "errors"
"fmt" "fmt"
"sync"
"github.com/ethereum-optimism/optimism/op-challenger/game/fault/solver" "github.com/ethereum-optimism/optimism/op-challenger/game/fault/solver"
"github.com/ethereum-optimism/optimism/op-challenger/game/fault/types" "github.com/ethereum-optimism/optimism/op-challenger/game/fault/types"
...@@ -18,6 +19,8 @@ import ( ...@@ -18,6 +19,8 @@ import (
type Responder interface { type Responder interface {
CallResolve(ctx context.Context) (gameTypes.GameStatus, error) CallResolve(ctx context.Context) (gameTypes.GameStatus, error)
Resolve(ctx context.Context) error Resolve(ctx context.Context) error
CallResolveClaim(ctx context.Context, claimIdx uint64) error
ResolveClaim(ctx context.Context, claimIdx uint64) error
PerformAction(ctx context.Context, action types.Action) error PerformAction(ctx context.Context, action types.Action) error
} }
...@@ -112,6 +115,10 @@ func (a *Agent) shouldResolve(status gameTypes.GameStatus) bool { ...@@ -112,6 +115,10 @@ func (a *Agent) shouldResolve(status gameTypes.GameStatus) bool {
// tryResolve resolves the game if it is in a winning state // tryResolve resolves the game if it is in a winning state
// Returns true if the game is resolvable (regardless of whether it was actually resolved) // Returns true if the game is resolvable (regardless of whether it was actually resolved)
func (a *Agent) tryResolve(ctx context.Context) bool { func (a *Agent) tryResolve(ctx context.Context) bool {
if err := a.resolveClaims(ctx); err != nil {
a.log.Error("Failed to resolve claims", "err", err)
return false
}
status, err := a.responder.CallResolve(ctx) status, err := a.responder.CallResolve(ctx)
if err != nil || status == gameTypes.GameStatusInProgress { if err != nil || status == gameTypes.GameStatusInProgress {
return false return false
...@@ -126,6 +133,60 @@ func (a *Agent) tryResolve(ctx context.Context) bool { ...@@ -126,6 +133,60 @@ func (a *Agent) tryResolve(ctx context.Context) bool {
return true return true
} }
var errNoResolvableClaims = errors.New("no resolvable claims")
func (a *Agent) tryResolveClaims(ctx context.Context) error {
claims, err := a.loader.FetchClaims(ctx)
if err != nil {
return fmt.Errorf("failed to fetch claims: %w", err)
}
if len(claims) == 0 {
return errNoResolvableClaims
}
var resolvableClaims []int64
for _, claim := range claims {
a.log.Debug("checking if claim is resolvable", "claimIdx", claim.ContractIndex)
if err := a.responder.CallResolveClaim(ctx, uint64(claim.ContractIndex)); err == nil {
a.log.Info("Resolving claim", "claimIdx", claim.ContractIndex)
resolvableClaims = append(resolvableClaims, int64(claim.ContractIndex))
}
}
a.log.Info("Resolving claims", "numClaims", len(resolvableClaims))
if len(resolvableClaims) == 0 {
return errNoResolvableClaims
}
var wg sync.WaitGroup
wg.Add(len(resolvableClaims))
for _, claimIdx := range resolvableClaims {
claimIdx := claimIdx
go func() {
defer wg.Done()
err := a.responder.ResolveClaim(ctx, uint64(claimIdx))
if err != nil {
a.log.Error("Failed to resolve claim", "err", err)
}
}()
}
wg.Wait()
return nil
}
func (a *Agent) resolveClaims(ctx context.Context) error {
for {
err := a.tryResolveClaims(ctx)
switch err {
case errNoResolvableClaims:
return nil
case nil:
continue
default:
return err
}
}
}
// newGameFromContracts initializes a new game state from the state in the contract // newGameFromContracts initializes a new game state from the state in the contract
func (a *Agent) newGameFromContracts(ctx context.Context) (types.Game, error) { func (a *Agent) newGameFromContracts(ctx context.Context) (types.Game, error) {
claims, err := a.loader.FetchClaims(ctx) claims, err := a.loader.FetchClaims(ctx)
......
...@@ -77,7 +77,7 @@ func TestDoNotMakeMovesWhenGameIsResolvable(t *testing.T) { ...@@ -77,7 +77,7 @@ func TestDoNotMakeMovesWhenGameIsResolvable(t *testing.T) {
require.NoError(t, agent.Act(ctx)) require.NoError(t, agent.Act(ctx))
require.Equal(t, 1, responder.callResolveCount, "should check if game is resolvable") require.Equal(t, 1, responder.callResolveCount, "should check if game is resolvable")
require.Zero(t, claimLoader.callCount, "should not fetch claims for resolvable game") require.Equal(t, 1, claimLoader.callCount, "should fetch claims once for resolveClaim")
if test.shouldResolve { if test.shouldResolve {
require.EqualValues(t, 1, responder.resolveCount, "should resolve winning game") require.EqualValues(t, 1, responder.resolveCount, "should resolve winning game")
...@@ -92,6 +92,7 @@ func TestLoadClaimsWhenGameNotResolvable(t *testing.T) { ...@@ -92,6 +92,7 @@ func TestLoadClaimsWhenGameNotResolvable(t *testing.T) {
// Checks that if the game isn't resolvable, that the agent continues on to start checking claims // Checks that if the game isn't resolvable, that the agent continues on to start checking claims
agent, claimLoader, responder := setupTestAgent(t, false) agent, claimLoader, responder := setupTestAgent(t, false)
responder.callResolveErr = errors.New("game is not resolvable") responder.callResolveErr = errors.New("game is not resolvable")
responder.callResolveClaimErr = errors.New("claim is not resolvable")
depth := 4 depth := 4
claimBuilder := test.NewClaimBuilder(t, depth, alphabet.NewTraceProvider("abcdefg", uint64(depth))) claimBuilder := test.NewClaimBuilder(t, depth, alphabet.NewTraceProvider("abcdefg", uint64(depth)))
...@@ -101,7 +102,9 @@ func TestLoadClaimsWhenGameNotResolvable(t *testing.T) { ...@@ -101,7 +102,9 @@ func TestLoadClaimsWhenGameNotResolvable(t *testing.T) {
require.NoError(t, agent.Act(context.Background())) require.NoError(t, agent.Act(context.Background()))
require.EqualValues(t, 1, claimLoader.callCount, "should load claims for unresolvable game") require.EqualValues(t, 2, claimLoader.callCount, "should load claims for unresolvable game")
require.EqualValues(t, responder.callResolveClaimCount, 1, "should check if claim is resolvable")
require.Zero(t, responder.resolveClaimCount, "should not send resolveClaim")
} }
func setupTestAgent(t *testing.T, agreeWithProposedOutput bool) (*Agent, *stubClaimLoader, *stubResponder) { func setupTestAgent(t *testing.T, agreeWithProposedOutput bool) (*Agent, *stubClaimLoader, *stubResponder) {
...@@ -132,6 +135,10 @@ type stubResponder struct { ...@@ -132,6 +135,10 @@ type stubResponder struct {
resolveCount int resolveCount int
resolveErr error resolveErr error
callResolveClaimCount int
callResolveClaimErr error
resolveClaimCount int
} }
func (s *stubResponder) CallResolve(ctx context.Context) (gameTypes.GameStatus, error) { func (s *stubResponder) CallResolve(ctx context.Context) (gameTypes.GameStatus, error) {
...@@ -144,8 +151,18 @@ func (s *stubResponder) Resolve(ctx context.Context) error { ...@@ -144,8 +151,18 @@ func (s *stubResponder) Resolve(ctx context.Context) error {
return s.resolveErr return s.resolveErr
} }
func (s *stubResponder) CallResolveClaim(ctx context.Context, clainIdx uint64) error {
s.callResolveClaimCount++
return s.callResolveClaimErr
}
func (s *stubResponder) ResolveClaim(ctx context.Context, clainIdx uint64) error {
s.resolveClaimCount++
return nil
}
func (s *stubResponder) PerformAction(ctx context.Context, response types.Action) error { func (s *stubResponder) PerformAction(ctx context.Context, response types.Action) error {
panic("Not implemented") return nil
} }
type stubUpdater struct { type stubUpdater struct {
......
...@@ -94,6 +94,34 @@ func (r *FaultResponder) Resolve(ctx context.Context) error { ...@@ -94,6 +94,34 @@ func (r *FaultResponder) Resolve(ctx context.Context) error {
return r.sendTxAndWait(ctx, txData) return r.sendTxAndWait(ctx, txData)
} }
// buildResolveClaimData creates the transaction data for the ResolveClaim function.
func (r *FaultResponder) buildResolveClaimData(ctx context.Context, claimIdx uint64) ([]byte, error) {
return r.fdgAbi.Pack("resolveClaim", big.NewInt(int64(claimIdx)))
}
// CallResolveClaim determines if the resolveClaim function on the fault dispute game contract
// would succeed.
func (r *FaultResponder) CallResolveClaim(ctx context.Context, claimIdx uint64) error {
txData, err := r.buildResolveClaimData(ctx, claimIdx)
if err != nil {
return err
}
_, err = r.txMgr.Call(ctx, ethereum.CallMsg{
To: &r.fdgAddr,
Data: txData,
}, nil)
return err
}
// ResolveClaim executes a resolveClaim transaction to resolve a fault dispute game.
func (r *FaultResponder) ResolveClaim(ctx context.Context, claimIdx uint64) error {
txData, err := r.buildResolveClaimData(ctx, claimIdx)
if err != nil {
return err
}
return r.sendTxAndWait(ctx, txData)
}
func (r *FaultResponder) PerformAction(ctx context.Context, action types.Action) error { func (r *FaultResponder) PerformAction(ctx context.Context, action types.Action) error {
var txData []byte var txData []byte
var err error var err error
......
...@@ -73,6 +73,40 @@ func TestResolve(t *testing.T) { ...@@ -73,6 +73,40 @@ func TestResolve(t *testing.T) {
}) })
} }
func TestCallResolveClaim(t *testing.T) {
t.Run("SendFails", func(t *testing.T) {
responder, mockTxMgr := newTestFaultResponder(t)
mockTxMgr.callFails = true
err := responder.CallResolveClaim(context.Background(), 0)
require.ErrorIs(t, err, mockCallError)
require.Equal(t, 0, mockTxMgr.calls)
})
t.Run("Success", func(t *testing.T) {
responder, mockTxMgr := newTestFaultResponder(t)
err := responder.CallResolveClaim(context.Background(), 0)
require.NoError(t, err)
require.Equal(t, 1, mockTxMgr.calls)
})
}
func TestResolveClaim(t *testing.T) {
t.Run("SendFails", func(t *testing.T) {
responder, mockTxMgr := newTestFaultResponder(t)
mockTxMgr.sendFails = true
err := responder.ResolveClaim(context.Background(), 0)
require.ErrorIs(t, err, mockSendError)
require.Equal(t, 0, mockTxMgr.sends)
})
t.Run("Success", func(t *testing.T) {
responder, mockTxMgr := newTestFaultResponder(t)
err := responder.ResolveClaim(context.Background(), 0)
require.NoError(t, err)
require.Equal(t, 1, mockTxMgr.sends)
})
}
// TestRespond tests the [Responder.Respond] method. // TestRespond tests the [Responder.Respond] method.
func TestPerformAction(t *testing.T) { func TestPerformAction(t *testing.T) {
t.Run("send fails", func(t *testing.T) { t.Run("send fails", func(t *testing.T) {
......
...@@ -48,7 +48,10 @@ func (s *GameSolver) calculateStep(ctx context.Context, game types.Game, claim t ...@@ -48,7 +48,10 @@ func (s *GameSolver) calculateStep(ctx context.Context, game types.Game, claim t
if game.AgreeWithClaimLevel(claim) { if game.AgreeWithClaimLevel(claim) {
return nil, nil return nil, nil
} }
step, err := s.claimSolver.AttemptStep(ctx, claim, game.AgreeWithClaimLevel(claim)) step, err := s.claimSolver.AttemptStep(ctx, game, claim)
if err == ErrStepIgnoreInvalidPath {
return nil, nil
}
if err != nil { if err != nil {
return nil, err return nil, err
} }
...@@ -63,11 +66,14 @@ func (s *GameSolver) calculateStep(ctx context.Context, game types.Game, claim t ...@@ -63,11 +66,14 @@ func (s *GameSolver) calculateStep(ctx context.Context, game types.Game, claim t
} }
func (s *GameSolver) calculateMove(ctx context.Context, game types.Game, claim types.Claim) (*types.Action, error) { func (s *GameSolver) calculateMove(ctx context.Context, game types.Game, claim types.Claim) (*types.Action, error) {
move, err := s.claimSolver.NextMove(ctx, claim, game.AgreeWithClaimLevel(claim)) if game.AgreeWithClaimLevel(claim) {
return nil, nil
}
move, err := s.claimSolver.NextMove(ctx, claim, game)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to calculate next move for claim index %v: %w", claim.ContractIndex, err) return nil, fmt.Errorf("failed to calculate next move for claim index %v: %w", claim.ContractIndex, err)
} }
if move == nil || game.IsDuplicate(move.ClaimData) { if move == nil || game.IsDuplicate(*move) {
return nil, nil return nil, nil
} }
return &types.Action{ return &types.Action{
......
...@@ -48,7 +48,6 @@ func TestCalculateNextActions(t *testing.T) { ...@@ -48,7 +48,6 @@ func TestCalculateNextActions(t *testing.T) {
rootClaimCorrect: true, rootClaimCorrect: true,
setupGame: func(builder *faulttest.GameBuilder) {}, setupGame: func(builder *faulttest.GameBuilder) {},
}, },
{ {
name: "DoNotPerformDuplicateMoves", name: "DoNotPerformDuplicateMoves",
agreeWithOutputRoot: true, agreeWithOutputRoot: true,
...@@ -93,16 +92,15 @@ func TestCalculateNextActions(t *testing.T) { ...@@ -93,16 +92,15 @@ func TestCalculateNextActions(t *testing.T) {
maliciousStateHash := common.Hash{0x01, 0xaa} maliciousStateHash := common.Hash{0x01, 0xaa}
// Dishonest actor counters their own claims to set up a situation with an invalid prestate // Dishonest actor counters their own claims to set up a situation with an invalid prestate
// The honest actor should attack all claims that support the root claim (disagree with the output root) // The honest actor should ignore path created by the dishonest actor, only supporting its own attack on the root claim
builder.Seq().ExpectAttack(). // This expected action is the winning move. honestMove := builder.Seq().AttackCorrect() // This expected action is the winning move.
Attack(maliciousStateHash). dishonestMove := honestMove.Attack(maliciousStateHash)
Defend(maliciousStateHash).ExpectAttack(). // The expected action by the honest actor
Attack(maliciousStateHash). dishonestMove.ExpectAttack()
Attack(maliciousStateHash).ExpectStepAttack() // The honest actor will ignore this poisoned path
dishonestMove.
// The attempt to step against our malicious leaf node will fail because the pre-state won't match our Defend(maliciousStateHash).
// malicious state hash. However, it is the very first expected action, attacking the root claim with Attack(maliciousStateHash)
// the correct hash that wins the game since it will be the left-most uncountered claim.
}, },
}, },
} }
......
...@@ -13,7 +13,6 @@ var rules = []actionRule{ ...@@ -13,7 +13,6 @@ var rules = []actionRule{
parentMustExist, parentMustExist,
onlyStepAtMaxDepth, onlyStepAtMaxDepth,
onlyMoveBeforeMaxDepth, onlyMoveBeforeMaxDepth,
onlyCounterClaimsAtDisagreeingLevels,
doNotDuplicateExistingMoves, doNotDuplicateExistingMoves,
doNotDefendRootClaim, doNotDefendRootClaim,
} }
...@@ -57,20 +56,12 @@ func onlyMoveBeforeMaxDepth(game types.Game, action types.Action) error { ...@@ -57,20 +56,12 @@ func onlyMoveBeforeMaxDepth(game types.Game, action types.Action) error {
return nil return nil
} }
func onlyCounterClaimsAtDisagreeingLevels(game types.Game, action types.Action) error {
parentClaim := game.Claims()[action.ParentIdx]
if game.AgreeWithClaimLevel(parentClaim) {
return fmt.Errorf("countering a claim at depth %v that supports our view of the root", parentClaim.Position.Depth())
}
return nil
}
func doNotDuplicateExistingMoves(game types.Game, action types.Action) error { func doNotDuplicateExistingMoves(game types.Game, action types.Action) error {
newClaimData := types.ClaimData{ newClaimData := types.ClaimData{
Value: action.Value, Value: action.Value,
Position: resultingPosition(game, action), Position: resultingPosition(game, action),
} }
if game.IsDuplicate(newClaimData) { if game.IsDuplicate(types.Claim{ClaimData: newClaimData, ParentContractIndex: action.ParentIdx}) {
return fmt.Errorf("creating duplicate claim at %v with value %v", newClaimData.Position.ToGIndex(), newClaimData.Value) return fmt.Errorf("creating duplicate claim at %v with value %v", newClaimData.Position.ToGIndex(), newClaimData.Value)
} }
return nil return nil
......
...@@ -11,8 +11,9 @@ import ( ...@@ -11,8 +11,9 @@ import (
) )
var ( var (
ErrStepNonLeafNode = errors.New("cannot step on non-leaf claims") ErrStepNonLeafNode = errors.New("cannot step on non-leaf claims")
ErrStepAgreedClaim = errors.New("cannot step on claims we agree with") ErrStepAgreedClaim = errors.New("cannot step on claims we agree with")
ErrStepIgnoreInvalidPath = errors.New("cannot step on claims that dispute invalid paths")
) )
// claimSolver uses a [TraceProvider] to determine the moves to make in a dispute game. // claimSolver uses a [TraceProvider] to determine the moves to make in a dispute game.
...@@ -30,10 +31,7 @@ func newClaimSolver(gameDepth int, traceProvider types.TraceProvider) *claimSolv ...@@ -30,10 +31,7 @@ func newClaimSolver(gameDepth int, traceProvider types.TraceProvider) *claimSolv
} }
// NextMove returns the next move to make given the current state of the game. // NextMove returns the next move to make given the current state of the game.
func (s *claimSolver) NextMove(ctx context.Context, claim types.Claim, agreeWithClaimLevel bool) (*types.Claim, error) { func (s *claimSolver) NextMove(ctx context.Context, claim types.Claim, game types.Game) (*types.Claim, error) {
if agreeWithClaimLevel {
return nil, nil
}
if claim.Depth() == s.gameDepth { if claim.Depth() == s.gameDepth {
return nil, types.ErrGameDepthReached return nil, types.ErrGameDepthReached
} }
...@@ -41,6 +39,24 @@ func (s *claimSolver) NextMove(ctx context.Context, claim types.Claim, agreeWith ...@@ -41,6 +39,24 @@ func (s *claimSolver) NextMove(ctx context.Context, claim types.Claim, agreeWith
if err != nil { if err != nil {
return nil, err return nil, err
} }
// Before challenging this claim, first check that the move wasn't warranted.
// If the parent claim is on a dishonest path, then we would have moved against it anyways. So we don't move.
// Avoiding dishonest paths ensures that there's always a valid claim available to support ours during step.
if !claim.IsRoot() {
parent, err := game.GetParent(claim)
if err != nil {
return nil, err
}
agreeWithParent, err := s.agreeWithClaimPath(ctx, game, parent)
if err != nil {
return nil, err
}
if !agreeWithParent {
return nil, nil
}
}
if agree { if agree {
return s.defend(ctx, claim) return s.defend(ctx, claim)
} else { } else {
...@@ -58,13 +74,25 @@ type StepData struct { ...@@ -58,13 +74,25 @@ type StepData struct {
// AttemptStep determines what step should occur for a given leaf claim. // AttemptStep determines what step should occur for a given leaf claim.
// An error will be returned if the claim is not at the max depth. // An error will be returned if the claim is not at the max depth.
func (s *claimSolver) AttemptStep(ctx context.Context, claim types.Claim, agreeWithClaimLevel bool) (StepData, error) { // Returns ErrStepIgnoreInvalidPath if the claim disputes an invalid path
func (s *claimSolver) AttemptStep(ctx context.Context, game types.Game, claim types.Claim) (StepData, error) {
if claim.Depth() != s.gameDepth { if claim.Depth() != s.gameDepth {
return StepData{}, ErrStepNonLeafNode return StepData{}, ErrStepNonLeafNode
} }
if agreeWithClaimLevel {
return StepData{}, ErrStepAgreedClaim // Step only on claims that dispute a valid path
parent, err := game.GetParent(claim)
if err != nil {
return StepData{}, err
} }
parentValid, err := s.agreeWithClaimPath(ctx, game, parent)
if err != nil {
return StepData{}, err
}
if !parentValid {
return StepData{}, ErrStepIgnoreInvalidPath
}
claimCorrect, err := s.agreeWithClaim(ctx, claim.ClaimData) claimCorrect, err := s.agreeWithClaim(ctx, claim.ClaimData)
if err != nil { if err != nil {
return StepData{}, err return StepData{}, err
...@@ -142,3 +170,26 @@ func (s *claimSolver) traceAtPosition(ctx context.Context, p types.Position) (co ...@@ -142,3 +170,26 @@ func (s *claimSolver) traceAtPosition(ctx context.Context, p types.Position) (co
hash, err := s.trace.Get(ctx, index) hash, err := s.trace.Get(ctx, index)
return hash, err return hash, err
} }
// agreeWithClaimPath returns true if the every other claim in the path to root is correct according to the internal [TraceProvider].
func (s *claimSolver) agreeWithClaimPath(ctx context.Context, game types.Game, claim types.Claim) (bool, error) {
agree, err := s.agreeWithClaim(ctx, claim.ClaimData)
if err != nil {
return false, err
}
if !agree {
return false, nil
}
if claim.IsRoot() || claim.Parent.IsRootPosition() {
return true, nil
}
parent, err := game.GetParent(claim)
if err != nil {
return false, err
}
grandParent, err := game.GetParent(parent)
if err != nil {
return false, err
}
return s.agreeWithClaimPath(ctx, game, grandParent)
}
...@@ -79,12 +79,13 @@ func (c *ClaimBuilder) claim(idx uint64, correct bool) common.Hash { ...@@ -79,12 +79,13 @@ func (c *ClaimBuilder) claim(idx uint64, correct bool) common.Hash {
func (c *ClaimBuilder) CreateRootClaim(correct bool) types.Claim { func (c *ClaimBuilder) CreateRootClaim(correct bool) types.Claim {
value := c.claim((1<<c.maxDepth)-1, correct) value := c.claim((1<<c.maxDepth)-1, correct)
return types.Claim{ claim := types.Claim{
ClaimData: types.ClaimData{ ClaimData: types.ClaimData{
Value: value, Value: value,
Position: types.NewPosition(0, 0), Position: types.NewPosition(0, 0),
}, },
} }
return claim
} }
func (c *ClaimBuilder) CreateLeafClaim(traceIndex uint64, correct bool) types.Claim { func (c *ClaimBuilder) CreateLeafClaim(traceIndex uint64, correct bool) types.Claim {
......
...@@ -23,8 +23,12 @@ type Game interface { ...@@ -23,8 +23,12 @@ type Game interface {
// Claims returns all of the claims in the game. // Claims returns all of the claims in the game.
Claims() []Claim Claims() []Claim
// IsDuplicate returns true if the provided [Claim] already exists in the game state. // GetParent returns the parent of the provided claim.
IsDuplicate(claim ClaimData) bool GetParent(claim Claim) (Claim, error)
// IsDuplicate returns true if the provided [Claim] already exists in the game state
// referencing the same parent claim
IsDuplicate(claim Claim) bool
// AgreeWithClaimLevel returns if the game state agrees with the provided claim level. // AgreeWithClaimLevel returns if the game state agrees with the provided claim level.
AgreeWithClaimLevel(claim Claim) bool AgreeWithClaimLevel(claim Claim) bool
...@@ -32,31 +36,37 @@ type Game interface { ...@@ -32,31 +36,37 @@ type Game interface {
MaxDepth() uint64 MaxDepth() uint64
} }
type claimEntry struct {
ClaimData
ParentContractIndex int
}
type extendedClaim struct { type extendedClaim struct {
self Claim self Claim
children []ClaimData children []claimEntry
} }
// gameState is a struct that represents the state of a dispute game. // gameState is a struct that represents the state of a dispute game.
// The game state implements the [Game] interface. // The game state implements the [Game] interface.
type gameState struct { type gameState struct {
agreeWithProposedOutput bool agreeWithProposedOutput bool
root ClaimData root claimEntry
claims map[ClaimData]*extendedClaim claims map[claimEntry]*extendedClaim
depth uint64 depth uint64
} }
// NewGameState returns a new game state. // NewGameState returns a new game state.
// The provided [Claim] is used as the root node. // The provided [Claim] is used as the root node.
func NewGameState(agreeWithProposedOutput bool, root Claim, depth uint64) *gameState { func NewGameState(agreeWithProposedOutput bool, root Claim, depth uint64) *gameState {
claims := make(map[ClaimData]*extendedClaim) claims := make(map[claimEntry]*extendedClaim)
claims[root.ClaimData] = &extendedClaim{ rootClaimEntry := makeClaimEntry(root)
claims[rootClaimEntry] = &extendedClaim{
self: root, self: root,
children: make([]ClaimData, 0), children: make([]claimEntry, 0),
} }
return &gameState{ return &gameState{
agreeWithProposedOutput: agreeWithProposedOutput, agreeWithProposedOutput: agreeWithProposedOutput,
root: root.ClaimData, root: rootClaimEntry,
claims: claims, claims: claims,
depth: depth, depth: depth,
} }
...@@ -87,29 +97,29 @@ func (g *gameState) PutAll(claims []Claim) error { ...@@ -87,29 +97,29 @@ func (g *gameState) PutAll(claims []Claim) error {
// Put adds a claim into the game state. // Put adds a claim into the game state.
func (g *gameState) Put(claim Claim) error { func (g *gameState) Put(claim Claim) error {
if claim.IsRoot() || g.IsDuplicate(claim.ClaimData) { if claim.IsRoot() || g.IsDuplicate(claim) {
return ErrClaimExists return ErrClaimExists
} }
parent, ok := g.claims[claim.Parent]
if !ok { parent := g.getParent(claim)
if parent == nil {
return errors.New("no parent claim") return errors.New("no parent claim")
} else {
parent.children = append(parent.children, claim.ClaimData)
} }
g.claims[claim.ClaimData] = &extendedClaim{ parent.children = append(parent.children, makeClaimEntry(claim))
g.claims[makeClaimEntry(claim)] = &extendedClaim{
self: claim, self: claim,
children: make([]ClaimData, 0), children: make([]claimEntry, 0),
} }
return nil return nil
} }
func (g *gameState) IsDuplicate(claim ClaimData) bool { func (g *gameState) IsDuplicate(claim Claim) bool {
_, ok := g.claims[claim] _, ok := g.claims[makeClaimEntry(claim)]
return ok return ok
} }
func (g *gameState) Claims() []Claim { func (g *gameState) Claims() []Claim {
queue := []ClaimData{g.root} queue := []claimEntry{g.root}
var out []Claim var out []Claim
for len(queue) > 0 { for len(queue) > 0 {
item := queue[0] item := queue[0]
...@@ -124,17 +134,34 @@ func (g *gameState) MaxDepth() uint64 { ...@@ -124,17 +134,34 @@ func (g *gameState) MaxDepth() uint64 {
return g.depth return g.depth
} }
func (g *gameState) getChildren(c ClaimData) []ClaimData { func (g *gameState) getChildren(c claimEntry) []claimEntry {
return g.claims[c].children return g.claims[c].children
} }
func (g *gameState) getParent(claim Claim) (Claim, error) { func (g *gameState) GetParent(claim Claim) (Claim, error) {
if claim.IsRoot() { parent := g.getParent(claim)
if parent == nil {
return Claim{}, ErrClaimNotFound return Claim{}, ErrClaimNotFound
} }
if parent, ok := g.claims[claim.Parent]; !ok { return parent.self, nil
return Claim{}, ErrClaimNotFound }
} else {
return parent.self, nil func (g *gameState) getParent(claim Claim) *extendedClaim {
if claim.IsRoot() {
return nil
}
// TODO(inphi): refactor gameState for faster parent lookups
for _, c := range g.claims {
if c.self.ContractIndex == claim.ParentContractIndex {
return c
}
}
return nil
}
func makeClaimEntry(claim Claim) claimEntry {
return claimEntry{
ClaimData: claim.ClaimData,
ParentContractIndex: claim.ParentContractIndex,
} }
} }
...@@ -24,14 +24,18 @@ func createTestClaims() (Claim, Claim, Claim, Claim) { ...@@ -24,14 +24,18 @@ func createTestClaims() (Claim, Claim, Claim, Claim) {
Value: common.HexToHash("0x0000000000000000000000000000000000000000000000000000000000000364"), Value: common.HexToHash("0x0000000000000000000000000000000000000000000000000000000000000364"),
Position: NewPosition(1, 0), Position: NewPosition(1, 0),
}, },
Parent: root.ClaimData, Parent: root.ClaimData,
ContractIndex: 1,
ParentContractIndex: 0,
} }
middle := Claim{ middle := Claim{
ClaimData: ClaimData{ ClaimData: ClaimData{
Value: common.HexToHash("0x0000000000000000000000000000000000000000000000000000000000000578"), Value: common.HexToHash("0x0000000000000000000000000000000000000000000000000000000000000578"),
Position: NewPosition(2, 2), Position: NewPosition(2, 2),
}, },
Parent: top.ClaimData, Parent: top.ClaimData,
ContractIndex: 2,
ParentContractIndex: 1,
} }
bottom := Claim{ bottom := Claim{
...@@ -39,7 +43,9 @@ func createTestClaims() (Claim, Claim, Claim, Claim) { ...@@ -39,7 +43,9 @@ func createTestClaims() (Claim, Claim, Claim, Claim) {
Value: common.HexToHash("0x0000000000000000000000000000000000000000000000000000000000000465"), Value: common.HexToHash("0x0000000000000000000000000000000000000000000000000000000000000465"),
Position: NewPosition(3, 4), Position: NewPosition(3, 4),
}, },
Parent: middle.ClaimData, Parent: middle.ClaimData,
ContractIndex: 3,
ParentContractIndex: 2,
} }
return root, top, middle, bottom return root, top, middle, bottom
...@@ -52,12 +58,12 @@ func TestIsDuplicate(t *testing.T) { ...@@ -52,12 +58,12 @@ func TestIsDuplicate(t *testing.T) {
require.NoError(t, g.Put(top)) require.NoError(t, g.Put(top))
// Root + Top should be duplicates // Root + Top should be duplicates
require.True(t, g.IsDuplicate(root.ClaimData)) require.True(t, g.IsDuplicate(root))
require.True(t, g.IsDuplicate(top.ClaimData)) require.True(t, g.IsDuplicate(top))
// Middle + Bottom should not be a duplicate // Middle + Bottom should not be a duplicate
require.False(t, g.IsDuplicate(middle.ClaimData)) require.False(t, g.IsDuplicate(middle))
require.False(t, g.IsDuplicate(bottom.ClaimData)) require.False(t, g.IsDuplicate(bottom))
} }
// TestGame_Put_RootAlreadyExists tests the [Game.Put] method using a [gameState] // TestGame_Put_RootAlreadyExists tests the [Game.Put] method using a [gameState]
...@@ -104,20 +110,20 @@ func TestGame_PutAll_ParentsAndChildren(t *testing.T) { ...@@ -104,20 +110,20 @@ func TestGame_PutAll_ParentsAndChildren(t *testing.T) {
g := NewGameState(false, root, testMaxDepth) g := NewGameState(false, root, testMaxDepth)
// We should not be able to get the parent of the root claim. // We should not be able to get the parent of the root claim.
parent, err := g.getParent(root) parent, err := g.GetParent(root)
require.ErrorIs(t, err, ErrClaimNotFound) require.ErrorIs(t, err, ErrClaimNotFound)
require.Equal(t, parent, Claim{}) require.Equal(t, parent, Claim{})
// Put the rest of the claims in the state. // Put the rest of the claims in the state.
err = g.PutAll([]Claim{top, middle, bottom}) err = g.PutAll([]Claim{top, middle, bottom})
require.NoError(t, err) require.NoError(t, err)
parent, err = g.getParent(top) parent, err = g.GetParent(top)
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, parent, root) require.Equal(t, parent, root)
parent, err = g.getParent(middle) parent, err = g.GetParent(middle)
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, parent, top) require.Equal(t, parent, top)
parent, err = g.getParent(bottom) parent, err = g.GetParent(bottom)
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, parent, middle) require.Equal(t, parent, middle)
} }
...@@ -145,28 +151,28 @@ func TestGame_Put_ParentsAndChildren(t *testing.T) { ...@@ -145,28 +151,28 @@ func TestGame_Put_ParentsAndChildren(t *testing.T) {
g := NewGameState(false, root, testMaxDepth) g := NewGameState(false, root, testMaxDepth)
// We should not be able to get the parent of the root claim. // We should not be able to get the parent of the root claim.
parent, err := g.getParent(root) parent, err := g.GetParent(root)
require.ErrorIs(t, err, ErrClaimNotFound) require.ErrorIs(t, err, ErrClaimNotFound)
require.Equal(t, parent, Claim{}) require.Equal(t, parent, Claim{})
// Put + Check Top // Put + Check Top
err = g.Put(top) err = g.Put(top)
require.NoError(t, err) require.NoError(t, err)
parent, err = g.getParent(top) parent, err = g.GetParent(top)
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, parent, root) require.Equal(t, parent, root)
// Put + Check Top Middle // Put + Check Top Middle
err = g.Put(middle) err = g.Put(middle)
require.NoError(t, err) require.NoError(t, err)
parent, err = g.getParent(middle) parent, err = g.GetParent(middle)
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, parent, top) require.Equal(t, parent, top)
// Put + Check Top Bottom // Put + Check Top Bottom
err = g.Put(bottom) err = g.Put(bottom)
require.NoError(t, err) require.NoError(t, err)
parent, err = g.getParent(bottom) parent, err = g.GetParent(bottom)
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, parent, middle) require.Equal(t, parent, middle)
} }
...@@ -194,27 +200,3 @@ func TestGame_ClaimPairs(t *testing.T) { ...@@ -194,27 +200,3 @@ func TestGame_ClaimPairs(t *testing.T) {
claims := g.Claims() claims := g.Claims()
require.ElementsMatch(t, expected, claims) require.ElementsMatch(t, expected, claims)
} }
func TestAgreeWithClaimLevelDisagreeWithOutput(t *testing.T) {
// Setup the game state.
root, top, middle, bottom := createTestClaims()
g := NewGameState(false, root, testMaxDepth)
require.NoError(t, g.PutAll([]Claim{top, middle, bottom}))
require.True(t, g.AgreeWithClaimLevel(root))
require.False(t, g.AgreeWithClaimLevel(top))
require.True(t, g.AgreeWithClaimLevel(middle))
require.False(t, g.AgreeWithClaimLevel(bottom))
}
func TestAgreeWithClaimLevelAgreeWithOutput(t *testing.T) {
// Setup the game state.
root, top, middle, bottom := createTestClaims()
g := NewGameState(true, root, testMaxDepth)
require.NoError(t, g.PutAll([]Claim{top, middle, bottom}))
require.False(t, g.AgreeWithClaimLevel(root))
require.True(t, g.AgreeWithClaimLevel(top))
require.False(t, g.AgreeWithClaimLevel(middle))
require.True(t, g.AgreeWithClaimLevel(bottom))
}
...@@ -68,6 +68,12 @@ func WithAlphabet(alphabet string) Option { ...@@ -68,6 +68,12 @@ func WithAlphabet(alphabet string) Option {
} }
} }
func WithPollInterval(pollInterval time.Duration) Option {
return func(c *config.Config) {
c.PollInterval = pollInterval
}
}
func WithCannon( func WithCannon(
t *testing.T, t *testing.T,
rollupCfg *rollup.Config, rollupCfg *rollup.Config,
...@@ -98,7 +104,7 @@ func WithCannon( ...@@ -98,7 +104,7 @@ func WithCannon(
} }
func NewChallenger(t *testing.T, ctx context.Context, l1Endpoint string, name string, options ...Option) *Helper { func NewChallenger(t *testing.T, ctx context.Context, l1Endpoint string, name string, options ...Option) *Helper {
log := testlog.Logger(t, log.LvlInfo).New("role", name) log := testlog.Logger(t, log.LvlDebug).New("role", name)
log.Info("Creating challenger", "l1", l1Endpoint) log.Info("Creating challenger", "l1", l1Endpoint)
cfg := NewChallengerConfig(t, l1Endpoint, options...) cfg := NewChallengerConfig(t, l1Endpoint, options...)
......
...@@ -4,6 +4,7 @@ import ( ...@@ -4,6 +4,7 @@ import (
"context" "context"
"github.com/ethereum-optimism/optimism/op-challenger/config" "github.com/ethereum-optimism/optimism/op-challenger/config"
"github.com/ethereum-optimism/optimism/op-challenger/game/fault/trace/alphabet"
"github.com/ethereum-optimism/optimism/op-e2e/e2eutils/challenger" "github.com/ethereum-optimism/optimism/op-e2e/e2eutils/challenger"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
...@@ -33,3 +34,12 @@ func (g *AlphabetGameHelper) StartChallenger(ctx context.Context, l1Endpoint str ...@@ -33,3 +34,12 @@ func (g *AlphabetGameHelper) StartChallenger(ctx context.Context, l1Endpoint str
}) })
return c return c
} }
func (g *AlphabetGameHelper) CreateHonestActor(ctx context.Context, alphabetTrace string, depth uint64) *HonestHelper {
return &HonestHelper{
t: g.t,
require: g.require,
game: &g.FaultGameHelper,
correctTrace: alphabet.NewTraceProvider(alphabetTrace, depth),
}
}
...@@ -2,16 +2,19 @@ package disputegame ...@@ -2,16 +2,19 @@ package disputegame
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"math/big" "math/big"
"testing" "testing"
"time" "time"
"github.com/ethereum-optimism/optimism/op-bindings/bindings" "github.com/ethereum-optimism/optimism/op-bindings/bindings"
"github.com/ethereum-optimism/optimism/op-challenger/game/fault"
"github.com/ethereum-optimism/optimism/op-challenger/game/fault/types" "github.com/ethereum-optimism/optimism/op-challenger/game/fault/types"
"github.com/ethereum-optimism/optimism/op-e2e/e2eutils/wait" "github.com/ethereum-optimism/optimism/op-e2e/e2eutils/wait"
"github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/ethereum/go-ethereum/accounts/abi/bind"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
gethtypes "github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/ethclient" "github.com/ethereum/go-ethereum/ethclient"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
...@@ -130,6 +133,10 @@ func (g *FaultGameHelper) getClaim(ctx context.Context, claimIdx int64) Contract ...@@ -130,6 +133,10 @@ func (g *FaultGameHelper) getClaim(ctx context.Context, claimIdx int64) Contract
return claimData return claimData
} }
func (g *FaultGameHelper) GetClaimUnsafe(ctx context.Context, claimIdx int64) ContractClaim {
return g.getClaim(ctx, claimIdx)
}
func (g *FaultGameHelper) WaitForClaimAtDepth(ctx context.Context, depth int) { func (g *FaultGameHelper) WaitForClaimAtDepth(ctx context.Context, depth int) {
g.waitForClaim( g.waitForClaim(
ctx, ctx,
...@@ -169,6 +176,12 @@ func (g *FaultGameHelper) Resolve(ctx context.Context) { ...@@ -169,6 +176,12 @@ func (g *FaultGameHelper) Resolve(ctx context.Context) {
g.require.NoError(err) g.require.NoError(err)
} }
func (g *FaultGameHelper) Status(ctx context.Context) Status {
status, err := g.game.Status(&bind.CallOpts{Context: ctx})
g.require.NoError(err)
return Status(status)
}
func (g *FaultGameHelper) WaitForGameStatus(ctx context.Context, expected Status) { func (g *FaultGameHelper) WaitForGameStatus(ctx context.Context, expected Status) {
g.t.Logf("Waiting for game %v to have status %v", g.addr, expected) g.t.Logf("Waiting for game %v to have status %v", g.addr, expected)
timedCtx, cancel := context.WithTimeout(ctx, time.Minute) timedCtx, cancel := context.WithTimeout(ctx, time.Minute)
...@@ -186,6 +199,46 @@ func (g *FaultGameHelper) WaitForGameStatus(ctx context.Context, expected Status ...@@ -186,6 +199,46 @@ func (g *FaultGameHelper) WaitForGameStatus(ctx context.Context, expected Status
g.require.NoErrorf(err, "wait for game status. Game state: \n%v", g.gameData(ctx)) g.require.NoErrorf(err, "wait for game status. Game state: \n%v", g.gameData(ctx))
} }
func (g *FaultGameHelper) WaitForInactivity(ctx context.Context, numInactiveBlocks int, untilGameEnds bool) {
g.t.Logf("Waiting for game %v to have no activity for %v blocks", g.addr, numInactiveBlocks)
headCh := make(chan *gethtypes.Header, 100)
headSub, err := g.client.SubscribeNewHead(ctx, headCh)
g.require.NoError(err)
defer headSub.Unsubscribe()
var lastActiveBlock uint64
for {
if untilGameEnds && g.Status(ctx) != StatusInProgress {
break
}
select {
case head := <-headCh:
if lastActiveBlock == 0 {
lastActiveBlock = head.Number.Uint64()
continue
} else if lastActiveBlock+uint64(numInactiveBlocks) < head.Number.Uint64() {
return
}
block, err := g.client.BlockByNumber(ctx, head.Number)
g.require.NoError(err)
numActions := 0
for _, tx := range block.Transactions() {
if tx.To().Hex() == g.addr.Hex() {
numActions++
}
}
if numActions != 0 {
g.t.Logf("Game %v has %v actions in block %d. Resetting inactivity timeout", g.addr, numActions, block.NumberU64())
lastActiveBlock = head.Number.Uint64()
}
case err := <-headSub.Err():
g.require.NoError(err)
case <-ctx.Done():
g.require.Fail("Context canceled", ctx.Err())
}
}
}
// Mover is a function that either attacks or defends the claim at parentClaimIdx // Mover is a function that either attacks or defends the claim at parentClaimIdx
type Mover func(parentClaimIdx int64) type Mover func(parentClaimIdx int64)
...@@ -239,6 +292,21 @@ func (g *FaultGameHelper) ChallengeRootClaim(ctx context.Context, performMove Mo ...@@ -239,6 +292,21 @@ func (g *FaultGameHelper) ChallengeRootClaim(ctx context.Context, performMove Mo
attemptStep(maxDepth) attemptStep(maxDepth)
} }
func (g *FaultGameHelper) WaitForNewClaim(ctx context.Context, checkPoint int64) (int64, error) {
timedCtx, cancel := context.WithTimeout(ctx, 2*time.Minute)
defer cancel()
var newClaimLen int64
err := wait.For(timedCtx, time.Second, func() (bool, error) {
actual, err := g.game.ClaimDataLen(&bind.CallOpts{Context: ctx})
if err != nil {
return false, err
}
newClaimLen = actual.Int64()
return actual.Cmp(big.NewInt(checkPoint)) > 0, nil
})
return newClaimLen, err
}
func (g *FaultGameHelper) Attack(ctx context.Context, claimIdx int64, claim common.Hash) { func (g *FaultGameHelper) Attack(ctx context.Context, claimIdx int64, claim common.Hash) {
tx, err := g.game.Attack(g.opts, big.NewInt(claimIdx), claim) tx, err := g.game.Attack(g.opts, big.NewInt(claimIdx), claim)
g.require.NoError(err, "Attack transaction did not send") g.require.NoError(err, "Attack transaction did not send")
...@@ -266,6 +334,33 @@ func (g *FaultGameHelper) StepFails(claimIdx int64, isAttack bool, stateData []b ...@@ -266,6 +334,33 @@ func (g *FaultGameHelper) StepFails(claimIdx int64, isAttack bool, stateData []b
g.require.Equal("0xfb4e40dd", errData.ErrorData(), "Revert reason should be abi encoded ValidStep()") g.require.Equal("0xfb4e40dd", errData.ErrorData(), "Revert reason should be abi encoded ValidStep()")
} }
// ResolveClaim resolves a single subgame
func (g *FaultGameHelper) ResolveClaim(ctx context.Context, claimIdx int64) {
tx, err := g.game.ResolveClaim(g.opts, big.NewInt(claimIdx))
g.require.NoError(err, "ResolveClaim transaction did not send")
_, err = wait.ForReceiptOK(ctx, g.client, tx.Hash())
g.require.NoError(err, "ResolveClaim transaction was not OK")
}
// ResolveAllClaims resolves all subgames
// This function does not resolve the game. That's the responsibility of challengers
func (g *FaultGameHelper) ResolveAllClaims(ctx context.Context) {
loader := fault.NewLoader(g.game)
claims, err := loader.FetchClaims(ctx)
g.require.NoError(err, "Failed to fetch claims")
subgames := make(map[int]bool)
for i := len(claims) - 1; i > 0; i-- {
subgames[claims[i].ParentContractIndex] = true
// Subgames containing only one node are implicitly resolved
// i.e. uncountered and claims at MAX_DEPTH
if !subgames[i] {
continue
}
g.ResolveClaim(ctx, int64(i))
}
g.ResolveClaim(ctx, 0)
}
func (g *FaultGameHelper) gameData(ctx context.Context) string { func (g *FaultGameHelper) gameData(ctx context.Context) string {
opts := &bind.CallOpts{Context: ctx} opts := &bind.CallOpts{Context: ctx}
maxDepth := int(g.MaxDepth(ctx)) maxDepth := int(g.MaxDepth(ctx))
...@@ -277,8 +372,8 @@ func (g *FaultGameHelper) gameData(ctx context.Context) string { ...@@ -277,8 +372,8 @@ func (g *FaultGameHelper) gameData(ctx context.Context) string {
g.require.NoErrorf(err, "Fetch claim %v", i) g.require.NoErrorf(err, "Fetch claim %v", i)
pos := types.NewPositionFromGIndex(claim.Position.Uint64()) pos := types.NewPositionFromGIndex(claim.Position.Uint64())
info = info + fmt.Sprintf("%v - Position: %v, Depth: %v, IndexAtDepth: %v Trace Index: %v, Value: %v, Countered: %v\n", info = info + fmt.Sprintf("%v - Position: %v, Depth: %v, IndexAtDepth: %v Trace Index: %v, Value: %v, Countered: %v, ParentIndex: %v\n",
i, claim.Position.Int64(), pos.Depth(), pos.IndexAtDepth(), pos.TraceIndex(maxDepth), common.Hash(claim.Claim).Hex(), claim.Countered) i, claim.Position.Int64(), pos.Depth(), pos.IndexAtDepth(), pos.TraceIndex(maxDepth), common.Hash(claim.Claim).Hex(), claim.Countered, claim.ParentIndex)
} }
status, err := g.game.Status(opts) status, err := g.game.Status(opts)
g.require.NoError(err, "Load game status") g.require.NoError(err, "Load game status")
...@@ -288,3 +383,106 @@ func (g *FaultGameHelper) gameData(ctx context.Context) string { ...@@ -288,3 +383,106 @@ func (g *FaultGameHelper) gameData(ctx context.Context) string {
func (g *FaultGameHelper) LogGameData(ctx context.Context) { func (g *FaultGameHelper) LogGameData(ctx context.Context) {
g.t.Log(g.gameData(ctx)) g.t.Log(g.gameData(ctx))
} }
type dishonestClaim struct {
ParentIndex int64
IsAttack bool
Valid bool
}
type DishonestHelper struct {
*FaultGameHelper
*HonestHelper
claims map[dishonestClaim]bool
defender bool
}
func NewDishonestHelper(g *FaultGameHelper, correctTrace *HonestHelper, defender bool) *DishonestHelper {
return &DishonestHelper{g, correctTrace, make(map[dishonestClaim]bool), defender}
}
func (t *DishonestHelper) Attack(ctx context.Context, claimIndex int64) {
c := dishonestClaim{claimIndex, true, false}
if t.claims[c] {
return
}
t.claims[c] = true
t.FaultGameHelper.Attack(ctx, claimIndex, common.Hash{byte(claimIndex)})
}
func (t *DishonestHelper) Defend(ctx context.Context, claimIndex int64) {
c := dishonestClaim{claimIndex, false, false}
if t.claims[c] {
return
}
t.claims[c] = true
t.FaultGameHelper.Defend(ctx, claimIndex, common.Hash{byte(claimIndex)})
}
func (t *DishonestHelper) AttackCorrect(ctx context.Context, claimIndex int64) {
c := dishonestClaim{claimIndex, true, true}
if t.claims[c] {
return
}
t.claims[c] = true
t.HonestHelper.Attack(ctx, claimIndex)
}
func (t *DishonestHelper) DefendCorrect(ctx context.Context, claimIndex int64) {
c := dishonestClaim{claimIndex, false, true}
if t.claims[c] {
return
}
t.claims[c] = true
t.HonestHelper.Defend(ctx, claimIndex)
}
// ExhaustDishonestClaims makes all possible significant moves (mod honest challenger's) in a game.
// It is very inefficient and should NOT be used on games with large depths
func (d *DishonestHelper) ExhaustDishonestClaims(ctx context.Context) {
depth := d.MaxDepth(ctx)
move := func(claimIndex int64, claimData ContractClaim) {
// dishonest level, valid attack
// dishonest level, invalid attack
// dishonest level, valid defense
// dishonest level, invalid defense
// honest level, invalid attack
// honest level, invalid defense
pos := types.NewPositionFromGIndex(claimData.Position.Uint64())
if int64(pos.Depth()) == depth {
return
}
d.LogGameData(ctx)
d.FaultGameHelper.t.Logf("Dishonest moves against claimIndex %d", claimIndex)
agreeWithLevel := d.defender == (pos.Depth()%2 == 0)
if !agreeWithLevel {
d.AttackCorrect(ctx, claimIndex)
if claimIndex != 0 {
d.DefendCorrect(ctx, claimIndex)
}
}
d.Attack(ctx, claimIndex)
if claimIndex != 0 {
d.Defend(ctx, claimIndex)
}
}
var numClaimsSeen int64
for {
newCount, err := d.WaitForNewClaim(ctx, numClaimsSeen)
if errors.Is(err, context.DeadlineExceeded) {
// we assume that the honest challenger has stopped responding
// There's nothing to respond to.
break
}
d.FaultGameHelper.require.NoError(err)
for i := numClaimsSeen; i < newCount; i++ {
claimData := d.getClaim(ctx, numClaimsSeen)
move(numClaimsSeen, claimData)
numClaimsSeen++
}
}
}
...@@ -3,7 +3,9 @@ package op_e2e ...@@ -3,7 +3,9 @@ package op_e2e
import ( import (
"context" "context"
"testing" "testing"
"time"
"github.com/ethereum-optimism/optimism/op-challenger/game/fault/types"
"github.com/ethereum-optimism/optimism/op-e2e/e2eutils/challenger" "github.com/ethereum-optimism/optimism/op-e2e/e2eutils/challenger"
"github.com/ethereum-optimism/optimism/op-e2e/e2eutils/disputegame" "github.com/ethereum-optimism/optimism/op-e2e/e2eutils/disputegame"
l2oo2 "github.com/ethereum-optimism/optimism/op-e2e/e2eutils/l2oo" l2oo2 "github.com/ethereum-optimism/optimism/op-e2e/e2eutils/l2oo"
...@@ -62,8 +64,12 @@ func TestMultipleCannonGames(t *testing.T) { ...@@ -62,8 +64,12 @@ func TestMultipleCannonGames(t *testing.T) {
sys.TimeTravelClock.AdvanceTime(gameDuration) sys.TimeTravelClock.AdvanceTime(gameDuration)
require.NoError(t, wait.ForNextBlock(ctx, l1Client)) require.NoError(t, wait.ForNextBlock(ctx, l1Client))
game1.WaitForGameStatus(ctx, disputegame.StatusChallengerWins) game1.WaitForInactivity(ctx, 10, true)
game2.WaitForGameStatus(ctx, disputegame.StatusChallengerWins) game2.WaitForInactivity(ctx, 10, true)
game1.LogGameData(ctx)
game2.LogGameData(ctx)
require.EqualValues(t, disputegame.StatusChallengerWins, game1.Status(ctx))
require.EqualValues(t, disputegame.StatusChallengerWins, game2.Status(ctx))
// Check that the game directories are removed // Check that the game directories are removed
challenger.WaitForGameDataDeletion(ctx, game1, game2) challenger.WaitForGameDataDeletion(ctx, game1, game2)
...@@ -168,11 +174,72 @@ func TestChallengerCompleteDisputeGame(t *testing.T) { ...@@ -168,11 +174,72 @@ func TestChallengerCompleteDisputeGame(t *testing.T) {
sys.TimeTravelClock.AdvanceTime(gameDuration) sys.TimeTravelClock.AdvanceTime(gameDuration)
require.NoError(t, wait.ForNextBlock(ctx, l1Client)) require.NoError(t, wait.ForNextBlock(ctx, l1Client))
game.WaitForGameStatus(ctx, test.expectedResult) game.WaitForInactivity(ctx, 10, true)
game.LogGameData(ctx)
require.EqualValues(t, test.expectedResult, game.Status(ctx))
}) })
} }
} }
func TestChallengerCompleteExhaustiveDisputeGame(t *testing.T) {
InitParallel(t)
testCase := func(t *testing.T, isRootCorrect bool) {
ctx := context.Background()
sys, l1Client := startFaultDisputeSystem(t)
t.Cleanup(sys.Close)
disputeGameFactory := disputegame.NewFactoryHelper(t, ctx, sys.cfg.L1Deployments, l1Client)
rootClaimedAlphabet := disputegame.CorrectAlphabet
if !isRootCorrect {
rootClaimedAlphabet = "abcdexyz"
}
game := disputeGameFactory.StartAlphabetGame(ctx, rootClaimedAlphabet)
require.NotNil(t, game)
gameDuration := game.GameDuration(ctx)
// Start honest challenger
game.StartChallenger(ctx, sys.NodeEndpoint("l1"), "Challenger",
challenger.WithAgreeProposedOutput(!isRootCorrect),
challenger.WithAlphabet(disputegame.CorrectAlphabet),
challenger.WithPrivKey(sys.cfg.Secrets.Alice),
// Ensures the challenger responds to all claims before test timeout
challenger.WithPollInterval(time.Millisecond*400),
)
// Start dishonest challenger
correctTrace := game.CreateHonestActor(ctx, disputegame.CorrectAlphabet, 4)
dishonestHelper := disputegame.NewDishonestHelper(&game.FaultGameHelper, correctTrace, !isRootCorrect)
dishonestHelper.ExhaustDishonestClaims(ctx)
// Wait until we've reached max depth before checking for inactivity
game.WaitForClaimAtDepth(ctx, int(game.MaxDepth(ctx)))
// Wait for 4 blocks of no challenger responses. The challenger may still be stepping on invalid claims at max depth
game.WaitForInactivity(ctx, 4, false)
sys.TimeTravelClock.AdvanceTime(gameDuration)
require.NoError(t, wait.ForNextBlock(ctx, l1Client))
expectedStatus := disputegame.StatusChallengerWins
if isRootCorrect {
expectedStatus = disputegame.StatusDefenderWins
}
game.WaitForInactivity(ctx, 10, true)
game.LogGameData(ctx)
require.EqualValues(t, expectedStatus, game.Status(ctx))
}
t.Run("RootCorrect", func(t *testing.T) {
InitParallel(t)
testCase(t, true)
})
t.Run("RootIncorrect", func(t *testing.T) {
InitParallel(t)
testCase(t, false)
})
}
func TestCannonDisputeGame(t *testing.T) { func TestCannonDisputeGame(t *testing.T) {
InitParallel(t) InitParallel(t)
...@@ -217,8 +284,9 @@ func TestCannonDisputeGame(t *testing.T) { ...@@ -217,8 +284,9 @@ func TestCannonDisputeGame(t *testing.T) {
sys.TimeTravelClock.AdvanceTime(game.GameDuration(ctx)) sys.TimeTravelClock.AdvanceTime(game.GameDuration(ctx))
require.NoError(t, wait.ForNextBlock(ctx, l1Client)) require.NoError(t, wait.ForNextBlock(ctx, l1Client))
game.WaitForGameStatus(ctx, disputegame.StatusChallengerWins) game.WaitForInactivity(ctx, 10, true)
game.LogGameData(ctx) game.LogGameData(ctx)
require.EqualValues(t, disputegame.StatusChallengerWins, game.Status(ctx))
}) })
} }
} }
...@@ -260,8 +328,9 @@ func TestCannonDefendStep(t *testing.T) { ...@@ -260,8 +328,9 @@ func TestCannonDefendStep(t *testing.T) {
sys.TimeTravelClock.AdvanceTime(game.GameDuration(ctx)) sys.TimeTravelClock.AdvanceTime(game.GameDuration(ctx))
require.NoError(t, wait.ForNextBlock(ctx, l1Client)) require.NoError(t, wait.ForNextBlock(ctx, l1Client))
game.WaitForGameStatus(ctx, disputegame.StatusChallengerWins) game.WaitForInactivity(ctx, 10, true)
game.LogGameData(ctx) game.LogGameData(ctx)
require.EqualValues(t, disputegame.StatusChallengerWins, game.Status(ctx))
} }
func TestCannonProposedOutputRootInvalid(t *testing.T) { func TestCannonProposedOutputRootInvalid(t *testing.T) {
...@@ -335,14 +404,14 @@ func TestCannonProposedOutputRootInvalid(t *testing.T) { ...@@ -335,14 +404,14 @@ func TestCannonProposedOutputRootInvalid(t *testing.T) {
sys.TimeTravelClock.AdvanceTime(game.GameDuration(ctx)) sys.TimeTravelClock.AdvanceTime(game.GameDuration(ctx))
require.NoError(t, wait.ForNextBlock(ctx, l1Client)) require.NoError(t, wait.ForNextBlock(ctx, l1Client))
game.WaitForGameStatus(ctx, disputegame.StatusDefenderWins) game.WaitForInactivity(ctx, 10, true)
game.LogGameData(ctx) game.LogGameData(ctx)
require.EqualValues(t, disputegame.StatusDefenderWins, game.Status(ctx))
}) })
} }
} }
func TestCannonPoisonedPostState(t *testing.T) { func TestCannonPoisonedPostState(t *testing.T) {
t.Skip("Known failure case")
InitParallel(t) InitParallel(t)
ctx := context.Background() ctx := context.Background()
...@@ -365,9 +434,12 @@ func TestCannonPoisonedPostState(t *testing.T) { ...@@ -365,9 +434,12 @@ func TestCannonPoisonedPostState(t *testing.T) {
// Honest defense at "dishonest" level // Honest defense at "dishonest" level
correctTrace.Defend(ctx, 1) correctTrace.Defend(ctx, 1)
// Dishonest attack at "honest" level - honest move would be to defend // Dishonest attack at "honest" level - honest move would be to ignore
game.Attack(ctx, 2, common.Hash{0x03, 0xaa}) game.Attack(ctx, 2, common.Hash{0x03, 0xaa})
// Honest attack at "dishonest" level - honest move would be to ignore
correctTrace.Attack(ctx, 3)
// Start the honest challenger // Start the honest challenger
game.StartChallenger(ctx, sys.RollupConfig, sys.L2GenesisCfg, l1Endpoint, l2Endpoint, "Honest", game.StartChallenger(ctx, sys.RollupConfig, sys.L2GenesisCfg, l1Endpoint, l2Endpoint, "Honest",
// Agree with the proposed output, so disagree with the root claim // Agree with the proposed output, so disagree with the root claim
...@@ -376,29 +448,40 @@ func TestCannonPoisonedPostState(t *testing.T) { ...@@ -376,29 +448,40 @@ func TestCannonPoisonedPostState(t *testing.T) {
) )
// Start dishonest challenger that posts correct claims // Start dishonest challenger that posts correct claims
game.StartChallenger(ctx, sys.RollupConfig, sys.L2GenesisCfg, l1Endpoint, l2Endpoint, "DishonestCorrect", // It participates in the subgame root the honest claim index 4
// Disagree with the proposed output, so agree with the root claim func() {
challenger.WithAgreeProposedOutput(false), claimCount := int64(5)
challenger.WithPrivKey(sys.cfg.Secrets.Mallory), depth := game.MaxDepth(ctx)
) for {
game.LogGameData(ctx)
// Give the challengers time to progress down the full game depth claimCount++
depth := game.MaxDepth(ctx) // Wait for the challenger to counter
for i := 3; i <= int(depth); i++ { game.WaitForClaimCount(ctx, claimCount)
game.WaitForClaimAtDepth(ctx, i)
game.LogGameData(ctx) // Respond with our own move
} correctTrace.Defend(ctx, claimCount-1)
claimCount++
game.WaitForClaimCount(ctx, claimCount)
// Defender moves last. If we're at max depth, then we're done
dishonestClaim := game.GetClaimUnsafe(ctx, claimCount-1)
pos := types.NewPositionFromGIndex(dishonestClaim.Position.Uint64())
if int64(pos.Depth()) == depth {
break
}
}
}()
// Wait for all the leaf nodes to be countered // Wait for the challenger to drive the subgame at 4 to the leaf node, which should be countered
// Wait for the challengers to drive the game down to the leaf node which should be countered game.WaitForClaimAtMaxDepth(ctx, true)
game.WaitForAllClaimsCountered(ctx)
// Time travel past when the game will be resolvable. // Time travel past when the game will be resolvable.
sys.TimeTravelClock.AdvanceTime(game.GameDuration(ctx)) sys.TimeTravelClock.AdvanceTime(game.GameDuration(ctx))
require.NoError(t, wait.ForNextBlock(ctx, l1Client)) require.NoError(t, wait.ForNextBlock(ctx, l1Client))
game.WaitForGameStatus(ctx, disputegame.StatusChallengerWins) game.WaitForInactivity(ctx, 10, true)
game.LogGameData(ctx) game.LogGameData(ctx)
require.EqualValues(t, disputegame.StatusChallengerWins, game.Status(ctx))
} }
// setupDisputeGameForInvalidOutputRoot sets up an L2 chain with at least one valid output root followed by an invalid output root. // setupDisputeGameForInvalidOutputRoot sets up an L2 chain with at least one valid output root followed by an invalid output root.
...@@ -470,8 +553,9 @@ func TestCannonChallengeWithCorrectRoot(t *testing.T) { ...@@ -470,8 +553,9 @@ func TestCannonChallengeWithCorrectRoot(t *testing.T) {
sys.TimeTravelClock.AdvanceTime(game.GameDuration(ctx)) sys.TimeTravelClock.AdvanceTime(game.GameDuration(ctx))
require.NoError(t, wait.ForNextBlock(ctx, l1Client)) require.NoError(t, wait.ForNextBlock(ctx, l1Client))
game.WaitForGameStatus(ctx, disputegame.StatusChallengerWins) game.WaitForInactivity(ctx, 10, true)
game.LogGameData(ctx) game.LogGameData(ctx)
require.EqualValues(t, disputegame.StatusChallengerWins, game.Status(ctx))
} }
func startFaultDisputeSystem(t *testing.T) (*System, *ethclient.Client) { func startFaultDisputeSystem(t *testing.T) (*System, *ethclient.Client) {
......
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