Skip to content

Conversation

@julienrbrt
Copy link
Member

@julienrbrt julienrbrt commented Nov 20, 2025

Overview

Retrieve highest da height in cache. We should still remove DaHeight from state to avoid confusion. Actually we should keep it, to have a fallback in case of user errors.

@claude
Copy link
Contributor

claude bot commented Nov 20, 2025

Claude finished @julienrbrt's task —— View job


Code Review Complete

I've reviewed the PR "refactor: retrieve highest da height in cache" and analyzed the changes across 6 files. This refactoring improves DA height tracking by caching the maximum DA height atomically rather than iterating through all items.


Summary

Overall Assessment: ✅ Approved with Minor Suggestions

This is a well-implemented performance optimization that replaces O(n) iteration with O(1) atomic operations for DA height retrieval. The changes are clean, well-tested, and maintain backward compatibility.


Detailed Review

1. Code Quality & Architecture

Strengths:

  • Clean abstraction: The maxDAHeight atomic field encapsulates the optimization without changing the public API
  • Idiomatic Go: Proper use of atomic.Uint64 and compare-and-swap pattern
  • Zero breaking changes: The refactoring is internal and preserves all existing behavior
  • Well-structured: Changes are logically grouped by component (cache, manager, syncer)

Observations:

  • The refactoring eliminates the need to iterate through sync.Map entries to find the maximum DA height
  • generic_cache.go:95-103: The CAS loop in setDAIncluded is lock-free and efficient
  • manager.go:171: Smart use of max() to aggregate heights from both header and data caches

Minor Suggestion:
Consider documenting the performance improvement motivation in a comment near the maxDAHeight field:

// maxDAHeight tracks the maximum DA height seen
// Cached atomically to avoid O(n) iteration over daIncluded map
maxDAHeight *atomic.Uint64

2. Error Handling & Resilience

Strengths:

  • generic_cache.go:247-254: Proper restoration of maxDAHeight during LoadFromDisk ensures persistence across restarts
  • The CAS pattern handles concurrent updates correctly without locks
  • Initial value of 0 is semantically correct for "no DA height seen"

Observations:

  • The CompareAndSwap return value is intentionally ignored (_) since we only care about updating when our value is higher, and concurrent updates are acceptable
  • generic_cache.go:102: The >= comparison (not just >) is correct for the CAS retry logic

3. Security

No security concerns identified:

  • No credential handling or sensitive data exposure
  • Atomic operations prevent race conditions
  • No new external inputs or attack surfaces

4. Performance & Resource EfficiencyExcellent

Major Improvement:

  • Before: daHeight() would iterate through all entries in daIncluded sync.Map (O(n))
  • After: Single atomic load operation (O(1))
  • This is especially beneficial when cache contains many DA-included items

Observations:

  • syncer.go:180: The GetDAHeight() method now queries the cache on every call, which is fine since it's just an atomic load
  • syncer.go:220: Initialization uses max(genesis.DAStartHeight, cache.DaHeight()) which ensures DA height never goes below genesis
  • Memory overhead is minimal: one additional atomic.Uint64 per cache instance

Potential Concern:
The setDAIncluded CAS loop at generic_cache.go:100-103 could theoretically retry indefinitely under extreme contention. However:

  • In practice, contention should be rare (DA heights are mostly sequential)
  • Each retry is very fast (just atomic ops)
  • This is standard practice for lock-free updates

5. Testing & Reliability

