Commit 1740df6a authored by Adrian Sutton's avatar Adrian Sutton Committed by GitHub

op-e2e: Convert output_cannon tests to use claim helper (#8743)

parent 21fa288c
......@@ -3,6 +3,7 @@ package disputegame
import (
"context"
"fmt"
"slices"
"time"
"github.com/ethereum-optimism/optimism/op-challenger/game/fault/types"
......@@ -35,6 +36,10 @@ func (c *ClaimHelper) AgreesWithOutputRoot() bool {
return c.position.Depth()%2 == 0
}
func (c *ClaimHelper) IsRootClaim() bool {
return c.position.IsRootPosition()
}
func (c *ClaimHelper) IsOutputRoot(ctx context.Context) bool {
splitDepth := c.game.SplitDepth(ctx)
return int64(c.position.Depth()) <= splitDepth
......@@ -45,16 +50,25 @@ func (c *ClaimHelper) IsOutputRootLeaf(ctx context.Context) bool {
return int64(c.position.Depth()) == splitDepth
}
func (c *ClaimHelper) IsBottomGameRoot(ctx context.Context) bool {
splitDepth := c.game.SplitDepth(ctx)
return int64(c.position.Depth()) == splitDepth+1
}
func (c *ClaimHelper) IsMaxDepth(ctx context.Context) bool {
maxDepth := c.game.MaxDepth(ctx)
return int64(c.position.Depth()) == maxDepth
}
func (c *ClaimHelper) Depth() int64 {
return int64(c.position.Depth())
}
// WaitForCounterClaim waits for the claim to be countered by another claim being posted.
// It returns a helper for the claim that countered this one.
func (c *ClaimHelper) WaitForCounterClaim(ctx context.Context) *ClaimHelper {
counterIdx, counterClaim := c.game.waitForClaim(ctx, fmt.Sprintf("failed to find claim with parent idx %v", c.index), func(claim ContractClaim) bool {
return int64(claim.ParentIndex) == c.index
func (c *ClaimHelper) WaitForCounterClaim(ctx context.Context, ignoreClaims ...*ClaimHelper) *ClaimHelper {
counterIdx, counterClaim := c.game.waitForClaim(ctx, fmt.Sprintf("failed to find claim with parent idx %v", c.index), func(claimIdx int64, claim ContractClaim) bool {
return int64(claim.ParentIndex) == c.index && !containsClaim(claimIdx, ignoreClaims)
})
return newClaimHelper(c.game, counterIdx, counterClaim)
}
......@@ -92,3 +106,23 @@ func (c *ClaimHelper) Defend(ctx context.Context, value common.Hash) *ClaimHelpe
func (c *ClaimHelper) RequireDifferentClaimValue(other *ClaimHelper) {
c.require.NotEqual(c.claim, other.claim, "should have posted different claims")
}
func (c *ClaimHelper) RequireOnlyCounteredBy(ctx context.Context, expected ...*ClaimHelper) {
claims := c.game.getAllClaims(ctx)
for idx, claim := range claims {
if int64(claim.ParentIndex) != c.index {
// Doesn't counter this claim, so ignore
continue
}
if !containsClaim(int64(idx), expected) {
// Found a countering claim not in the expected list. Fail.
c.require.FailNowf("Found unexpected countering claim", "Parent claim index: %v Game state:\n%v", c.index, c.game.gameData(ctx))
}
}
}
func containsClaim(claimIdx int64, haystack []*ClaimHelper) bool {
return slices.ContainsFunc(haystack, func(candidate *ClaimHelper) bool {
return candidate.index == claimIdx
})
}
......@@ -246,7 +246,7 @@ func (g *FaultGameHelper) WaitForInactivity(ctx context.Context, numInactiveBloc
// DefendRootClaim uses the supplied Mover to perform moves in an attempt to defend the root claim.
// It is assumed that the output root being disputed is valid and that an honest op-challenger is already running.
// When the game has reached the maximum depth it waits for the honest challenger to counter the leaf claim with step.
func (g *FaultGameHelper) DefendRootClaim(ctx context.Context, performMove Mover) {
func (g *FaultGameHelper) DefendRootClaim(ctx context.Context, performMove func(parentClaimIdx int64)) {
maxDepth := g.MaxDepth(ctx)
for claimCount := int64(1); claimCount < maxDepth; {
g.LogGameData(ctx)
......@@ -268,7 +268,7 @@ func (g *FaultGameHelper) DefendRootClaim(ctx context.Context, performMove Mover
// It is assumed that the output root being disputed is invalid and that an honest op-challenger is already running.
// When the game has reached the maximum depth it calls the Stepper to attempt to counter the leaf claim.
// Since the output root is invalid, it should not be possible for the Stepper to call step successfully.
func (g *FaultGameHelper) ChallengeRootClaim(ctx context.Context, performMove Mover, attemptStep Stepper) {
func (g *FaultGameHelper) ChallengeRootClaim(ctx context.Context, performMove func(parentClaimIdx int64), attemptStep Stepper) {
maxDepth := g.MaxDepth(ctx)
for claimCount := int64(1); claimCount < maxDepth; {
g.LogGameData(ctx)
......
......@@ -145,7 +145,7 @@ func (g *OutputGameHelper) MaxDepth(ctx context.Context) int64 {
return depth.Int64()
}
func (g *OutputGameHelper) waitForClaim(ctx context.Context, errorMsg string, predicate func(claim ContractClaim) bool) (int64, ContractClaim) {
func (g *OutputGameHelper) waitForClaim(ctx context.Context, errorMsg string, predicate func(claimIdx int64, claim ContractClaim) bool) (int64, ContractClaim) {
timedCtx, cancel := context.WithTimeout(ctx, defaultTimeout)
defer cancel()
var matchedClaim ContractClaim
......@@ -161,7 +161,7 @@ func (g *OutputGameHelper) waitForClaim(ctx context.Context, errorMsg string, pr
if err != nil {
return false, fmt.Errorf("retrieve claim %v: %w", i, err)
}
if predicate(claimData) {
if predicate(i, claimData) {
matchClaimIdx = i
matchedClaim = claimData
return true, nil
......@@ -206,10 +206,13 @@ func (g *OutputGameHelper) GetClaimValue(ctx context.Context, claimIdx int64) co
return claim.Claim
}
func (g *OutputGameHelper) GetClaimPosition(ctx context.Context, claimIdx int64) types.Position {
g.WaitForClaimCount(ctx, claimIdx+1)
claim := g.getClaim(ctx, claimIdx)
return types.NewPositionFromGIndex(claim.Position)
func (g *OutputGameHelper) getAllClaims(ctx context.Context) []ContractClaim {
count := g.getClaimCount(ctx)
var claims []ContractClaim
for i := int64(0); i < count; i++ {
claims = append(claims, g.getClaim(ctx, i))
}
return claims
}
// getClaim retrieves the claim data for a specific index.
......@@ -226,7 +229,7 @@ func (g *OutputGameHelper) WaitForClaimAtDepth(ctx context.Context, depth int) {
g.waitForClaim(
ctx,
fmt.Sprintf("Could not find claim depth %v", depth),
func(claim ContractClaim) bool {
func(_ int64, claim ContractClaim) bool {
pos := types.NewPositionFromGIndex(claim.Position)
return pos.Depth() == depth
})
......@@ -237,7 +240,7 @@ func (g *OutputGameHelper) WaitForClaimAtMaxDepth(ctx context.Context, countered
g.waitForClaim(
ctx,
fmt.Sprintf("Could not find claim depth %v with countered=%v", maxDepth, countered),
func(claim ContractClaim) bool {
func(_ int64, claim ContractClaim) bool {
pos := types.NewPositionFromGIndex(claim.Position)
return int64(pos.Depth()) == maxDepth && claim.Countered == countered
})
......@@ -325,50 +328,42 @@ func (g *OutputGameHelper) WaitForInactivity(ctx context.Context, numInactiveBlo
}
// Mover is a function that either attacks or defends the claim at parentClaimIdx
type Mover func(parentClaimIdx int64)
type Mover func(parent *ClaimHelper) *ClaimHelper
// Stepper is a function that attempts to perform a step against the claim at parentClaimIdx
type Stepper func(parentClaimIdx int64)
// DefendRootClaim uses the supplied Mover to perform moves in an attempt to defend the root claim.
// It is assumed that the output root being disputed is valid and that an honest op-challenger is already running.
// DefendClaim uses the supplied Mover to perform moves in an attempt to defend the supplied claim.
// It is assumed that the specified claim is invalid and that an honest op-challenger is already running.
// When the game has reached the maximum depth it waits for the honest challenger to counter the leaf claim with step.
func (g *OutputGameHelper) DefendRootClaim(ctx context.Context, performMove Mover) {
maxDepth := g.MaxDepth(ctx)
for claimCount := g.getClaimCount(ctx); claimCount < maxDepth; {
func (g *OutputGameHelper) DefendClaim(ctx context.Context, claim *ClaimHelper, performMove Mover) {
g.t.Logf("Defending claim %v at depth %v", claim.index, claim.Depth())
for !claim.IsMaxDepth(ctx) {
g.LogGameData(ctx)
claimCount++
// Wait for the challenger to counter
g.WaitForClaimCount(ctx, claimCount)
claim = claim.WaitForCounterClaim(ctx)
g.LogGameData(ctx)
// Respond with our own move
performMove(claimCount - 1)
claimCount++
g.WaitForClaimCount(ctx, claimCount)
claim = performMove(claim)
}
// Wait for the challenger to call step and counter our invalid claim
g.WaitForClaimAtMaxDepth(ctx, true)
claim.WaitForCountered(ctx)
}
// ChallengeRootClaim uses the supplied Mover and Stepper to perform moves and steps in an attempt to challenge the root claim.
// It is assumed that the output root being disputed is invalid and that an honest op-challenger is already running.
// ChallengeClaim uses the supplied functions to perform moves and steps in an attempt to challenge the supplied claim.
// It is assumed that the claim being disputed is valid and that an honest op-challenger is already running.
// When the game has reached the maximum depth it calls the Stepper to attempt to counter the leaf claim.
// Since the output root is invalid, it should not be possible for the Stepper to call step successfully.
func (g *OutputGameHelper) ChallengeRootClaim(ctx context.Context, performMove Mover, attemptStep Stepper) {
maxDepth := g.MaxDepth(ctx)
for claimCount := g.getClaimCount(ctx); claimCount < maxDepth; {
// Since the output root is valid, it should not be possible for the Stepper to call step successfully.
func (g *OutputGameHelper) ChallengeClaim(ctx context.Context, claim *ClaimHelper, performMove Mover, attemptStep Stepper) {
for !claim.IsMaxDepth(ctx) {
g.LogGameData(ctx)
// Perform our move
performMove(claimCount - 1)
claimCount++
g.WaitForClaimCount(ctx, claimCount)
claim = performMove(claim)
// Wait for the challenger to counter
claimCount++
g.WaitForClaimCount(ctx, claimCount)
g.LogGameData(ctx)
claim = claim.WaitForCounterClaim(ctx)
}
// Confirm the game has reached max depth and the last claim hasn't been countered
......@@ -376,7 +371,7 @@ func (g *OutputGameHelper) ChallengeRootClaim(ctx context.Context, performMove M
g.LogGameData(ctx)
// It's on us to call step if we want to win but shouldn't be possible
attemptStep(maxDepth)
attemptStep(claim.index)
}
func (g *OutputGameHelper) getClaimCount(ctx context.Context) int64 {
......@@ -405,6 +400,7 @@ func (g *OutputGameHelper) waitForNewClaim(ctx context.Context, checkPoint int64
}
func (g *OutputGameHelper) Attack(ctx context.Context, claimIdx int64, claim common.Hash) {
g.t.Logf("Attacking claim %v with value %v", claimIdx, claim)
tx, err := g.game.Attack(g.opts, big.NewInt(claimIdx), claim)
if err != nil {
g.require.NoErrorf(err, "Attack transaction did not send. Game state: \n%v", g.gameData(ctx))
......@@ -416,6 +412,7 @@ func (g *OutputGameHelper) Attack(ctx context.Context, claimIdx int64, claim com
}
func (g *OutputGameHelper) Defend(ctx context.Context, claimIdx int64, claim common.Hash) {
g.t.Logf("Defending claim %v with value %v", claimIdx, claim)
tx, err := g.game.Defend(g.opts, big.NewInt(claimIdx), claim)
if err != nil {
g.require.NoErrorf(err, "Defend transaction did not send. Game state: \n%v", g.gameData(ctx))
......
......@@ -18,6 +18,16 @@ type OutputHonestHelper struct {
correctTrace types.TraceAccessor
}
func (h *OutputHonestHelper) AttackClaim(ctx context.Context, claim *ClaimHelper) *ClaimHelper {
h.Attack(ctx, claim.index)
return claim.WaitForCounterClaim(ctx)
}
func (h *OutputHonestHelper) DefendClaim(ctx context.Context, claim *ClaimHelper) *ClaimHelper {
h.Defend(ctx, claim.index)
return claim.WaitForCounterClaim(ctx)
}
func (h *OutputHonestHelper) Attack(ctx context.Context, claimIdx int64) {
// Ensure the claim exists
h.game.WaitForClaimCount(ctx, claimIdx+1)
......
......@@ -123,18 +123,19 @@ func TestOutputCannonDisputeGame(t *testing.T) {
require.NotNil(t, game)
game.LogGameData(ctx)
game.DisputeLastBlock(ctx)
outputClaim := game.DisputeLastBlock(ctx)
splitDepth := game.SplitDepth(ctx)
game.StartChallenger(ctx, "sequencer", "Challenger", challenger.WithPrivKey(sys.Cfg.Secrets.Alice))
game.DefendRootClaim(
game.DefendClaim(
ctx,
func(parentClaimIdx int64) {
if parentClaimIdx+1 == splitDepth+test.defendClaimDepth {
game.Defend(ctx, parentClaimIdx, common.Hash{byte(parentClaimIdx)})
outputClaim,
func(claim *disputegame.ClaimHelper) *disputegame.ClaimHelper {
if claim.Depth()+1 == splitDepth+test.defendClaimDepth {
return claim.Defend(ctx, common.Hash{byte(claim.Depth())})
} else {
game.Attack(ctx, parentClaimIdx, common.Hash{byte(parentClaimIdx)})
return claim.Attack(ctx, common.Hash{byte(claim.Depth())})
}
})
......@@ -157,21 +158,21 @@ func TestOutputCannonDefendStep(t *testing.T) {
disputeGameFactory := disputegame.NewFactoryHelper(t, ctx, sys)
game := disputeGameFactory.StartOutputCannonGame(ctx, "sequencer", 1, common.Hash{0x01, 0xaa})
require.NotNil(t, game)
game.DisputeLastBlock(ctx)
outputRootClaim := game.DisputeLastBlock(ctx)
game.LogGameData(ctx)
game.StartChallenger(ctx, "sequencer", "Challenger", challenger.WithPrivKey(sys.Cfg.Secrets.Alice))
correctTrace := game.CreateHonestActor(ctx, "sequencer", challenger.WithPrivKey(sys.Cfg.Secrets.Mallory))
splitDepth := game.SplitDepth(ctx)
game.DefendRootClaim(ctx, func(parentClaimIdx int64) {
maxDepth := game.MaxDepth(ctx)
game.DefendClaim(ctx, outputRootClaim, func(claim *disputegame.ClaimHelper) *disputegame.ClaimHelper {
// Post invalid claims for most steps to get down into the early part of the trace
if parentClaimIdx < splitDepth+27 {
game.Attack(ctx, parentClaimIdx, common.Hash{byte(parentClaimIdx)})
if claim.Depth() < maxDepth-3 {
return claim.Attack(ctx, common.Hash{0xaa})
} else {
// Post our own counter but using the correct hash in low levels to force a defense step
correctTrace.Attack(ctx, parentClaimIdx)
return correctTrace.AttackClaim(ctx, claim)
}
})
......@@ -198,7 +199,7 @@ func TestOutputCannonProposedOutputRootValid(t *testing.T) {
// performMove is called to respond to each claim posted by the honest op-challenger.
// It should either attack or defend the claim at parentClaimIdx
performMove func(ctx context.Context, game *disputegame.OutputCannonGameHelper, correctTrace *disputegame.OutputHonestHelper, parentClaimIdx int64)
performMove func(ctx context.Context, game *disputegame.OutputCannonGameHelper, correctTrace *disputegame.OutputHonestHelper, claim *disputegame.ClaimHelper) *disputegame.ClaimHelper
// performStep is called once the maximum game depth is reached. It should perform a step to counter the
// claim at parentClaimIdx. Since the proposed output root is invalid, the step call should always revert.
......@@ -206,38 +207,33 @@ func TestOutputCannonProposedOutputRootValid(t *testing.T) {
}{
{
name: "AttackWithCorrectTrace",
performMove: func(ctx context.Context, game *disputegame.OutputCannonGameHelper, correctTrace *disputegame.OutputHonestHelper, parentClaimIdx int64) {
performMove: func(ctx context.Context, game *disputegame.OutputCannonGameHelper, correctTrace *disputegame.OutputHonestHelper, claim *disputegame.ClaimHelper) *disputegame.ClaimHelper {
// Attack everything but oddly using the correct hash.
// Except the root of the cannon game must have an invalid VM status code.
splitDepth := game.SplitDepth(ctx)
if splitDepth == parentClaimIdx {
if claim.IsOutputRootLeaf(ctx) {
// TODO(client-pod#262): Verify that an attack with a valid status code is rejected
game.Attack(ctx, parentClaimIdx, common.Hash{0x01})
return
return claim.Attack(ctx, common.Hash{0x01})
}
correctTrace.Attack(ctx, parentClaimIdx)
return correctTrace.AttackClaim(ctx, claim)
},
performStep: honestStepsFail,
},
{
name: "DefendWithCorrectTrace",
performMove: func(ctx context.Context, game *disputegame.OutputCannonGameHelper, correctTrace *disputegame.OutputHonestHelper, parentClaimIdx int64) {
splitDepth := game.SplitDepth(ctx)
performMove: func(ctx context.Context, game *disputegame.OutputCannonGameHelper, correctTrace *disputegame.OutputHonestHelper, claim *disputegame.ClaimHelper) *disputegame.ClaimHelper {
// Can only attack the root claim or the first cannon claim
if parentClaimIdx == 0 {
correctTrace.Attack(ctx, parentClaimIdx)
return
if claim.IsRootClaim() {
return correctTrace.AttackClaim(ctx, claim)
}
// The root of the cannon game must have an invalid VM status code
// Attacking ensure we're running the cannon trace between two different blocks
// instead of being in the trace extension of the output root bisection
if splitDepth == parentClaimIdx {
if claim.IsOutputRootLeaf(ctx) {
// TODO(client-pod#262): Verify that an attack with a valid status code is rejected
game.Attack(ctx, parentClaimIdx, common.Hash{0x01})
return
return claim.Attack(ctx, common.Hash{0x01})
}
// Otherwise, defend everything using the correct hash.
correctTrace.Defend(ctx, parentClaimIdx)
return correctTrace.DefendClaim(ctx, claim)
},
performStep: honestStepsFail,
},
......@@ -259,9 +255,10 @@ func TestOutputCannonProposedOutputRootValid(t *testing.T) {
game.StartChallenger(ctx, "sequencer", "Challenger", challenger.WithPrivKey(sys.Cfg.Secrets.Alice))
// Now maliciously play the game and it should be impossible to win
game.ChallengeRootClaim(ctx,
func(parentClaimIdx int64) {
test.performMove(ctx, game, correctTrace, parentClaimIdx)
game.ChallengeClaim(ctx,
game.RootClaim(ctx),
func(claim *disputegame.ClaimHelper) *disputegame.ClaimHelper {
return test.performMove(ctx, game, correctTrace, claim)
},
func(parentClaimIdx int64) {
test.performStep(ctx, game, correctTrace, parentClaimIdx)
......@@ -291,52 +288,48 @@ func TestOutputCannonPoisonedPostState(t *testing.T) {
correctTrace := game.CreateHonestActor(ctx, "sequencer", challenger.WithPrivKey(sys.Cfg.Secrets.Alice))
// Honest first attack at "honest" level
correctTrace.Attack(ctx, 0)
claim := correctTrace.AttackClaim(ctx, game.RootClaim(ctx))
// Honest defense at "dishonest" level
correctTrace.Defend(ctx, 1)
claim = correctTrace.DefendClaim(ctx, claim)
// Dishonest attack at "honest" level - honest move would be to ignore
game.Attack(ctx, 2, common.Hash{0x03, 0xaa})
claimToIgnore1 := claim.Attack(ctx, common.Hash{0x03, 0xaa})
// Honest attack at "dishonest" level - honest move would be to ignore
correctTrace.Attack(ctx, 3)
claimToIgnore2 := correctTrace.AttackClaim(ctx, claimToIgnore1)
game.LogGameData(ctx)
// Start the honest challenger
game.StartChallenger(ctx, "sequencer", "Honest", challenger.WithPrivKey(sys.Cfg.Secrets.Bob))
// Start dishonest challenger that posts correct claims
// It participates in the subgame root the honest claim index 4
claimCount := int64(5)
depth := game.MaxDepth(ctx)
splitDepth := game.SplitDepth(ctx)
for {
game.LogGameData(ctx)
claimCount++
// Wait for the challenger to counter
game.WaitForClaimCount(ctx, claimCount)
// Note that we need to ignore claimToIgnore1 which already counters this...
claim = claim.WaitForCounterClaim(ctx, claimToIgnore1)
// Respond with our own move
if claimCount == splitDepth+4 {
if claim.IsBottomGameRoot(ctx) {
// Root of the cannon game must have the right VM status code (so it can't be honest).
// Note this occurs when there are splitDepth + 4 claims because there are multiple forks in this game.
game.Attack(ctx, claimCount-1, common.Hash{0x01})
claim = claim.Attack(ctx, common.Hash{0x01})
} else {
correctTrace.Defend(ctx, claimCount-1)
claim = correctTrace.DefendClaim(ctx, claim)
}
claimCount++
game.WaitForClaimCount(ctx, claimCount)
// Defender moves last. If we're at max depth, then we're done
pos := game.GetClaimPosition(ctx, claimCount-1)
if int64(pos.Depth()) == depth {
if claim.IsMaxDepth(ctx) {
break
}
}
// Wait for the challenger to drive the subgame at 4 to the leaf node, which should be countered
game.WaitForClaimAtMaxDepth(ctx, true)
// Wait for the challenger to call step
claim.WaitForCountered(ctx)
// Verify that the challenger didn't challenge our poisoned claims
claimToIgnore1.RequireOnlyCounteredBy(ctx, claimToIgnore2)
claimToIgnore2.RequireOnlyCounteredBy(ctx /* nothing */)
// Time travel past when the game will be resolvable.
sys.TimeTravelClock.AdvanceTime(game.GameDuration(ctx))
......
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