Skip to content

Conversation

@julienrbrt
Copy link
Member

@julienrbrt julienrbrt commented Nov 21, 2025

Overview

Fetch once startup da height from store metadata (included da height from inclusion loop).
It is useful when a user cleared the cache.

The cache remains updated and is faster. The cache value == the store metadata value. Except it is cheaper to get.

@github-actions
Copy link
Contributor

github-actions bot commented Nov 21, 2025

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

BuildFormatLintBreakingUpdated (UTC)
✅ passed⏩ skipped✅ passed✅ passedNov 21, 2025, 3:24 PM

@claude
Copy link
Contributor

claude bot commented Nov 21, 2025

Claude finished @julienrbrt's task —— View job


Code Review Complete

I've completed a comprehensive review of this PR. The implementation correctly addresses the need to fetch DA height from persistent storage on startup, which improves resilience when the cache is cleared. The changes are well-structured with good test coverage. Below are my findings organized by focus area:


1. Code Quality & Architecture ✅

Strengths:

  • Clean abstraction: The new key generation functions GetHeightToDAHeightHeaderKey() and GetHeightToDAHeightDataKey() in pkg/store/keys.go (lines 65-75) properly centralize metadata key construction, eliminating string duplication across the codebase
  • Consistent naming: Functions follow Go naming conventions and the existing patterns in the store package
  • Single responsibility: The new getHighestStoredDAHeight() function in block/internal/syncing/syncer.go (lines 728-756) has a clear, focused purpose

Observations:

  • The implementation correctly uses the max of multiple sources for DA height initialization (block/internal/syncing/syncer.go:207):
    s.daRetrieverHeight.Store(max(s.genesis.DAStartHeight, s.cache.DaHeight(), state.DAHeight, s.getHighestStoredDAHeight()))
    This is good defensive programming ensuring the node never regresses in DA height tracking.

2. Error Handling & Resilience ⚠️

Issue 1: Silent error suppression in getHighestStoredDAHeight()

The function silently ignores all errors from GetMetadata() calls:

// block/internal/syncing/syncer.go:731-732
daIncludedHeightBytes, err := s.store.GetMetadata(s.ctx, store.DAIncludedHeightKey)
if err != nil || len(daIncludedHeightBytes) != 8 {
    return 0
}

Concern: While datastore.ErrNotFound is expected and should be silent, other errors (I/O failures, corruption) might indicate serious issues. The author intentionally chose this approach (per review comment response), but this makes debugging storage issues harder.

Recommendation: Consider at minimum logging non-ErrNotFound errors at debug level:

if err != nil {
    if !errors.Is(err, datastore.ErrNotFound) {
        s.logger.Debug().Err(err).Msg("failed to get DA included height from store")
    }
    return 0
}

Issue 2: Inconsistent error handling pattern

Compare getHighestStoredDAHeight() (lines 728-756) with initializeDAIncludedHeight() in submitter.go:310-315:

  • submitter.go also silently ignores errors but is simpler (single call)
  • syncer.go has 3 metadata calls, making silent failures harder to debug

The inconsistency isn't necessarily wrong, but could be confusing for maintainers.


3. Security 🔒

No security concerns identified. The changes:

  • Do not handle sensitive data
  • Use safe binary encoding (LittleEndian) with proper length checks
  • Do not introduce new attack surfaces
  • Properly validate byte slice lengths before decoding (len(daIncludedHeightBytes) != 8)

4. Performance & Resource Efficiency ⚡

Strengths:

  • Efficient initialization: getHighestStoredDAHeight() is only called once during startup (syncer.go:207), so the 3 metadata reads are acceptable
  • Cache-first approach maintained: The runtime still prioritizes the in-memory cache over store reads for performance
  • No allocations in hot path: The new code runs during initialization, not in processing loops

Note: The three sequential GetMetadata() calls in getHighestStoredDAHeight() could theoretically be parallelized with goroutines, but given this only runs once at startup and the operations are fast, the added complexity isn't justified.


5. Testing & Reliability ✅

Strengths:

  • Comprehensive test coverage: TestSyncer_getHighestStoredDAHeight() in syncer_test.go (lines 649-702) covers all critical scenarios:
    • No DA included height set
    • DA included height set but no mappings
    • Header mapping only
    • Both header and data mappings (tests max() logic)
    • Multiple heights (ensures it uses the latest)
  • Integration with existing tests: The mock expectations were properly updated (e.g., syncer_test.go:610)
  • Test quality: Uses table-driven approach where appropriate, proper assertions, and realistic test data

Coverage gap noted by Codecov (72.72% patch coverage):

  • Missing coverage in pkg/store/keys.go (4 lines) - These are trivial string formatting functions, acceptable gap
  • Missing coverage in submitter.go and syncer.go - Some error paths in the new/modified code

