Skip to content

feat(p2p): add inbound BlocksByRange req/resp support#348

Merged
MegaRedHand merged 7 commits into
lambdaclass:mainfrom
dicethedev:feat/blocks-by-range-inbound-support
May 12, 2026
Merged

feat(p2p): add inbound BlocksByRange req/resp support#348
MegaRedHand merged 7 commits into
lambdaclass:mainfrom
dicethedev:feat/blocks-by-range-inbound-support

Conversation

@dicethedev
Copy link
Copy Markdown
Contributor

🗒️ Description / Motivation

This PR adds inbound BlocksByRange request-response support to the P2P req/resp protocol implementation.

The change follows the recently merged spec update:

This is needed so peers can request canonical blocks by slot range, similar to the existing BlocksByRoot protocol.

The implementation:

  • registers the new protocol
  • adds SSZ request/response handling
  • supports serving canonical blocks from local storage
  • validates malformed requests

This improves interoperability with other clients implementing the updated spec.


What Changed

Req/Resp Protocol

  • Added BlocksByRangeRequest
  • Added BlocksByRange response payload variant
  • Added protocol ID:
    • /leanconsensus/req/blocks_by_range/1/ssz_snappy

Codec

  • Updated request/response codec read paths
  • Updated request/response codec write paths

Behaviour Registration

  • Registered BlocksByRange in the libp2p request-response behaviour

Inbound Request Handling

  • Added inbound request handler for BlocksByRange
  • Serves canonical blocks by walking backward from the current fork-choice head
  • Skips:
    • empty slots
    • side forks

Validation

Added validation for:

  • step == 0
  • count > 1024

Invalid requests return protocol error responses.

Tests

  • Added unit test for canonical range selection and ordering

Correctness / Behavior Guarantees

Preserved Invariants

  • Only canonical blocks are returned
  • Returned blocks preserve requested slot ordering
  • Empty slots are skipped
  • Non-canonical side forks are ignored

Behavior Notes

  • Invalid requests are rejected early with error responses
  • Maximum request size is capped at 1024 blocks
  • Implementation mirrors existing BlocksByRoot handling patterns for consistency

Tests Added / Run

Added

  • blocks_by_range_returns_canonical_blocks_in_requested_order

Verified With

cargo fmt --check
cargo check -p ethlambda-p2p
cargo test -p ethlambda-p2p blocks_by_range_returns_canonical_blocks_in_requested_order
git diff --check

Related Issues / PRs

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented May 7, 2026

Greptile Summary

This PR adds inbound BlocksByRange request-response support to the P2P layer, enabling peers to request canonical blocks by slot range. It follows the existing BlocksByRoot pattern closely — registering the new protocol, adding SSZ codec paths, validating requests (step == 0, count == 0, count > 1024), and serving canonical blocks by walking backward from the fork-choice head.

  • canonical_blocks_by_range collects matching canonical blocks by traversing the chain from store.head() to start_slot, skipping non-canonical side forks and empty slots before assembling the result in ascending slot order.
  • ResponsePayload::BlocksByRoot is renamed to ResponsePayload::Blocks, unifying the response type for both protocols; the existing unit test is complemented by a new blocks_by_range_returns_canonical_blocks_in_requested_order test.
  • Validation now rejects step == 0, count == 0, and count > 1024 with INVALID_REQUEST — the error_message helper and ResponseCode that were previously dead code are now live.

Confidence Score: 4/5

Safe to merge with the traversal performance concern addressed; no correctness bugs in block selection or validation.

The block-selection logic is correct and the validation catches the required bad inputs. The one concern is that canonical_blocks_by_range always starts from store.head() and walks all the way back to start_slot on every request — a peer asking for very old slots forces O(head_slot) chain traversal regardless of how small the requested range is, which is exploitable as a cheap DoS.

crates/net/p2p/src/req_resp/handlers.rs — specifically the canonical_blocks_by_range traversal loop.

Important Files Changed

