Skip to content

feat(l1): implement EIP-7975 eth/70 partial receipt fetching#6327

Merged
iovoid merged 8 commits into
mainfrom
feat/eip-7975-eth70-partial-receipts
Mar 12, 2026
Merged

feat(l1): implement EIP-7975 eth/70 partial receipt fetching#6327
iovoid merged 8 commits into
mainfrom
feat/eip-7975-eth70-partial-receipts

Conversation

@edg-l

@edg-l edg-l commented Mar 6, 2026

Copy link
Copy Markdown
Contributor

Summary

Implements EIP-7975 — a new eth/70 protocol version that extends the GetReceipts and Receipts messages with partial receipt list support.

As Ethereum scales with higher block gas limits, complete block receipt lists can exceed the 10 MiB devp2p message size limit, causing synchronization failures. eth/70 solves this by adding:

  • GetReceipts (eth/70): [request-id, firstBlockReceiptIndex, [blockhash₁, ...]] — allows the client to request receipts starting from a specific index in the first block (for pagination of large receipt lists).
  • Receipts (eth/70): [request-id, lastBlockIncomplete, [[receipt₁, ...], ...]] — includes a flag indicating whether the last block's receipt list is incomplete, signaling the client to make a follow-up request.

Changes

New files:

  • crates/networking/p2p/rlpx/eth/eth70/GetReceipts70, Receipts70, and StatusMessage70 message types with RLP encode/decode

Modified files:

  • message.rsV70 variant in EthCapVersion, new Message enum variants, updated dispatch (decode/encode/code/request_id/Display)
  • p2p.rsCapability::eth(70) added to SUPPORTED_ETH_CAPABILITIES
  • server.rs — V70 capability negotiation, Status70 handling, GetReceipts70 handler with 10 MiB soft response limit and partial receipt pagination
  • store.rsget_receipts_for_block_from_index() for partial receipt retrieval

Server-side partial receipt logic

  • Accumulates receipts per block using RLPEncode::length() for zero-allocation size estimation
  • Stops when the 10 MiB soft limit would be exceeded and sets lastBlockIncomplete = true
  • Guarantees at least one receipt is always included, even if it alone exceeds the limit
  • Avoids pushing empty trailing receipt lists that could mislead peers

Test plan

  • 21 new unit tests for GetReceipts70 and Receipts70 roundtrip encoding
  • Wire format compatibility tests (eth/70 differs from eth/69, cross-decode rejected)
  • All 93 p2p crate tests pass
  • cargo fmt, make lint clean
  • Hive eth/70 protocol tests (when available upstream)

@github-actions github-actions Bot added the L1 Ethereum client label Mar 6, 2026
@edg-l edg-l moved this to In Progress in ethrex_l1 Mar 6, 2026
@github-actions

github-actions Bot commented Mar 6, 2026

Copy link
Copy Markdown

🤖 Kimi Code Review

Review Summary

This PR adds support for eth/70 protocol version, implementing EIP-7975 (partial receipt requests). The implementation is well-structured and follows the existing patterns in the codebase. Here are the key findings:

Issues Found

1. Critical: Missing StatusMessage70 validation implementation (server.rs:708)

The validate_status function is called for StatusMessage70 but there's no implementation for this type. This will cause a compilation error.

// Line 708: Missing implementation
backend::validate_status(msg_data, &state.storage, &eth).await?

Fix needed: Add validate_status implementation for StatusMessage70 in the backend module.

2. Minor: Typo in StatusMessage70 field name (eth70/status.rs:22)

pub(crate) lastest_block: u64,        // Should be "latest_block"
pub(crate) lastest_block_hash: BlockHash,  // Should be "latest_block_hash"

3. Minor: Inconsistent capability offsets (message.rs:28-29)

Both SNAP_CAPABILITY_OFFSET_ETH_69 and SNAP_CAPABILITY_OFFSET_ETH_70 are set to 0x22, which seems intentional but should be verified against the spec.

4. Potential: Missing error handling for get_receipts_for_block_from_index (server.rs:1044-1077)

The new storage method could return an error that isn't properly handled in the message processing loop.

Security Considerations

Good: The soft response limit (10 MiB) is properly implemented to prevent DoS attacks
Good: Input validation through RLP decoding
Good: Proper error handling for storage operations

Performance

Good: Efficient streaming of receipts with size checking
Good: Early termination when soft limit is reached
⚠️ Minor: Consider using Vec::with_capacity for all_receipts to reduce allocations