Recommendation: The test coverage is adequate for the core functionality. The missing lines are mostly error handling paths that are difficult to trigger in tests.


6. Documentation & Developer Experience 📝

Strengths:

  • Clear comments: The new functions have descriptive docstrings explaining their purpose
  • Good inline documentation: The comment on syncer.go:205-206 explains the multi-source max logic and recovery scenario
  • CHANGELOG updated: Entry at line 16 properly documents the change

Suggestions:

  1. The docstring for getHighestStoredDAHeight() (lines 725-727) could be more explicit about error handling:

    // getHighestStoredDAHeight retrieves the highest DA height from the store by checking
    // the DA heights stored for the last DA included height.
    // Returns 0 if no data is found or if any errors occur during retrieval.
    // This relies on the node syncing with DA and setting included heights.
  2. Consider adding a comment explaining why errors are intentionally suppressed in getHighestStoredDAHeight() for future maintainers.


7. Specific Code Issues

Non-blocking observations:

  1. Inconsistent key construction pattern (pkg/store/keys.go:68, 74):

    • New functions use direct string concatenation: HeightToDAHeightKey + "/" + strconv.FormatUint(height, 10) + "/h"
    • Existing functions use GenerateKey() helper: GenerateKey([]string{headerPrefix, strconv.FormatUint(height, 10)})

    Reason: This is acceptable because the new keys are metadata keys (not data keys), but it creates a minor inconsistency in style.

  2. Magic string literals (pkg/store/keys.go:68, 74):

    • The suffixes "/h" and "/d" are inline strings
    • Could be constants for consistency with headerPrefix and dataPrefix on lines 28-29, but current approach is clear enough

Summary

Overall Assessment: ✅ Approve with minor suggestions

This PR successfully implements the stated goal of fetching DA height from persistent storage on startup to handle cache clearing scenarios. The implementation is solid with:

  • ✅ Clean architecture with proper abstraction
  • ✅ Comprehensive test coverage for critical paths
  • ✅ No security issues
  • ✅ Good performance characteristics
  • ⚠️ Error handling trade-off (intentionally silent, but could impact debuggability)

Key Recommendations (non-blocking):

  1. Consider adding debug-level logging for non-ErrNotFound errors in getHighestStoredDAHeight() to aid future debugging
  2. Add a comment explaining the intentional error suppression strategy
  3. Minor: Consider documenting the return value (0) in the function docstring

The author's choice to keep the code concise by suppressing errors is a valid engineering trade-off, especially since the function gracefully degrades (returns 0) and the system has multiple fallback mechanisms for DA height tracking.


@gemini-code-assist
Copy link
Contributor

Summary of Changes

Hello @julienrbrt, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request improves the reliability of the system's Data Availability (DA) height tracking. It ensures that upon startup, the node correctly identifies the highest known DA height by consulting its persistent storage, rather than solely relying on potentially volatile cached values. This change prevents potential synchronization issues and provides a more robust foundation for block processing, alongside standardizing how DA height metadata keys are constructed and accessed across different components.

Highlights

  • Robust DA Height Initialization: The system now fetches the highest Data Availability (DA) height from persistent storage during initialization, enhancing resilience against cache clearing on restarts.
  • Dedicated Key Generation: Introduced specific utility functions (GetHeightToDAHeightHeaderKey, GetHeightToDAHeightDataKey) for generating metadata keys related to DA heights, improving key management consistency.
  • Refactored DA Height Storage/Retrieval: Updated the setSequencerHeightToDAHeight function in the submitter and GetBlock in the RPC server to utilize the new dedicated key generation functions for storing and retrieving DA height metadata.
  • New getHighestStoredDAHeight Function: Added a new internal function in the syncer to query the store for the highest DA height associated with the last included DA height.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request refactors the generation of store keys for DA height mappings into dedicated functions within the store package, which improves code consistency and maintainability. It also enhances the syncer's initialization logic to read the highest known DA height from the store, making it more resilient to cache clears on restart. The changes are well-implemented, but I have one suggestion to improve the new getHighestStoredDAHeight function for better robustness and readability.