Strengths:

  • generic_cache_test.go:18-56: New TestCache_MaxDAHeight covers:
    • Initial zero value ✅
    • Increasing heights ✅
    • Non-increasing heights (shouldn't update) ✅
    • Persistence across save/load ✅
  • Existing tests in syncer_test.go were updated appropriately
  • syncer_test.go:256-259: Test correctly verifies DA inclusion markers in cache

Test Quality:

  • Tests are deterministic and focused
  • Good use of table-driven patterns in existing tests
  • Integration test TestSyncLoopPersistState verifies end-to-end behavior

Observation:
Line syncer_test.go:436 removes an assertion that was too strict. The new approach is more flexible since DA height can come from either state or cache.

6. Documentation & Developer Experience

Strengths:

  • Function comment on daHeight() clearly describes behavior: generic_cache.go:111-112
  • Manager comment explains the aggregation logic: manager.go:169
  • Changes are self-explanatory and minimal

Minor Issue:
tools/da-debug/README.md:77: The markdown escaping change (http://<http://>) appears to be a markdown linting fix. This is fine but unrelated to the main refactoring.


Specific Code Analysis

generic_cache.go (lines 95-103)

// Update max DA height if necessary
current := c.maxDAHeight.Load()
if daHeight >= current {
    _ = c.maxDAHeight.CompareAndSwap(current, daHeight)
}

Analysis:

  • ✅ Correct CAS pattern for lock-free maximum update
  • ⚠️ The >= comparison means we might CAS even when daHeight == current, which is unnecessary but harmless
  • Suggestion: Change to if daHeight > current to avoid unnecessary CAS when equal

generic_cache.go (lines 247-254)

for k, v := range daIncludedMap {
    c.daIncluded.Store(k, v)
    // Update max DA height during load
    current := c.maxDAHeight.Load()
    if v > current {
        _ = c.maxDAHeight.CompareAndSwap(current, v)
    }
}

Analysis:

  • ✅ Correctly reconstructs maxDAHeight during load
  • ✅ Uses > (not >=) which is more efficient than the setter
  • Potential issue: This loop could be inefficient if there are many items. Consider finding max first, then setting once:
for k, v := range daIncludedMap {
    c.daIncluded.Store(k, v)
}

// Find and set max DA height in one pass
var maxDA uint64
for _, v := range daIncludedMap {
    if v > maxDA {
        maxDA = v
    }
}
c.maxDAHeight.Store(maxDA)

This eliminates CAS contention during load (when there's no concurrency anyway).

syncer.go (lines 180, 220)

// Line 180
return max(s.daHeight.Load(), s.cache.DaHeight())

// Line 220  
s.SetDAHeight(max(s.genesis.DAStartHeight, s.cache.DaHeight()))

Analysis:

  • ✅ Always considers cache DA height, ensuring we don't lose track of DA-included items
  • ✅ Genesis height provides a floor value
  • Question: Is there a scenario where s.daHeight and s.cache.DaHeight() diverge significantly? The code handles this gracefully with max(), but it might be worth documenting why both sources are needed.

syncer_test.go (line 436)

-	require.Equal(t, myFutureDAHeight, syncerInst1.GetDAHeight())
-	lastStateDAHeight := syncerInst1.GetLastState().DAHeight
+	require.Equal(t, myFutureDAHeight, syncerInst1.GetDAHeight())

Analysis:

  • ✅ Removes overly strict assertion that assumed DA height only comes from state
  • The new approach correctly acknowledges that cache can also contribute to DA height

Recommendations

High Priority: None

The PR is production-ready as-is.

Medium Priority

  1. Optimize LoadFromDisk (generic_cache.go:247-254): Find max in single pass before storing, rather than CAS in loop
  2. Document maxDAHeight field purpose to explain the performance motivation

Low Priority

  1. Change daHeight >= current to daHeight > current in setDAIncluded to avoid unnecessary CAS
  2. Add a comment in syncer.go explaining why both daHeight and cache.DaHeight() are needed in GetDAHeight()

Conclusion

This is a high-quality refactoring that delivers a meaningful performance improvement (O(n) → O(1)) while maintaining code clarity and test coverage. The atomic operations are correctly implemented, persistence is handled properly, and the changes integrate cleanly with the existing architecture.

The PR is approved for merge. The suggestions above are minor optimizations that could be addressed in a follow-up if desired.


Files Reviewed

  • block/internal/cache/generic_cache.go - Core implementation
  • block/internal/cache/generic_cache_test.go - Test coverage
  • block/internal/cache/manager.go - Manager integration
  • block/internal/syncing/syncer.go - Syncer integration
  • block/internal/syncing/syncer_test.go - Test updates
  • tools/da-debug/README.md - Documentation fix (unrelated)

@github-actions
Copy link
Contributor

github-actions bot commented Nov 20, 2025

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

BuildFormatLintBreakingUpdated (UTC)
✅ passed⏩ skipped✅ passed✅ passedNov 20, 2025, 4:43 PM

@julienrbrt julienrbrt marked this pull request as ready for review November 20, 2025 16:43
@codecov
Copy link

codecov bot commented Nov 20, 2025

Codecov Report

❌ Patch coverage is 84.61538% with 2 lines in your changes missing coverage. Please review.
✅ Project coverage is 64.83%. Comparing base (12b9559) to head (5d62a7e).
⚠️ Report is 2 commits behind head on main.

Files with missing lines Patch % Lines
block/internal/cache/manager.go 0.00% 2 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #2870      +/-   ##
==========================================
+ Coverage   64.80%   64.83%   +0.02%     
==========================================
  Files          81       81              
  Lines        7243     7254      +11     
==========================================
+ Hits         4694     4703       +9     
- Misses       2005     2007       +2     
  Partials      544      544              
Flag Coverage Δ
combined 64.83% <84.61%> (+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.

@tac0turtle tac0turtle merged commit faabb32 into main Nov 20, 2025
30 of 31 checks passed
@tac0turtle tac0turtle deleted the julien/da-height branch November 20, 2025 17:11
tac0turtle pushed a commit that referenced this pull request Nov 20, 2025
alpe added a commit that referenced this pull request Nov 21, 2025
* main:
  refactor: use state da height as well (#2872)
  refactor: retrieve highest da height in cache (#2870)
  chore: change from event count to start and end height (#2871)
alpe added a commit that referenced this pull request Nov 21, 2025
* main:
  chore: fix some comments (#2874)
  chore: bump node in evm-single (#2875)
  refactor(syncer,cache): use compare and swap loop and add comments (#2873)
  refactor: use state da height as well (#2872)
  refactor: retrieve highest da height in cache (#2870)
  chore: change from event count to start and end height (#2871)
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.

3 participants