ethereum / ethereum/execution-specs

RLP nesting bound is unspecified; reference acceptance depth depends on sys.getrecursionlimit

Open
#3,469 0 comments 0 reactions 0 assignees View on GitHub
Dominant language
Python
Stars
1.2k
Forks
505
Avg merge
2d 14h
Merged PRs (30d)
116

Description

## Summary

**The reference RLP decoder accepts or rejects a block depending on the host interpreter's `sys.getrecursionlimit()`, not on the block itself.** For sufficiently nested RLP, *block validity as defined by the reference becomes a function of OS/runtime configuration.* This is an under-specification, not a bug: nothing in the specification states an RLP nesting bound, yet the reference's acceptance boundary is set by an interpreter setting that callers can change.

### Metadata

* **Hardfork**: amsterdam (relevant to all forks — the decoder is fork-independent)
* **Component**: `ethereum_rlp.rlp.decode` (the pinned decoder), consumed by block RLP decoding via `from ethereum_rlp import rlp` (e.g. `src/ethereum/forks/amsterdam/blocks.py:15`)

## What was measured

On pinned artifacts, 2026-08-30.

| | result | artifact |
|---|---|---|
| reference | depths **331, 332 decode**; **333, 334, 340 raise `RecursionError`** | `ethereum-rlp 0.1.6`, wheel sha256 `f4144caa96b975720c62b967bbfd6c98601e95ee3c36457ece11ef4ce0cffa04`, pinned by `execution-specs/uv.lock:1221-1232`, CPython 3.12.3, untouched default `sys.getrecursionlimit() == 1000` |

Inputs were canonical nested RLP lists built **iteratively**, so the harness contributes no frames to the thing being measured. Boundary confirmed in fresh processes.

## The mechanism

`decode` → `decode_to_sequence` → `decode_joined_encodings` → `decode` costs **three CPython frames per nesting level**, so the threshold lands near `recursionlimit / 3`:

- `decode` — `src/ethereum_rlp/rlp.py:143`
- `decode_to_sequence` — `src/ethereum_rlp/rlp.py:427`
- `decode_joined_encodings` — `src/ethereum_rlp/rlp.py:464`, which recurses back into `decode` at its `decoded_sequence.append(decode(encoded_item))` line

The `v0.1.6` source is verbatim at these lines. Nothing in the specification states a nesting bound; the bound exists only as a property of the runtime, settable by the caller.

## Why this is consensus-relevant

`RecursionError` is an `Exception`, so it propagates to the stateless entry point's `except Exception:` handler (in the fork carrying the stateless guest, `forks/amsterdam/stateless.py:363`) and becomes `successful_validation = False` — **a rejection verdict, not a crash.** Concretely:

- A block whose RLP nests 333 levels is **invalid** under a default CPython.
- The same block is **valid** under an interpreter started with a raised `sys.setrecursionlimit`.
- Two conforming runs of the same reference code can therefore disagree on the same block.

A reference whose acceptance boundary moves with an environment variable cannot be used to settle a disagreement about a specific block — which is the main thing a reference is for.

## Suggested resolutions (either is coherent; maintainers may prefer one)

1. **State a normative nesting bound in the specification**, so implementations agree by construction. Real-world encodings are shallow — the deepest RLP nesting across our entire fixture corpus is **7** — so a generous fixed bound costs nothing operationally.
2. **State explicitly that deep nesting is implementation-defined**, and that conformance is not claimed beyond some depth. Weaker, but honest, and it tells implementers they must choose their own bound.

## Notes

- This is an under-specification report about the reference's own determinism. It is not a claim that any particular implementation diverges from the reference — the divergence that motivated us (a fixed-buffer verifier rejecting at a hard depth cap, on constant-memory grounds) is deliberate and is not the subject of this report.
- The decoder library may prefer to scope this to itself; file there if so.

## References

- Upstream report context: Verified-zkEVM/evm-asm#13100
- Pinned decoder source (v0.1.6): https://github.com/ethereum/ethereum-rlp/blob/v0.1.6/src/ethereum_rlp/rlp.py
- Adjacent decoder issue (per-level payload copying, different concern): ethereum/ethereum-rlp#20

Contributor guide

Open the contributing guide

Research direction

Start with src/ethereum_rlp/rlp.py at decode, decode_to_sequence, and decode_joined_encodings, then review block use in src/ethereum/forks/amsterdam/blocks.py and the rejection path in forks/amsterdam/stateless.py. Reproduce the reported nested-input boundary in fresh processes and trace the specification's treatment of RLP depth. Done means the project adopts and documents a consistent normative or implementation-defined policy.

Written by the indexing model from the issue text.

Assessment

Tech stack
python
Domain
blockchain
Issue type
Documentation
Difficulty
5/5
Estimated time
Over a week
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.