Code Quality

Good: Comprehensive test coverage for new message types
Good: Consistent naming and structure with existing eth versions
Good: Proper documentation of EIP-7975 compliance

Recommendations

  1. Add the missing validate_status implementation for StatusMessage70
  2. Fix the typo in field names (lastestlatest)
  3. Consider adding a test for the storage method get_receipts_for_block_from_index
  4. Verify capability offsets against the eth/70 specification

The implementation is solid and follows Ethereum protocol specifications correctly. The main blocker is the missing validation function.


Automated review by Kimi (Moonshot AI) · custom prompt

@edg-l edg-l marked this pull request as ready for review March 6, 2026 08:42
@edg-l edg-l requested a review from a team as a code owner March 6, 2026 08:42
@edg-l edg-l moved this from In Progress to In Review in ethrex_l1 Mar 6, 2026
@github-actions

github-actions Bot commented Mar 6, 2026

Copy link
Copy Markdown

🤖 Kimi Code Review

Review Summary

This PR adds support for eth/70 (EIP-7975) with partial receipt requests. The implementation is clean and follows the established patterns in the codebase. Below are specific findings:

Correctness & EIP Compliance

  • EIP-7975 compliance: The new GetReceipts70 and Receipts70 messages correctly implement the EIP-7975 specification.
  • Soft response limit: The 10 MiB limit is properly enforced in server.rs (lines 1047-1056).

⚠️ Issues Found

1. Typo in StatusMessage70 (eth70/status.rs:20)

pub(crate) lastest_block: u64,        // Should be "latest_block"
pub(crate) lastest_block_hash: BlockHash,  // Should be "latest_block_hash"

This typo appears in multiple places in the file.

2. Potential Integer Overflow (server.rs:1047)

if total_size + receipt_size > SOFT_RESPONSE_LIMIT
  • total_size and receipt_size are both usize, but receipt.length() returns usize. On 32-bit systems, this could overflow. Consider using saturating_add or explicit bounds checking.

3. Inconsistent Capability Offsets (message.rs:28-31)

const SNAP_CAPABILITY_OFFSET_ETH_70: u8 = 0x22;  // Same as ETH_69
const BASED_CAPABILITY_OFFSET_ETH_70: u8 = 0x31;  // Same as ETH_69

These should probably be incremented (0x23 and 0x32 respectively) to avoid potential conflicts.

4. Missing Error Handling (server.rs:1040)

let block_receipts = state
    .storage
    .get_receipts_for_block_from_index(hash, start_index)
    .await?;

If a block doesn't exist, this returns an empty vec, which is correct behavior, but consider logging this case for debugging.

🔒 Security Considerations

  • DoS Protection: The soft response limit is properly implemented to prevent memory exhaustion.
  • Input Validation: All RLP decoding includes proper error handling.

🏗️ Code Quality

  • Test Coverage: Excellent - comprehensive roundtrip tests for all new message types.
  • Code Duplication: Some duplication between eth68/69/70 modules, but this follows the existing pattern and aids maintainability.

📝 Minor Suggestions

  1. Consider adding a comment in server.rs explaining why empty receipt lists are skipped (lines 1065-1070).
  2. The Receipt::length() method should be documented to clarify what it measures (RLP bytes?).

Overall Assessment

The implementation is solid and follows Ethereum's P2P protocol evolution correctly. The issues found are minor and easily addressable.


Automated review by Kimi (Moonshot AI) · custom prompt

@github-actions

github-actions Bot commented Mar 6, 2026

Copy link
Copy Markdown

🤖 Codex Code Review

  1. Potential DoS/memory pressure in eth/70 receipt serving path
    The new handler loads all receipts for each requested block into memory before applying SOFT_RESPONSE_LIMIT, so the soft cap does not bound DB read/alloc cost. A peer can repeatedly request receipt-heavy blocks and force high memory/IO churn.
    Refs: server.rs:1045, server.rs:1051, store.rs:1057, store.rs:1062.
    Suggested change: add a bounded/streaming store API (e.g. fetch receipts incrementally with start_index + byte budget) so limits are enforced during retrieval, not after full materialization.

  2. Index increment can wrap on malformed/extreme data
    index += 1 in receipt iteration can overflow if start_index == u64::MAX and a receipt exists at that key (corrupt DB / adversarial data scenarios), causing wraparound reads from 0.
    Ref: store.rs:1072.
    Suggested change: use checked_add(1) and return an error or break safely on overflow.

