@@ -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+
230372func TestAdvanceChallengeOrigin (t * testing.T ) {
231373 logger := testlog .Logger (t , log .LevelWarn )
232374 ctx := context .Background ()
0 commit comments