@@ -38,11 +38,17 @@ type CacheManager interface {
3838 // Header operations
3939 IsHeaderSeen (hash string ) bool
4040 SetHeaderSeen (hash string , blockHeight uint64 )
41+ GetHeaderHashByHeight (blockHeight uint64 ) (string , bool )
4142 GetHeaderDAIncludedByHash (hash string ) (uint64 , bool )
4243 GetHeaderDAIncludedByHeight (blockHeight uint64 ) (uint64 , bool )
4344 SetHeaderDAIncluded (hash string , daHeight uint64 , blockHeight uint64 )
4445 RemoveHeaderDAIncluded (hash string )
4546
47+ // Pending signed header operations (in-flight, pre-persistence)
48+ SetPendingSignedHeader (h * types.SignedHeader , source string )
49+ GetPendingSignedHeader (blockHeight uint64 ) (* types.SignedHeader , string , bool )
50+ RemovePendingSignedHeader (blockHeight uint64 )
51+
4652 // Data operations
4753 IsDataSeen (hash string ) bool
4854 SetDataSeen (hash string , blockHeight uint64 )
@@ -92,17 +98,24 @@ type Manager interface {
9298var _ Manager = (* implementation )(nil )
9399
94100type implementation struct {
95- headerCache * Cache
96- dataCache * Cache
97- txCache * Cache
98- txTimestamps * sync.Map // map[string]time.Time
99- pendingEvents map [uint64 ]* common.DAHeightEvent
100- pendingMu sync.Mutex
101- pendingHeaders * PendingHeaders
102- pendingData * PendingData
103- store store.Store
104- config config.Config
105- logger zerolog.Logger
101+ headerCache * Cache
102+ dataCache * Cache
103+ txCache * Cache
104+ txTimestamps * sync.Map // map[string]time.Time
105+ pendingEvents map [uint64 ]* common.DAHeightEvent
106+ pendingMu sync.Mutex
107+ pendingHeaders * PendingHeaders
108+ pendingData * PendingData
109+ pendingSignedHeaders map [uint64 ]pendingSignedHeader
110+ pendingSignedHeadersMu sync.RWMutex
111+ store store.Store
112+ config config.Config
113+ logger zerolog.Logger
114+ }
115+
116+ type pendingSignedHeader struct {
117+ header * types.SignedHeader
118+ source string
106119}
107120
108121// NewManager creates a new Manager, restoring or clearing persisted state as configured.
@@ -122,16 +135,17 @@ func NewManager(cfg config.Config, st store.Store, logger zerolog.Logger) (Manag
122135 }
123136
124137 impl := & implementation {
125- headerCache : headerCache ,
126- dataCache : dataCache ,
127- txCache : txCache ,
128- txTimestamps : new (sync.Map ),
129- pendingEvents : make (map [uint64 ]* common.DAHeightEvent ),
130- pendingHeaders : pendingHeaders ,
131- pendingData : pendingData ,
132- store : st ,
133- config : cfg ,
134- logger : logger ,
138+ headerCache : headerCache ,
139+ dataCache : dataCache ,
140+ txCache : txCache ,
141+ txTimestamps : new (sync.Map ),
142+ pendingEvents : make (map [uint64 ]* common.DAHeightEvent ),
143+ pendingHeaders : pendingHeaders ,
144+ pendingData : pendingData ,
145+ pendingSignedHeaders : make (map [uint64 ]pendingSignedHeader ),
146+ store : st ,
147+ config : cfg ,
148+ logger : logger ,
135149 }
136150
137151 if cfg .ClearCache {
@@ -157,6 +171,11 @@ func (m *implementation) SetHeaderSeen(hash string, blockHeight uint64) {
157171 m .headerCache .setSeen (hash , blockHeight )
158172}
159173
174+ // GetHeaderHashByHeight returns the first-seen header hash at the given height.
175+ func (m * implementation ) GetHeaderHashByHeight (blockHeight uint64 ) (string , bool ) {
176+ return m .headerCache .getHashByHeight (blockHeight )
177+ }
178+
160179func (m * implementation ) GetHeaderDAIncludedByHash (hash string ) (uint64 , bool ) {
161180 return m .headerCache .getDAIncluded (hash )
162181}
@@ -173,6 +192,42 @@ func (m *implementation) RemoveHeaderDAIncluded(hash string) {
173192 m .headerCache .removeDAIncluded (hash )
174193}
175194
195+ // SetPendingSignedHeader records the first SignedHeader seen at this height.
196+ // First-write-wins: later writes at the same height are ignored so the
197+ // double-sign detector can match alternates against the original observation.
198+ func (m * implementation ) SetPendingSignedHeader (h * types.SignedHeader , source string ) {
199+ if h == nil {
200+ return
201+ }
202+ height := h .Height ()
203+ m .pendingSignedHeadersMu .Lock ()
204+ defer m .pendingSignedHeadersMu .Unlock ()
205+ if _ , exists := m .pendingSignedHeaders [height ]; exists {
206+ return
207+ }
208+ m .pendingSignedHeaders [height ] = pendingSignedHeader {header : h , source : source }
209+ }
210+
211+ // GetPendingSignedHeader returns the first-seen SignedHeader and the source
212+ // ("da" or "p2p") it was observed from.
213+ func (m * implementation ) GetPendingSignedHeader (blockHeight uint64 ) (* types.SignedHeader , string , bool ) {
214+ m .pendingSignedHeadersMu .RLock ()
215+ defer m .pendingSignedHeadersMu .RUnlock ()
216+ entry , ok := m .pendingSignedHeaders [blockHeight ]
217+ if ! ok {
218+ return nil , "" , false
219+ }
220+ return entry .header , entry .source , true
221+ }
222+
223+ // RemovePendingSignedHeader evicts the entry once the height is persisted, so
224+ // the store becomes the authoritative source for double-sign comparison.
225+ func (m * implementation ) RemovePendingSignedHeader (blockHeight uint64 ) {
226+ m .pendingSignedHeadersMu .Lock ()
227+ delete (m .pendingSignedHeaders , blockHeight )
228+ m .pendingSignedHeadersMu .Unlock ()
229+ }
230+
176231// DaHeight returns the highest DA height seen across header and data caches.
177232func (m * implementation ) DaHeight () uint64 {
178233 return max (m .headerCache .daHeight (), m .dataCache .daHeight ())
@@ -263,6 +318,7 @@ func (m *implementation) DeleteHeight(blockHeight uint64) {
263318 m .pendingMu .Lock ()
264319 delete (m .pendingEvents , blockHeight )
265320 m .pendingMu .Unlock ()
321+ m .RemovePendingSignedHeader (blockHeight )
266322
267323 // Note: txCache is intentionally NOT deleted here because:
268324 // 1. Transactions are tracked by hash, not by block height (they use height 0)
@@ -408,6 +464,9 @@ func (m *implementation) ClearFromStore() error {
408464 m .dataCache = NewCache (m .store , DataDAIncludedPrefix )
409465 m .txCache = NewCache (nil , "" )
410466 m .pendingEvents = make (map [uint64 ]* common.DAHeightEvent )
467+ m .pendingSignedHeadersMu .Lock ()
468+ m .pendingSignedHeaders = make (map [uint64 ]pendingSignedHeader )
469+ m .pendingSignedHeadersMu .Unlock ()
411470
412471 // Initialize DA height from store metadata to ensure DaHeight() is never 0.
413472 m .initDAHeightFromStore (ctx )
0 commit comments