Commit 267aac82 authored by Adrian Sutton's avatar Adrian Sutton Committed by GitHub

op-e2e: Introduce ClaimHelper to simplify test logic (#8547)

parent 5e60f1dc
package disputegame
import (
"context"
"fmt"
"time"
"github.com/ethereum-optimism/optimism/op-challenger/game/fault/types"
"github.com/ethereum-optimism/optimism/op-e2e/e2eutils/wait"
"github.com/ethereum/go-ethereum/common"
"github.com/stretchr/testify/require"
)
type ClaimHelper struct {
require *require.Assertions
game *OutputGameHelper
index int64
parentIndex uint32
position types.Position
claim common.Hash
}
func newClaimHelper(game *OutputGameHelper, idx int64, claim ContractClaim) *ClaimHelper {
return &ClaimHelper{
require: game.require,
game: game,
index: idx,
parentIndex: claim.ParentIndex,
position: types.NewPositionFromGIndex(claim.Position),
claim: claim.Claim,
}
}
func (c *ClaimHelper) AgreesWithOutputRoot() bool {
return c.position.Depth()%2 == 0
}
func (c *ClaimHelper) IsOutputRoot(ctx context.Context) bool {
splitDepth := c.game.SplitDepth(ctx)
return int64(c.position.Depth()) <= splitDepth
}
func (c *ClaimHelper) IsOutputRootLeaf(ctx context.Context) bool {
splitDepth := c.game.SplitDepth(ctx)
return int64(c.position.Depth()) == splitDepth
}
func (c *ClaimHelper) IsMaxDepth(ctx context.Context) bool {
maxDepth := c.game.MaxDepth(ctx)
return int64(c.position.Depth()) == maxDepth
}
// 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
})
return newClaimHelper(c.game, counterIdx, counterClaim)
}
// WaitForCountered waits until the claim is countered either by a child claim or by a step call.
func (c *ClaimHelper) WaitForCountered(ctx context.Context) {
timedCtx, cancel := context.WithTimeout(ctx, defaultTimeout)
defer cancel()
err := wait.For(timedCtx, time.Second, func() (bool, error) {
latestData := c.game.getClaim(ctx, c.index)
return latestData.Countered, nil
})
if err != nil { // Avoid waiting time capturing game data when there's no error
c.require.NoErrorf(err, "Claim %v was not countered\n%v", c.index, c.game.gameData(ctx))
}
}
func (c *ClaimHelper) RequireCorrectOutputRoot(ctx context.Context) {
c.require.True(c.IsOutputRoot(ctx), "Should not expect a valid output root in the bottom game")
expected, err := c.game.correctOutputProvider.Get(ctx, c.position)
c.require.NoError(err, "Failed to get correct output root")
c.require.Equalf(expected, c.claim, "Should have correct output root in claim %v and position %v", c.index, c.position)
}
func (c *ClaimHelper) Attack(ctx context.Context, value common.Hash) *ClaimHelper {
c.game.Attack(ctx, c.index, value)
return c.WaitForCounterClaim(ctx)
}
func (c *ClaimHelper) Defend(ctx context.Context, value common.Hash) *ClaimHelper {
c.game.Defend(ctx, c.index, value)
return c.WaitForCounterClaim(ctx)
}
...@@ -57,32 +57,39 @@ func (g *OutputGameHelper) GenesisBlockNum(ctx context.Context) uint64 { ...@@ -57,32 +57,39 @@ func (g *OutputGameHelper) GenesisBlockNum(ctx context.Context) uint64 {
// DisputeLastBlock posts claims from both the honest and dishonest actor to progress the output root part of the game // DisputeLastBlock posts claims from both the honest and dishonest actor to progress the output root part of the game
// through to the split depth and the claims are setup such that the last block in the game range is the block // through to the split depth and the claims are setup such that the last block in the game range is the block
// to execute cannon on. ie the first block the honest and dishonest actors disagree about is the l2 block of the game. // to execute cannon on. ie the first block the honest and dishonest actors disagree about is the l2 block of the game.
func (g *OutputGameHelper) DisputeLastBlock(ctx context.Context) { func (g *OutputGameHelper) DisputeLastBlock(ctx context.Context) *ClaimHelper {
rootClaim := g.GetClaimValue(ctx, 0) rootClaim := g.GetClaimValue(ctx, 0)
disputeBlockNum := g.L2BlockNum(ctx) disputeBlockNum := g.L2BlockNum(ctx)
splitDepth := int(g.SplitDepth(ctx))
pos := types.NewPositionFromGIndex(big.NewInt(1)) pos := types.NewPositionFromGIndex(big.NewInt(1))
getClaimValue := func(parentClaimIdx int, claimPos types.Position) common.Hash { getClaimValue := func(parentClaim *ClaimHelper, claimPos types.Position) common.Hash {
claimBlockNum, err := g.correctOutputProvider.BlockNumber(claimPos) claimBlockNum, err := g.correctOutputProvider.BlockNumber(claimPos)
g.require.NoError(err, "failed to calculate claim block number") g.require.NoError(err, "failed to calculate claim block number")
// Use the correct output root for the challenger and incorrect for the defender // Use the correct output root for the challenger and incorrect for the defender
if parentClaimIdx%2 == 0 || claimBlockNum < disputeBlockNum { if parentClaim.AgreesWithOutputRoot() || claimBlockNum < disputeBlockNum {
return g.correctOutputRoot(ctx, claimPos) return g.correctOutputRoot(ctx, claimPos)
} else { } else {
return rootClaim return rootClaim
} }
} }
for i := 0; i < splitDepth; i++ {
claim := g.RootClaim(ctx)
for !claim.IsOutputRootLeaf(ctx) {
parentClaimBlockNum, err := g.correctOutputProvider.BlockNumber(pos) parentClaimBlockNum, err := g.correctOutputProvider.BlockNumber(pos)
g.require.NoError(err, "failed to calculate parent claim block number") g.require.NoError(err, "failed to calculate parent claim block number")
if parentClaimBlockNum >= disputeBlockNum { if parentClaimBlockNum >= disputeBlockNum {
pos = pos.Attack() pos = pos.Attack()
g.Attack(ctx, int64(i), getClaimValue(i, pos)) claim = claim.Attack(ctx, getClaimValue(claim, pos))
} else { } else {
pos = pos.Defend() pos = pos.Defend()
g.Defend(ctx, int64(i), getClaimValue(i, pos)) claim = claim.Defend(ctx, getClaimValue(claim, pos))
} }
} }
return claim
}
func (g *OutputGameHelper) RootClaim(ctx context.Context) *ClaimHelper {
claim := g.getClaim(ctx, 0)
return newClaimHelper(g, 0, claim)
} }
func (g *OutputGameHelper) WaitForCorrectOutputRoot(ctx context.Context, claimIdx int64) { func (g *OutputGameHelper) WaitForCorrectOutputRoot(ctx context.Context, claimIdx int64) {
...@@ -138,9 +145,11 @@ func (g *OutputGameHelper) MaxDepth(ctx context.Context) int64 { ...@@ -138,9 +145,11 @@ func (g *OutputGameHelper) MaxDepth(ctx context.Context) int64 {
return depth.Int64() return depth.Int64()
} }
func (g *OutputGameHelper) waitForClaim(ctx context.Context, errorMsg string, predicate func(claim ContractClaim) bool) { func (g *OutputGameHelper) waitForClaim(ctx context.Context, errorMsg string, predicate func(claim ContractClaim) bool) (int64, ContractClaim) {
timedCtx, cancel := context.WithTimeout(ctx, defaultTimeout) timedCtx, cancel := context.WithTimeout(ctx, defaultTimeout)
defer cancel() defer cancel()
var matchedClaim ContractClaim
var matchClaimIdx int64
err := wait.For(timedCtx, time.Second, func() (bool, error) { err := wait.For(timedCtx, time.Second, func() (bool, error) {
count, err := g.game.ClaimDataLen(&bind.CallOpts{Context: timedCtx}) count, err := g.game.ClaimDataLen(&bind.CallOpts{Context: timedCtx})
if err != nil { if err != nil {
...@@ -153,6 +162,8 @@ func (g *OutputGameHelper) waitForClaim(ctx context.Context, errorMsg string, pr ...@@ -153,6 +162,8 @@ func (g *OutputGameHelper) waitForClaim(ctx context.Context, errorMsg string, pr
return false, fmt.Errorf("retrieve claim %v: %w", i, err) return false, fmt.Errorf("retrieve claim %v: %w", i, err)
} }
if predicate(claimData) { if predicate(claimData) {
matchClaimIdx = i
matchedClaim = claimData
return true, nil return true, nil
} }
} }
...@@ -161,6 +172,7 @@ func (g *OutputGameHelper) waitForClaim(ctx context.Context, errorMsg string, pr ...@@ -161,6 +172,7 @@ func (g *OutputGameHelper) waitForClaim(ctx context.Context, errorMsg string, pr
if err != nil { // Avoid waiting time capturing game data when there's no error if err != nil { // Avoid waiting time capturing game data when there's no error
g.require.NoErrorf(err, "%v\n%v", errorMsg, g.gameData(ctx)) g.require.NoErrorf(err, "%v\n%v", errorMsg, g.gameData(ctx))
} }
return matchClaimIdx, matchedClaim
} }
func (g *OutputGameHelper) waitForNoClaim(ctx context.Context, errorMsg string, predicate func(claim ContractClaim) bool) { func (g *OutputGameHelper) waitForNoClaim(ctx context.Context, errorMsg string, predicate func(claim ContractClaim) bool) {
......
...@@ -28,33 +28,41 @@ func TestOutputCannonGame(t *testing.T) { ...@@ -28,33 +28,41 @@ func TestOutputCannonGame(t *testing.T) {
game.StartChallenger(ctx, "sequencer", "Challenger", challenger.WithPrivKey(sys.Cfg.Secrets.Alice)) game.StartChallenger(ctx, "sequencer", "Challenger", challenger.WithPrivKey(sys.Cfg.Secrets.Alice))
game.LogGameData(ctx) game.LogGameData(ctx)
// Challenger should post an output root to counter claims down to the leaf level of the top game // Challenger should post an output root to counter claims down to the leaf level of the top game
splitDepth := game.SplitDepth(ctx) claim := game.RootClaim(ctx)
for i := int64(1); i < splitDepth; i += 2 { for claim.IsOutputRoot(ctx) && !claim.IsOutputRootLeaf(ctx) {
game.WaitForCorrectOutputRoot(ctx, i) if claim.AgreesWithOutputRoot() {
game.Attack(ctx, i, common.Hash{0xaa}) // If the latest claim agrees with the output root, expect the honest challenger to counter it
game.LogGameData(ctx) claim = claim.WaitForCounterClaim(ctx)
game.LogGameData(ctx)
claim.RequireCorrectOutputRoot(ctx)
} else {
// Otherwise we should counter
claim = claim.Attack(ctx, common.Hash{0xaa})
game.LogGameData(ctx)
}
} }
// Wait for the challenger to post the first claim in the cannon trace // Wait for the challenger to post the first claim in the cannon trace
game.WaitForClaimAtDepth(ctx, int(splitDepth+1)) claim = claim.WaitForCounterClaim(ctx)
game.LogGameData(ctx) game.LogGameData(ctx)
game.Attack(ctx, splitDepth+1, common.Hash{0x00, 0xcc}) // Attack the root of the cannon trace subgame
gameDepth := game.MaxDepth(ctx) claim = claim.Attack(ctx, common.Hash{0x00, 0xcc})
for i := splitDepth + 3; i < gameDepth; i += 2 { for !claim.IsMaxDepth(ctx) {
// Wait for challenger to respond if claim.AgreesWithOutputRoot() {
game.WaitForClaimAtDepth(ctx, int(i)) // If the latest claim supports the output root, wait for the honest challenger to respond
game.LogGameData(ctx) claim = claim.WaitForCounterClaim(ctx)
game.LogGameData(ctx)
// Respond to push the game down to the max depth } else {
game.Defend(ctx, i, common.Hash{0x00, 0xdd}) // Otherwise we need to counter the honest claim
game.LogGameData(ctx) claim = claim.Defend(ctx, common.Hash{0x00, 0xdd})
game.LogGameData(ctx)
}
} }
game.LogGameData(ctx)
// Challenger should be able to call step and counter the leaf claim. // Challenger should be able to call step and counter the leaf claim.
game.WaitForClaimAtMaxDepth(ctx, true) claim.WaitForCountered(ctx)
game.LogGameData(ctx) game.LogGameData(ctx)
sys.TimeTravelClock.AdvanceTime(game.GameDuration(ctx)) 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