Comment on lines +728 to +756
func (s *Syncer) getHighestStoredDAHeight() uint64 {
// Get the DA included height from store
daIncludedHeightBytes, err := s.store.GetMetadata(s.ctx, store.DAIncludedHeightKey)
if err != nil || len(daIncludedHeightBytes) != 8 {
return 0
}
daIncludedHeight := binary.LittleEndian.Uint64(daIncludedHeightBytes)
if daIncludedHeight == 0 {
return 0
}

var highestDAHeight uint64

// Get header DA height for the last included height
headerKey := store.GetHeightToDAHeightHeaderKey(daIncludedHeight)
if headerBytes, err := s.store.GetMetadata(s.ctx, headerKey); err == nil && len(headerBytes) == 8 {
headerDAHeight := binary.LittleEndian.Uint64(headerBytes)
highestDAHeight = max(highestDAHeight, headerDAHeight)
}

// Get data DA height for the last included height
dataKey := store.GetHeightToDAHeightDataKey(daIncludedHeight)
if dataBytes, err := s.store.GetMetadata(s.ctx, dataKey); err == nil && len(dataBytes) == 8 {
dataDAHeight := binary.LittleEndian.Uint64(dataBytes)
highestDAHeight = max(highestDAHeight, dataDAHeight)
}

return highestDAHeight
}
Copy link
Contributor

Choose a reason for hiding this comment

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

medium

This function can be improved for robustness and maintainability.

  1. Error Handling: Errors from s.store.GetMetadata are silently ignored. It's better to log these errors (unless it's a datastore.ErrNotFound) to help debug potential issues with the data store. This applies to all three GetMetadata calls in this function.
  2. Code Duplication: The logic to fetch and decode the header and data DA heights is duplicated. Extracting this into a helper function or a local closure would reduce redundancy.

Here is an example of how it could be refactored:

func (s *Syncer) getHighestStoredDAHeight() uint64 {
	daIncludedHeightBytes, err := s.store.GetMetadata(s.ctx, store.DAIncludedHeightKey)
	if err != nil {
		if !errors.Is(err, datastore.ErrNotFound) {
			s.logger.Warn().Err(err).Msg("Failed to get DA included height from store")
		}
		return 0
	}
	if len(daIncludedHeightBytes) != 8 {
		// Potentially log this as it indicates data corruption
		return 0
	}

	daIncludedHeight := binary.LittleEndian.Uint64(daIncludedHeightBytes)
	if daIncludedHeight == 0 {
		return 0
	}

	// Helper to fetch and decode a DA height from a given key.
	getDAHeight := func(key string) uint64 {
		bz, err := s.store.GetMetadata(s.ctx, key)
		if err != nil {
			if !errors.Is(err, datastore.ErrNotFound) {
				s.logger.Warn().Err(err).Str("key", key).Msg("Failed to get DA height from store")
			}
			return 0
		}
		if len(bz) != 8 {
			// Potentially log this as it indicates data corruption
			return 0
		}
		return binary.LittleEndian.Uint64(bz)
	}

	headerDAHeight := getDAHeight(store.GetHeightToDAHeightHeaderKey(daIncludedHeight))
	dataDAHeight := getDAHeight(store.GetHeightToDAHeightDataKey(daIncludedHeight))

	return max(headerDAHeight, dataDAHeight)
}

Copy link
Member Author

Choose a reason for hiding this comment

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

it is shorter the way it currently is, and we do want to skip errors and not clutter the logs

@codecov
Copy link

codecov bot commented Nov 21, 2025

Codecov Report

❌ Patch coverage is 72.72727% with 9 lines in your changes missing coverage. Please review.
✅ Project coverage is 64.81%. Comparing base (82299ed) to head (3f96f05).
⚠️ Report is 1 commits behind head on main.

Files with missing lines Patch % Lines
pkg/store/keys.go 0.00% 4 Missing ⚠️
block/internal/submitting/submitter.go 66.66% 1 Missing and 2 partials ⚠️
block/internal/syncing/syncer.go 88.88% 1 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #2880      +/-   ##
==========================================
- Coverage   64.87%   64.81%   -0.06%     
==========================================
  Files          81       81              
  Lines        7330     7350      +20     
==========================================
+ Hits         4755     4764       +9     
- Misses       2034     2045      +11     
  Partials      541      541              
Flag Coverage Δ
combined 64.81% <72.72%> (-0.06%) ⬇️

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 added this pull request to the merge queue Nov 24, 2025
Merged via the queue into main with commit bfa745c Nov 24, 2025
26 of 28 checks passed
@tac0turtle tac0turtle deleted the julien/metadata branch November 24, 2025 08:22
alpe added a commit that referenced this pull request Nov 24, 2025
* main:
  chore: remove extra github action yml file (#2882)
  fix(execution/evm): verify payload status (#2863)
  feat: fetch included da height from store (#2880)
  chore: better output on errors (#2879)
  refactor!: create da client and split cache interface (#2878)
  chore!: rename `evm-single` and `grpc-single` (#2839)
  build(deps): Bump golang.org/x/crypto from 0.42.0 to 0.45.0 in /tools/da-debug in the go_modules group across 1 directory (#2876)
  chore: parallel cache de/serialization (#2868)
  chore: bump blob size (#2877)
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