--- eip: 8437 title: Proof Object Transport over devp2p description: Defines request-driven proof transfer over devp2p using authenticated chunks, bounded reassembly, and recovery. author: Marchhill (@Marchhill) discussions-to: status: Draft type: Standards Track category: Networking created: 2026-10-05 requires: 2718, 8288 --- ## Abstract This EIP defines the `lean/1` devp2p capability for discovering and retrieving [EIP-8288](./eip-8288.md) proof objects: mempool wrappers, block proofs and inclusion-list packages. Receivers request bounded sets of independently verifiable chunks and can resume retrieval from multiple peers. Transport integrity is kept separate from proof validity, and no consensus rule changes. ## Motivation Dependency witnesses and aggregated proofs can be much larger than ordinary transactions. Sending a whole object as one message occupies the connection until it has been written, resends bytes the receiver already has, and makes interrupted retrieval expensive. Unsolicited objects force receivers to allocate memory and schedule cryptographic work before deciding whether they need them. Small requested chunks bound storage, allow selective recovery, and let proof traffic interleave with other devp2p messages. A canonical commitment lets chunks from different peers be combined without accepting inconsistent bytes. Proof validity remains a separate check after reconstruction. ## Specification The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in [RFC 2119](https://www.rfc-editor.org/rfc/rfc2119) and [RFC 8174](https://www.rfc-editor.org/rfc/rfc8174). ### Scope and constants This EIP specifies transport only. [EIP-8288](./eip-8288.md) and the activated fork define dependencies, proof statements, transaction admission, block validity and inclusion-list obligations; successful transfer establishes none of them. | Name | Value | Meaning | | --- | --- | --- | | `CAPABILITY` | `lean/1` | Capability name and version | | `MESSAGE_COUNT` | `10` | Relative message IDs `0x00` through `0x09` | | `CHUNK_BYTES` | `65536` | Fixed object chunk size | | `MAX_OBJECT_BYTES` | `67108864` | Transport object ceiling, 64 MiB | | `MAX_MESSAGE_BYTES` | `131072` | Uncompressed capability message-data ceiling | | `MAX_RLP_DEPTH` | `8` | Maximum nesting of decoded transport lists | | `MAX_PROFILES` | `16` | Profile IDs in Status | | `MAX_ANNOUNCEMENTS` | `64` | Descriptors in one announcement | | `MAX_LOOKUPS` | `16` | Selectors in one GetObjects request | | `MAX_CHUNKS_PER_REQUEST` | `32` | Chunk indices in one GetChunks request | | `MAX_REQUESTS_PER_PEER` | `4` | Outstanding requests per peer, per direction | | `MAX_TXS_PER_OBJECT` | `4096` | Transaction entries in a kind-1 or kind-3 body | | `MAX_TXS_PER_REQUEST` | `16` | Transaction hashes in one GetTransactions request | | `MAX_TX_RESPONSE_BYTES` | `65536` | Transactions message ceiling | | `MAX_METADATA_RESPONSE_BYTES` | `65536` | Objects message ceiling | | `MAX_HEADER_SKELETON_BYTES` | `16384` | Encoded header skeleton ceiling | | `MAX_HEADER_FIELDS` | `64` | Fields in a header skeleton | | `MAX_REQUEST_IDLE_SECONDS` | `30` | Request expiry without useful progress | | `MAX_REQUEST_AGE_SECONDS` | `120` | Absolute request lifetime | | `MAX_ASSEMBLY_IDLE_SECONDS` | `30` | Assembly expiry without a newly verified chunk | | `MAX_ASSEMBLY_AGE_SECONDS` | `300` | Absolute assembly lifetime | These are wire limits, not validity limits. A client MAY enforce lower local limits and refuse work without penalizing the peer. A transport refusal MUST NOT be treated as an invalid transaction or block. ### Encoding Message data is canonical RLP with exactly the field counts specified. Integers use the shortest unsigned encoding, with zero as the empty string; stated widths are range bounds, not fixed-width fields. Leading zeros, extra fields, trailing bytes, nonminimal length prefixes and wrong list/string types are invalid. Hashes are exactly 32 bytes. Lists MUST be checked against their count bounds before allocating elements, and transport RLP nesting MUST NOT exceed `MAX_RLP_DEPTH`. Transaction envelopes and proofs are opaque byte strings at this layer, parsed afterward by their own bounded parsers. `H` is Keccak-256, not SHA3-256. It is distinct from EIP-8288's `DEPS_HASH`, which this EIP uses only through `get_deps_hash`. `RLP(x)` is canonical RLP. In commitment formulas, `U8`, `U32` and `U64` are fixed-width big-endian unsigned integers, `||` is byte concatenation, and a domain label is its ASCII bytes followed by one zero byte. [RLPx](https://github.com/ethereum/devp2p/blob/76cf0a141e8aa8616e3305738d51101945387d3c/rlpx.md) compression, encryption, message-ID multiplexing and authentication are unchanged. Implementations MUST check the advertised uncompressed size before decompression and reject messages exceeding `MAX_MESSAGE_BYTES`. Body bytes travel only in requested Chunk messages, one chunk per message; there is no whole-object message, and implementations MUST NOT rely on RLPx frame fragmentation to carry an object. ### Capability and profiles Peers negotiate `lean/1` through the RLPx Hello exchange. All ten message IDs are reserved even if an optional object kind is unsupported. A peer MUST NOT send any message other than Status before it has accepted the other peer's Status. A **profile ID** identifies the verification key for an object's recursive STARK: ```text profile_id = H("lean/1/profile\0" || AGGREGATED_VK) ``` `AGGREGATED_VK` is the [EIP-8288](./eip-8288.md) protocol-level verification key, as bytes, of the fork that applies to the object's context: the block's fork for kind 2, and the fork of the block the transactions are candidates for in kinds 1 and 3. A fork that changes `AGGREGATED_VK` yields a new profile ID; all other proof rules are those of EIP-8288. Clients derive acceptable profile IDs from the `AGGREGATED_VK` values in their local fork configuration, never from peer-supplied keys, so negotiation selects a known verifier and cannot introduce one. A **common profile** is one that is acceptable locally and listed in the peer's Status. The **STARK check** of a `stark_proof` against a dependency list `deps` follows the EIP-8288 block validity rule: if `deps` is empty, `stark_proof` MUST be the empty byte string; otherwise it MUST verify against `get_deps_hash(deps)` and the `AGGREGATED_VK` identified by `profile_id`. Status is sent exactly once in each direction: ```text Status (0x00) = [1, chain_id, genesis_hash, profiles, kinds, max_object_bytes] ``` - The first field is the Status version, `1`. - `chain_id` is an unsigned integer less than `2**256`. - `genesis_hash` is 32 bytes. - `profiles` is a nonempty, strictly ascending list of at most `MAX_PROFILES` 32-byte profile IDs. - `kinds` is a bit mask in which bit `kind - 1` advertises support for that kind. Bit zero MUST be set and bits above two MUST be zero. - `max_object_bytes` is an integer in `[1, MAX_OBJECT_BYTES]`: a local receive ceiling, not a promise to accept every smaller object. The chain ID and genesis hash MUST match local configuration, and there MUST be at least one common profile. A well-formed incompatible Status disables this capability; clients SHOULD keep other protocols on the connection. Receiving another message before the peer's Status, a second Status, or malformed fields is a protocol violation. ### Object kinds and canonical bodies An **object** is a descriptor and its canonical body, whose length is in `[1, MAX_OBJECT_BYTES]`. Throughout, a dependency is a 96-byte EIP-8288 triple, dependency lists are in `deduplicate_and_sort` order, and `dependencies(tx)` is as defined by EIP-8288. #### Kind 1: mempool wrapper The context is the empty list. The body is the EIP-8288 wrapper `[transactions, mode, content]` with this encoding: ```text RLP([transactions, mode, [deps, proof_content]]) transactions = [entry, ...] entry = [0, transaction_envelope] | [1, transaction_hash] deps = [dependency_bytes96, ...] proof_content = [proof, ...] if mode == 0 proof_content = stark_proof if mode == 1 ``` `transaction_envelope` is the exact [EIP-2718](./eip-2718.md) encoding, with a legacy transaction as its RLP, carried as an RLP byte string; its hash is `H(transaction_envelope)`. A hash entry carries exactly 32 bytes. The tag, not the length, distinguishes the two forms. `transactions` holds 1 to `MAX_TXS_PER_OBJECT` entries in strictly ascending transaction-hash order. Replacing a full entry with a hash entry produces a different object. `deps` MUST equal `deduplicate_and_sort` of the union of `dependencies(tx)` over all transactions. Mode 0 carries exactly one proof per dependency, in `deps` order. Mode 1 carries one `stark_proof`, which MUST pass the STARK check against `deps`. No other mode is valid. The wrapper is otherwise validated as specified by EIP-8288, including its per-wrapper dependency limits. Every hash entry MUST be resolved to its envelope before the wrapper is validated. An unresolved wrapper MAY be retained within a bounded recovery budget but MUST NOT be admitted, reaggregated or announced. Senders SHOULD use full entries unless the peer is known to hold the transaction. #### Kind 2: block-proof sidecar ```text context = [block_hash, block_number, transactions_root, block_deps_hash, skeleton_hash] body = RLP([stark_proof]) ``` `block_number` is a 64-bit unsigned integer; the other fields are 32-byte hashes. `stark_proof` is exactly the first element of the block's EIP-8288 `recursive_stark` header field. The one-element list keeps the body nonempty when `stark_proof` is empty. An Objects response carries the descriptor with a **header skeleton**: ```text skeleton = [proof_field_index, field_encodings] skeleton_hash = H("lean/1/skeleton\0" || RLP(skeleton)) ``` `field_encodings` lists at most `MAX_HEADER_FIELDS` byte strings, one per top-level field of the block's canonical header, in order. Each entry is the complete canonical RLP encoding of one field, including its prefix, except the entry at `proof_field_index`, which is the empty byte string. `proof_field_index` MUST be the position of `recursive_stark` in that fork's header. The encoded skeleton is at most `MAX_HEADER_SKELETON_BYTES`. Before requesting chunks, the receiver MUST check the skeleton against `skeleton_hash` and its field count, order, types, widths and values against the fork's header schema, parsing nested fields with depth and allocation bounds no larger than `MAX_RLP_DEPTH`. After retrieving the body, it reconstructs the header by replacing the placeholder with `RLP([stark_proof, block_deps_hash])`, concatenating all entries, and prefixing an RLP list header for the concatenated length; entries are not re-encoded as RLP strings. `H` of the reconstructed header MUST equal `block_hash`, and its block number, transactions root and `recursive_stark` dependency hash MUST equal the context. The skeleton itself MUST NOT be hashed, validated or stored as a header. Before a sidecar is used as a block proof, the receiver MUST obtain the block body through the existing block protocol and check that its transactions root matches the header, that `block_deps_hash == get_deps_hash(dependencies(block))`, and that `stark_proof` passes the STARK check against `dependencies(block)`. A header whose hash matches a peer-requested `block_hash` does not establish canonical-chain membership. This kind transfers the proof already committed by the header in bounded messages. It does not change header contents or hashing, ETH header responses or block-body availability, and does not permit processing a block without its proof. #### Kind 3: inclusion-list package The context is `[package_hash]`, where `package_hash = H(body)`. The body is the EIP-8288 FOCIL `[transactions, recursive_stark]`: ```text RLP([transactions, [stark_proof, deps_hash]]) transactions = [transaction_envelope, ...] ``` `transactions` holds at most `MAX_TXS_PER_OBJECT` full envelopes, encoded as in kind 1, in inclusion-list order; the transport neither sorts them nor replaces them with hashes. Let `dependencies(transactions)` be `deduplicate_and_sort` of the union of `dependencies(tx)` over `transactions`. The receiver MUST check that `deps_hash == get_deps_hash(dependencies(transactions))` and that `stark_proof` passes the STARK check against `dependencies(transactions)`. A package with an invalid proof or a `deps_hash` mismatch is invalid; its effect on inclusion obligations is defined by EIP-8288 and [EIP-7805](./eip-7805.md), whose eligibility and omission rules apply after reconstruction. This kind carries no signature, author or slot. Consensus-layer authentication and inclusion-list identifiers travel through the existing inclusion-list protocol, and a peer-supplied `package_hash` is only a lookup key. ### Descriptors and identity ```text descriptor = [kind, profile_id, context, byte_length, content_hash, chunk_root] content_hash = H(body) context_hash = H("lean/1/context\0" || RLP(context)) object_id = H("lean/1/object\0" || RLP(descriptor)) ``` `kind` is 1, 2 or 3 and `context` has that kind's form. `byte_length` is a 64-bit unsigned integer within the object bounds. An encoded descriptor is at most 512 bytes. A receiver MUST check a descriptor's encoding, context and geometry before accepting chunks for it. An **assembly** is a receiver's partial copy of an object. Clients key objects and assemblies by `object_id` and transfers by `(connection, request_id)`. Announcing a known descriptor MUST NOT create another assembly, and claims from different peers merge only when their descriptors are identical. ### Chunk commitments The body is split at fixed `CHUNK_BYTES` offsets without padding: ```text N = ceil(byte_length / CHUNK_BYTES) W = smallest power of two >= N depth = log2(W) S = U8(kind) || profile_id || context_hash || content_hash || U64(byte_length) || U32(N) chunk[i] = body[i * CHUNK_BYTES : min((i + 1) * CHUNK_BYTES, byte_length)] ``` Thus `1 <= N <= 1024` and `0 <= depth <= 10`. Every chunk except the last is exactly `CHUNK_BYTES`; the last is `byte_length - (N - 1) * CHUNK_BYTES` bytes. `N`, these lengths and `depth` are the object's **geometry**. The commitment is a perfect binary tree of `W` leaves: ```text leaf[i] = H("lean/1/leaf\0" || S || U32(i) || U32(len(chunk[i])) || chunk[i]) for i < N leaf[i] = H("lean/1/empty\0" || S || U32(i)) for N <= i < W parent = H("lean/1/node\0" || U8(level) || left || right) chunk_root = H("lean/1/root\0" || S || tree_root) ``` `level` is zero for parents of leaves and increases toward the root. For `W == 1`, `tree_root` is the single leaf. A **branch** lists exactly `depth` sibling hashes from the leaf level upward. At each `level`, bit `level` of `index` selects the order: zero hashes `(current, sibling)`, one hashes `(sibling, current)`. The result, under the root domain, MUST equal the descriptor's `chunk_root`, and `index` MUST be less than `N`. A sibling covering only padding leaves MUST equal the canonical empty subtree. After reconstruction the receiver MUST recompute the full canonical root and `content_hash`. ### Messages and request lifecycle IDs are relative to the capability's negotiated message offset: | ID | Message | Direction | | --- | --- | --- | | `0x00` | Status | Both | | `0x01` | AnnounceObjects | Both | | `0x02` | GetObjects | Request | | `0x03` | Objects | Response | | `0x04` | GetChunks | Request | | `0x05` | Chunk | Response | | `0x06` | Complete | Terminal GetChunks response | | `0x07` | Cancel | Requester to responder | | `0x08` | GetTransactions | Request | | `0x09` | Transactions | Response | Request IDs are 64-bit unsigned integers. On each connection a peer numbers its requests of all three types consecutively from one, never wrapping or reusing an ID; each direction has its own sequence, and responses echo the request's ID. A new incoming request whose ID is not one more than the previous incoming ID (or one, for the first) is a protocol violation, even if the request is refused. A requester MUST NOT have more than `MAX_REQUESTS_PER_PEER` requests outstanding; a responder MAY accept fewer and return Busy for the rest. **Useful progress** is a requested, previously absent, valid result; duplicate or malformed responses do not refresh timers. A request expires at its idle or absolute deadline, whichever comes first, and the responder MUST stop serving it by then. Disconnection releases all request state. Request expiry or cancellation does not reset an assembly's absolute age. #### AnnounceObjects (0x01) ```text [descriptor, ...] ``` The list holds 1 to `MAX_ANNOUNCEMENTS` descriptors in strictly ascending `object_id` order. A sender MUST announce only objects it holds in full and has validated as specified for their kind, including, for kind 2, the block itself. An announcement is an availability hint. Receivers need not store it, request the object, allocate its size or treat it as valid, and repeating it earns no credit or retention. Peers SHOULD suppress unchanged announcements and rotate bounded selections fairly across useful objects. #### GetObjects (0x02) and Objects (0x03) ```text GetObjects = [request_id, selectors] selector = [kind, profile_id, lookup_kind, lookup_key] Objects = [request_id, results] result = [status, descriptor_or_empty, auxiliary] ``` `selectors` holds 1 to `MAX_LOOKUPS` distinct entries in strictly ascending `(kind, profile_id, lookup_kind, lookup_key)` order. `lookup_key` is 32 bytes. `lookup_kind = 0` looks up the primary identity: the object ID for kind 1, the block hash for kind 2, or the package hash for kind 3. `lookup_kind = 1` is valid only for kind 1 and looks up a transaction hash whose full envelope is wanted. No other lookup kind is valid. For a transaction-hash lookup, the server returns any validated kind-1 object it holds under that profile that contains the transaction as a full entry; it need not build a new one. Once the returned body passes the integrity and encoding checks, the receiver MAY extract that entry, check its hash, and use it to resolve a hash entry in another wrapper. This neither requires validating nor permits admitting or relaying the returned wrapper. Clients MUST NOT chain hash recoveries to obtain the envelope. This lookup is the recovery route for envelopes too large for a Transactions message. Results correspond one-for-one to selectors. Statuses are `0 = OK`, `1 = Unavailable` (unknown or no longer retained), `2 = Busy` (local resource pressure; no later response is promised), `3 = Unsupported` (profile not common or kind not implemented), and `4 = TooLarge` (exceeds the requester's `max_object_bytes`). A non-OK result has empty byte strings in both remaining fields. For OK, `descriptor_or_empty` is a descriptor matching the selector and within the requester's ceiling, and `auxiliary` is the skeleton for kind 2 and empty otherwise. Receivers MUST check descriptors and skeletons before retaining them. Announcements do not carry skeletons, so a receiver obtains a kind-2 skeleton through GetObjects before requesting chunks, unless it already has it. The uncompressed Objects message is at most `MAX_METADATA_RESPONSE_BYTES`. OK results that would exceed it are returned as Busy; the responder MUST NOT omit or split results. The response is terminal. A request never obliges a peer to prove, fetch from other peers, retain history, or allocate an object it does not hold. #### GetChunks (0x04), Chunk (0x05), and Complete (0x06) ```text GetChunks = [request_id, object_id, indices] Chunk = [request_id, object_id, index, chunk_bytes, branch] Complete = [request_id, object_id, status] ``` The requester MUST already hold a well-formed descriptor for `object_id`. `indices` is a strictly ascending list of 1 to `MAX_CHUNKS_PER_REQUEST` 32-bit integers less than `N`. The request grants **credit** for exactly those chunks, at most 2 MiB of body data. The responder sends at most one Chunk per requested index, in request order, and nothing unrequested. It MAY stop early for unavailability or resource pressure but MUST then send Complete. Complete statuses are `0 = Served`, `1 = Unavailable`, `2 = Busy`, `3 = Unsupported`, `4 = Cancelled` and `5 = TooLarge`. Served means every requested chunk was written; it does not mean the receiver accepted anything. Complete is terminal and releases unused credit, and a request never expands to further indices. A Chunk is valid only for an outstanding request and a requested index, with the descriptor's chunk length and branch depth. The receiver MUST verify its geometry and branch before retaining its bytes. An identical duplicate MUST NOT be allocated again or refresh progress; a conflicting chunk is invalid even if another peer supplied a valid one. Served with chunks missing is a protocol violation unless the receiver discarded them locally; discarded chunks MAY be requested again. #### Cancel (0x07) ```text [request_id] ``` Cancel names a request originated by the sender of Cancel. The responder MUST stop scheduling work for it and, for a live GetChunks request, send Complete with Cancelled unless a Complete has already been written; a Chunk being written MAY finish first. For other requests, Cancel has no effect once the response is written. Cancels for unknown or completed requests are ignored. The requester keeps a bounded record of each live request until its terminal response or expiry. A response with an ID that was issued but is no longer live MUST be discarded before allocation or proof work and earns no credit; a response with ID zero or above the highest issued ID is invalid. This requires only the highest issued ID and the live-request map. Discarded messages still count toward framing, size and rate bounds. Cancellation does not withdraw any validity claim and MUST NOT discard chunks retained from other peers. #### GetTransactions (0x08) and Transactions (0x09) ```text GetTransactions = [request_id, transaction_hashes] Transactions = [request_id, results] result = [status, transaction_envelope_or_empty] ``` `transaction_hashes` is a strictly ascending list of 1 to `MAX_TXS_PER_REQUEST` hashes, each referenced by a retained wrapper being resolved. Clients MUST bound concurrent unresolved wrappers and requested hashes, and MUST NOT request hashes from any other source. Results correspond one-for-one to the hashes and use the Objects statuses. An OK value is a canonical envelope whose hash equals the requested hash; other statuses carry the empty byte string. The uncompressed Transactions message is at most `MAX_TX_RESPONSE_BYTES`: the responder returns Busy for entries that would exceed it and TooLarge for an envelope that cannot fit even with all other results empty. A requester MAY use the negotiated ETH protocol's transaction retrieval instead. Once all hash entries are resolved, the receiver validates the wrapper as kind 1 and applies normal transaction admission. Pool rejection, nonce changes and fee policy are not protocol violations, and an unavailable entry is not evidence of misbehavior. ### Reassembly, resume, and multiple peers Clients MUST keep at most one assembly per object ID, indexing verified chunks by index. Chunks may arrive in any order from any requests and peers, and resuming means requesting only missing indices. Announcing an object does not attribute existing chunks to the announcer. Before allocating or copying a chunk, a client MUST charge its bytes, branch, bookkeeping and pending verification work to finite per-peer and global budgets; a declared length MUST NOT trigger allocation of that length. Completed objects MAY be streamed into a bounded store; a completion copy MUST be reserved before copying and stay charged through validation and admission. Assemblies expire at their idle and absolute deadlines on a timer independent of incoming messages. Only a newly verified chunk refreshes the idle deadline; nothing extends the absolute one. Expiry releases incomplete bytes and metadata. Completed objects awaiting validation stay charged to finite budgets, including when moved between queues. Receivers SHOULD spread disjoint missing indices across useful peers, limiting duplicate requests and outstanding credit, and MAY retry stalled indices on another peer within the assembly's lifetime. Invalid data from one source MUST NOT cause valid chunks from other sources to be discarded, and a disconnected or unavailable peer does not invalidate the object. Source lists, announcement and duplicate caches, and retry schedules MUST be bounded, including under many peer identities. Peers MAY evict data and later answer Unavailable or Busy; completion is not guaranteed under withholding or insufficient capacity. ### Validation and relay With all chunks present, the receiver MUST check the length, `content_hash`, canonical `chunk_root` and canonical body encoding, then apply the kind's validation. Sender-supplied metadata MUST NOT substitute for these checks. A branch authenticates a chunk only to the descriptor's `chunk_root`, which is untrusted: neither a known block hash, a matching content hash nor RLPx peer authentication authenticates it. This EIP defines no producer signature or consensus commitment to `chunk_root`, so a client MUST NOT announce, serve, admit or reaggregate any part of an object until it has reconstructed and fully validated it. Proof validity and transaction admission are distinct. A wrapper with valid proofs whose transactions fail local pool policy MAY be retained as a proof object but MUST NOT be presented as admitted. No Complete status acknowledges admission or retention. ### Scheduling and errors Senders MUST apply backpressure before serializing chunks, MUST yield between Chunk messages so that queued messages of other protocols are serviced at the next message boundary, and MUST NOT queue a whole object as serialized messages. Implementations SHOULD use small write windows and schedule fairly across objects, peers and capabilities. Malformed encodings, bad branches, inconsistent geometry, mismatched transaction hashes, unrequested data and invalid proofs are invalid peer data: the client MUST discard the contribution and stop the associated transfer, and MAY disable `lean/1` for, disconnect or penalize the peer. Size violations MUST be rejected before large allocation or decompression. Unknown message IDs are protocol violations. Incompatibility, Unavailable, Busy, TooLarge, local queue exhaustion, cancellation, expiry and pool rejection of a validly proven transaction MUST NOT by themselves be treated as misbehavior. Verification and proving queues MUST have finite concurrency and backlog, and Busy results MUST NOT create unbounded retries. Clients SHOULD throttle sources that repeatedly send invalid data or consume credit without useful progress, while tolerating isolated stalls. ## Rationale Fixed chunks and domain-separated hashes give exactly one tree per descriptor, avoiding geometry negotiation, ambiguous padding and index aliasing. A small request window bounds retained data and permits recovery without unsolicited objects. Metadata lookups let a node request a known block's proof without waiting for an announcement. Deriving the profile ID from `AGGREGATED_VK` lets peers agree on a verifier without a registry: each client computes the IDs from its own fork configuration, a handshake cannot introduce a peer-supplied key, and a new key automatically gets a new ID. [EIP-8411](./eip-8411.md) authenticates its chunk root through a signed builder bid. Proof objects here have no such commitment, so branch verification provides integrity and multi-source retrieval, while relay waits for full validation. Request-driven transfer replaces rebroadcast of whole objects with explicit demand and leaves EIP-8288's aggregation cadence unchanged. Kind-2 sidecars carry proof bytes the header already commits to; the skeleton lets a client rebuild the canonical header from bounded messages. Removing the proof from the header would be a separate Core change. Kind 3 carries the EIP-8288 FOCIL unchanged. Its STARK covers the dependencies of all its transactions, so the receiver recomputes the dependency list from the transactions instead of trusting one supplied by the sender. ## Backwards Compatibility Clients without `lean/1` continue using their existing protocols. This EIP does not change RLPx, the ETH protocol, transaction or block validity, block hashing, Engine API encodings or inclusion-list obligations, and transport availability is not a consensus prerequisite. ## Test Cases The commitment vectors below use `kind = 1`, a zero profile ID, empty context, and `body[i] = i % 251`. They test the commitment only; the bodies are not valid wrappers. Hex strings omit `0x`. The branch is for the last chunk, from the leaf upward. ```json [ { "size": 1, "chunk_count": 1, "content_hash": "bc36789e7a1e281436464229828f817d6612f7b477d66591ff96a9e064bcc98a", "context_hash": "c0f2b9dd6c5fa856eedea5db76f1e33f1c263f5ebf391755b7d6cb2216100f3f", "last_leaf": "21738102e26669e761d6e6828f6e7642b560948e39e89efd18a05cf1ac9f9788", "padding_leaf": null, "chunk_root": "1deb89d1898605e8b1bbf8af186a1efeaa5d828f3587a30cf015ac684e692484", "object_id": "d39b2e3efc881ed2746601367bd3985a6707d29426ea14ed339ca992e8eb94e6", "last_index": 0, "branch": [] }, { "size": 65537, "chunk_count": 2, "content_hash": "4faa2deae4c869a3cddf91ae8646575699f5d568e57df4914b0536d59bf6a4c9", "context_hash": "c0f2b9dd6c5fa856eedea5db76f1e33f1c263f5ebf391755b7d6cb2216100f3f", "last_leaf": "c650f7216ac9af040a4d7186f70cd12d0e7c8183e72a42dbcb9af09a9f7451ec", "padding_leaf": null, "chunk_root": "ff33e4b630c69ecd67518e1213f5a648df50fc1b5c7a8592439e2f4c5ca21e7e", "object_id": "ee5cedb6e340e576b1d4b7d6be2002bbac911e1ee26db7a18587b7f8ee616028", "last_index": 1, "branch": [ "b17ef669920b42856d351293191ee28da75c15de4d483c535d333d86b05adaea" ] }, { "size": 131073, "chunk_count": 3, "content_hash": "35ae5a437a69fef01f453512998f4e90f1ba613a1d10962f80ebefcc836dbd09", "context_hash": "c0f2b9dd6c5fa856eedea5db76f1e33f1c263f5ebf391755b7d6cb2216100f3f", "last_leaf": "c733e3dfb0db3dd59d8b5e30b1a8efda92b67ac886c0835c47ccc7d5a169b0c3", "padding_leaf": "95da608e046232746cce65132a6525b04d1104a68ef4eb2cdb6787d2493baf7b", "chunk_root": "c648bf887bb5c49d3eba6a968f6e21507ea3ca0a61120aa16e7c980baf7434cd", "object_id": "f0d141fc8ee27459e661bd8459f51199967afdcb76b0155459b660954be94fe2", "last_index": 2, "branch": [ "95da608e046232746cce65132a6525b04d1104a68ef4eb2cdb6787d2493baf7b", "0d2a51ca27b23b2e78b66c81dd790fccc2fb1fe65e01a41c867f62798ee4d181" ] } ] ``` Message-data vectors, excluding the message ID and RLPx framing: | Message | Decoded value | Canonical hex | | --- | --- | --- | | GetChunks | request 7, the three-chunk object above, indices `[0, 2]` | `e507a0f0d141fc8ee27459e661bd8459f51199967afdcb76b0155459b660954be94fe2c28002` | | Cancel | request 7 | `c107` | | Kind-1 body | one full entry with envelope `7f00`, mode 0, empty `deps` and proofs | `cac5c480827f0080c2c0c0` | The last row tests the tagged encoding only; its transaction is invalid. Rejection cases include: zero-length bodies; objects above the negotiated ceiling; nonminimal RLP integers; duplicate or unsorted transaction hashes; untagged entries; dependencies that are not 96 bytes; a kind-1 `deps` that differs from the transactions' dependency union; oversized announcement or request lists; duplicate indices; indices equal to `N`; wrong final-chunk lengths; wrong branch depth; noncanonical padding; changed profile, context or content hash; chunks without outstanding credit; a nonempty `stark_proof` with an empty dependency list; and a kind-3 package whose `deps_hash` differs from `get_deps_hash(dependencies(transactions))` even though `stark_proof` verifies against `deps_hash`. Lifecycle cases cover interrupted resume, disjoint ranges from two peers, corrupt bytes from one peer alongside a healthy peer's chunks, cancellation during a write, duplicate chunks not extending idle time, absolute expiry despite new sources, expiry without incoming messages, completion-copy pressure, bounded verification Busy, hash-entry recovery, pool rejection after a valid proof, and control traffic between chunk writes. Kind-2 cases cover correct header reconstruction and hash, double RLP encoding of field entries, a misplaced or nonempty proof placeholder, a wrong field count or proof index, a wrong skeleton hash, a valid proof with a changed non-proof header field, oversized or deeply nested skeletons rejected before proof allocation, and a self-consistent descriptor that does not match the block's header or body. ## Security Considerations Announcements are untrusted. Peer identity, an object hash and a valid branch do not make a proof valid. Credit and finite memory and work budgets apply even to well-formed chunks, since many identities could otherwise fill storage with objects that never complete or validate. The commitment binds length, index, kind, profile and context; only full EIP-8288 verification binds the dependency claims. Relaying chunks against an unauthenticated root would amplify spam, which is why relay waits for full validation. Both the content hash and the canonical Merkle root are checked, so alternative trees or padding are not alternate encodings of an accepted object. Reassembly stays charged through verification and admission, including queue moves and disconnect races, and absolute deadlines stop slow streams from retaining memory. Arithmetic on counts, offsets, lengths and credit must be checked before allocation. Proof decoders need their own work and allocation bounds in addition to transport limits. Chunking lets messages interleave on a connection; it does not remove TCP head-of-line blocking, reduce proving work, ensure block availability or guarantee completion within a slot. An unavailable sidecar or an unsupported profile is not a consensus-invalidity condition. ## Copyright Copyright and related rights waived via [CC0](../LICENSE.md).