Uh oh!
There was an error while loading. Please reload this page.
feat: p2p exchange wrapper - #2855
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
ifew.getter!=nil {
ifh, err:=ew.getter(ctx, hash); err==nil&&!h.IsZero() {
returnh, 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: ifew.getter!=nil {
h, err:=ew.getter(ctx, hash)
iferr==nil&&!h.IsZero() {
returnh, nil
}
iferr!=nil&&!errors.Is(err, datastore.ErrNotFound) {
// Log unexpected errors from DA storelog.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 tooreturnd, err
}Recommendation: Add a
rangeGetter=func(ctx context.Context, from, touint64) ([]*types.Data, uint64, error) {
returngetContiguousRange(ctx, from, to, func(ctx context.Context, huint64) (*types.Data, error) {
_, d, err:=daStore.GetBlockData(ctx, h)
returnd, 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.gogetter=func(ctx context.Context, hash header.Hash) (*types.Data, error) {
_, d, err:=daStore.GetBlockByHash(ctx, hash)
returnd, err
}
// Laterex:=&exchangeWrapper[H]{
getter: getter,
// ...
}Better (per @renaynay): // Define as standalone functions or methodsfuncgetDataByHash(daStore store.Store) GetterFunc[*types.Data] {
returnfunc(ctx context.Context, hash header.Hash) (*types.Data, error) {
_, d, err:=daStore.GetBlockByHash(ctx, hash)
returnd, err
}
}
// Then passex:=&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: funcisNotFoundError(errerror) bool {
returnerr!=nil&& (
errors.Is(err, datastore.ErrNotFound) ||errors.Is(err, header.ErrNotFound) ||strings.Contains(err.Error(), "not found")
)
}
// Then in Get:ifew.getter!=nil {
h, err:=ew.getter(ctx, hash)
iferr==nil&&!h.IsZero() {
returnh, nil
}
iferr!=nil&&!isNotFoundError(err) {
// Log unexpected errorslog.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)
defercancel()
_, d, err:=daStore.GetBlockByHash(ctx, hash)
returnd, err
}This prevents DA store issues from blocking P2P fallback indefinitely. GetRangeByHeight Partial Success HandlingIn p2pHeaders, err:=ew.p2pExchange.GetRangeByHeight(ctx, lastDAHeader, to)
iferr!=nil {
returnnil, 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, fromH, touint64) ([]H, error) {
fromHeight:=from.Height() +1// Validate rangeiffromHeight>=to {
returnnil, fmt.Errorf("invalid range: from=%d >= to=%d", fromHeight, to)
}
// Prevent excessive range requestsconstmaxRangeSize=10000ifto-fromHeight>maxRangeSize {
returnnil, 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: typecachedGetterstruct {
cache*lru.CachegetterGetterFunc
}Append Operation in GetRangeByHeightreturnappend(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)
returnresult, 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: funcTestExchangeWrapper_GetDAError(t*testing.T) {
// Test that non-"not found" errors are handled gracefully
}
funcTestExchangeWrapper_ContextCancellation(t*testing.T) {
// Test cancellation during DA fetch and P2P fallback
}
funcTestExchangeWrapper_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.typeGetterFunc[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.typeGetterFunc[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:
|
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
* 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