Filename Overview
crates/net/p2p/src/req_resp/handlers.rs Adds inbound BlocksByRange handling and canonical_blocks_by_range; the traversal walks from head to start_slot unconditionally, creating an O(head_slot) DoS surface for requests targeting old slot ranges.
crates/net/p2p/src/req_resp/messages.rs Adds BlocksByRangeRequest struct, BLOCKS_BY_RANGE_PROTOCOL_V1 constant, MAX_REQUEST_BLOCKS, and renames ResponsePayload::BlocksByRoot to ResponsePayload::Blocks; changes look correct and well-typed.
crates/net/p2p/src/req_resp/codec.rs Adds BlocksByRange decode path and unifies block response decoding under decode_blocks_response; logic is correct and symmetric with the existing BlocksByRoot path.
crates/net/p2p/src/req_resp/mod.rs Re-exports new types and constants; straightforward bookkeeping, no issues.
crates/net/p2p/src/lib.rs Registers BLOCKS_BY_RANGE_PROTOCOL_V1 with ProtocolSupport::Full; consistent with how BlocksByRoot is registered.

Sequence Diagram

sequenceDiagram
    participant Peer
    participant Codec
    participant Handler
    participant Store

    Peer->>Codec: BlocksByRange SSZ request
    Codec->>Handler: "Request::BlocksByRange { start_slot, count, step }"
    Handler->>Handler: "validate (step==0, count==0, count>1024)"
    alt invalid request
        Handler-->>Peer: Response::Error(INVALID_REQUEST)
    else valid request
        Handler->>Store: store.head()
        loop walk backward from head to start_slot
            Store-->>Handler: block header (slot, parent_root)
        end
        Handler->>Store: store.get_signed_block(root) per matched slot
        Store-->>Handler: "Vec<SignedBlock>"
        Handler-->>Codec: Response::Success(ResponsePayload::Blocks)
        Codec-->>Peer: SSZ-encoded block chunks
    end
Loading
Prompt To Fix All With AI
Fix the following 1 code review issue. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 1
crates/net/p2p/src/req_resp/handlers.rs:210-236
**Unbounded chain traversal is a DoS vector**

`canonical_blocks_by_range` always starts from `store.head()` and walks backward block-by-block until it passes `start_slot`, regardless of how far `end_slot` is from the head. A peer sending a valid request (passes all validation) for `start_slot=1, count=1024, step=1` on a node whose head is at slot 1,000,000 forces ~1,000,000 `store.get_block_header` calls per request. The 1024-block cap on `count` bounds the range width but places no limit on `head_slot – end_slot`, so repeated requests for old slot ranges can saturate the node with O(head_slot) work.

Adding an early exit once all matching slots have been collected would cap the traversal at the first filled slot in the range, and a separate guard rejecting requests where `end_slot` is unreasonably far below the head would prevent the worst-case walk entirely.

Reviews (2): Last reviewed commit: "Merge branch 'main' into feat/blocks-by-..." | Re-trigger Greptile

Comment thread crates/net/p2p/src/req_resp/handlers.rs Outdated
Comment thread crates/net/p2p/src/req_resp/handlers.rs Outdated
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
Copy link
Copy Markdown
Collaborator

@MegaRedHand MegaRedHand left a comment

Choose a reason for hiding this comment

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

Left some comments and questions

Comment thread crates/net/p2p/src/req_resp/codec.rs Outdated
Comment thread crates/net/p2p/src/req_resp/messages.rs Outdated
Comment thread crates/net/p2p/src/req_resp/handlers.rs Outdated
Comment thread crates/net/p2p/src/req_resp/handlers.rs
@dicethedev
Copy link
Copy Markdown
Contributor Author

@MegaRedHand You can review again

@MegaRedHand MegaRedHand reopened this May 12, 2026
@MegaRedHand
Copy link
Copy Markdown
Collaborator

CI's failing due to a linter error

Comment thread crates/net/p2p/src/req_resp/handlers.rs
@dicethedev
Copy link
Copy Markdown
Contributor Author

dicethedev commented May 12, 2026

CI's failing due to a linter error

let me see if I can fix from my end. @MegaRedHand You review issue again.

Copy link
Copy Markdown
Collaborator

@MegaRedHand MegaRedHand left a comment

Choose a reason for hiding this comment

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

LGTM

@MegaRedHand MegaRedHand merged commit c5705af into lambdaclass:main May 12, 2026
2 checks passed
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.

Add inbound BlocksByRange request support

2 participants