Conversation
|
The latest Buf updates on your PR. Results from workflow CI / buf-check (pull_request).
|
|
Claude finished @tac0turtle's task in 2m 39s —— View job Comprehensive Code ReviewI've completed a thorough review of this PR. The exchange wrapper implementation is a solid architectural improvement that addresses the issue of missing blocks in the p2p store. However, there are several areas that need attention before merging. 1. Code Quality & ArchitectureCritical Issues
if ew.getter != nil {
if h, err := ew.getter(ctx, hash); err == nil && !h.IsZero() {
return h, nil
}
}Problem: Errors from the DA store getter are silently ignored. If the DA store returns a genuine error (not "not found"), we should distinguish between "not found" and "actual error". This masks potential issues like:
Recommendation: Add logging for non-"not found" errors to aid debugging: if ew.getter != nil {
h, err := ew.getter(ctx, hash)
if err == nil && !h.IsZero() {
return h, nil
}
if err != nil && !errors.Is(err, datastore.ErrNotFound) {
// Log unexpected errors from DA store
log.Debug().Err(err).Msg("DA store getter failed, falling back to p2p")
}
}Same issue in:
The
Recommendation:
The getter functions are tightly coupled to the specific
Current code: getter = func(ctx context.Context, hash header.Hash) (*types.Data, error) {
_, d, err := daStore.GetBlockByHash(ctx, hash) // Wasteful: retrieves header too
return d, err
}Recommendation: Add a
rangeGetter = func(ctx context.Context, from, to uint64) ([]*types.Data, uint64, error) {
return getContiguousRange(ctx, from, to, func(ctx context.Context, h uint64) (*types.Data, error) {
_, d, err := daStore.GetBlockData(ctx, h)
return d, err
})
}This iterates through each height individually calling Design ConcernsFunction Composition Pattern (@renaynay's feedback) @renaynay suggested passing function implementations directly instead of storing the DA datastore on the wrapper. This is a valid point about reducing coupling: Current approach: Store Current: // In sync_service.go
getter = func(ctx context.Context, hash header.Hash) (*types.Data, error) {
_, d, err := daStore.GetBlockByHash(ctx, hash)
return d, err
}
// Later
ex := &exchangeWrapper[H]{
getter: getter,
// ...
}Better (per @renaynay): // Define as standalone functions or methods
func getDataByHash(daStore store.Store) GetterFunc[*types.Data] {
return func(ctx context.Context, hash header.Hash) (*types.Data, error) {
_, d, err := daStore.GetBlockByHash(ctx, hash)
return d, err
}
}
// Then pass
ex := &exchangeWrapper[H]{
getter: getDataByHash(daStore),
// ...
}However, the current approach is acceptable. The improvement would be marginal unless we need to swap implementations at runtime. Confusing Naming (@renaynay's feedback on sync_service.go) The naming of
This makes it immediately clear these are DA-backed getters. 2. Error Handling & ResilienceMissing Error DifferentiationThe implementation treats all errors from DA store as "not found" and falls back to P2P. This is problematic for:
Recommendation: Implement error classification: func isNotFoundError(err error) bool {
return err != nil && (
errors.Is(err, datastore.ErrNotFound) ||
errors.Is(err, header.ErrNotFound) ||
strings.Contains(err.Error(), "not found")
)
}
// Then in Get:
if ew.getter != nil {
h, err := ew.getter(ctx, hash)
if err == nil && !h.IsZero() {
return h, nil
}
if err != nil && !isNotFoundError(err) {
// Log unexpected errors
log.Warn().Err(err).Str("hash", hash.String()).Msg("DA store error, falling back to P2P")
}
}No Timeout or Cancellation in DA Store CallsThe getter functions don't enforce timeouts. If the DA store is slow, we should have a timeout before falling back to P2P: getter = func(ctx context.Context, hash header.Hash) (*types.Data, error) {
ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel()
_, d, err := daStore.GetBlockByHash(ctx, hash)
return d, err
}This prevents DA store issues from blocking P2P fallback indefinitely. GetRangeByHeight Partial Success HandlingIn p2pHeaders, err := ew.p2pExchange.GetRangeByHeight(ctx, lastDAHeader, to)
if err != nil {
return nil, err // Lost daHeaders!
}Consider: Return partial results with error, or document this behavior clearly. The caller might prefer partial data over nothing. 3. SecurityNo Input ValidationThe implementation doesn't validate inputs:
Recommendation: func (ew *exchangeWrapper[H]) GetRangeByHeight(ctx context.Context, from H, to uint64) ([]H, error) {
fromHeight := from.Height() + 1
// Validate range
if fromHeight >= to {
return nil, fmt.Errorf("invalid range: from=%d >= to=%d", fromHeight, to)
}
// Prevent excessive range requests
const maxRangeSize = 10000
if to - fromHeight > maxRangeSize {
return nil, fmt.Errorf("range too large: %d blocks (max %d)", to-fromHeight, maxRangeSize)
}
// ... rest of implementation
}No Rate Limiting on P2P FallbackIf DA store consistently fails, every request will fall back to P2P, potentially overwhelming the P2P network. Consider:
4. Performance & Resource EfficiencyMemory Allocation in GetRangeByHeightThe result := make([]H, 0, to-from) // pkg/sync/sync_service.go:183For large ranges, this could allocate significant memory upfront. Consider:
Redundant IsZero ChecksThe code checks both No Caching LayerThe implementation directly calls DA store on every request. Consider adding a small in-memory cache for recently accessed headers to reduce DA store load: type cachedGetter struct {
cache *lru.Cache
getter GetterFunc
}Append Operation in GetRangeByHeightreturn append(daHeaders, p2pHeaders...), nil // Line 94This creates a new slice and copies both. For large ranges, this could be expensive. Consider pre-allocating the final slice: result := make([]H, len(daHeaders) + len(p2pHeaders))
copy(result, daHeaders)
copy(result[len(daHeaders):], p2pHeaders)
return result, nil5. Testing & ReliabilityGood CoverageThe tests cover the main scenarios:
Coverage: 69.09% for the patch is reasonable but could be improved. Missing Test Cases
Recommendation: Add these test cases to improve reliability confidence: func TestExchangeWrapper_GetDAError(t *testing.T) {
// Test that non-"not found" errors are handled gracefully
}
func TestExchangeWrapper_ContextCancellation(t *testing.T) {
// Test cancellation during DA fetch and P2P fallback
}
func TestExchangeWrapper_ConcurrentAccess(t *testing.T) {
// Test race conditions
}Mock QualityThe generated mocks are appropriate. The mockery configuration correctly defines the 6. Documentation & Developer ExperienceMissing Package DocumentationThe
Recommendation: Add package doc: // Package sync provides P2P synchronization services with DA store optimization.
//
// The exchangeWrapper implements a two-tier caching strategy:
// 1. First checks local DA store for blocks/headers
// 2. Falls back to P2P network if not found locally
//
// This reduces P2P bandwidth and improves performance when data is available locally.
package syncFunction Documentation Could Be ImprovedCurrent documentation is minimal. For example: // GetterFunc retrieves a header by hash from a backing store.
type GetterFunc[H header.Header[H]] func(context.Context, header.Hash) (H, error)Better: // GetterFunc retrieves a header by hash from a backing store (typically DA store).
// Returns the header if found, or an error if not found or if retrieval failed.
// Implementations should return datastore.ErrNotFound for missing headers to enable
// proper fallback behavior.
type GetterFunc[H header.Header[H]] func(context.Context, header.Hash) (H, error)No Examples or Usage GuidanceConsider adding examples in the doc comments or in a separate example file showing:
Confusing Interface NamingThe
Consider: 7. Addressing Existing Review Comments@julienrbrt's comment: "GetHeader is less expensive"✅ Valid: Using @julienrbrt's comment: "What is this used for now?"Refers to line in @renaynay's comment: "don't need to nest this call"✅ Valid: If @renaynay's comment: "define as functions that don't take the store"✅ Valid: As discussed above, this would reduce coupling slightly but is not critical. @renaynay's comment on GetRangeByHeight:61✅ Critical: "You need to actually implement this method w/ the p2p as a fallback system bc this is the method called by syncer." This is already implemented (lines 61-95), so this comment might be outdated or misunderstood. The implementation does exactly what was requested:
@renaynay's comment: "confusing naming"✅ Valid: As discussed above. Summary of RecommendationsMust Fix (Blocking)
Should Fix (Important)
Nice to Have (Optional)
Positive Aspects✅ Clean abstraction - The function type approach is flexible and testable ConclusionThis is a solid architectural improvement that addresses a real problem. The core design is sound, but the implementation needs refinement in error handling, efficiency, and documentation. The issues identified are not fundamental flaws but areas where production-grade polish is needed. Recommendation: Request changes focusing on error handling, logging, and efficiency improvements before merging. |
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #2855 +/- ##
==========================================
- Coverage 59.26% 59.18% -0.09%
==========================================
Files 108 109 +1
Lines 10103 10180 +77
==========================================
+ Hits 5988 6025 +37
- Misses 3484 3518 +34
- Partials 631 637 +6
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
* main: fix(docs): remove blog link from sidebar to fix 404 (#3014) build(deps): Bump github.com/cometbft/cometbft from 0.38.20 to 0.38.21 in /execution/evm/test in the go_modules group across 1 directory (#3011) refactor: use slices.Contains to simplify code (#3010) chore: Bump mermaid version and dependencies (#3009) chore: Bump github.com/consensys/gnark-crypto only (#3008) test: evm contract interaction (#3006) chore: remove redundant log (#3007) fix: return values correctly not nil (#3004) feat: expose execution client params to ev-node (#2982) feat(tracing): HTTP propagation (#3000) fix: deploy docs token (#3003) feat(tracing): add store tracing (#3001) feat: p2p exchange wrapper (#2855) build(deps): Bump the all-go group across 5 directories with 5 updates (#2999) feat(tracing): adding forced inclusion tracing (#2997) chore: update calculator for strategies (#2995) chore: adding tracing for da submitter (#2993) feat(tracing): part 10 da retriever tracing (#2991) chore: add da posting strategy to docs (#2992)
* refactor(store): replace go-header store by ev-node store * simplify * go mod tidy * append in cache instead of duplicate store * clean-up * remove exchanger (partial revert #2855) * add changelog * updates * use generic and improve tail * lint * simplify code * wait for height * cleanup duplicates * remove unecessary go-header store write * update test based on new behavior * kiss * update sync service
Overview
This pr was an idea from @renaynay in which we create an exchange wrapper so that the exchange can check our store before making public requests for data. This would help the edge case i ran into today where the p2p store was missing 40 blocks randomly