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 {
// 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
// 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)
disputeBlockNum := g.L2BlockNum(ctx)
splitDepth := int(g.SplitDepth(ctx))
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)
g.require.NoError(err, "failed to calculate claim block number")
// 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)
} else {
return rootClaim
}
}
for i := 0; i < splitDepth; i++ {
claim := g.RootClaim(ctx)
for !claim.IsOutputRootLeaf(ctx) {
parentClaimBlockNum, err := g.correctOutputProvider.BlockNumber(pos)
g.require.NoError(err, "failed to calculate parent claim block number")
if parentClaimBlockNum >= disputeBlockNum {
pos = pos.Attack()
g.Attack(ctx, int64(i), getClaimValue(i, pos))
claim = claim.Attack(ctx, getClaimValue(claim, pos))
} else {
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) {
......@@ -138,9 +145,11 @@ 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) {
func (g *OutputGameHelper) waitForClaim(ctx context.Context, errorMsg string, predicate func(claim ContractClaim) bool) (int64, ContractClaim) {
timedCtx, cancel := context.WithTimeout(ctx, defaultTimeout)
defer cancel()
var matchedClaim ContractClaim
var matchClaimIdx int64
err := wait.For(timedCtx, time.Second, func() (bool, error) {
count, err := g.game.ClaimDataLen(&bind.CallOpts{Context: timedCtx})
if err != nil {
......@@ -153,6 +162,8 @@ func (g *OutputGameHelper) waitForClaim(ctx context.Context, errorMsg string, pr
return false, fmt.Errorf("retrieve claim %v: %w", i, err)
}
if predicate(claimData) {
matchClaimIdx = i
matchedClaim = claimData
return true, nil
}
}
......@@ -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
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) {
......
......@@ -28,33 +28,41 @@ func TestOutputCannonGame(t *testing.T) {
game.StartChallenger(ctx, "sequencer", "Challenger", challenger.WithPrivKey(sys.Cfg.Secrets.Alice))
game.LogGameData(ctx)
// Challenger should post an output root to counter claims down to the leaf level of the top game
splitDepth := game.SplitDepth(ctx)
for i := int64(1); i < splitDepth; i += 2 {
game.WaitForCorrectOutputRoot(ctx, i)
game.Attack(ctx, i, common.Hash{0xaa})
game.LogGameData(ctx)
claim := game.RootClaim(ctx)
for claim.IsOutputRoot(ctx) && !claim.IsOutputRootLeaf(ctx) {
if claim.AgreesWithOutputRoot() {
// If the latest claim agrees with the output root, expect the honest challenger to counter it
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
game.WaitForClaimAtDepth(ctx, int(splitDepth+1))
claim = claim.WaitForCounterClaim(ctx)
game.LogGameData(ctx)
game.Attack(ctx, splitDepth+1, common.Hash{0x00, 0xcc})
gameDepth := game.MaxDepth(ctx)
for i := splitDepth + 3; i < gameDepth; i += 2 {
// Wait for challenger to respond
game.WaitForClaimAtDepth(ctx, int(i))
game.LogGameData(ctx)
// Respond to push the game down to the max depth
game.Defend(ctx, i, common.Hash{0x00, 0xdd})
game.LogGameData(ctx)
// Attack the root of the cannon trace subgame
claim = claim.Attack(ctx, common.Hash{0x00, 0xcc})
for !claim.IsMaxDepth(ctx) {
if claim.AgreesWithOutputRoot() {
// If the latest claim supports the output root, wait for the honest challenger to respond
claim = claim.WaitForCounterClaim(ctx)
game.LogGameData(ctx)
} else {
// Otherwise we need to counter the honest claim
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.
game.WaitForClaimAtMaxDepth(ctx, true)
claim.WaitForCountered(ctx)
game.LogGameData(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