Overall, eth/70 message wiring and version-gated decoding look solid; I didn’t see EVM/gas/consensus-rule regressions in this PR’s scope (networking/protocol handling).


Automated review by OpenAI Codex · custom prompt

@github-actions

github-actions Bot commented Mar 6, 2026

Copy link
Copy Markdown

Lines of code report

Total lines added: 628
Total lines removed: 0
Total lines changed: 628

Detailed view
+---------------------------------------------------------+-------+------+
| File                                                    | Lines | Diff |
+---------------------------------------------------------+-------+------+
| ethrex/crates/networking/p2p/rlpx/connection/server.rs  | 1124  | +54  |
+---------------------------------------------------------+-------+------+
| ethrex/crates/networking/p2p/rlpx/eth/eth70/mod.rs      | 2     | +2   |
+---------------------------------------------------------+-------+------+
| ethrex/crates/networking/p2p/rlpx/eth/eth70/receipts.rs | 264   | +264 |
+---------------------------------------------------------+-------+------+
| ethrex/crates/networking/p2p/rlpx/eth/eth70/status.rs   | 115   | +115 |
+---------------------------------------------------------+-------+------+
| ethrex/crates/networking/p2p/rlpx/eth/mod.rs            | 8     | +1   |
+---------------------------------------------------------+-------+------+
| ethrex/crates/networking/p2p/rlpx/eth/receipts.rs       | 220   | +139 |
+---------------------------------------------------------+-------+------+
| ethrex/crates/networking/p2p/rlpx/eth/status.rs         | 10    | +1   |
+---------------------------------------------------------+-------+------+
| ethrex/crates/networking/p2p/rlpx/message.rs            | 377   | +41  |
+---------------------------------------------------------+-------+------+
| ethrex/crates/networking/p2p/rlpx/p2p.rs                | 319   | +4   |
+---------------------------------------------------------+-------+------+
| ethrex/crates/storage/store.rs                          | 2554  | +7   |
+---------------------------------------------------------+-------+------+

@github-actions

github-actions Bot commented Mar 6, 2026

Copy link
Copy Markdown

🤖 Claude Code Review

Here is the review for PR #6327:


Code Review: feat(l1) — EIP-7975 eth/70 partial receipt fetching

Overall the implementation is well-structured and the test coverage is solid. There is one meaningful correctness issue with the lastBlockIncomplete flag semantics, plus a few smaller points.


Correctness — lastBlockIncomplete semantic violation

crates/networking/p2p/rlpx/connection/server.rs:1050–1078

The soft-limit loop sets last_block_incomplete = true in two distinct situations, but only one of them is correct per EIP-7975:

  • Case A — some receipts from block i were already accumulated (block_receipt_list non-empty) and the next one would exceed the limit. Here last_block_incomplete = true is correct: the last receipt list in the response is genuinely incomplete.

  • Case B — the limit was already reached from previous blocks (all_receipts non-empty) before even the first receipt of block i could be examined. Here block_receipt_list is empty so we correctly skip pushing it, but last_block_incomplete is still set to true and included in the response.

In Case B the last inner list in the response belongs to block i-1, which is complete. Sending lastBlockIncomplete = 1 for a complete list violates the spec ("if 1, the last inner list is an incomplete receipt list of the block") and could cause a conformant peer to re-request starting at receipt offset N within block i-1 rather than advancing to block i, producing an extra round-trip or a logic loop.

The condition that guards setting the flag is:

if total_size + receipt_size > SOFT_RESPONSE_LIMIT
    && (!block_receipt_list.is_empty() || !all_receipts.is_empty())

The !all_receipts.is_empty() arm fires in Case B. The fix is to only set last_block_incomplete = true when block_receipt_list is non-empty (i.e., we did start accumulating receipts for the current block). If no receipts from block i fit, the response should end with last_block_incomplete = false; the peer can infer truncation from the fact that fewer block receipt lists were returned than requested.


Minor Issues

Duplicate capability offset constants — message.rs:27–28

const SNAP_CAPABILITY_OFFSET_ETH_70: u8 = 0x22;
const BASED_CAPABILITY_OFFSET_ETH_70: u8 = 0x31;

These are identical to the V69 constants. If they are intentionally the same value they should either alias the V69 constants or carry a brief comment explaining why — otherwise a reader might wonder whether this is an oversight or a future placeholder.

Pre-existing typo propagated — eth70/status.rs:22–23

