@@ -116,7 +116,6 @@ func (f *daFollower) HandleEvent(ctx context.Context, ev datypes.SubscriptionEve
116116 }
117117 }
118118 if len (events ) != 0 {
119- f .subscriber .SetHeadReached ()
120119 f .logger .Debug ().Uint64 ("da_height" , ev .Height ).Int ("events" , len (events )).
121120 Msg ("processed subscription blobs inline (fast path)" )
122121 } else {
@@ -131,38 +130,63 @@ func (f *daFollower) HandleEvent(ctx context.Context, ev datypes.SubscriptionEve
131130
132131// HandleCatchup retrieves events at a single DA height and pipes them
133132// to the event sink. Checks priority heights first.
133+ //
134+ // ErrHeightFromFuture is handled here rather than in fetchAndPipeHeight because
135+ // the two call sites need different behaviour:
136+ // - Normal catchup: mark head reached and return da.ErrCaughtUp to stop the loop.
137+ // - Priority hint: the hint points to a future height — ignore it without
138+ // skipping the current daHeight or stopping catchup.
134139func (f * daFollower ) HandleCatchup (ctx context.Context , daHeight uint64 ) error {
135- // Check for priority heights from P2P hints first.
136- if priorityHeight := f .popPriorityHeight (); priorityHeight > 0 {
137- if priorityHeight >= daHeight {
138- f .logger .Debug ().
139- Uint64 ("da_height" , priorityHeight ).
140- Msg ("fetching priority DA height from P2P hint" )
141- if err := f .fetchAndPipeHeight (ctx , priorityHeight ); err != nil {
142- return err
140+ // 1. Drain stale or future priority heights from P2P hints
141+ for {
142+ priorityHeight := f .popPriorityHeight ()
143+ if priorityHeight == 0 {
144+ break
145+ }
146+ if priorityHeight < daHeight {
147+ continue // skip stale hints without yielding back to the catchup loop
148+ }
149+
150+ f .logger .Debug ().
151+ Uint64 ("da_height" , priorityHeight ).
152+ Msg ("fetching priority DA height from P2P hint" )
153+
154+ if err := f .fetchAndPipeHeight (ctx , priorityHeight ); err != nil {
155+ if errors .Is (err , datypes .ErrHeightFromFuture ) {
156+ // Priority hint points to a future height — silently ignore.
157+ f .logger .Debug ().Uint64 ("priority_da_height" , priorityHeight ).
158+ Msg ("priority hint is from future, ignoring" )
159+ continue
143160 }
161+ // Roll back so daHeight is attempted again next cycle after backoff.
162+ f .subscriber .SetLocalHeight (daHeight )
163+ return err
144164 }
145- // Re-queue the current height by rolling back (the subscriber already advanced).
165+
166+ // We successfully handled a priority height (we didn't actually process `daHeight`)
167+ // Roll back so daHeight is attempted again next cycle.
146168 f .subscriber .SetLocalHeight (daHeight )
147169 return nil
148170 }
149171
150- return f .fetchAndPipeHeight (ctx , daHeight )
172+ // 2. Normal sequential fetch
173+ if err := f .fetchAndPipeHeight (ctx , daHeight ); err != nil {
174+ return err
175+ }
176+ return nil
151177}
152178
153179// fetchAndPipeHeight retrieves events at a single DA height and pipes them.
180+ // It does NOT handle ErrHeightFromFuture — callers must decide how to react
181+ // because the correct response depends on whether this is a normal sequential
182+ // catchup or a priority-hint fetch.
154183func (f * daFollower ) fetchAndPipeHeight (ctx context.Context , daHeight uint64 ) error {
155184 events , err := f .retriever .RetrieveFromDA (ctx , daHeight )
156185 if err != nil {
157- switch {
158- case errors .Is (err , datypes .ErrBlobNotFound ):
186+ if errors .Is (err , datypes .ErrBlobNotFound ) {
159187 return nil
160- case errors .Is (err , datypes .ErrHeightFromFuture ):
161- f .subscriber .SetHeadReached ()
162- return err
163- default :
164- return err
165188 }
189+ return err
166190 }
167191
168192 for _ , event := range events {
@@ -200,5 +224,8 @@ func (f *daFollower) popPriorityHeight() uint64 {
200224 }
201225 height := f .priorityHeights [0 ]
202226 f .priorityHeights = f .priorityHeights [1 :]
227+ if len (f .priorityHeights ) == 0 {
228+ f .priorityHeights = nil
229+ }
203230 return height
204231}
0 commit comments