Sitelet https://github.com/ethereum-optimism/optimism/commit/95cde134435111150470bfee12cd45f207014d15
Skip to content

Commit 95cde13

Browse files
authored
fix: alt da: handle no commitments case when finalized head is updated (#18866)
* fix: alt da: handle no commitments case when finalized head is updated * add tests * update fix
1 parent 48a7a09 commit 95cde13

2 files changed

Lines changed: 150 additions & 0 deletions

File tree

‎op-alt-da/damgr.go‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,14 @@ func (d *DA) OnFinalizedHeadSignal(f HeadSignalFn) {
117117
// It is called by the Finalize function, as it has an L1 finalized head to use.
118118
func (d *DA) updateFinalizedHead(l1Finalized eth.L1BlockRef) {
119119
d.l1FinalizedHead = l1Finalized
120+
121+
// If there are no commitments or challenges being tracked, finalizedHead is managed
122+
// by updateFinalizedFromL1 (called from AdvanceL1Origin) which calculates it based
123+
// on l1FinalizedHead - challengeWindow. Preserve that value.
124+
if d.state.NoCommitments() {
125+
return
126+
}
127+
120128
// Prune the state to the finalized head
121129
d.state.Prune(l1Finalized.ID())
122130
d.finalizedHead = d.state.lastPrunedCommitment

‎op-alt-da/damgr_test.go‎

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -227,6 +227,148 @@ func (m *mockL1Fetcher) ExpectL1BlockRefByNumber(num uint64, ref eth.L1BlockRef,
227227
m.Mock.On("L1BlockRefByNumber", num).Once().Return(ref, err)
228228
}
229229

230+
// TestUpdateFinalizedHead tests the updateFinalizedHead behavior with and without commitments.
231+
// When there are no commitments, updateFinalizedHead preserves the existing finalizedHead,
232+
// which is managed by updateFinalizedFromL1 called from AdvanceL1Origin.
233+
func TestUpdateFinalizedHead(t *testing.T) {
234+
logger := testlog.Logger(t, log.LevelInfo)
235+
cfg := Config{
236+
ResolveWindow: 6,
237+
ChallengeWindow: 6,
238+
}
239+
240+
t.Run("no commitments preserves existing finalizedHead unchanged", func(t *testing.T) {
241+
state := NewState(logger, &NoopMetrics{}, cfg)
242+
storage := NewMockDAClient(logger)
243+
da := NewAltDAWithState(logger, cfg, storage, &NoopMetrics{}, state)
244+
245+
// Verify state has no commitments
246+
require.True(t, state.NoCommitments())
247+
248+
// Set an initial finalizedHead (simulating what updateFinalizedFromL1 would do)
249+
initialFinalizedHead := l1Ref(50)
250+
da.finalizedHead = initialFinalizedHead
251+
252+
// Call Finalize with l1Finalized
253+
l1Finalized := l1Ref(100)
254+
da.Finalize(l1Finalized)
255+
256+
// finalizedHead should be preserved (not overwritten) since there are no commitments
257+
require.Equal(t, initialFinalizedHead, da.finalizedHead)
258+
// l1FinalizedHead should be updated
259+
require.Equal(t, l1Finalized, da.l1FinalizedHead)
260+
})
261+
262+
t.Run("no commitments after all pruned preserves existing finalizedHead", func(t *testing.T) {
263+
rng := rand.New(rand.NewSource(1234))
264+
state := NewState(logger, &NoopMetrics{}, cfg)
265+
storage := NewMockDAClient(logger)
266+
da := NewAltDAWithState(logger, cfg, storage, &NoopMetrics{}, state)
267+
268+
// Track and expire a commitment to set lastPrunedCommitment
269+
c1 := RandomCommitment(rng)
270+
bn1 := uint64(10)
271+
state.TrackCommitment(c1, l1Ref(bn1))
272+
require.NoError(t, state.ExpireCommitments(bID(bn1+cfg.ChallengeWindow)))
273+
state.Prune(bID(bn1 + cfg.ChallengeWindow))
274+
275+
// Verify lastPrunedCommitment is set and no more commitments
276+
require.Equal(t, l1Ref(bn1), state.lastPrunedCommitment)
277+
require.True(t, state.NoCommitments())
278+
279+
// Simulate updateFinalizedFromL1 having set the finalizedHead
280+
initialFinalizedHead := l1Ref(80)
281+
da.finalizedHead = initialFinalizedHead
282+
283+
// Call Finalize with l1Finalized
284+
l1Finalized := l1Ref(100)
285+
da.Finalize(l1Finalized)
286+
287+
// finalizedHead should be preserved since there are no commitments
288+
require.Equal(t, initialFinalizedHead, da.finalizedHead)
289+
})
290+
291+
t.Run("with pending commitments prunes and uses lastPrunedCommitment", func(t *testing.T) {
292+
rng := rand.New(rand.NewSource(1234))
293+
state := NewState(logger, &NoopMetrics{}, cfg)
294+
storage := NewMockDAClient(logger)
295+
da := NewAltDAWithState(logger, cfg, storage, &NoopMetrics{}, state)
296+
297+
// Track a commitment that will be pruned
298+
c1 := RandomCommitment(rng)
299+
bn1 := uint64(10)
300+
state.TrackCommitment(c1, l1Ref(bn1))
301+
require.NoError(t, state.ExpireCommitments(bID(bn1+cfg.ChallengeWindow)))
302+
state.Prune(bID(bn1 + cfg.ChallengeWindow))
303+
304+
// Track another commitment that won't be expired/pruned
305+
c2 := RandomCommitment(rng)
306+
bn2 := uint64(50)
307+
state.TrackCommitment(c2, l1Ref(bn2))
308+
309+
// Verify state has pending commitments
310+
require.False(t, state.NoCommitments())
311+
require.Equal(t, l1Ref(bn1), state.lastPrunedCommitment)
312+
313+
// Call Finalize with l1Finalized higher than lastPrunedCommitment
314+
l1Finalized := l1Ref(100)
315+
da.Finalize(l1Finalized)
316+
317+
// finalizedHead should be lastPrunedCommitment because there are pending commitments
318+
require.Equal(t, l1Ref(bn1), da.finalizedHead)
319+
})
320+
321+
t.Run("with commitments prunes up to l1Finalized and updates finalizedHead", func(t *testing.T) {
322+
rng := rand.New(rand.NewSource(1234))
323+
state := NewState(logger, &NoopMetrics{}, cfg)
324+
storage := NewMockDAClient(logger)
325+
da := NewAltDAWithState(logger, cfg, storage, &NoopMetrics{}, state)
326+
327+
// Track and expire multiple commitments
328+
c1 := RandomCommitment(rng)
329+
bn1 := uint64(10)
330+
state.TrackCommitment(c1, l1Ref(bn1))
331+
332+
c2 := RandomCommitment(rng)
333+
bn2 := uint64(20)
334+
state.TrackCommitment(c2, l1Ref(bn2))
335+
336+
// Expire both commitments
337+
require.NoError(t, state.ExpireCommitments(bID(bn2+cfg.ChallengeWindow)))
338+
339+
// Verify we have expired commitments ready to prune
340+
require.False(t, state.NoCommitments())
341+
342+
// Call Finalize - this should prune up to l1Finalized
343+
l1Finalized := l1Ref(bn2 + cfg.ChallengeWindow)
344+
da.Finalize(l1Finalized)
345+
346+
// Both commitments should be pruned, finalizedHead should be the last pruned one
347+
require.Equal(t, l1Ref(bn2), da.finalizedHead)
348+
})
349+
350+
t.Run("finalized head signal handler is called with correct value", func(t *testing.T) {
351+
state := NewState(logger, &NoopMetrics{}, cfg)
352+
storage := NewMockDAClient(logger)
353+
da := NewAltDAWithState(logger, cfg, storage, &NoopMetrics{}, state)
354+
355+
// Set initial finalizedHead (simulating updateFinalizedFromL1)
356+
initialFinalizedHead := l1Ref(50)
357+
da.finalizedHead = initialFinalizedHead
358+
359+
var receivedHead eth.L1BlockRef
360+
da.OnFinalizedHeadSignal(func(ref eth.L1BlockRef) {
361+
receivedHead = ref
362+
})
363+
364+
l1Finalized := l1Ref(100)
365+
da.Finalize(l1Finalized)
366+
367+
// Handler should receive the preserved finalizedHead (since no commitments)
368+
require.Equal(t, initialFinalizedHead, receivedHead)
369+
})
370+
}
371+
230372
func TestAdvanceChallengeOrigin(t *testing.T) {
231373
logger := testlog.Logger(t, log.LevelWarn)
232374
ctx := context.Background()

0 commit comments

Comments
 (0)