lastest_block / lastest_block_hash are misspellings of latest_*. These were copied from StatusMessage69 where the same typo exists. This is pre-existing, but since StatusMessage70 is new code this is a low-cost opportunity to spell it correctly (internal fields only, no wire impact).

eth_version decoded as u32, stored as u8eth70/status.rs:48,67

The decode reads eth_version as u32, validates eth_version != 70, then stores it as eth_version as u8. Because the != 70 check runs before the cast the truncation is safe here, but the pattern is slightly fragile. u8::try_from(eth_version).map_err(|_| RLPDecodeError::...) would make the intent explicit. (Same pattern exists in StatusMessage69.)

Misleading test name — eth70/receipts.rs test get_receipts70_and_receipts70_share_no_code_collision

The test asserts GetReceipts70::CODE != Receipts70::CODE (request ≠ response), not that there is no cross-version collision. The name implies the former but tests the latter. A name like get_receipts70_request_and_response_have_distinct_codes would be clearer.


What looks good

  • The "at least one receipt always included" guarantee (the (!block_receipt_list.is_empty() || !all_receipts.is_empty()) double-guard) is the right design.
  • Routing GetReceipts::CODE vs GetReceipts70::CODE via !matches!(eth_version, EthCapVersion::V70) guards in the decoder is clean and correct, since both share code 0x0F.
  • get_receipts_for_block delegating to get_receipts_for_block_from_index(hash, 0) is a clean refactor with no behaviour change.
  • SOFT_RESPONSE_LIMIT being defined alongside the message types (not scattered in server code) is good.
  • Test coverage for roundtrip encoding, cross-version wire-format incompatibility, and cross-decode rejection is thorough.

Automated review by Claude (Anthropic) · custom prompt

@edg-l edg-l force-pushed the feat/eip-7975-eth70-partial-receipts branch from 625ab2b to c9e2715 Compare March 6, 2026 08:45
@github-actions

github-actions Bot commented Mar 6, 2026

Copy link
Copy Markdown

🤖 Claude Code Review

Now I have enough information to produce a thorough review.


Code Review: PR #6327 — EIP-7975 eth/70 Partial Receipt Fetching

Summary

Overall, this is a well-structured incremental implementation. The message framing, dispatch logic, and server-side pagination logic are correct. The test coverage is commendable. Below are findings ordered by severity.


Correctness Issues

bool encoding for lastBlockIncomplete — verify wire compatibility
(crates/networking/p2p/rlpx/eth/eth70/receipts.rs:202, decode at line 236)

EIP-7975 defines lastBlockIncomplete as {0, 1} — an integer type. The ethrex_rlp bool decode implementation (in crates/common/rlp/decode.rs:41-44) is strict: it only accepts 0x80 (false) and 0x01 (true), and rejects 0x00 with MalformedBoolean. Since bool false → 0x80 and u8 0 → 0x80 are identical, the wire encoding happens to match. However, if a permissive peer sends 0x00 as integer 0 for "false", the decode will fail. Consider either:

  • Using u8 with a manual boolean check to be maximally interoperable, or
  • Documenting this strict encoding assumption explicitly.

get_receipts_for_block_from_index does not distinguish "block not found" from "empty receipts"
(crates/storage/store.rs:1057-1079)

Both a missing block and a block with zero transactions return Ok(vec![]). This is a pre-existing issue inherited from get_receipts_for_block, but it propagates to the eth/70 handler where an unknown block hash in the request silently yields an empty receipt list rather than being skipped or flagged. The handler in server.rs will push an empty Vec<Receipt> for that block (line 105-107), which a peer might misinterpret as "block exists with no transactions."

GetReceipts70 handler: unreachable block after last_block_incomplete set in non-first iteration
(crates/networking/p2p/rlpx/connection/server.rs:91-111)

The condition at line 92:

if total_size + receipt_size > SOFT_RESPONSE_LIMIT
    && (!block_receipt_list.is_empty() || !all_receipts.is_empty())

For the very first receipt of the very first block (both lists empty), this allows the oversized receipt to be included unconditionally — correct per the spec's "at least one receipt" guarantee. However, for the very first receipt of a subsequent block when all_receipts is already non-empty, this condition will always be true (because !all_receipts.is_empty() holds), meaning a single giant receipt in block N will cut off the response immediately without including any receipt from block N. The response will correctly stop and set last_block_incomplete = false (it's not set here, the outer if last_block_incomplete break handles it), but since block_receipt_list is empty and last_block_incomplete is still false, the empty list for that block gets pushed (line 105-106). This is the same "empty list misleads the peer" case described above.


