Skip to content

Commit f1bff36

Browse files
Inphiclaude
andcommitted
op-acceptance-tests: migrate TestInteropFaultProofs_IntraBlock from op-e2e
Migrate the intra-block fault proof consolidation tests from op-e2e/actions/interop/proofs_test.go to op-acceptance-tests using the devstack DSL and supernode infrastructure. Six of the original seven sub-cases are ported: - CascadeInvalid / SwapCascadeInvalid (transitive invalidation) - CyclicDependencyValid (valid cross-chain cycle) - CyclicDependencyInvalid (both chains invalid) - SameChainValid / SameChainInvalid (same-chain messaging) The longDependencyChainValid case and a faithful cyclicDependencyInvalid (pure exec→exec cycle) are left as TODOs — both require constructing exec messages that reference other exec messages' ExecutingMessage events, which the SameTimestampPair API does not yet support. Closes #19010 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 9d68e89 commit f1bff36

File tree

2 files changed

+305
-0
lines changed

2 files changed

+305
-0
lines changed

op-acceptance-tests/tests/interop/proofs/serial/interop_fault_proofs_test.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,3 +73,13 @@ func TestInteropFaultProofs_InvalidBlock(gt *testing.T) {
7373
sys := presets.NewSimpleInteropSupernodeProofs(t, presets.WithChallengerCannonKonaEnabled())
7474
sfp.RunInvalidBlockTest(t, sys)
7575
}
76+
77+
func TestInteropFaultProofs_IntraBlock(gt *testing.T) {
78+
for _, tc := range sfp.IntraBlockCases() {
79+
gt.Run(tc.Name, func(gt *testing.T) {
80+
t := devtest.SerialT(gt)
81+
sys := presets.NewSimpleInteropSupernodeProofs(t, presets.WithChallengerCannonKonaEnabled())
82+
sfp.RunIntraBlockConsolidationTest(t, sys, tc)
83+
})
84+
}
85+
}
Lines changed: 295 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,295 @@
1+
package superfaultproofs
2+
3+
import (
4+
"bytes"
5+
"math/rand"
6+
7+
gameTypes "github.com/ethereum-optimism/optimism/op-challenger/game/types"
8+
"github.com/ethereum-optimism/optimism/op-devstack/devtest"
9+
"github.com/ethereum-optimism/optimism/op-devstack/dsl"
10+
"github.com/ethereum-optimism/optimism/op-devstack/presets"
11+
"github.com/ethereum-optimism/optimism/op-service/eth"
12+
"github.com/ethereum-optimism/optimism/op-service/txplan"
13+
"github.com/ethereum/go-ethereum/common"
14+
"github.com/ethereum/go-ethereum/crypto"
15+
)
16+
17+
// IntraBlockTestCase describes a single intra-block scenario.
18+
type IntraBlockTestCase struct {
19+
Name string
20+
BuildTxs func(s *intraBlockSetup) (txsA, txsB []*txplan.PlannedTx)
21+
}
22+
23+
// intraBlockSetup holds shared state for building same-timestamp transactions.
24+
type intraBlockSetup struct {
25+
rng *rand.Rand
26+
alice *dsl.EOA // funded on chain A
27+
bob *dsl.EOA // funded on chain B
28+
eventLoggerA common.Address
29+
eventLoggerB common.Address
30+
expectedBlockNumA uint64
31+
expectedBlockNumB uint64
32+
nextTimestamp uint64
33+
}
34+
35+
func (s *intraBlockSetup) prepareInitA(logIdx uint32) *dsl.SameTimestampPair {
36+
return s.alice.PrepareSameTimestampInit(s.rng, s.eventLoggerA, s.expectedBlockNumA, logIdx, s.nextTimestamp)
37+
}
38+
39+
func (s *intraBlockSetup) prepareInitB(logIdx uint32) *dsl.SameTimestampPair {
40+
return s.bob.PrepareSameTimestampInit(s.rng, s.eventLoggerB, s.expectedBlockNumB, logIdx, s.nextTimestamp)
41+
}
42+
43+
// RunIntraBlockConsolidationTest verifies that the consolidation step in the
44+
// super-root transition correctly handles intra-block cross-chain messages.
45+
//
46+
// It builds one block per chain at the same timestamp with the given transaction
47+
// set, waits for the supernode to validate and replace invalid blocks, then
48+
// verifies the consolidation transition via kona and the challenger trace provider.
49+
func RunIntraBlockConsolidationTest(t devtest.T, sys *presets.SimpleInterop, tc *IntraBlockTestCase) {
50+
t.Require().NotNil(sys.SuperRoots, "supernode is required for this test")
51+
t.Require().NotNil(sys.TestSequencer, "test sequencer is required for same-timestamp block building")
52+
53+
rng := rand.New(rand.NewSource(98765))
54+
chains := orderedChains(sys)
55+
t.Require().Len(chains, 2, "expected exactly 2 interop chains")
56+
57+
// --- Create funded EOAs and deploy event loggers -------------------------
58+
alice := sys.FunderA.NewFundedEOA(eth.OneEther)
59+
bob := sys.FunderB.NewFundedEOA(eth.OneEther)
60+
61+
eventLoggerA := alice.DeployEventLogger()
62+
eventLoggerB := bob.DeployEventLogger()
63+
64+
// --- Sync chains and prepare for same-timestamp block building -----------
65+
sys.L2ChainB.CatchUpTo(sys.L2ChainA)
66+
sys.L2ChainA.CatchUpTo(sys.L2ChainB)
67+
sys.SuperRoots.EnsureInteropPaused(sys.L2CLA, sys.L2CLB, 10)
68+
69+
sys.L2CLA.StopSequencer()
70+
sys.L2CLB.StopSequencer()
71+
72+
unsafeA := sys.L2ELA.BlockRefByLabel(eth.Unsafe)
73+
unsafeB := sys.L2ELB.BlockRefByLabel(eth.Unsafe)
74+
// Synchronize chains to the same timestamp
75+
for range 10 {
76+
if unsafeA.Time == unsafeB.Time {
77+
break
78+
}
79+
if unsafeA.Time < unsafeB.Time {
80+
sys.L2CLA.StartSequencer()
81+
sys.L2ELA.WaitForTime(unsafeB.Time)
82+
sys.L2CLA.StopSequencer()
83+
unsafeA = sys.L2ELA.BlockRefByLabel(eth.Unsafe)
84+
} else {
85+
sys.L2CLB.StartSequencer()
86+
sys.L2ELB.WaitForTime(unsafeA.Time)
87+
sys.L2CLB.StopSequencer()
88+
unsafeB = sys.L2ELB.BlockRefByLabel(eth.Unsafe)
89+
}
90+
}
91+
t.Require().Equal(unsafeA.Time, unsafeB.Time, "chains must be at same timestamp")
92+
93+
blockTime := sys.L2ChainA.Escape().RollupConfig().BlockTime
94+
nextTimestamp := unsafeA.Time + blockTime
95+
96+
setup := &intraBlockSetup{
97+
rng: rng,
98+
alice: alice,
99+
bob: bob,
100+
eventLoggerA: eventLoggerA,
101+
eventLoggerB: eventLoggerB,
102+
expectedBlockNumA: unsafeA.Number + 1,
103+
expectedBlockNumB: unsafeB.Number + 1,
104+
nextTimestamp: nextTimestamp,
105+
}
106+
107+
// --- Build and include transactions in same-timestamp blocks -------------
108+
txsA, txsB := tc.BuildTxs(setup)
109+
110+
// Assign deterministic nonces
111+
baseNonceA := alice.PendingNonce()
112+
for i, ptx := range txsA {
113+
txplan.WithStaticNonce(baseNonceA + uint64(i))(ptx)
114+
}
115+
baseNonceB := bob.PendingNonce()
116+
for i, ptx := range txsB {
117+
txplan.WithStaticNonce(baseNonceB + uint64(i))(ptx)
118+
}
119+
120+
ctx := t.Ctx()
121+
var rawTxsA, rawTxsB [][]byte
122+
for _, ptx := range txsA {
123+
signedTx, err := ptx.Signed.Eval(ctx)
124+
t.Require().NoError(err, "failed to sign tx for chain A")
125+
rawBytes, err := signedTx.MarshalBinary()
126+
t.Require().NoError(err, "failed to marshal tx for chain A")
127+
rawTxsA = append(rawTxsA, rawBytes)
128+
}
129+
for _, ptx := range txsB {
130+
signedTx, err := ptx.Signed.Eval(ctx)
131+
t.Require().NoError(err, "failed to sign tx for chain B")
132+
rawBytes, err := signedTx.MarshalBinary()
133+
t.Require().NoError(err, "failed to marshal tx for chain B")
134+
rawTxsB = append(rawTxsB, rawBytes)
135+
}
136+
137+
sys.TestSequencer.SequenceBlockWithTxs(t, sys.L2ChainA.ChainID(), unsafeA.Hash, rawTxsA)
138+
sys.TestSequencer.SequenceBlockWithTxs(t, sys.L2ChainB.ChainID(), unsafeB.Hash, rawTxsB)
139+
140+
// --- Resume interop and wait for validation ------------------------------
141+
sys.SuperRoots.ResumeInterop()
142+
sys.SuperRoots.AwaitValidatedTimestamp(nextTimestamp)
143+
144+
endTimestamp := nextTimestamp
145+
startTimestamp := endTimestamp - 1
146+
147+
// --- Capture optimistic and cross-safe super roots -----------------------
148+
queryAPI := sys.SuperRoots.QueryAPI()
149+
firstOptimistic := optimisticBlockAtTimestamp(t, queryAPI, chains[0].ID, endTimestamp)
150+
secondOptimistic := optimisticBlockAtTimestamp(t, queryAPI, chains[1].ID, endTimestamp)
151+
152+
start := superRootAtTimestamp(t, chains, startTimestamp)
153+
preConsolidation := marshalTransition(start, consolidateStep, firstOptimistic, secondOptimistic)
154+
155+
crossSafeEnd := superRootAtTimestamp(t, chains, endTimestamp)
156+
optimisticEnd := eth.NewSuperV1(endTimestamp,
157+
eth.ChainIDAndOutput{ChainID: chains[0].ID, Output: firstOptimistic.OutputRoot},
158+
eth.ChainIDAndOutput{ChainID: chains[1].ID, Output: secondOptimistic.OutputRoot},
159+
)
160+
optimisticIsCrossSafe := bytes.Equal(optimisticEnd.Marshal(), crossSafeEnd.Marshal())
161+
162+
l1HeadCurrent := latestRequiredL1(sys.SuperRoots.SuperRootAtTimestamp(endTimestamp))
163+
164+
// --- Build and run consolidation transition tests ------------------------
165+
tests := []*transitionTest{
166+
{
167+
Name: "Consolidate",
168+
AgreedClaim: preConsolidation,
169+
DisputedClaim: crossSafeEnd.Marshal(),
170+
DisputedTraceIndex: consolidateStep,
171+
L1Head: l1HeadCurrent,
172+
ClaimTimestamp: endTimestamp,
173+
ExpectValid: true,
174+
},
175+
{
176+
Name: "Consolidate-InvalidNoChange",
177+
AgreedClaim: preConsolidation,
178+
DisputedClaim: preConsolidation,
179+
DisputedTraceIndex: consolidateStep,
180+
L1Head: l1HeadCurrent,
181+
ClaimTimestamp: endTimestamp,
182+
ExpectValid: false,
183+
},
184+
}
185+
if !optimisticIsCrossSafe {
186+
tests = append(tests, &transitionTest{
187+
Name: "Consolidate-ExpectInvalidPendingBlock",
188+
AgreedClaim: preConsolidation,
189+
DisputedClaim: optimisticEnd.Marshal(),
190+
DisputedTraceIndex: consolidateStep,
191+
L1Head: l1HeadCurrent,
192+
ClaimTimestamp: endTimestamp,
193+
ExpectValid: false,
194+
})
195+
}
196+
197+
challengerCfg := sys.L2ChainA.Escape().L2Challengers()[0].Config()
198+
gameDepth := sys.DisputeGameFactory().GameImpl(gameTypes.SuperCannonKonaGameType).SplitDepth()
199+
200+
for _, test := range tests {
201+
t.Run(test.Name+"-fpp", func(t devtest.T) {
202+
runKonaInteropProgram(t, challengerCfg.CannonKona, test.L1Head.Hash,
203+
test.AgreedClaim, crypto.Keccak256Hash(test.DisputedClaim),
204+
test.ClaimTimestamp, test.ExpectValid)
205+
})
206+
t.Run(test.Name+"-challenger", func(t devtest.T) {
207+
runChallengerProviderTest(t, queryAPI, gameDepth, startTimestamp, test.ClaimTimestamp, test)
208+
})
209+
}
210+
}
211+
212+
// IntraBlockCases returns all intra-block test scenarios.
213+
//
214+
// TODO(#19010): Add longDependencyChainValid case (depth-10 exec→exec chain across chains)
215+
// and a faithful cyclicDependencyInvalid case (pure exec→exec cycle with no init messages).
216+
// Both require constructing exec messages that reference other exec messages'
217+
// ExecutingMessage events, which the SameTimestampPair API doesn't support yet.
218+
//
219+
// The current CyclicDependencyInvalid case uses two independent invalid execs (bad log
220+
// index) rather than a pure exec→exec cycle. This means kona's cycle detection codepath
221+
// during consolidation is not exercised — only the "missing log" rejection path is tested.
222+
// Cycle detection at the supernode level is covered by TestSupernodeSameTimestampCycle,
223+
// but that test does not invoke kona/FPP.
224+
func IntraBlockCases() []*IntraBlockTestCase {
225+
return []*IntraBlockTestCase{
226+
{
227+
// Init(A) + valid Exec(B→A) on chain A,
228+
// Init(B) + invalid Exec(A→B) on chain B.
229+
// Chain B is invalid (bad exec), chain A is transitively invalid.
230+
Name: "CascadeInvalid",
231+
BuildTxs: func(s *intraBlockSetup) ([]*txplan.PlannedTx, []*txplan.PlannedTx) {
232+
pairA := s.prepareInitA(0)
233+
pairB := s.prepareInitB(0)
234+
return []*txplan.PlannedTx{pairA.SubmitInit(), pairB.SubmitExecTo(s.alice)},
235+
[]*txplan.PlannedTx{pairB.SubmitInit(), pairA.SubmitInvalidExecTo(s.bob)}
236+
},
237+
},
238+
{
239+
// Same as CascadeInvalid but with chains swapped.
240+
// Init(A) + invalid Exec(B→A) on chain A,
241+
// Init(B) + valid Exec(A→B) on chain B.
242+
Name: "SwapCascadeInvalid",
243+
BuildTxs: func(s *intraBlockSetup) ([]*txplan.PlannedTx, []*txplan.PlannedTx) {
244+
pairA := s.prepareInitA(0)
245+
pairB := s.prepareInitB(0)
246+
return []*txplan.PlannedTx{pairA.SubmitInit(), pairB.SubmitInvalidExecTo(s.alice)},
247+
[]*txplan.PlannedTx{pairB.SubmitInit(), pairA.SubmitExecTo(s.bob)}
248+
},
249+
},
250+
{
251+
// Valid cyclic cross-chain dependency:
252+
// Init(A) + Exec(B→A) on chain A,
253+
// Init(B) + Exec(A→B) on chain B.
254+
// Both blocks survive.
255+
Name: "CyclicDependencyValid",
256+
BuildTxs: func(s *intraBlockSetup) ([]*txplan.PlannedTx, []*txplan.PlannedTx) {
257+
pairA := s.prepareInitA(0)
258+
pairB := s.prepareInitB(0)
259+
return []*txplan.PlannedTx{pairA.SubmitInit(), pairB.SubmitExecTo(s.alice)},
260+
[]*txplan.PlannedTx{pairB.SubmitInit(), pairA.SubmitExecTo(s.bob)}
261+
},
262+
},
263+
{
264+
// Invalid cyclic dependency: both chains have invalid execs.
265+
// Init(A) + invalid Exec(B→A) on chain A,
266+
// Init(B) + invalid Exec(A→B) on chain B.
267+
// Both blocks replaced.
268+
Name: "CyclicDependencyInvalid",
269+
BuildTxs: func(s *intraBlockSetup) ([]*txplan.PlannedTx, []*txplan.PlannedTx) {
270+
pairA := s.prepareInitA(0)
271+
pairB := s.prepareInitB(0)
272+
return []*txplan.PlannedTx{pairA.SubmitInit(), pairB.SubmitInvalidExecTo(s.alice)},
273+
[]*txplan.PlannedTx{pairB.SubmitInit(), pairA.SubmitInvalidExecTo(s.bob)}
274+
},
275+
},
276+
{
277+
// Same-chain valid: Init + Exec on chain A, empty chain B.
278+
Name: "SameChainValid",
279+
BuildTxs: func(s *intraBlockSetup) ([]*txplan.PlannedTx, []*txplan.PlannedTx) {
280+
pair := s.prepareInitA(0)
281+
return []*txplan.PlannedTx{pair.SubmitInit(), pair.SubmitExecTo(s.alice)},
282+
nil
283+
},
284+
},
285+
{
286+
// Same-chain invalid: Init + invalid Exec on chain A, empty chain B.
287+
Name: "SameChainInvalid",
288+
BuildTxs: func(s *intraBlockSetup) ([]*txplan.PlannedTx, []*txplan.PlannedTx) {
289+
pair := s.prepareInitA(0)
290+
return []*txplan.PlannedTx{pair.SubmitInit(), pair.SubmitInvalidExecTo(s.alice)},
291+
nil
292+
},
293+
},
294+
}
295+
}

0 commit comments

Comments
 (0)