fix(l1): use 0x80 sentinel for missing eth/71 BAL per EIP-8159#6744
fix(l1): use 0x80 sentinel for missing eth/71 BAL per EIP-8159#6744edg-l wants to merge 1 commit into
Conversation
The eth/71 BlockAccessLists handler emitted 0xc0 (RLP empty list) for missing entries, but EIP-8159 §"BlockAccessLists (0x13)" mandates the RLP empty string 0x80: an empty list is a valid BAL encoding (block with no state changes), so only the empty string can never alias a real BAL. Affects both encode and decode of OptionalBal. geth uses rlp.EmptyString at eth/protocols/eth/handlers.go:693; the 0xc0 value would have caused silent data confusion on interop (geth decoding our missing slots as valid empty BALs). Adds a byte-exact test asserting 0x80 is present and 0xc0 is absent in a single-None response payload.
|
🤖 Kimi Code ReviewThis PR fixes an interoperability-critical encoding bug in EIP-8159 Block Access Lists (BAL) handling. The change is correct and well-tested. SummaryIssue: The previous implementation used RLP empty list ( Fix: Correctly uses Detailed Review
|
🤖 Claude Code ReviewCode Review: PR #6744 — Fix eth/71
|
Lines of code reportTotal lines added: Detailed view |
Greptile SummaryFixes the
Confidence Score: 5/5Safe to merge — a single, surgical byte-constant change with matching encode/decode symmetry and a new wire-level regression test. The change is minimal: two byte literals flipped from 0xc0 to 0x80 with a matching decode guard. The fix aligns with both the EIP-8159 spec and geth's reference implementation. All pre-existing roundtrip tests continue to pass because both sides of the codec were updated together. The new test independently checks the decompressed wire bytes, ensuring the sentinel cannot silently regress. No files require special attention.
|
| Filename | Overview |
|---|---|
| crates/networking/p2p/rlpx/eth/block_access_lists.rs | Sentinel byte corrected from 0xc0 to 0x80 in both encode and decode paths for OptionalBal; doc comment updated with EIP-8159 rationale; new regression test locks the wire format. |
Sequence Diagram
sequenceDiagram
participant ethrex as ethrex (eth/71)
participant wire as Wire (RLP)
participant geth as go-ethereum (eth/71)
Note over ethrex,geth: BlockAccessLists (0x13) response — missing BAL slot
ethrex->>wire: OptionalBal(None) → encode 0x80 (RLP empty string, EIP-8159)
wire->>geth: byte 0x80
geth->>geth: "rlp.EmptyString check passes → slot = unavailable ✓"
Note over ethrex,geth: Before fix (wrong sentinel 0xc0)
ethrex-->>wire: OptionalBal(None) → encode 0xc0 (RLP empty list)
wire-->>geth: byte 0xc0
geth-->>geth: "decode as zero-element list → slot = valid empty BAL ✗ (silent confusion)"
Reviews (1): Last reviewed commit: "fix(l1): use 0x80 sentinel for missing e..." | Re-trigger Greptile
🤖 Codex Code Review
The wire-format fix itself looks correct and aligns with the spec; the concerns above are about backward compatibility and test precision. I could not run Automated review by OpenAI Codex · gpt-5.4 · custom prompt |
| /// is the only byte that can't alias a real BAL. geth uses the same | ||
| /// sentinel (`rlp.EmptyString` in `eth/protocols/eth/handlers.go`); any | ||
| /// drift here is silent interop breakage. | ||
| #[test] |
There was a problem hiding this comment.
This should be on the test/ folder
| "decompressed payload must contain the 0x80 None sentinel" | ||
| ); | ||
| assert!( | ||
| !decompressed.contains(&0xc0), |
There was a problem hiding this comment.
!decompressed.contains(&0xc0) is correct today but happens to hold via RLP-encoding luck. The outer [request_id, [bals]] wrapper is encoded with a 0xc0 + length prefix; for (request_id=0, bals=[None]) the lengths come out to 0xc4 / 0xc1 (not 0xc0). The moment someone changes the test fixture in a way that makes the outer list have exactly zero payload bytes, the prefix becomes 0xc0 and the test fails spuriously even though the sentinel is right.
More robust assertion: target OptionalBal directly rather than the whole message.
let mut bytes = Vec::new();
OptionalBal(None).encode(&mut bytes);
assert_eq!(bytes, vec![0x80]);Keeps the intent ("None encodes as exactly 0x80") and survives unrelated encoding changes elsewhere.
Non-blocking — the current test is correct, just brittle.
| impl RLPDecode for OptionalBal { | ||
| fn decode_unfinished(rlp: &[u8]) -> Result<(Self, &[u8]), RLPDecodeError> { | ||
| if rlp.first() == Some(&0xc0) { | ||
| if rlp.first() == Some(&0x80) { |
There was a problem hiding this comment.
Just confirming this is now consistent with the snap/2 codec in #6544 — that PR's Snap2OptionalBal::decode_unfinished does the same rlp.first() == Some(&0x80) check (codec.rs:376 there). I left an inline on the snap/2 side asking for an invariant comment about why 0x80 is unambiguous (BAL is always RLP-encoded as a list, so >= 0xc0). The same comment is worth landing here so a future refactor of BlockAccessList's encoding doesn't silently break this decoder.
One-line suffix on the existing comment block at lines 19–24 is enough — something like "INVARIANT: BlockAccessList always encodes as an RLP list (first byte ≥ 0xc0), so 0x80 is unambiguously the None sentinel." Non-blocking.
Summary
eth/71
BlockAccessLists(0x13) returns the wrong sentinel byte for missing BALs. EIP-8159 §"BlockAccessLists (0x13)" mandates the RLP empty string (`0x80`); our implementation emitted the RLP empty list (`0xc0`).`0xc0` is a valid encoding for an empty `BlockAccessList` (a block with no state changes), so using it as the "unavailable" sentinel makes "missing BAL" indistinguishable from "valid empty BAL" on the wire. The spec uses `0x80` because it can never alias a real BAL.
Why this matters
go-ethereum's eth/71 handler uses `rlp.EmptyString` (= `0x80`) at `eth/protocols/eth/handlers.go:693`, with the comment:
With ethrex on `0xc0` and geth on `0x80`:
Found while cross-checking the snap/2 (EIP-8189) PR against go-ethereum.
Changes
Test plan