Protocol Correctness

GetReceipts (eth/68-69 format) can still be sent over eth/70 connections

The Message::GetReceipts variant is decoded only for non-V70 connections (correct, line 217), but the Message::code() function and encode() dispatch do not prevent constructing and sending a Message::GetReceipts(...) over an eth/70 connection. If any call site creates Message::GetReceipts(...) and sends it over a negotiated eth/70 connection, the peer will try to parse it as GetReceipts70 (expecting three RLP fields) and fail. A grep confirms no call site currently constructs GetReceipts::new(...), so this is a latent risk rather than a current bug — but it should be guarded against as the client-side receipt fetching is completed.


Minor Issues

Typo in struct field names — lastest_block and lastest_block_hash
(crates/networking/p2p/rlpx/eth/eth70/status.rs:470-471, and lines 509, 519-520, 534, 543, 552-553)

This typo ("lastest" instead of "latest") is copied from StatusMessage69. These are pre-existing names but the PR introduces a new struct that repeats them. Since this is a new file, it is a good opportunity to fix — or at minimum, it should be a tracked issue to fix across all status message types.

eth_version decoded as u32 then truncated to u8
(crates/networking/p2p/rlpx/eth/eth70/status.rs:496, 515)

let (eth_version, decoder): (u32, _) = decoder.decode_field("protocolVersion")?;
// ...
eth_version: eth_version as u8,

The field is decoded as u32 but stored as u8, silently truncating values ≥ 256. In practice eth protocol versions won't reach 256, but this diverges from the struct's own type. This is also copied from StatusMessage69; both should use the same type for decode and storage.

Duplicate make_receipt test helper
(crates/networking/p2p/rlpx/eth/eth70/receipts.rs:252-261 and crates/networking/p2p/rlpx/eth/receipts.rs:614-623)

Identical helper defined in two modules. Consider extracting to a test utility module shared across receipt tests.

Redundant constants with identical values
(crates/networking/p2p/rlpx/message.rs:27-31)

const SNAP_CAPABILITY_OFFSET_ETH_69: u8 = 0x22;
const SNAP_CAPABILITY_OFFSET_ETH_70: u8 = 0x22;  // same value
const BASED_CAPABILITY_OFFSET_ETH_69: u8 = 0x31;
const BASED_CAPABILITY_OFFSET_ETH_70: u8 = 0x31;  // same value

These are intentionally equal because eth/70 reuses the same message codes (0x0F / 0x10) as eth/68-69, so the capability offset doesn't change. A comment explaining why they share the value would help future maintainers and prevent someone from incorrectly assuming this is a copy-paste error.

eth70 module is mod (private) while eth69 is also mod (private), but eth68 is pub(crate)
(crates/networking/p2p/rlpx/eth/mod.rs)

Consistent visibility is fine; just noting the pattern is already established.


Missing Client-Side Implementation (Informational)

The PR correctly implements server-side handling of GetReceipts70. It does not implement client-side receipt requesting (first_block_receipt_index selection, following up on lastBlockIncomplete = true responses). While this is an intentional incremental approach, eth(70) is now advertised in SUPPORTED_ETH_CAPABILITIES. When a peer negotiates eth/70 and this node needs to request receipts from that peer, the correct behavior would be to send GetReceipts70. The absence of that code path should be tracked and completed before merging to avoid a half-implemented protocol version in production.


Positive Highlights

  • The pagination logic's "always include at least one receipt" guarantee is correctly implemented.
  • Version-dispatched decode (!matches!(eth_version, EthCapVersion::V70) guard) properly prevents cross-version decode confusion.
  • SOFT_RESPONSE_LIMIT is used consistently and exported from the right place.
  • Test coverage for wire format differences and cross-decode rejection is solid.
  • The store.rs refactor (get_receipts_for_block now delegates to get_receipts_for_block_from_index) is clean and non-breaking.

Automated review by Claude (Anthropic) · custom prompt

@github-actions

github-actions Bot commented Mar 6, 2026

Copy link
Copy Markdown

