Skip to content

refactor: remove unnecessary usage of lru#3204

Open
julienrbrt wants to merge 17 commits intomainfrom
julien/cache
Open

refactor: remove unnecessary usage of lru#3204
julienrbrt wants to merge 17 commits intomainfrom
julien/cache

Conversation

@julienrbrt
Copy link
Copy Markdown
Member

@julienrbrt julienrbrt commented Mar 26, 2026

Overview

My previous PR got merged with a test commit (longer version of this). This should have been merged instead.
Same behavior.

Summary by CodeRabbit

  • New Features

    • Added evnode.da.start_height configuration flag to override Data Availability start height.
  • Refactor

    • Optimized internal caching system for improved performance and maintainability.
    • Simplified Data Availability retriever height initialization logic for better state consistency.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Mar 26, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

This PR modifies DA (Data Availability) height initialization logic to support configuration-driven overrides, removes the generic type parameter from the cache layer and replaces LRU-backed storage with plain map-backed storage under unified locking, and updates dependent code to use the simplified cache API.

Changes

Cohort / File(s) Summary
DA Height & Configuration
block/internal/syncing/syncer.go, pkg/config/config.go, pkg/config/config_test.go
Modified DA retriever height initialization to simplify baseline derivation from genesis and persisted state, add conditional cache consultation when P2P syncing is active, and support config-driven override via new DAConfig.StartHeight flag. Updated test flag count to reflect new evnode.da.start_height flag.
Cache Layer Refactoring
block/internal/cache/generic_cache.go, block/internal/cache/generic_cache_test.go, block/internal/cache/manager.go
Converted generic Cache[T] type to non-generic Cache backed by plain Go maps instead of multiple LRU caches, removed item-by-height storage and related methods, consolidated locking to a single mutex, and replaced pending-event cache with in-memory map in manager. Updated all cache call sites and tests to use new API.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

Suggested reviewers

  • tac0turtle

Poem

🐰 The cache hops lighter with maps so fleet,
No LRU chains to skip and retreat!
DA heights now dance to config's tune,
One mutex guards all—a locking commune!
Simpler paths, cleaner state,
Our refactored burrow feels first-rate! ✨

🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 65.52% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Description check ❓ Inconclusive The description is largely incomplete. While it mentions a previous PR with a test commit, it lacks substantive details about the changes, rationale, or behavior verification. Expand the description with specific details about why LRU caches were removed, what simpler implementation replaces them, and confirm the behavior equivalence through testing.
✅ Passed checks (1 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main refactoring objective: removing unnecessary LRU cache usage and replacing it with simpler map-based implementations.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch julien/cache

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Mar 26, 2026

The latest Buf updates on your PR. Results from workflow CI / buf-check (pull_request).

BuildFormatLintBreakingUpdated (UTC)
✅ passed⏩ skipped✅ passed✅ passedMar 27, 2026, 4:05 PM

@claude
Copy link
Copy Markdown
Contributor

claude bot commented Mar 26, 2026

Claude finished @julienrbrt's task in 2m 19s —— View job


Code Review

  • Read AGENTS.md and repo guidelines
  • Review cache layer changes (generic_cache.go, manager.go)
  • Review syncer DA height initialization (syncer.go)
  • Review config changes (config.go)
  • Review tests

Summary

This is a clean simplification — removing the LRU dependency and replacing bounded caches with plain Go maps backed by a single mutex. The behavior is equivalent for the production workloads this system sees, and the code is significantly easier to reason about. The DA height initialization refactor also cleanly addresses the prior min(state.DAHeight-1, 0) uint64 underflow bug.

A few things worth discussing below.


Issues

1. hashByHeight shared between setSeen and setDAIncluded — stale hashes entries possible [Medium]

block/internal/cache/generic_cache.go:61-66

Both setSeen and setDAIncluded write to hashByHeight[height]. If setSeen("hashA", 1) is followed by setDAIncluded("hashB", 100, 1), the hashByHeight[1] will be overwritten to "hashB", and "hashA" will remain in c.hashes with no way to remove it via deleteAllForHeight(1) (which only removes whatever is currently at hashByHeight[1]).

In normal flow (header seen → same header DA-confirmed) both calls use the same hash so this is a non-issue. But if a header is re-submitted under a different hash before finalization, the first hash leaks in c.hashes indefinitely. The old LRU code had this same behavior (LRU eviction would eventually clean up), but with unbounded plain maps it becomes a permanent leak for those keys.

Worth noting this is unlikely in practice, but it's worth documenting the invariant or adding an assertion that the hash passed to setDAIncluded always matches the one from setSeen.

2. ClearFromStore does not reinitialize txTimestamps [Minor]

block/internal/cache/manager.go:399-402

m.txCache = NewCache(nil, "")
m.pendingEvents = make(map[uint64]*common.DAHeightEvent)
// m.txTimestamps is NOT reset

After ClearFromStore, m.txCache is a fresh empty cache but m.txTimestamps still holds entries from before the clear. The next call to CleanupOldTxs will iterate those stale entries and call m.txCache.removeSeen(hash) — which is a no-op since the fresh cache doesn't know about them. Eventually they get cleaned up by time, so this is functionally harmless. Still worth resetting for correctness: m.txTimestamps = new(sync.Map). Fix this →

3. Typo in help text [Minor]

pkg/config/config.go:570

cmd.Flags().String(FlagDBPath, def.DBPath, "path for for node database")
//                                                      ^^^^^  ^^^

Duplicate "for". Fix this →


Observations

DA height initialization change is a behavioral shift worth calling out

block/internal/syncing/syncer.go:356-365

The old code consulted the per-block cache (GetHeaderDAIncludedByHeight(state.LastBlockHeight)) to get the DA height of the last applied block. The new code uses state.DAHeight - 1 as the primary watermark and falls back to the global cache.DaHeight() only when P2P is ahead.

The rationale is sound: state.DAHeight is persisted and reliable, while per-block cache entries are only present if the snapshot has them. DaHeight() is seeded from store metadata (initDAHeightFromStore) so it's also reliable. The semantics change slightly: previously we resumed from the DA height of the last specific block we applied; now we resume from state.DAHeight - 1 (which is the DA height recorded in the chain state). These should be equivalent in steady state, but the new approach is cleaner.

DA start height override is a powerful but well-guarded escape hatch

block/internal/syncing/syncer.go:367-374

The flag is hidden (cmd.Flags().MarkHidden) and intended for operator recovery. One nit: the comment says // dev mode for da start height — for an operator-facing recovery tool (not just dev), a more descriptive comment would help future readers understand when to reach for this knob. A validation guard (e.g., refuse values below s.genesis.DAStartHeight) would also protect against easy misconfiguration.

setMaxDAHeight spin-loop bound

block/internal/cache/generic_cache.go:117-126

The retry loop runs for range 1_000. Under very high contention from multiple goroutines writing to the same *atomic.Uint64, 1000 CAS retries could theoretically be exhausted (silent no-op drop). This is astronomically unlikely in practice given the low write rate here, but worth a comment explaining why 1000 is sufficient.

Test coverage is good

The RestoreFromStore suite covers: empty store, fully finalized, in-flight window, placeholder eviction, round-trip, and restart idempotency. These are exactly the cases that matter most for correctness of the snapshot mechanism. The TestCache_DeleteAllForHeight_CleansHashAndDA test covers the cleanup path well.


Overall this is a solid refactor. The LRU removal simplifies both the code and the mental model (no more per-cache size tuning), and the plain map + mutex approach is correct and idiomatic. The three minor issues above are low priority but worth a follow-up.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@block/internal/syncing/syncer.go`:
- Around line 359-362: The daHeight computation uses min(state.DAHeight-1, 0)
which always yields 0; change it to compute a safe candidate from state.DAHeight
(use candidate = state.DAHeight-1 when state.DAHeight > 0, else candidate = 0)
and then set daHeight = max(s.genesis.DAStartHeight, candidate). Keep the
subsequent conditional that bumps daHeight to max(daHeight, s.cache.DaHeight())
when s.headerStore != nil && s.headerStore.Height() > state.LastBlockHeight;
update the code around s.genesis.DAStartHeight, state.DAHeight, s.headerStore,
s.cache.DaHeight(), and state.LastBlockHeight accordingly.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 08094f15-06b3-4f6a-bf87-9ba1939c5d75

📥 Commits

Reviewing files that changed from the base of the PR and between 162cda6 and 240a0dd.

📒 Files selected for processing (1)
  • block/internal/syncing/syncer.go

@codecov
Copy link
Copy Markdown

codecov bot commented Mar 26, 2026

Codecov Report

❌ Patch coverage is 88.88889% with 13 lines in your changes missing coverage. Please review.
✅ Project coverage is 61.45%. Comparing base (162cda6) to head (952339c).

Files with missing lines Patch % Lines
block/internal/syncing/syncer.go 27.27% 6 Missing and 2 partials ⚠️
block/internal/cache/manager.go 91.17% 3 Missing ⚠️
block/internal/cache/generic_cache.go 97.10% 1 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #3204      +/-   ##
==========================================
+ Coverage   61.42%   61.45%   +0.02%     
==========================================
  Files         120      120              
  Lines       12449    12462      +13     
==========================================
+ Hits         7647     7658      +11     
- Misses       3942     3945       +3     
+ Partials      860      859       -1     
Flag Coverage Δ
combined 61.45% <88.88%> (+0.02%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (1)
block/internal/syncing/syncer.go (1)

359-362: ⚠️ Potential issue | 🔴 Critical

Bug: min(state.DAHeight-1, 0) always evaluates to 0 for uint64.

This is a critical logic error that was flagged in a previous review but remains unfixed:

  • When state.DAHeight > 0: e.g., if state.DAHeight = 5, then min(4, 0) = 0
  • When state.DAHeight = 0: underflows to MaxUint64, then min(MaxUint64, 0) = 0

The result is that daHeight always equals s.genesis.DAStartHeight, completely ignoring the persisted state.DAHeight. This contradicts the comment at line 356-358 which states the intent is to use the maximum of genesis, state, and cache DA heights.

🐛 Proposed fix
 	// Set DA height to the maximum of the genesis start height, the state's DA height, and the cached DA height.
 	// The cache's DaHeight() is initialized from store metadata, so it's always correct even after cache clear.
 	// Only use cache.DaHeight() when P2P is actively syncing (headerStore has higher height than current state).
-	daHeight := max(s.genesis.DAStartHeight, min(state.DAHeight-1, 0))
+	daHeight := s.genesis.DAStartHeight
+	if state.DAHeight > 0 {
+		// Use state.DAHeight - 1 to re-fetch the last DA height in case it wasn't fully processed
+		daHeight = max(daHeight, state.DAHeight-1)
+	}
 	if s.headerStore != nil && s.headerStore.Height() > state.LastBlockHeight {
 		daHeight = max(daHeight, s.cache.DaHeight())
 	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@block/internal/syncing/syncer.go` around lines 359 - 362, The computation for
daHeight incorrectly uses min(state.DAHeight-1, 0) on a uint64 (always 0);
instead compute a safe state candidate by checking if state.DAHeight > 0 then
use state.DAHeight-1 else use 0, then set daHeight =
max(s.genesis.DAStartHeight, stateCandidate) and finally, if s.headerStore !=
nil && s.headerStore.Height() > state.LastBlockHeight keep the existing behavior
of daHeight = max(daHeight, s.cache.DaHeight()); update the logic around the
daHeight variable and reference s.genesis.DAStartHeight, state.DAHeight,
s.cache.DaHeight(), s.headerStore.Height(), and state.LastBlockHeight when
making the change.
🧹 Nitpick comments (1)
block/internal/syncing/syncer.go (1)

357-358: Comment may be misleading about cache behavior after clear.

The comment states "The cache's DaHeight() is initialized from store metadata, so it's always correct even after cache clear." However, based on the relevant code snippets:

  1. RestoreFromStore() (context snippet 1) only runs once at initialization
  2. maxDAHeight (context snippet 3) is updated via setMaxDAHeight() only when items are added
  3. After a cache clear during syncing, maxDAHeight can regress if no new items with higher DA heights are subsequently added

This means s.cache.DaHeight() may return a lower value than expected after a cache clear, which could cause re-fetching of already-processed DA heights. Consider clarifying the comment or verifying the intended behavior.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@block/internal/syncing/syncer.go` around lines 357 - 358, The comment
incorrectly guarantees cache.DaHeight() stays correct after a clear; because
RestoreFromStore() runs only at init and maxDAHeight is updated only via
setMaxDAHeight() when items are added, clearing the cache can leave maxDAHeight
too low and cause re-fetches. Fix by either updating the comment to state that
DaHeight may regress after a cache clear unless restored from persistent
metadata, or change the logic: on cache.Clear()/during syncing ensure
RestoreFromStore() or a persistent max DA readback is invoked (or retain/restore
maxDAHeight from store metadata) so that s.cache.DaHeight() reflects the true
maximum; reference RestoreFromStore, setMaxDAHeight, maxDAHeight and
cache.DaHeight in your change.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@pkg/config/config.go`:
- Line 570: Fix the typo in the flag help string passed to cmd.Flags().String
for FlagDBPath in config.go: change the duplicated wording "path for for node
database" to a correct phrase such as "path for node database" (or "path to node
database"); ensure the call creating the flag remains
cmd.Flags().String(FlagDBPath, def.DBPath, "<corrected help text>").

---

Duplicate comments:
In `@block/internal/syncing/syncer.go`:
- Around line 359-362: The computation for daHeight incorrectly uses
min(state.DAHeight-1, 0) on a uint64 (always 0); instead compute a safe state
candidate by checking if state.DAHeight > 0 then use state.DAHeight-1 else use
0, then set daHeight = max(s.genesis.DAStartHeight, stateCandidate) and finally,
if s.headerStore != nil && s.headerStore.Height() > state.LastBlockHeight keep
the existing behavior of daHeight = max(daHeight, s.cache.DaHeight()); update
the logic around the daHeight variable and reference s.genesis.DAStartHeight,
state.DAHeight, s.cache.DaHeight(), s.headerStore.Height(), and
state.LastBlockHeight when making the change.

---

Nitpick comments:
In `@block/internal/syncing/syncer.go`:
- Around line 357-358: The comment incorrectly guarantees cache.DaHeight() stays
correct after a clear; because RestoreFromStore() runs only at init and
maxDAHeight is updated only via setMaxDAHeight() when items are added, clearing
the cache can leave maxDAHeight too low and cause re-fetches. Fix by either
updating the comment to state that DaHeight may regress after a cache clear
unless restored from persistent metadata, or change the logic: on
cache.Clear()/during syncing ensure RestoreFromStore() or a persistent max DA
readback is invoked (or retain/restore maxDAHeight from store metadata) so that
s.cache.DaHeight() reflects the true maximum; reference RestoreFromStore,
setMaxDAHeight, maxDAHeight and cache.DaHeight in your change.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 7b99f109-c982-4c0c-a05c-190556abf106

📥 Commits

Reviewing files that changed from the base of the PR and between 240a0dd and b580bc5.

📒 Files selected for processing (3)
  • block/internal/syncing/syncer.go
  • pkg/config/config.go
  • pkg/config/config_test.go
✅ Files skipped from review due to trivial changes (1)
  • pkg/config/config_test.go

@julienrbrt julienrbrt requested a review from tac0turtle March 26, 2026 22:10
@julienrbrt julienrbrt enabled auto-merge March 26, 2026 22:10
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (1)
pkg/config/config.go (1)

570-570: ⚠️ Potential issue | 🟡 Minor

Fix the duplicated word in the DB path help text.

--help currently prints path for for node database.

📝 Suggested fix
-	cmd.Flags().String(FlagDBPath, def.DBPath, "path for for node database")
+	cmd.Flags().String(FlagDBPath, def.DBPath, "path to node database")
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/config/config.go` at line 570, The help text for the DB path flag
contains a duplicated word; update the call to cmd.Flags().String(FlagDBPath,
def.DBPath, ...) to remove the extra "for" so the description reads "path for
node database" (or a clearer alternative like "path to node database"); edit the
string literal passed to cmd.Flags().String where FlagDBPath and def.DBPath are
used to fix the typo.
🧹 Nitpick comments (1)
block/internal/syncing/syncer.go (1)

367-374: Treat the manual DA start-height as an unsafe override and validate it.

A mistyped value here can move the follower before s.genesis.DAStartHeight or beyond the recovered watermark and silently skip historical DA heights. Please reject impossible values, and at least emit a warning when operators opt into an unsafe jump.

As per coding guidelines "Validate all inputs from external sources in Go code".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@block/internal/syncing/syncer.go` around lines 367 - 374, The manual DA
start-height override (s.config.DA.StartHeight) must be validated: ensure
startHeight >= s.genesis.DAStartHeight and startHeight <= daHeight (the
recovered watermark) and reject (return an error) if out of that range; if the
value is in-range but not equal to the current daHeight emit a warning via
s.logger.Warn() that this is an unsafe jump/override; keep the existing info log
when accepted but do not silently accept impossible values—use s.logger to warn
and return a clear error when validation fails.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@block/internal/syncing/syncer.go`:
- Around line 356-365: The current logic uses s.cache.DaHeight() (a global max)
to bump daHeight when headerStore is ahead, which can skip DA ranges; instead,
derive the bump from the per-block included-by entry for state.LastBlockHeight
(the persisted per-block inclusion height written by the submitter) or leave
daHeight unchanged if that per-block entry is missing. Concretely: replace the
branch that uses s.cache.DaHeight() with a lookup of the included-height for
state.LastBlockHeight (or call the cache/manager API that returns the
included-by height for a specific block) and max that value into daHeight only
when that per-block value exists; otherwise do not use the cache-wide
s.cache.DaHeight(). Ensure you still respect s.genesis.DAStartHeight and the
state.DAHeight-1 logic.

---

Duplicate comments:
In `@pkg/config/config.go`:
- Line 570: The help text for the DB path flag contains a duplicated word;
update the call to cmd.Flags().String(FlagDBPath, def.DBPath, ...) to remove the
extra "for" so the description reads "path for node database" (or a clearer
alternative like "path to node database"); edit the string literal passed to
cmd.Flags().String where FlagDBPath and def.DBPath are used to fix the typo.

---

Nitpick comments:
In `@block/internal/syncing/syncer.go`:
- Around line 367-374: The manual DA start-height override
(s.config.DA.StartHeight) must be validated: ensure startHeight >=
s.genesis.DAStartHeight and startHeight <= daHeight (the recovered watermark)
and reject (return an error) if out of that range; if the value is in-range but
not equal to the current daHeight emit a warning via s.logger.Warn() that this
is an unsafe jump/override; keep the existing info log when accepted but do not
silently accept impossible values—use s.logger to warn and return a clear error
when validation fails.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 63b1d6c5-1133-4102-8ea6-6061279613ef

📥 Commits

Reviewing files that changed from the base of the PR and between b580bc5 and df4d8a3.

📒 Files selected for processing (2)
  • block/internal/syncing/syncer.go
  • pkg/config/config.go

Comment on lines 356 to 365
// Set DA height to the maximum of the genesis start height, the state's DA height, and the cached DA height.
// Use the DA height from the last executed block instead of the maximum from all blocks,
// because P2P-fetched heights may be lost on restart.
daHeight := max(s.genesis.DAStartHeight, min(state.DAHeight-1, 0))
if state.LastBlockHeight > 0 {
if lastHeaderDA, ok := s.cache.GetHeaderDAIncludedByHeight(state.LastBlockHeight); ok {
daHeight = max(daHeight, lastHeaderDA)
}
if lastDataDA, ok := s.cache.GetDataDAIncludedByHeight(state.LastBlockHeight); ok {
daHeight = max(daHeight, lastDataDA)
}
// The cache's DaHeight() is initialized from store metadata, so it's always correct even after cache clear.
// Only use cache.DaHeight() when P2P is actively syncing (headerStore has higher height than current state).
daHeight := s.genesis.DAStartHeight
if state.DAHeight > s.genesis.DAStartHeight {
daHeight = max(daHeight, state.DAHeight-1)
}
if s.headerStore != nil && s.headerStore.Height() > state.LastBlockHeight {
daHeight = max(daHeight, s.cache.DaHeight())
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Resume from the last applied block's DA height, not the cache-wide max.

block/internal/cache/manager.go:174-176 defines DaHeight() as the highest DA height ever seen across the caches, while block/internal/submitting/submitter.go:440-458 still persists per-block inclusion heights. Using the global max here can jump the retriever past untouched DA ranges whenever later submissions are already in cache metadata, and those skipped heights are no longer covered by the sequential catchup path when P2P priority hints are dropped. Please derive this bump from the included-by-height entries for state.LastBlockHeight, or leave the recovered watermark unchanged when they are missing.

🛠️ Safer resume logic
-	if s.headerStore != nil && s.headerStore.Height() > state.LastBlockHeight {
-		daHeight = max(daHeight, s.cache.DaHeight())
-	}
+	if s.headerStore != nil && s.headerStore.Height() > state.LastBlockHeight {
+		if headerDAHeight, ok := s.cache.GetHeaderDAIncludedByHeight(state.LastBlockHeight); ok {
+			daHeight = max(daHeight, headerDAHeight)
+		}
+		if dataDAHeight, ok := s.cache.GetDataDAIncludedByHeight(state.LastBlockHeight); ok {
+			daHeight = max(daHeight, dataDAHeight)
+		}
+	}
Based on learnings "DA priority heights (queued via `QueuePriorityHeight` in `block/internal/syncing/da_follower.go`) are untrusted, best-effort optimizations sourced from P2P hints. Dropping a hint on a transient fetch failure is intentional — the sequential catchup loop in `da.Subscriber` will cover every height eventually."
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@block/internal/syncing/syncer.go` around lines 356 - 365, The current logic
uses s.cache.DaHeight() (a global max) to bump daHeight when headerStore is
ahead, which can skip DA ranges; instead, derive the bump from the per-block
included-by entry for state.LastBlockHeight (the persisted per-block inclusion
height written by the submitter) or leave daHeight unchanged if that per-block
entry is missing. Concretely: replace the branch that uses s.cache.DaHeight()
with a lookup of the included-height for state.LastBlockHeight (or call the
cache/manager API that returns the included-by height for a specific block) and
max that value into daHeight only when that per-block value exists; otherwise do
not use the cache-wide s.cache.DaHeight(). Ensure you still respect
s.genesis.DAStartHeight and the state.DAHeight-1 logic.

@julienrbrt julienrbrt changed the title chore(syncer): update last commit refactor: remove unnecessary usage of lru Mar 27, 2026
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (3)
block/internal/cache/pending_events_map_test.go (1)

20-75: Consider table-driving these pending-event scenarios.

The three tests all exercise the same insert/update/delete surface; a single table would remove repeated setup and make it easier to add edge cases like missing heights or overwrite-then-delete.

As per coding guidelines, "Use table-driven tests in Go unit tests".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@block/internal/cache/pending_events_map_test.go` around lines 20 - 75, The
three tests TestPendingEventsMap_BasicCRUD, TestPendingEventsMap_UpdateExisting
and TestPendingEventsMap_DeleteAllForHeight duplicate setup and exercising of
newPendingEventsMap, setItem, getNextItem, deleteAllForHeight and itemCount;
convert them into a single table-driven test that enumerates cases (e.g., insert
sequence + expects, update-then-expect, delete-for-height + expects) and
iterates over each case asserting counts and returned events, collapsing
duplicated setup into a loop over test cases to improve readability and make
adding edge cases simpler.
block/internal/cache/generic_cache_test.go (1)

54-190: Consider table-driving the restore permutations.

The restore tests from Line 54 through Line 190 only vary by seeded snapshot and expected heights/counts. Collapsing them into a table will cut the copy/paste and make it easier to add the same-height replacement case this cache should cover.

As per coding guidelines, "Use table-driven tests in Go unit tests".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@block/internal/cache/generic_cache_test.go` around lines 54 - 190, Several
nearly-identical tests (TestCache_RestoreFromStore_EmptyChain, _FullyFinalized,
_InFlightWindow, _SingleEntry, _PlaceholderOverwrittenByRealHash, _RoundTrip)
repeat snapshot seeding and assertions; convert them into a single table-driven
test that iterates cases describing the snapshot entries, actions (e.g.,
writeSnapshot, setDAIncluded, removeDAIncluded, SaveToStore), and expected
daIncludedLen/daHeight and presence checks. Implement a new
TestCache_RestoreFromStore_TableDriven that constructs NewCache(st, prefix) per
case, uses writeSnapshot/st ops and optional pre/post actions, calls
RestoreFromStore(ctx), and runs the same assertions currently in each
specialized test (refer to RestoreFromStore, NewCache, writeSnapshot,
setDAIncluded, removeDAIncluded, SaveToStore,
getDAIncluded/getDAIncludedByHeight). Keep each case small and name them to
cover the existing scenarios including the same-height replacement case.
block/internal/cache/manager.go (1)

387-390: Clear the TX timestamp index alongside txCache.

ClearFromStore rebuilds m.txCache but leaves the old m.txTimestamps map alive. If this helper is reused after startup, CleanupOldTxs will still traverse stale hashes from before the clear. Reinitialize the timestamp map here so the in-memory TX state is fully reset.

Suggested change
 m.headerCache = NewCache(m.store, HeaderDAIncludedPrefix)
 m.dataCache = NewCache(m.store, DataDAIncludedPrefix)
 m.txCache = NewCache(nil, "")
+m.txTimestamps = new(sync.Map)
 m.pendingEvents = newPendingEventsMap[common.DAHeightEvent]()
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@block/internal/cache/manager.go` around lines 387 - 390, The tx timestamp
index (m.txTimestamps) isn't reinitialized when rebuilding m.txCache, causing
CleanupOldTxs to iterate stale entries; in the same place where you set
m.txCache = NewCache(nil, "") (inside ClearFromStore), reinitialize the
timestamp map (m.txTimestamps) to an empty/new map/structure so the in-memory TX
state is fully reset alongside m.txCache and prevent traversal of old hashes.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@block/internal/cache/generic_cache.go`:
- Around line 29-31: hashByHeight is mixing two responsibilities (tracking every
seen hash per height for cleanup and storing the single DA-included hash),
causing overwrites and stale entries; split it into a DA-specific index (e.g.,
daHashByHeight map[uint64]string) and a seen-index (e.g., seenByHeight
map[uint64]map[string]bool) and update code paths that touch hashByHeight: when
adding a hash record, insert into seenByHeight[height][hash]=true; when marking
DA inclusion update daIncluded and daHashByHeight[height]=hash; change
getDAIncludedByHeight to consult daHashByHeight/daIncluded instead of
hashByHeight; change deleteAllForHeight to iterate seenByHeight[height] to
remove all hashes and their entries from hashes/daIncluded/daHashByHeight, and
remove empty maps to avoid leaks; update any uses of hashByHeight in functions
add/delete/get (including the code around getDAIncludedByHeight and
deleteAllForHeight) to use the new structures accordingly.

---

Nitpick comments:
In `@block/internal/cache/generic_cache_test.go`:
- Around line 54-190: Several nearly-identical tests
(TestCache_RestoreFromStore_EmptyChain, _FullyFinalized, _InFlightWindow,
_SingleEntry, _PlaceholderOverwrittenByRealHash, _RoundTrip) repeat snapshot
seeding and assertions; convert them into a single table-driven test that
iterates cases describing the snapshot entries, actions (e.g., writeSnapshot,
setDAIncluded, removeDAIncluded, SaveToStore), and expected
daIncludedLen/daHeight and presence checks. Implement a new
TestCache_RestoreFromStore_TableDriven that constructs NewCache(st, prefix) per
case, uses writeSnapshot/st ops and optional pre/post actions, calls
RestoreFromStore(ctx), and runs the same assertions currently in each
specialized test (refer to RestoreFromStore, NewCache, writeSnapshot,
setDAIncluded, removeDAIncluded, SaveToStore,
getDAIncluded/getDAIncludedByHeight). Keep each case small and name them to
cover the existing scenarios including the same-height replacement case.

In `@block/internal/cache/manager.go`:
- Around line 387-390: The tx timestamp index (m.txTimestamps) isn't
reinitialized when rebuilding m.txCache, causing CleanupOldTxs to iterate stale
entries; in the same place where you set m.txCache = NewCache(nil, "") (inside
ClearFromStore), reinitialize the timestamp map (m.txTimestamps) to an empty/new
map/structure so the in-memory TX state is fully reset alongside m.txCache and
prevent traversal of old hashes.

In `@block/internal/cache/pending_events_map_test.go`:
- Around line 20-75: The three tests TestPendingEventsMap_BasicCRUD,
TestPendingEventsMap_UpdateExisting and TestPendingEventsMap_DeleteAllForHeight
duplicate setup and exercising of newPendingEventsMap, setItem, getNextItem,
deleteAllForHeight and itemCount; convert them into a single table-driven test
that enumerates cases (e.g., insert sequence + expects, update-then-expect,
delete-for-height + expects) and iterates over each case asserting counts and
returned events, collapsing duplicated setup into a loop over test cases to
improve readability and make adding edge cases simpler.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 4c6f0031-ff90-4e05-aaf8-a7443e244ed5

📥 Commits

Reviewing files that changed from the base of the PR and between df4d8a3 and 48c735e.

📒 Files selected for processing (5)
  • block/internal/cache/generic_cache.go
  • block/internal/cache/generic_cache_test.go
  • block/internal/cache/manager.go
  • block/internal/cache/pending_events_map.go
  • block/internal/cache/pending_events_map_test.go

Comment on lines +29 to +31
hashes map[string]bool
daIncluded map[string]uint64
hashByHeight map[uint64]string
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Split the height index by responsibility.

hashByHeight is doing two incompatible jobs: cleanup for every seen hash and lookup for the DA-included hash at a height. Once two hashes share a block height, later writes overwrite earlier ones, so getDAIncludedByHeight can follow the wrong hash and deleteAllForHeight can only purge one of them. With the LRU gone, those superseded hashes stop aging out. Keep a DA-specific height index, and track seen hashes per height separately.

Also applies to: 61-65, 81-89, 95-103, 128-137

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@block/internal/cache/generic_cache.go` around lines 29 - 31, hashByHeight is
mixing two responsibilities (tracking every seen hash per height for cleanup and
storing the single DA-included hash), causing overwrites and stale entries;
split it into a DA-specific index (e.g., daHashByHeight map[uint64]string) and a
seen-index (e.g., seenByHeight map[uint64]map[string]bool) and update code paths
that touch hashByHeight: when adding a hash record, insert into
seenByHeight[height][hash]=true; when marking DA inclusion update daIncluded and
daHashByHeight[height]=hash; change getDAIncludedByHeight to consult
daHashByHeight/daIncluded instead of hashByHeight; change deleteAllForHeight to
iterate seenByHeight[height] to remove all hashes and their entries from
hashes/daIncluded/daHashByHeight, and remove empty maps to avoid leaks; update
any uses of hashByHeight in functions add/delete/get (including the code around
getDAIncludedByHeight and deleteAllForHeight) to use the new structures
accordingly.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
block/internal/cache/manager.go (1)

399-403: Consider acquiring pendingMu when reassigning pendingEvents.

The pendingEvents map is reassigned without holding pendingMu. Currently safe because ClearFromStore is only called during initialization in NewManager before the instance is exposed. However, since this is an exported method, adding the lock would provide defensive protection against future misuse.

🛡️ Suggested defensive fix
 m.headerCache = NewCache(m.store, HeaderDAIncludedPrefix)
 m.dataCache = NewCache(m.store, DataDAIncludedPrefix)
 m.txCache = NewCache(nil, "")
+m.pendingMu.Lock()
 m.pendingEvents = make(map[uint64]*common.DAHeightEvent)
+m.pendingMu.Unlock()
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@block/internal/cache/manager.go` around lines 399 - 403, When reassigning the
pendingEvents map in ClearFromStore, acquire the pendingMu lock to guard the
mutation: wrap the reassignment (m.pendingEvents =
make(map[uint64]*common.DAHeightEvent)) with m.pendingMu.Lock() and defer
m.pendingMu.Unlock() so the map is protected; do the same defensive locking in
any other method that mutates pendingEvents (e.g., ensure NewManager and
ClearFromStore both respect pendingMu) to prevent races if ClearFromStore
becomes callable after initialization.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@block/internal/cache/manager.go`:
- Around line 399-403: When reassigning the pendingEvents map in ClearFromStore,
acquire the pendingMu lock to guard the mutation: wrap the reassignment
(m.pendingEvents = make(map[uint64]*common.DAHeightEvent)) with
m.pendingMu.Lock() and defer m.pendingMu.Unlock() so the map is protected; do
the same defensive locking in any other method that mutates pendingEvents (e.g.,
ensure NewManager and ClearFromStore both respect pendingMu) to prevent races if
ClearFromStore becomes callable after initialization.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 1bba03bd-37a1-4654-8db8-5deb1f86a313

📥 Commits

Reviewing files that changed from the base of the PR and between 48c735e and dad4eb2.

📒 Files selected for processing (1)
  • block/internal/cache/manager.go

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant