Break on unknown receipts; ignore unknown blocks#11263
Break on unknown receipts; ignore unknown blocks#11263
Conversation
|
Claude finished @flcl42's task in 9m 45s —— View job PR Review —
|
| Area | Before | After |
|---|---|---|
GetBlockBodies |
Block block with no null guard → null body silently included (or NPE risk if type was non-nullable) |
continue — unknown blocks skipped, shorter response |
GetReceipts (eth63/69) |
GetReceipts called for every hash regardless of knowledge |
FindHeader guard → break at first unknown block |
GetReceipts (eth70) |
Same | Same FindHeader guard → break |
The receipt fix is clean, correct, and important — serving receipts for blocks you don't have is a protocol error. The break semantics are right because receipts must be a contiguous sequence from the front of the requested range.
The body fix is functionally safe but carries a subtle semantic shift (see Medium finding below).
Findings
Medium
FulfillBlockBodiesRequest: continue changes from null-placeholder to sparse-list (line 336)
The old code effectively added a null BlockBody entry per unknown hash (serialised as Rlp.OfEmptyList), preserving positional alignment. BodiesSyncFeed.InsertBodies uses responses[i] positional matching, so a sparse response with a gap in the middle maps bodies to wrong block infos — triggering hasBreachedProtocol = true and false peer-breach reports.
In normal operation (fully-synced, non-pruned serving node) gaps never occur, so this is low-impact in practice. The behaviour is also consistent with Geth. A comment explaining the intentional continue (vs break) would prevent future confusion, and a test covering [known, unknown, known] would lock in the semantics.
Low
-
Extra
FindHeaderper hash in receipt serving (line 379) — doubles header lookups on the serving path; acceptable if headers are cache-resident. -
Test coverage — unknown hash in middle of
GetBlockBodies(V62 test) —[known, unknown, known]case not tested. -
Test coverage — first hash unknown in
GetReceipts(V69 test) — all tests start with a known block; immediate-break on first-unknown not covered.
What looks good
- Receipt
breakis the right call — serving receipts for unknown blocks was incorrect. - V63/V69/V70 test-setup fixes (
FindHeaderdefault stub) are correct and minimal. - Tests clearly document the intended server-side truncation behaviour.
- No security or consensus-correctness issues found.
|
Claude finished @flcl42's task in 4m 43s —— View job PR Review —
|
| Finding | Previous | Now |
|---|---|---|
Medium — continue creates sparse body response, breaks positional matching in BodiesSyncFeed |
Open | ✅ Resolved — InsertBodies refactored with responseIndex; ShouldHandleSparseBodyResponseWithoutReportingBreach test validates it |
Low — Double FindHeader lookup on receipt-serving hot path |
Open | Still present, acceptable trade-off |
Low — V62 missing [known, unknown, known] test |
Open | ✅ Added: Should_omit_unknown_hash_in_middle_from_get_block_bodies |
| Low — V69 missing first-hash-unknown test | Open | ✅ Added: Should_return_empty_receipts_response_when_first_hash_is_unknown |
| (New) V70 unknown-block coverage missing | — | ✅ Added: Should_stop_receipts_response_at_first_unknown_block_hash + Should_return_empty_receipts_response_when_first_hash_is_unknown |
New Observations
Low — responseIndex not advancing on TryPrepareBlock failure (BodiesSyncFeed.cs:274)
Intentional for sparse responses (body for B retried against C), safe for genuinely corrupt bodies (breach caught at loop end). Comment explains the intent; clarified the implicit "corrupt body still triggers breach" path for future readers.
Low — Always.Valid validator in sparse-response test (BodiesSyncFeedTests.cs:172)
Test correctly validates index tracking and no-breach behavior, but Always.Valid means body-to-header cryptographic matching isn't exercised here. Acceptable for this layer.
What looks good
- Receipt
breakon unknown block is the correct protocol behavior — verified in eth63/69 and eth70, with tests for both[known, unknown]and[unknown, ...]cases. BodiesSyncFeed.InsertBodiesrefactoring is logically sound:responseIndexdecouples response position from request position, handling sparse responses without false breach reports or wasted re-requests.- The
cancellationTokenis correctly checked in eth63/69's outer loop (pre-existing gap in eth70 outer loop is out of scope). - No security or consensus-correctness issues found.
No Critical, High, or unresolved Medium findings remain.
| validResponsesCount++; | ||
| InsertOneBlock(block!); | ||
| } | ||
| else |
There was a problem hiding this comment.
Low — responseIndex does not advance on validation failure; trace-through for clarity
When TryPrepareBlock returns false, the same body at responses[responseIndex] is retried against the next blockInfo. This is intentional for sparse responses: if a server skips block B and returns [bodyA, bodyC] for [A, B, C], bodyC correctly fails for B and then succeeds for C.
One edge case worth documenting: a genuinely corrupt body also fails for every subsequent blockInfo, leaving responseIndex < responses.Length at the end — which triggers the breach report correctly. But during that loop, all remaining blockInfos are marked pending and re-requested from another peer, so correctness is preserved.
The comment already captures the intent; just ensuring the implied "breach is still caught at the end" path is understood by future maintainers.
| Block firstBlock = _syncingFromBlockTree.FindBlock(req.Infos[0]!.BlockNumber, BlockTreeLookupOptions.None)!; | ||
| Block skippedBlock = _syncingFromBlockTree.FindBlock(req.Infos[1]!.BlockNumber, BlockTreeLookupOptions.None)!; | ||
| Block thirdBlock = _syncingFromBlockTree.FindBlock(req.Infos[2]!.BlockNumber, BlockTreeLookupOptions.None)!; | ||
| req.Response = new OwnedBlockBodies([firstBlock.Body, thirdBlock.Body]); |
There was a problem hiding this comment.
Note — Always.Valid makes this test pass for any body-to-header pairing
CreateBlockValidator() wraps Always.Valid, so TryPrepareBlock succeeds regardless of tx-root or uncle-hash matching. The sparse response [firstBlock.Body, thirdBlock.Body] for [A, B, C] is handled correctly here because responseIndex advances only on success, but the test cannot verify that thirdBlock.Body is validated against thirdBlock's header (it would pass even if swapped).
This is fine for integration-style coverage of the index tracking and breach-report path. The unit-level body-to-header matching is exercised by TryPrepareBlock's own tests.
Fixes #11231
Changes
Types of changes
What types of changes does your code introduce?
Testing
Requires testing
If yes, did you write tests?