🤖 Codex Code Review

  1. Receipts70 responses are not routed to pending requests, so eth/70 receipt requests can fail at runtime.
  • In incoming-message handling, the “response messages to backend” match includes Receipts68 and Receipts69, but not Receipts70 (server.rs:1209, server.rs:1210).
  • Unmatched messages fall through to MessageNotHandled (server.rs:1222).
  • This is inconsistent with request_id() support for Receipts70 (message.rs:336).
  • Suggested fix: add | message @ Message::Receipts70(_) to that response-routing match arm.
  1. Test coverage misses the above eth/70 request-response plumbing path.
  • Current additions heavily test codec roundtrips, but not server request tracking/response dispatch for GetReceipts70 -> Receipts70.
  • A focused integration/unit test around handle_incoming_message would prevent regressions here.

Everything else in the diff looks structurally solid: eth/70 capability negotiation, status message support, and partial receipts encoding/decoding are implemented cleanly.

I couldn’t run cargo test in this environment because rustup cannot write to /home/runner/.rustup/tmp (permission denied).


Automated review by OpenAI Codex · custom prompt

@greptile-apps

greptile-apps Bot commented Mar 6, 2026

Copy link
Copy Markdown

Greptile Summary

This PR implements EIP-7975 (eth/70), adding partial receipt fetching to address scenarios where complete block receipt lists exceed the 10 MiB devp2p message limit. The implementation adds new GetReceipts70, Receipts70, and StatusMessage70 message types with correct RLP wire encoding, version-gated dispatch in the message router, and a new get_receipts_for_block_from_index storage method.

Critical issue:
The GetReceipts70 handler contains a logic bug in the last_block_incomplete flag. When the soft response limit is reached before any receipt from a new block can be added to the response, the flag is incorrectly set to true even though all previously-included blocks are complete. A peer receiving this response will misinterpret the last-included block as partial and attempt to re-request it, causing a synchronization stall.

Other findings:
A typo in StatusMessage70 field names (lastest_block/lastest_block_hash should be latest_block/latest_block_hash).

The RLP encode/decode for all three new message types is correct and well-tested (21 round-trip tests). Message code dispatch, capability negotiation, and storage layer are all implemented correctly.

Confidence Score: 1/5

  • Not safe to merge — the GetReceipts70 handler contains a logic bug that causes incorrect lastBlockIncomplete signaling, which can produce a synchronization stall with eth/70 peers.
  • The PR contains a critical logic defect in the core partial-receipt pagination logic. When the 10 MiB soft limit is hit before any receipt from a new block can be added, last_block_incomplete is set to true even though all previously-included blocks are complete. A peer receiving this response will misinterpret a complete block as partial and attempt to re-request it from an incorrect offset, causing a protocol-level deadlock. This is exactly the scenario EIP-7975 is intended to handle, so the bug would trigger in real-world high-gas-limit conditions. The message encoding/decoding, capability negotiation, and storage layer are all correct and well-tested, but the handler logic must be fixed before merging.
  • crates/networking/p2p/rlpx/connection/server.rs (GetReceipts70 handler, lines 1053–1058); crates/networking/p2p/rlpx/eth/eth70/status.rs (typo in field names, lines 21–23).

Sequence Diagram

sequenceDiagram
    participant Peer as Remote Peer (eth/70)
    participant Server as ethrex Server
    participant Store as Storage

    Peer->>Server: GetReceipts70(id, firstBlockReceiptIndex, [hash1, hash2, ...])
    activate Server

    loop for each block hash
        Server->>Store: get_receipts_for_block_from_index(hash, start_index)
        Store-->>Server: Vec Receipt
        Note over Server: Accumulate receipts up to 10 MiB limit. Set lastBlockIncomplete if mid-block cutoff.
    end

    Server-->>Peer: Receipts70(id, lastBlockIncomplete, receipts)
    deactivate Server

    alt lastBlockIncomplete == true
        Note over Peer: Re-request same block from offset len(partial_receipts)
        Peer->>Server: GetReceipts70(id, followUpIndex, [lastBlockHash])
    else lastBlockIncomplete == false
        Note over Peer: All included blocks complete. Request next blocks if needed.
    end
Loading

Last reviewed commit: c9e2715

Comment thread crates/networking/p2p/rlpx/connection/server.rs
Comment thread crates/networking/p2p/rlpx/eth/eth70/status.rs Outdated
@edg-l edg-l force-pushed the feat/eip-7975-eth70-partial-receipts branch from 58bbb02 to 891367e Compare March 6, 2026 09:10
Comment thread crates/networking/p2p/rlpx/connection/server.rs
@github-project-automation github-project-automation Bot moved this from In Review to In Progress in ethrex_l1 Mar 6, 2026
edg-l added 2 commits March 9, 2026 12:02
Introduce eth/70 protocol support with modified GetReceipts and Receipts
messages that allow requesting and serving partial block receipt lists.

