@@ -22,78 +22,59 @@ const (
2222 DefaultDAIncludedCacheSize = 200_000
2323)
2424
25- // snapshotEntry is one record in the persisted in-flight window snapshot.
26- // Encoded as 16 contiguous bytes: [blockHeight uint64 LE][daHeight uint64 LE].
25+ // snapshotEntry is one record in the persisted snapshot.
26+ // Encoded as 16 bytes: [blockHeight uint64 LE][daHeight uint64 LE].
2727type snapshotEntry struct {
2828 blockHeight uint64
2929 daHeight uint64
3030}
3131
3232const snapshotEntrySize = 16 // bytes per snapshotEntry
3333
34- // Cache is a generic cache that maintains items that are seen and hard confirmed.
35- // Uses bounded thread-safe LRU caches to prevent unbounded memory growth.
34+ // Cache tracks seen blocks and DA inclusion status using bounded LRU caches.
3635//
37- // # Persistence strategy (O(1) restore)
36+ // Persistence: on every DA inclusion mutation a single snapshot key
37+ // (storeKeyPrefix+"__snap") is rewritten with all current in-flight
38+ // [blockHeight, daHeight] pairs. RestoreFromStore reads that one key on
39+ // startup — O(1) regardless of chain length.
3840//
39- // Rather than persisting one store key per DA-included hash (which required an
40- // O(n) prefix scan on restore), the Cache maintains a single *window snapshot*
41- // key in the store. The snapshot encodes the full set of currently in-flight
42- // entries as a compact byte slice:
43- //
44- // [ blockHeight₀ uint64-LE | daHeight₀ uint64-LE ]
45- // [ blockHeight₁ uint64-LE | daHeight₁ uint64-LE ]
46- // …
47- //
48- // Every call to setDAIncluded, removeDAIncluded, or deleteAllForHeight
49- // rewrites this single key atomically. On startup RestoreFromStore issues
50- // exactly one GetMetadata call and deserialises the slice — O(1) regardless
51- // of chain height or node type.
52- //
53- // The snapshot key is: storeKeyPrefix + "__snap"
41+ // After a restart, real content hashes are not available until the DA
42+ // retriever re-fires. In the meantime, placeholder keys indexed by height
43+ // allow lookups to succeed via getDAIncludedByHeight.
5444type Cache [T any ] struct {
55- // itemsByHeight stores items keyed by uint64 height.
56- // Mutex needed for atomic get-and-remove in getNextItem.
5745 itemsByHeight * lru.Cache [uint64 , * T ]
5846 itemsByHeightMu sync.Mutex
5947
60- // hashes tracks whether a given hash has been seen
6148 hashes * lru.Cache [string , bool ]
6249
63- // daIncluded tracks the DA inclusion height for a given hash
50+ // daIncluded maps hash → daHeight. Hash may be a real content hash or a
51+ // height placeholder (see HeightPlaceholderKey) immediately after restore.
6452 daIncluded * lru.Cache [string , uint64 ]
6553
66- // hashByHeight tracks the hash associated with each height for pruning.
67- // Mutex needed for atomic operations in deleteAllForHeight.
54+ // hashByHeight maps blockHeight → hash, used for pruning and height-based
55+ // lookups. Protected by hashByHeightMu only in deleteAllForHeight where a
56+ // read-then-remove must be atomic.
6857 hashByHeight * lru.Cache [uint64 , string ]
6958 hashByHeightMu sync.Mutex
7059
71- // maxDAHeight tracks the maximum DA height seen
7260 maxDAHeight * atomic.Uint64
7361
74- // store is used for persisting the window snapshot (optional, nil = ephemeral).
75- store store.Store
76- // storeKeyPrefix is the prefix used for store keys (header or data).
62+ store store.Store // nil = ephemeral, no persistence
7763 storeKeyPrefix string
7864}
7965
80- // snapshotKey returns the single metadata key used to persist the in-flight window.
8166func (c * Cache [T ]) snapshotKey () string {
8267 return c .storeKeyPrefix + "__snap"
8368}
8469
85- // NewCache returns a new Cache struct with default sizes.
86- //
87- // When store and keyPrefix are non-empty, setDAIncluded / removeDAIncluded /
88- // deleteAllForHeight persist a compact window snapshot under a single metadata
89- // key so that RestoreFromStore can recover the in-flight state with one store
90- // read (O(1)).
70+ // NewCache creates a Cache. When store and keyPrefix are set, mutations
71+ // persist a snapshot so RestoreFromStore can recover in-flight state.
72+ // The third argument (heightKeyFn) is retained for API compatibility but unused.
9173func NewCache [T any ](s store.Store , keyPrefix string , _ func (uint64 ) string ) * Cache [T ] {
9274 // LRU cache creation only fails if size <= 0, which won't happen with our defaults
9375 itemsCache , _ := lru.New [uint64 , * T ](DefaultItemsCacheSize )
9476 hashesCache , _ := lru.New [string , bool ](DefaultHashesCacheSize )
9577 daIncludedCache , _ := lru.New [string , uint64 ](DefaultDAIncludedCacheSize )
96- // hashByHeight must be at least as large as hashes cache to ensure proper pruning.
9778 hashByHeightCache , _ := lru.New [uint64 , string ](DefaultHashesCacheSize )
9879
9980 return & Cache [T ]{
@@ -121,7 +102,7 @@ func (c *Cache[T]) setItem(height uint64, item *T) {
121102 c .itemsByHeight .Add (height , item )
122103}
123104
124- // getNextItem returns the item at the specified height and removes it from cache if found .
105+ // getNextItem returns and removes the item at height, or nil if absent .
125106func (c * Cache [T ]) getNextItem (height uint64 ) * T {
126107 c .itemsByHeightMu .Lock ()
127108 defer c .itemsByHeightMu .Unlock ()
@@ -137,10 +118,7 @@ func (c *Cache[T]) getNextItem(height uint64) *T {
137118// isSeen returns true if the hash has been seen.
138119func (c * Cache [T ]) isSeen (hash string ) bool {
139120 seen , ok := c .hashes .Get (hash )
140- if ! ok {
141- return false
142- }
143- return seen
121+ return ok && seen
144122}
145123
146124// setSeen sets the hash as seen and tracks its height for pruning.
@@ -151,17 +129,27 @@ func (c *Cache[T]) setSeen(hash string, height uint64) {
151129
152130// getDAIncluded returns the DA height if the hash has been DA-included.
153131func (c * Cache [T ]) getDAIncluded (hash string ) (uint64 , bool ) {
154- daHeight , ok := c .daIncluded .Get (hash )
132+ return c .daIncluded .Get (hash )
133+ }
134+
135+ // getDAIncludedByHeight resolves DA height via the height→hash index.
136+ // Works for both real hashes (steady state) and snapshot placeholders
137+ // (post-restart, before the DA retriever re-fires the real hash).
138+ func (c * Cache [T ]) getDAIncludedByHeight (blockHeight uint64 ) (uint64 , bool ) {
139+ hash , ok := c .hashByHeight .Get (blockHeight )
155140 if ! ok {
156141 return 0 , false
157142 }
158- return daHeight , true
143+ return c . getDAIncluded ( hash )
159144}
160145
161- // setDAIncluded sets the hash as DA-included with the given DA height and
162- // tracks block height for pruning. It also rewrites the window snapshot so
163- // the in-flight state survives a crash/restart .
146+ // setDAIncluded records DA inclusion and persists the snapshot.
147+ // If a previous entry already exists at blockHeight (e.g. a placeholder from
148+ // RestoreFromStore), it is evicted from daIncluded to avoid orphan leaks .
164149func (c * Cache [T ]) setDAIncluded (hash string , daHeight uint64 , blockHeight uint64 ) {
150+ if prev , ok := c .hashByHeight .Get (blockHeight ); ok && prev != hash {
151+ c .daIncluded .Remove (prev )
152+ }
165153 c .daIncluded .Add (hash , daHeight )
166154 c .hashByHeight .Add (blockHeight , hash )
167155 c .setMaxDAHeight (daHeight )
@@ -220,26 +208,14 @@ func (c *Cache[T]) deleteAllForHeight(height uint64) {
220208 c .persistSnapshot (context .Background ())
221209}
222210
223- // persistSnapshot serialises all current daIncluded entries into a compact
224- // byte slice and writes it to the store under the single snapshot key.
225- //
226- // Format: N × 16 bytes where each record is:
227- //
228- // [blockHeight uint64 LE][daHeight uint64 LE]
229- //
230- // We iterate hashByHeight (height→hash) rather than daIncluded (hash→daH)
231- // because hashByHeight gives us the blockHeight we need to include in the
232- // record without an inverse lookup.
233- //
234- // This write happens on every mutation so the store always reflects the exact
235- // current in-flight window. The payload is small (typically < 10 entries ×
236- // 16 bytes = 160 bytes), so the cost is negligible.
211+ // persistSnapshot writes all current in-flight [blockHeight, daHeight] pairs
212+ // to the store under a single key. Called on every mutation; payload is tiny
213+ // (typically <10 entries × 16 bytes).
237214func (c * Cache [T ]) persistSnapshot (ctx context.Context ) {
238215 if c .store == nil || c .storeKeyPrefix == "" {
239216 return
240217 }
241218
242- // Collect all height→daHeight pairs that are still in daIncluded.
243219 heights := c .hashByHeight .Keys ()
244220 entries := make ([]snapshotEntry , 0 , len (heights ))
245221 for _ , h := range heights {
@@ -254,8 +230,7 @@ func (c *Cache[T]) persistSnapshot(ctx context.Context) {
254230 entries = append (entries , snapshotEntry {blockHeight : h , daHeight : daH })
255231 }
256232
257- buf := encodeSnapshot (entries )
258- _ = c .store .SetMetadata (ctx , c .snapshotKey (), buf )
233+ _ = c .store .SetMetadata (ctx , c .snapshotKey (), encodeSnapshot (entries ))
259234}
260235
261236// encodeSnapshot serialises a slice of snapshotEntry values into a byte slice.
@@ -275,8 +250,7 @@ func decodeSnapshot(buf []byte) []snapshotEntry {
275250 if len (buf ) == 0 || len (buf )% snapshotEntrySize != 0 {
276251 return nil
277252 }
278- n := len (buf ) / snapshotEntrySize
279- entries := make ([]snapshotEntry , n )
253+ entries := make ([]snapshotEntry , len (buf )/ snapshotEntrySize )
280254 for i := range entries {
281255 off := i * snapshotEntrySize
282256 entries [i ].blockHeight = binary .LittleEndian .Uint64 (buf [off :])
@@ -285,58 +259,36 @@ func decodeSnapshot(buf []byte) []snapshotEntry {
285259 return entries
286260}
287261
288- // RestoreFromStore recovers the in-flight DA inclusion window from the store.
289- //
290- // # Complexity: O(1)
291- //
292- // A single GetMetadata call retrieves the window snapshot written by
293- // persistSnapshot. The snapshot encodes the complete set of in-flight entries
294- // as a compact byte slice, so no iteration, prefix scan, or per-height lookup
295- // is required.
296- //
297- // If the snapshot key is absent (brand-new node, or node upgraded from an
298- // older version that did not write snapshots) the function is a no-op; the
299- // in-flight state will be reconstructed naturally as the submitter / DA
300- // retriever re-processes blocks.
262+ // RestoreFromStore loads the in-flight snapshot with a single store read.
263+ // Each entry is installed as a height placeholder; real hashes replace them
264+ // once the DA retriever re-fires SetHeaderDAIncluded after startup.
265+ // Missing snapshot key is treated as a no-op (fresh node or pre-snapshot version).
301266func (c * Cache [T ]) RestoreFromStore (ctx context.Context ) error {
302267 if c .store == nil || c .storeKeyPrefix == "" {
303268 return nil
304269 }
305270
306271 buf , err := c .store .GetMetadata (ctx , c .snapshotKey ())
307272 if err != nil {
308- // Key absent — nothing to restore (new node or pre-snapshot version).
309- return nil //nolint:nilerr // ok to ignore
273+ return nil //nolint:nilerr // key absent = nothing to restore
310274 }
311275
312- entries := decodeSnapshot (buf )
313- for _ , e := range entries {
314- syntheticHash := HeightPlaceholderKey (c .storeKeyPrefix , e .blockHeight )
315- c .daIncluded .Add (syntheticHash , e .daHeight )
316- c .hashByHeight .Add (e .blockHeight , syntheticHash )
276+ for _ , e := range decodeSnapshot (buf ) {
277+ placeholder := HeightPlaceholderKey (c .storeKeyPrefix , e .blockHeight )
278+ c .daIncluded .Add (placeholder , e .daHeight )
279+ c .hashByHeight .Add (e .blockHeight , placeholder )
317280 c .setMaxDAHeight (e .daHeight )
318281 }
319282
320283 return nil
321284}
322285
323- // HeightPlaceholderKey returns a deterministic, unique key used to index an
324- // in-flight DA inclusion entry by block height when the real content hash is
325- // not available during store restoration.
326- //
327- // Format: "<storeKeyPrefix>__h/<height_big_endian_hex>"
328- //
329- // The "__h/" infix cannot collide with real content hashes because real
330- // hashes are hex-encoded 32-byte digests (64 chars) whereas height values
331- // are at most 16 hex digits.
332- //
333- // This is exported so that callers (e.g. the submitter's IsHeightDAIncluded)
334- // can perform a height-based fallback lookup immediately after a restart,
335- // before the DA retriever has had a chance to re-fire SetHeaderDAIncluded with
336- // the real content hash.
286+ // HeightPlaceholderKey returns a store key for a height-indexed DA inclusion
287+ // entry used when the real content hash is unavailable (e.g. after restore).
288+ // Format: "<prefix>__h/<height_hex_16>" — cannot collide with real 64-char hashes.
337289func HeightPlaceholderKey (prefix string , height uint64 ) string {
338290 const hexDigits = "0123456789abcdef"
339- buf := make ([]byte , len (prefix )+ 4 + 16 ) // prefix + "__h/" + 16 hex chars
291+ buf := make ([]byte , len (prefix )+ 4 + 16 )
340292 n := copy (buf , prefix )
341293 n += copy (buf [n :], "__h/" )
342294 for i := 15 ; i >= 0 ; i -- {
@@ -346,8 +298,7 @@ func HeightPlaceholderKey(prefix string, height uint64) string {
346298 return string (buf )
347299}
348300
349- // SaveToStore persists all current DA inclusion entries to the store by
350- // rewriting the window snapshot. This is a no-op if no store is configured.
301+ // SaveToStore flushes the current snapshot to the store.
351302func (c * Cache [T ]) SaveToStore (ctx context.Context ) error {
352303 if c .store == nil {
353304 return nil
@@ -356,12 +307,11 @@ func (c *Cache[T]) SaveToStore(ctx context.Context) error {
356307 return nil
357308}
358309
359- // ClearFromStore removes the window snapshot from the store.
310+ // ClearFromStore deletes the snapshot key from the store.
360311func (c * Cache [T ]) ClearFromStore (ctx context.Context , _ []string ) error {
361312 if c .store == nil {
362313 return nil
363314 }
364- // Delete the snapshot key; ignore not-found.
365315 _ = c .store .DeleteMetadata (ctx , c .snapshotKey ())
366316 return nil
367317}
0 commit comments