Commit 454060fb authored by Adrian Sutton's avatar Adrian Sutton Committed by GitHub

op-e2e: Add exhaustive alphabet test (#8806)

* op-e2e: Add test demonstrating incorrect alphabet trace implementation.

* rebase fixes

* op-e2e: Skip known broken tests.

* op-e2e: Add exhaustive alphabet test

* op-challenger: fix alphabet trace provider to use l2 block number

* fix up step function calculations

* fix challenger tests

* op-challenger: convert the absolute prestate directly to a big int

* op-challenger: stash inlined calculations to implement iterative step loop

* fix state data construction

* fix(op-challenger): Iterative Step Loop Implementation (#8843)

* Fix formatting.

* op-challenger: Fix off by one error in alphabet trace provider.

Updates the e2e tests to hit the case where the honest challenger calls step to prove their claim is correct.
This requires the on-chain VM execution to match the challenger's version.

---------
Co-authored-by: default avatarrefcell <abigger87@gmail.com>
parent a26430af
......@@ -50,12 +50,13 @@ func (ap *AlphabetTraceProvider) GetStepData(ctx context.Context, pos types.Posi
return absolutePrestate, []byte{}, preimageData, nil
}
// We want the pre-state which is the value prior to the one requested
prestateTraceIndex := posIndex.Sub(posIndex, big.NewInt(1))
prestateTraceIndex := new(big.Int).Sub(posIndex, big.NewInt(1))
if prestateTraceIndex.Cmp(new(big.Int).SetUint64(ap.maxLen)) >= 0 {
return nil, nil, nil, fmt.Errorf("%w depth: %v index: %v max: %v", ErrIndexTooLarge, ap.depth, posIndex, ap.maxLen)
}
claim := BuildAlphabetPreimage(big.NewInt(0), absolutePrestateInt)
for i := big.NewInt(0); i.Cmp(posIndex) < 0; i = i.Add(i, big.NewInt(1)) {
// First step expands the absolute preimage to its full form. Weird but it's how AlphabetVM works.
claim := ap.step(absolutePrestate)
for i := big.NewInt(0); i.Cmp(prestateTraceIndex) <= 0; i = i.Add(i, big.NewInt(1)) {
claim = ap.step(claim)
}
return claim, []byte{}, preimageData, nil
......@@ -66,7 +67,7 @@ func (ap *AlphabetTraceProvider) step(stateData []byte) []byte {
// Decode the stateData into the trace index and claim
traceIndex := new(big.Int).SetBytes(stateData[:32])
claim := stateData[32:]
if bytes.Equal(claim, absolutePrestate) {
if bytes.Equal(stateData, absolutePrestate) {
initTraceIndex := new(big.Int).Lsh(ap.startingBlockNumber, 4)
initClaim := new(big.Int).Add(absolutePrestateInt, initTraceIndex)
return BuildAlphabetPreimage(initTraceIndex, initClaim)
......
......@@ -2,6 +2,7 @@ package alphabet
import (
"context"
"fmt"
"math/big"
"testing"
......@@ -17,30 +18,30 @@ func alphabetClaim(index *big.Int, claim *big.Int) common.Hash {
return alphabetStateHash(BuildAlphabetPreimage(index, claim))
}
func TestAlphabetProvider_Step(t *testing.T) {
depth := types.Depth(2)
startingL2BlockNumber := big.NewInt(1)
func TestAlphabetProvider_Prestate(t *testing.T) {
depth := types.Depth(4)
startingL2BlockNumber := big.NewInt(2)
// Actual preimage values generated by the solidity AlphabetVM at each step.
expectedPrestates := []string{
"0000000000000000000000000000000000000000000000000000000000000060",
"00000000000000000000000000000000000000000000000000000000000000210000000000000000000000000000000000000000000000000000000000000081",
"00000000000000000000000000000000000000000000000000000000000000220000000000000000000000000000000000000000000000000000000000000082",
"00000000000000000000000000000000000000000000000000000000000000230000000000000000000000000000000000000000000000000000000000000083",
"00000000000000000000000000000000000000000000000000000000000000240000000000000000000000000000000000000000000000000000000000000084",
"00000000000000000000000000000000000000000000000000000000000000250000000000000000000000000000000000000000000000000000000000000085",
}
ap := NewTraceProvider(startingL2BlockNumber, depth)
// Start at the absolute prestate as the stateData
claim := BuildAlphabetPreimage(big.NewInt(0), absolutePrestateInt)
claim = ap.step(claim)
startingTraceIndex := new(big.Int).Lsh(startingL2BlockNumber, 4)
startingClaim := new(big.Int).Add(absolutePrestateInt, startingTraceIndex)
require.Equal(t, BuildAlphabetPreimage(startingTraceIndex, startingClaim), claim)
// Step again, which should increment both the claim and trace index by 1
claim = ap.step(claim)
nextTraceIndex := new(big.Int).Add(startingTraceIndex, big.NewInt(1))
nextClaim := new(big.Int).Add(startingClaim, big.NewInt(1))
require.Equal(t, BuildAlphabetPreimage(nextTraceIndex, nextClaim), claim)
// Step again, which should increment both the claim and trace index by 1
claim = ap.step(claim)
nextTraceIndex = new(big.Int).Add(nextTraceIndex, big.NewInt(1))
nextClaim = new(big.Int).Add(nextClaim, big.NewInt(1))
require.Equal(t, BuildAlphabetPreimage(nextTraceIndex, nextClaim), claim)
for i, expected := range expectedPrestates {
i, expected := i, expected
t.Run(fmt.Sprintf("Step_%v", i), func(t *testing.T) {
result, _, _, err := ap.GetStepData(context.Background(), types.NewPosition(4, big.NewInt(int64(i))))
require.NoError(t, err)
require.Equalf(t, expected, common.Bytes2Hex(result), "Incorrect prestate at trace index %v", i)
})
}
}
// TestAlphabetProvider_Get_ClaimsByTraceIndex tests the [fault.AlphabetProvider] Get function.
......@@ -59,23 +60,23 @@ func TestAlphabetProvider_Get_ClaimsByTraceIndex(t *testing.T) {
}{
{
types.NewPosition(depth, big.NewInt(7)),
alphabetClaim(new(big.Int).Add(sbn, big.NewInt(6)), new(big.Int).Add(startingTraceIndex, big.NewInt(6))),
alphabetClaim(new(big.Int).Add(sbn, big.NewInt(8)), new(big.Int).Add(startingTraceIndex, big.NewInt(8))),
},
{
types.NewPosition(depth, big.NewInt(3)),
alphabetClaim(new(big.Int).Add(sbn, big.NewInt(2)), new(big.Int).Add(startingTraceIndex, big.NewInt(2))),
alphabetClaim(new(big.Int).Add(sbn, big.NewInt(4)), new(big.Int).Add(startingTraceIndex, big.NewInt(4))),
},
{
types.NewPosition(depth, big.NewInt(5)),
alphabetClaim(new(big.Int).Add(sbn, big.NewInt(4)), new(big.Int).Add(startingTraceIndex, big.NewInt(4))),
alphabetClaim(new(big.Int).Add(sbn, big.NewInt(6)), new(big.Int).Add(startingTraceIndex, big.NewInt(6))),
},
}
// Execute each trace and check the alphabet provider returns the expected hash.
for _, trace := range traces {
for i, trace := range traces {
expectedHash, err := canonicalProvider.Get(context.Background(), trace.traceIndex)
require.NoError(t, err)
require.Equal(t, trace.expectedHash, expectedHash)
require.Equalf(t, trace.expectedHash, expectedHash, "Trace %v", i)
}
}
......@@ -85,8 +86,8 @@ func TestGetStepData_Succeeds(t *testing.T) {
depth := types.Depth(2)
startingL2BlockNumber := big.NewInt(1)
ap := NewTraceProvider(startingL2BlockNumber, depth)
expected := BuildAlphabetPreimage(big.NewInt(0), absolutePrestateInt)
pos := types.NewPosition(depth, big.NewInt(1))
expected := absolutePrestate
pos := types.NewPosition(depth, big.NewInt(0))
retrieved, proof, data, err := ap.GetStepData(context.Background(), pos)
require.NoError(t, err)
require.Equal(t, expected, retrieved)
......@@ -115,7 +116,7 @@ func TestGet_Succeeds(t *testing.T) {
pos := types.NewPosition(depth, big.NewInt(0))
claim, err := ap.Get(context.Background(), pos)
require.NoError(t, err)
expected := alphabetClaim(big.NewInt(0), absolutePrestateInt)
expected := alphabetClaim(big.NewInt(17), big.NewInt(113))
require.Equal(t, expected, claim)
}
......@@ -138,18 +139,3 @@ func TestGet_DepthTooLarge(t *testing.T) {
_, err := ap.Get(context.Background(), pos)
require.ErrorIs(t, err, ErrIndexTooLarge)
}
// TestGet_Extends tests the Get function with an index that is larger
// than the trace, but smaller than the maximum depth.
func TestGet_Extends(t *testing.T) {
depth := types.Depth(2)
startingL2BlockNumber := big.NewInt(1)
sbn := new(big.Int).Lsh(startingL2BlockNumber, 4)
startingTraceIndex := new(big.Int).Add(absolutePrestateInt, sbn)
ap := NewTraceProvider(startingL2BlockNumber, depth)
pos := types.NewPosition(depth, big.NewInt(3))
claim, err := ap.Get(context.Background(), pos)
require.NoError(t, err)
expected := alphabetClaim(new(big.Int).Add(sbn, big.NewInt(2)), new(big.Int).Add(startingTraceIndex, big.NewInt(2)))
require.Equal(t, expected, claim)
}
......@@ -35,7 +35,3 @@ func (g *AlphabetGameHelper) CreateHonestActor(alphabetTrace string, depth types
correctTrace: alphabet.NewTraceProvider(big.NewInt(0), depth),
}
}
func (g *AlphabetGameHelper) CreateDishonestHelper(alphabetTrace string, depth types.Depth, defender bool) *DishonestHelper {
return newDishonestHelper(&g.FaultGameHelper, g.CreateHonestActor(alphabetTrace, depth), defender)
}
......@@ -16,13 +16,13 @@ type dishonestClaim struct {
}
type DishonestHelper struct {
*FaultGameHelper
*HonestHelper
*OutputGameHelper
*OutputHonestHelper
claims map[dishonestClaim]bool
defender bool
}
func newDishonestHelper(g *FaultGameHelper, correctTrace *HonestHelper, defender bool) *DishonestHelper {
func newDishonestHelper(g *OutputGameHelper, correctTrace *OutputHonestHelper, defender bool) *DishonestHelper {
return &DishonestHelper{g, correctTrace, make(map[dishonestClaim]bool), defender}
}
......@@ -32,7 +32,7 @@ func (t *DishonestHelper) Attack(ctx context.Context, claimIndex int64) {
return
}
t.claims[c] = true
t.FaultGameHelper.Attack(ctx, claimIndex, common.Hash{byte(claimIndex)})
t.OutputGameHelper.Attack(ctx, claimIndex, common.Hash{byte(claimIndex)})
}
func (t *DishonestHelper) Defend(ctx context.Context, claimIndex int64) {
......@@ -41,7 +41,7 @@ func (t *DishonestHelper) Defend(ctx context.Context, claimIndex int64) {
return
}
t.claims[c] = true
t.FaultGameHelper.Defend(ctx, claimIndex, common.Hash{byte(claimIndex)})
t.OutputGameHelper.Defend(ctx, claimIndex, common.Hash{byte(claimIndex)})
}
func (t *DishonestHelper) AttackCorrect(ctx context.Context, claimIndex int64) {
......@@ -50,7 +50,7 @@ func (t *DishonestHelper) AttackCorrect(ctx context.Context, claimIndex int64) {
return
}
t.claims[c] = true
t.HonestHelper.Attack(ctx, claimIndex)
t.OutputHonestHelper.Attack(ctx, claimIndex)
}
func (t *DishonestHelper) DefendCorrect(ctx context.Context, claimIndex int64) {
......@@ -59,13 +59,14 @@ func (t *DishonestHelper) DefendCorrect(ctx context.Context, claimIndex int64) {
return
}
t.claims[c] = true
t.HonestHelper.Defend(ctx, claimIndex)
t.OutputHonestHelper.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) {
func (d *DishonestHelper) ExhaustDishonestClaims(ctx context.Context, rootClaim *ClaimHelper) {
depth := d.MaxDepth(ctx)
splitDepth := d.SplitDepth(ctx)
move := func(claimIndex int64, claimData ContractClaim) {
// dishonest level, valid attack
......@@ -81,21 +82,21 @@ func (d *DishonestHelper) ExhaustDishonestClaims(ctx context.Context) {
}
d.LogGameData(ctx)
d.FaultGameHelper.t.Logf("Dishonest moves against claimIndex %d", claimIndex)
d.OutputGameHelper.t.Logf("Dishonest moves against claimIndex %d", claimIndex)
agreeWithLevel := d.defender == (pos.Depth()%2 == 0)
if !agreeWithLevel {
d.AttackCorrect(ctx, claimIndex)
if claimIndex != 0 {
if claimIndex != 0 && pos.Depth() != splitDepth+1 {
d.DefendCorrect(ctx, claimIndex)
}
}
d.Attack(ctx, claimIndex)
if claimIndex != 0 {
if claimIndex != 0 && pos.Depth() != splitDepth+1 {
d.Defend(ctx, claimIndex)
}
}
var numClaimsSeen int64
numClaimsSeen := rootClaim.index
for {
// Use a short timeout since we don't know the challenger will respond,
// and this is only designed for the alphabet game where the response should be fast.
......@@ -105,12 +106,11 @@ func (d *DishonestHelper) ExhaustDishonestClaims(ctx context.Context) {
// There's nothing to respond to.
break
}
d.FaultGameHelper.require.NoError(err)
d.OutputGameHelper.require.NoError(err)
for i := numClaimsSeen; i < newCount; i++ {
for ; numClaimsSeen < newCount; numClaimsSeen++ {
claimData := d.getClaim(ctx, numClaimsSeen)
move(numClaimsSeen, claimData)
numClaimsSeen++
}
}
}
......@@ -55,3 +55,7 @@ func (g *OutputAlphabetGameHelper) CreateHonestActor(ctx context.Context, l2Node
correctTrace: correctTrace,
}
}
func (g *OutputAlphabetGameHelper) CreateDishonestHelper(ctx context.Context, l2Node string, defender bool) *DishonestHelper {
return newDishonestHelper(&g.OutputGameHelper, g.CreateHonestActor(ctx, l2Node), defender)
}
......@@ -3,6 +3,7 @@ package faultproofs
import (
"context"
"testing"
"time"
op_e2e "github.com/ethereum-optimism/optimism/op-e2e"
"github.com/ethereum-optimism/optimism/op-e2e/e2eutils/challenger"
......@@ -20,6 +21,7 @@ func TestOutputAlphabetGame_ChallengerWins(t *testing.T) {
disputeGameFactory := disputegame.NewFactoryHelper(t, ctx, sys)
game := disputeGameFactory.StartOutputAlphabetGame(ctx, "sequencer", 3, common.Hash{0xff})
correctTrace := game.CreateHonestActor(ctx, "sequencer")
game.LogGameData(ctx)
opts := challenger.WithPrivKey(sys.Cfg.Secrets.Alice)
......@@ -45,8 +47,8 @@ func TestOutputAlphabetGame_ChallengerWins(t *testing.T) {
claim = claim.WaitForCounterClaim(ctx)
game.LogGameData(ctx)
// Attack the root of the cannon trace subgame
claim = claim.Attack(ctx, common.Hash{0x00, 0xcc})
// Attack the root of the alphabet trace subgame
claim = correctTrace.AttackClaim(ctx, claim)
for !claim.IsMaxDepth(ctx) {
if claim.AgreesWithOutputRoot() {
// If the latest claim supports the output root, wait for the honest challenger to respond
......@@ -54,7 +56,7 @@ func TestOutputAlphabetGame_ChallengerWins(t *testing.T) {
game.LogGameData(ctx)
} else {
// Otherwise we need to counter the honest claim
claim = claim.Defend(ctx, common.Hash{0x00, 0xdd})
claim = correctTrace.AttackClaim(ctx, claim)
game.LogGameData(ctx)
}
}
......@@ -65,6 +67,7 @@ func TestOutputAlphabetGame_ChallengerWins(t *testing.T) {
sys.TimeTravelClock.AdvanceTime(game.GameDuration(ctx))
require.NoError(t, wait.ForNextBlock(ctx, l1Client))
game.WaitForGameStatus(ctx, disputegame.StatusChallengerWins)
game.LogGameData(ctx)
}
func TestOutputAlphabetGame_ValidOutputRoot(t *testing.T) {
......@@ -97,3 +100,70 @@ func TestOutputAlphabetGame_ValidOutputRoot(t *testing.T) {
require.NoError(t, wait.ForNextBlock(ctx, l1Client))
game.WaitForGameStatus(ctx, disputegame.StatusDefenderWins)
}
func TestChallengerCompleteExhaustiveDisputeGame(t *testing.T) {
op_e2e.InitParallel(t, op_e2e.UseExecutor(1))
testCase := func(t *testing.T, isRootCorrect bool) {
ctx := context.Background()
sys, l1Client := startFaultDisputeSystem(t)
t.Cleanup(sys.Close)
disputeGameFactory := disputegame.NewFactoryHelper(t, ctx, sys)
var game *disputegame.OutputAlphabetGameHelper
if isRootCorrect {
game = disputeGameFactory.StartOutputAlphabetGameWithCorrectRoot(ctx, "sequencer", 1)
} else {
game = disputeGameFactory.StartOutputAlphabetGame(ctx, "sequencer", 1, common.Hash{0xaa, 0xbb, 0xcc})
}
claim := game.DisputeLastBlock(ctx)
game.LogGameData(ctx)
// Start honest challenger
game.StartChallenger(ctx, "sequencer", "Challenger",
challenger.WithAlphabet(sys.RollupEndpoint("sequencer")),
challenger.WithPrivKey(sys.Cfg.Secrets.Alice),
// Ensures the challenger responds to all claims before test timeout
challenger.WithPollInterval(time.Millisecond*400),
)
if isRootCorrect {
// Attack the correct output root with an invalid alphabet trace
claim = claim.Attack(ctx, common.Hash{0x01})
} else {
// Wait for the challenger to counter the invalid output root
claim = claim.WaitForCounterClaim(ctx)
}
// Start dishonest challenger
dishonestHelper := game.CreateDishonestHelper(ctx, "sequencer", !isRootCorrect)
dishonestHelper.ExhaustDishonestClaims(ctx, claim)
// Wait until we've reached max depth before checking for inactivity
game.WaitForClaimAtDepth(ctx, 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)
gameDuration := game.GameDuration(ctx)
sys.TimeTravelClock.AdvanceTime(gameDuration)
require.NoError(t, wait.ForNextBlock(ctx, l1Client))
expectedStatus := disputegame.StatusChallengerWins
if isRootCorrect {
expectedStatus = disputegame.StatusDefenderWins
}
game.WaitForGameStatus(ctx, expectedStatus)
game.LogGameData(ctx)
}
t.Run("RootCorrect", func(t *testing.T) {
op_e2e.InitParallel(t, op_e2e.UseExecutor(1))
testCase(t, true)
})
t.Run("RootIncorrect", func(t *testing.T) {
op_e2e.InitParallel(t, op_e2e.UseExecutor(1))
testCase(t, false)
})
}
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