This prevents synchronization failures when complete receipt lists exceed
the 10 MiB devp2p message size limit at higher block gas limits.
- Fix last_block_incomplete flag: only set when the current block
  actually has a partial receipt list. Previously the flag was
  incorrectly set when the limit was hit at a block boundary,
  causing peers to re-request an already-complete block.
- Fix lastest -> latest typo in status message field names
  across eth68, eth69, and eth70 (including RLP decode field names).
@edg-l edg-l force-pushed the feat/eip-7975-eth70-partial-receipts branch from 891367e to 9f4cb5a Compare March 9, 2026 11:02
Prevents a malicious peer from triggering unbounded DB reads by
sending an excessive number of block hashes.
@edg-l edg-l requested a review from ElFantasma March 9, 2026 12:03
@edg-l edg-l moved this from In Progress to In Review in ethrex_l1 Mar 9, 2026
Comment thread crates/networking/p2p/rlpx/message.rs Outdated
…enum variants

Adds version-explicit naming to match the existing Receipts68/Receipts69/Receipts70
convention, making the message enum consistent across all receipt-related types.
@edg-l edg-l requested a review from iovoid March 12, 2026 08:28
@iovoid iovoid added this pull request to the merge queue Mar 12, 2026
Merged via the queue into main with commit 01f7970 Mar 12, 2026
52 checks passed
@iovoid iovoid deleted the feat/eip-7975-eth70-partial-receipts branch March 12, 2026 14:42
@github-project-automation github-project-automation Bot moved this from In Review to Done in ethrex_l1 Mar 12, 2026
ElFantasma pushed a commit that referenced this pull request Mar 12, 2026
Implements [EIP-7975](https://eips.ethereum.org/EIPS/eip-7975) — a new
`eth/70` protocol version that extends the `GetReceipts` and `Receipts`
messages with partial receipt list support.

As Ethereum scales with higher block gas limits, complete block receipt
lists can exceed the 10 MiB devp2p message size limit, causing
synchronization failures. eth/70 solves this by adding:

- **`GetReceipts` (eth/70)**: `[request-id, firstBlockReceiptIndex,
[blockhash₁, ...]]` — allows the client to request receipts starting
from a specific index in the first block (for pagination of large
receipt lists).
- **`Receipts` (eth/70)**: `[request-id, lastBlockIncomplete,
[[receipt₁, ...], ...]]` — includes a flag indicating whether the last
block's receipt list is incomplete, signaling the client to make a
follow-up request.

**New files:**
- `crates/networking/p2p/rlpx/eth/eth70/` — `GetReceipts70`,
`Receipts70`, and `StatusMessage70` message types with RLP encode/decode

**Modified files:**
- `message.rs` — `V70` variant in `EthCapVersion`, new `Message` enum
variants, updated dispatch (decode/encode/code/request_id/Display)
- `p2p.rs` — `Capability::eth(70)` added to `SUPPORTED_ETH_CAPABILITIES`
- `server.rs` — V70 capability negotiation, Status70 handling,
`GetReceipts70` handler with 10 MiB soft response limit and partial
receipt pagination
- `store.rs` — `get_receipts_for_block_from_index()` for partial receipt
retrieval

- Accumulates receipts per block using `RLPEncode::length()` for
zero-allocation size estimation
- Stops when the 10 MiB soft limit would be exceeded and sets
`lastBlockIncomplete = true`
- Guarantees at least one receipt is always included, even if it alone
exceeds the limit
- Avoids pushing empty trailing receipt lists that could mislead peers

- [x] 21 new unit tests for `GetReceipts70` and `Receipts70` roundtrip
encoding
- [x] Wire format compatibility tests (eth/70 differs from eth/69,
cross-decode rejected)
- [x] All 93 p2p crate tests pass
- [x] `cargo fmt`, `make lint` clean
- [ ] Hive eth/70 protocol tests (when available upstream)
iovoid pushed a commit that referenced this pull request Mar 16, 2026
## Summary

Implements [EIP-7975](https://eips.ethereum.org/EIPS/eip-7975) — a new
`eth/70` protocol version that extends the `GetReceipts` and `Receipts`
messages with partial receipt list support.

As Ethereum scales with higher block gas limits, complete block receipt
lists can exceed the 10 MiB devp2p message size limit, causing
synchronization failures. eth/70 solves this by adding:

- **`GetReceipts` (eth/70)**: `[request-id, firstBlockReceiptIndex,
[blockhash₁, ...]]` — allows the client to request receipts starting
from a specific index in the first block (for pagination of large
receipt lists).
- **`Receipts` (eth/70)**: `[request-id, lastBlockIncomplete,
[[receipt₁, ...], ...]]` — includes a flag indicating whether the last
block's receipt list is incomplete, signaling the client to make a
follow-up request.

### Changes

**New files:**
- `crates/networking/p2p/rlpx/eth/eth70/` — `GetReceipts70`,
`Receipts70`, and `StatusMessage70` message types with RLP encode/decode

**Modified files:**
- `message.rs` — `V70` variant in `EthCapVersion`, new `Message` enum
variants, updated dispatch (decode/encode/code/request_id/Display)
- `p2p.rs` — `Capability::eth(70)` added to `SUPPORTED_ETH_CAPABILITIES`
- `server.rs` — V70 capability negotiation, Status70 handling,
`GetReceipts70` handler with 10 MiB soft response limit and partial
receipt pagination
- `store.rs` — `get_receipts_for_block_from_index()` for partial receipt
retrieval

### Server-side partial receipt logic
- Accumulates receipts per block using `RLPEncode::length()` for
zero-allocation size estimation
- Stops when the 10 MiB soft limit would be exceeded and sets
`lastBlockIncomplete = true`
- Guarantees at least one receipt is always included, even if it alone
exceeds the limit
- Avoids pushing empty trailing receipt lists that could mislead peers

## Test plan

- [x] 21 new unit tests for `GetReceipts70` and `Receipts70` roundtrip
encoding
- [x] Wire format compatibility tests (eth/70 differs from eth/69,
cross-decode rejected)
- [x] All 93 p2p crate tests pass
- [x] `cargo fmt`, `make lint` clean
- [ ] Hive eth/70 protocol tests (when available upstream)
Muzry pushed a commit to Muzry/ethrex that referenced this pull request Mar 17, 2026
…lass#6327)

## Summary

Implements [EIP-7975](https://eips.ethereum.org/EIPS/eip-7975) — a new
`eth/70` protocol version that extends the `GetReceipts` and `Receipts`
messages with partial receipt list support.

As Ethereum scales with higher block gas limits, complete block receipt
lists can exceed the 10 MiB devp2p message size limit, causing
synchronization failures. eth/70 solves this by adding:

- **`GetReceipts` (eth/70)**: `[request-id, firstBlockReceiptIndex,
[blockhash₁, ...]]` — allows the client to request receipts starting
from a specific index in the first block (for pagination of large
receipt lists).
- **`Receipts` (eth/70)**: `[request-id, lastBlockIncomplete,
[[receipt₁, ...], ...]]` — includes a flag indicating whether the last
block's receipt list is incomplete, signaling the client to make a
follow-up request.

### Changes

**New files:**
- `crates/networking/p2p/rlpx/eth/eth70/` — `GetReceipts70`,
`Receipts70`, and `StatusMessage70` message types with RLP encode/decode

**Modified files:**
- `message.rs` — `V70` variant in `EthCapVersion`, new `Message` enum
variants, updated dispatch (decode/encode/code/request_id/Display)
- `p2p.rs` — `Capability::eth(70)` added to `SUPPORTED_ETH_CAPABILITIES`
- `server.rs` — V70 capability negotiation, Status70 handling,
`GetReceipts70` handler with 10 MiB soft response limit and partial
receipt pagination
- `store.rs` — `get_receipts_for_block_from_index()` for partial receipt
retrieval

### Server-side partial receipt logic
- Accumulates receipts per block using `RLPEncode::length()` for
zero-allocation size estimation
- Stops when the 10 MiB soft limit would be exceeded and sets
`lastBlockIncomplete = true`
- Guarantees at least one receipt is always included, even if it alone
exceeds the limit
- Avoids pushing empty trailing receipt lists that could mislead peers

## Test plan

- [x] 21 new unit tests for `GetReceipts70` and `Receipts70` roundtrip
encoding
- [x] Wire format compatibility tests (eth/70 differs from eth/69,
cross-decode rejected)
- [x] All 93 p2p crate tests pass
- [x] `cargo fmt`, `make lint` clean
- [ ] Hive eth/70 protocol tests (when available upstream)
ElFantasma added a commit that referenced this pull request Mar 27, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

L1 Ethereum client

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

4 participants