Skip to content

Commit e190c0a

Browse files
dailinsubjamphilippecamacho
authored andcommitted
Test Fraud Proof Game Compatibility with Espresso (#170)
* add init version on dispute game test * more comment and DRY * Update espresso/environment/13_dispute_game_test.go Co-authored-by: Phil <[email protected]> * make sure caff node can make progress * simplify caff node wait logic * clean up --------- Co-authored-by: Phil <[email protected]>
1 parent 7793d45 commit e190c0a

File tree

4 files changed

+253
-7
lines changed

4 files changed

+253
-7
lines changed
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
package environment_test
2+
3+
import (
4+
"context"
5+
"testing"
6+
7+
env "github.com/ethereum-optimism/optimism/espresso/environment"
8+
"github.com/stretchr/testify/require"
9+
10+
"github.com/ethereum-optimism/optimism/op-challenger/game/types"
11+
op_e2e "github.com/ethereum-optimism/optimism/op-e2e"
12+
"github.com/ethereum-optimism/optimism/op-e2e/e2eutils/challenger"
13+
"github.com/ethereum-optimism/optimism/op-e2e/e2eutils/disputegame"
14+
"github.com/ethereum-optimism/optimism/op-e2e/e2eutils/wait"
15+
"github.com/ethereum/go-ethereum/common"
16+
)
17+
18+
// TestOutputAlphabetGameWithEspresso_ChallengerWins verifies that fraud proof challenges work correctly
19+
// with Espresso integration enabled. It ensures a challenger can successfully detect and win
20+
// against a malicious proposer, validating that the dispute resolution process remains intact
21+
// when using Espresso for transaction finalization.
22+
//
23+
// This test mirrors the logic from TestOutputAlphabetGame_ChallengerWins in the non-Espresso
24+
// implementation (op-e2e/faultproofs/output_alphabet_test.go), but runs with the Espresso-mode
25+
// batcher enabled.
26+
//
27+
// Test structure:
28+
// - Setup: Initialize Sequencer and Batcher in Espresso mode
29+
// - Action: Deploy fault dispute system and trigger challenger response
30+
// - Assert: Verify challenger successfully wins the dispute game
31+
func TestOutputAlphabetGameWithEspresso_ChallengerWins(t *testing.T) {
32+
op_e2e.InitParallel(t)
33+
ctx := context.Background()
34+
35+
// Start a Espresso Dev Node
36+
launcher := new(env.EspressoDevNodeLauncherDocker)
37+
38+
// Start a Fault Dispute System with Espresso Dev Node
39+
sys, espressoDevNode, err := launcher.StartDevNetWithFaultDisputeSystem(ctx, t, env.WithL1FinalizedDistance(0), env.WithSequencerUseFinalized(true))
40+
41+
l1Client := sys.NodeClient("l1")
42+
43+
// Signal the testnet to shut down
44+
if have, want := err, error(nil); have != want {
45+
t.Fatalf("failed to start dev environment with espresso dev node:\nhave:\n\t\"%v\"\nwant:\n\t\"%v\"\n", have, want)
46+
}
47+
48+
// Close the system and stop the Espresso Dev Node
49+
defer sys.Close()
50+
defer func() {
51+
err = espressoDevNode.Stop()
52+
if err != nil {
53+
t.Fatalf("failed to stop espresso dev node: %v", err)
54+
}
55+
}()
56+
57+
// Launch a Caff Node and check it can still make progress
58+
caffNode, err := env.LaunchCaffNode(t, sys, espressoDevNode)
59+
if have, want := err, error(nil); have != want {
60+
t.Fatalf("failed to start caff node:\nhave:\n\t\"%v\"\nwant:\n\t\"%v\"\n", have, want)
61+
}
62+
63+
// Shut down the Caff Node
64+
defer env.Stop(t, caffNode)
65+
caffClient := sys.NodeClient(env.RoleCaffNode)
66+
// Make sure Caff Node still make progress
67+
require.NoError(t, wait.ForNextBlock(ctx, caffClient))
68+
69+
// All the following testing code is pasted from `TestOutputAlphabetGame_ChallengerWins` in `op-e2e/faultproofs/output_alphabet_test.go`
70+
disputeGameFactory := disputegame.NewFactoryHelper(t, ctx, sys)
71+
game := disputeGameFactory.StartOutputAlphabetGame(ctx, "sequencer", 3, common.Hash{0xff})
72+
correctTrace := game.CreateHonestActor(ctx, "sequencer")
73+
game.LogGameData(ctx)
74+
75+
opts := challenger.WithPrivKey(sys.Cfg.Secrets.Alice)
76+
game.StartChallenger(ctx, "sequencer", "Challenger", opts)
77+
game.LogGameData(ctx)
78+
79+
// Challenger should post an output root to counter claims down to the leaf level of the top game
80+
claim := game.RootClaim(ctx)
81+
for claim.IsOutputRoot(ctx) && !claim.IsOutputRootLeaf(ctx) {
82+
if claim.AgreesWithOutputRoot() {
83+
// If the latest claim agrees with the output root, expect the honest challenger to counter it
84+
claim = claim.WaitForCounterClaim(ctx)
85+
game.LogGameData(ctx)
86+
claim.RequireCorrectOutputRoot(ctx)
87+
} else {
88+
// Otherwise we should counter
89+
claim = claim.Attack(ctx, common.Hash{0xaa})
90+
game.LogGameData(ctx)
91+
}
92+
}
93+
94+
// Wait for the challenger to post the first claim in the cannon trace
95+
claim = claim.WaitForCounterClaim(ctx)
96+
game.LogGameData(ctx)
97+
98+
// Attack the root of the alphabet trace subgame
99+
claim = correctTrace.AttackClaim(ctx, claim)
100+
for !claim.IsMaxDepth(ctx) {
101+
if claim.AgreesWithOutputRoot() {
102+
// If the latest claim supports the output root, wait for the honest challenger to respond
103+
claim = claim.WaitForCounterClaim(ctx)
104+
game.LogGameData(ctx)
105+
} else {
106+
// Otherwise we need to counter the honest claim
107+
claim = correctTrace.AttackClaim(ctx, claim)
108+
game.LogGameData(ctx)
109+
}
110+
}
111+
// Challenger should be able to call step and counter the leaf claim.
112+
claim.WaitForCountered(ctx)
113+
game.LogGameData(ctx)
114+
115+
sys.TimeTravelClock.AdvanceTime(game.MaxClockDuration(ctx))
116+
require.NoError(t, wait.ForNextBlock(ctx, l1Client))
117+
game.WaitForGameStatus(ctx, types.GameStatusChallengerWon)
118+
game.LogGameData(ctx)
119+
}

espresso/environment/optitmism_espresso_test_helpers.go

Lines changed: 107 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import (
2525
"github.com/ethereum-optimism/optimism/op-batcher/flags"
2626
"github.com/ethereum-optimism/optimism/op-e2e/config"
2727
"github.com/ethereum-optimism/optimism/op-e2e/e2eutils/geth"
28+
"github.com/ethereum-optimism/optimism/op-e2e/faultproofs"
2829
"github.com/ethereum-optimism/optimism/op-e2e/system/e2esys"
2930
"github.com/ethereum/go-ethereum/common"
3031
"github.com/ethereum/go-ethereum/common/hexutil"
@@ -246,8 +247,8 @@ func (e EspressoDevNodeContainerInfo) Stop() error {
246247
// is meant to be.
247248
var ErrUnableToDetermineEspressoDevNodeSequencerHost = errors.New("unable to determine the host for the espresso-dev-node sequencer api")
248249

249-
func (l *EspressoDevNodeLauncherDocker) StartDevNet(ctx context.Context, t *testing.T, options ...DevNetLauncherOption) (*e2esys.System, EspressoDevNode, error) {
250-
originalCtx := ctx
250+
// GetDevNetConfig returns a configuration for a devnet
251+
func (l *EspressoDevNodeLauncherDocker) GetDevNetSysConfig(ctx context.Context, t *testing.T, options ...DevNetLauncherOption) e2esys.SystemConfig {
251252

252253
var allocOpt e2esys.SystemConfigOpt
253254
if l.EnclaveBatcher {
@@ -289,6 +290,57 @@ func (l *EspressoDevNodeLauncherDocker) StartDevNet(ctx context.Context, t *test
289290
sysConfig.L1Allocs[address] = account.State
290291
}
291292

293+
return sysConfig
294+
}
295+
296+
// GetDevNetWithFaultDisputeSysConfig returns a configuration for a devnet with a Fault Dispute System
297+
func (l *EspressoDevNodeLauncherDocker) GetDevNetWithFaultDisputeSysConfig(ctx context.Context, t *testing.T, options ...DevNetLauncherOption) e2esys.SystemConfig {
298+
var allocOpt e2esys.SystemConfigOpt
299+
if l.EnclaveBatcher {
300+
allocOpt = e2esys.WithAllocType(config.AllocTypeEspressoWithEnclave)
301+
} else {
302+
allocOpt = e2esys.WithAllocType(config.AllocTypeEspressoWithoutEnclave)
303+
}
304+
305+
// Get a Fault Dispute System configuration with Espresso Dev Node allocation
306+
sysConfig := faultproofs.GetFaultDisputeSystemConfigForEspresso(t, []e2esys.SystemConfigOpt{allocOpt})
307+
308+
if l.AltDa {
309+
sysConfig.DeployConfig.UseAltDA = true
310+
sysConfig.DeployConfig.DACommitmentType = "KeccakCommitment"
311+
sysConfig.DeployConfig.DAChallengeWindow = 16
312+
sysConfig.DeployConfig.DAResolveWindow = 16
313+
sysConfig.DeployConfig.DABondSize = 1000000
314+
sysConfig.DeployConfig.DAResolverRefundPercentage = 0
315+
sysConfig.BatcherMaxPendingTransactions = 0
316+
sysConfig.BatcherBatchType = 0
317+
sysConfig.DataAvailabilityType = flags.CalldataType
318+
}
319+
320+
// Set a short L1 block time and finalized distance to make tests faster and reach finality sooner
321+
sysConfig.DeployConfig.L1BlockTime = 2
322+
323+
sysConfig.DeployConfig.DeployCeloContracts = true
324+
325+
// Ensure that we fund the dev accounts
326+
sysConfig.DeployConfig.FundDevAccounts = true
327+
328+
espressoPremine := new(big.Int).Mul(new(big.Int).SetUint64(1_000_000), new(big.Int).SetUint64(params.Ether))
329+
sysConfig.L1Allocs[ESPRESSO_CONTRACT_ACCOUNT] = types.Account{
330+
Nonce: 100000, // Set the nonce to avoid collisions with predeployed contracts
331+
Balance: espressoPremine, // Pre-fund Espresso deployer acount with 1M Ether
332+
}
333+
334+
//Set up the L1Allocs in the system config
335+
for address, account := range ESPRESSO_ALLOCS {
336+
sysConfig.L1Allocs[address] = account.State
337+
}
338+
339+
return sysConfig
340+
}
341+
342+
// GetDevNetStartOptions returns the start options for the devnet
343+
func (l *EspressoDevNodeLauncherDocker) GetDevNetStartOptions(originalCtx context.Context, t *testing.T, sysConfig *e2esys.SystemConfig, options ...DevNetLauncherOption) ([]e2esys.StartOption, *DevNetLauncherContext) {
292344
initialOptions := []DevNetLauncherOption{
293345
allowHostDockerInternalVirtualHost(),
294346
launchEspressoDevNodeDocker(),
@@ -300,12 +352,11 @@ func (l *EspressoDevNodeLauncherDocker) StartDevNet(ctx context.Context, t *test
300352

301353
launchContext := DevNetLauncherContext{
302354
Ctx: originalCtx,
303-
SystemCfg: &sysConfig,
355+
SystemCfg: sysConfig,
304356
}
305357

306358
allOptions := append(initialOptions, options...)
307359

308-
// getOptions := map[string][]geth.GethOption{}
309360
startOptions := []e2esys.StartOption{}
310361

311362
for _, opt := range allOptions {
@@ -322,10 +373,20 @@ func (l *EspressoDevNodeLauncherDocker) StartDevNet(ctx context.Context, t *test
322373
}
323374

324375
if sysConfigOption := options.SysConfigOption; sysConfigOption != nil {
325-
sysConfigOption(&sysConfig)
376+
sysConfigOption(sysConfig)
326377
}
327378
}
328379

380+
return startOptions, &launchContext
381+
}
382+
383+
func (l *EspressoDevNodeLauncherDocker) StartDevNet(ctx context.Context, t *testing.T, options ...DevNetLauncherOption) (*e2esys.System, EspressoDevNode, error) {
384+
385+
sysConfig := l.GetDevNetSysConfig(ctx, t, options...)
386+
387+
originalCtx := ctx
388+
startOptions, launchContext := l.GetDevNetStartOptions(originalCtx, t, &sysConfig, options...)
389+
329390
// We want to run the espresso-dev-node. But we need it to be able to
330391
// access the L1 node.
331392

@@ -334,7 +395,47 @@ func (l *EspressoDevNodeLauncherDocker) StartDevNet(ctx context.Context, t *test
334395

335396
startOptions...,
336397
)
337-
launchContext.System = system
398+
399+
if err != nil {
400+
if system != nil {
401+
// We don't want the system running in a partial / incomplete
402+
// state. So we'll tell it to stop here, just in case.
403+
system.Close()
404+
}
405+
406+
return system, nil, err
407+
}
408+
409+
// Auto System Cleanup tied to the passed in context.
410+
{
411+
// We want to ensure that the lifecycle of the system node is tied to
412+
// the context we were given, just like the espresso-dev-node. So if
413+
// the context is canceled, or otherwise closed, it will automatically
414+
// clean up the system.
415+
go (func(ctx context.Context) {
416+
<-ctx.Done()
417+
418+
// The system is guaranteed to not be null here.
419+
system.Close()
420+
})(originalCtx)
421+
}
422+
423+
return system, launchContext.EspressoDevNode, launchContext.Error
424+
}
425+
426+
// StartDevNetWithFaultDisputeSystem starts a Fault Dispute System with an Espresso Dev Node
427+
func (l *EspressoDevNodeLauncherDocker) StartDevNetWithFaultDisputeSystem(ctx context.Context, t *testing.T, options ...DevNetLauncherOption) (*e2esys.System, EspressoDevNode, error) {
428+
429+
sysConfig := l.GetDevNetWithFaultDisputeSysConfig(ctx, t, options...)
430+
431+
originalCtx := ctx
432+
startOptions, launchContext := l.GetDevNetStartOptions(originalCtx, t, &sysConfig, options...)
433+
434+
system, err := sysConfig.Start(
435+
t,
436+
437+
startOptions...,
438+
)
338439

339440
if err != nil {
340441
if system != nil {

justfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ build-batcher-enclave-image:
2626
run-test4: compile-contracts
2727
go test ./espresso/environment/4_confirmation_integrity_with_reorgs_test.go -v
2828

29-
espresso_tests_timeout := "30m"
29+
espresso_tests_timeout := "35m"
3030
espresso-tests timeout=espresso_tests_timeout: compile-contracts
3131
go test -timeout={{timeout}} -p=1 -count=1 ./espresso/environment
3232

op-e2e/faultproofs/util.go

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,32 @@ func StartFaultDisputeSystem(t *testing.T, opts ...faultDisputeConfigOpts) (*e2e
108108
return sys, sys.NodeClient("l1")
109109
}
110110

111+
// GetFaultDisputeSystemConfigForEspresso returns a Fault Dispute System configuration with another set of options
112+
func GetFaultDisputeSystemConfigForEspresso(t *testing.T, original_opts []e2esys.SystemConfigOpt, opts ...faultDisputeConfigOpts) e2esys.SystemConfig {
113+
fdc := new(faultDisputeConfig)
114+
for _, opt := range opts {
115+
opt(fdc)
116+
}
117+
118+
// merge two sets of options
119+
cfg := e2esys.DefaultSystemConfig(t, append(original_opts, fdc.sysOpts...)...)
120+
121+
// all the following options are specific to Fault Dispute System
122+
// they're pasted from `StartFaultDisputeSystem` in `op-e2e/faultproofs/util.go`(same file)
123+
// and we remove the line `delete(cfg.Nodes, "verifier")`
124+
cfg.Nodes["sequencer"].SafeDBPath = t.TempDir()
125+
cfg.DeployConfig.SequencerWindowSize = 30
126+
cfg.DeployConfig.FinalizationPeriodSeconds = 2
127+
cfg.SupportL1TimeTravel = true
128+
// Disable proposer creating fast games automatically - required games are manually created
129+
cfg.DisableProposer = true
130+
for _, opt := range fdc.cfgModifiers {
131+
opt(&cfg)
132+
}
133+
134+
return cfg
135+
}
136+
111137
func SendKZGPointEvaluationTx(t *testing.T, sys *e2esys.System, l2Node string, privateKey *ecdsa.PrivateKey) *types.Receipt {
112138
return helpers.SendL2Tx(t, sys.Cfg, sys.NodeClient(l2Node), privateKey, func(opts *helpers.TxOpts) {
113139
precompile := common.BytesToAddress([]byte{0x0a})

0 commit comments

Comments
 (0)