EIP.tools

EIP.tools

⚠️ DraftStandards Track: Networking

EIP-8437: Proof Object Transport over devp2p

Defines request-driven proof transfer over devp2p using authenticated chunks, bounded reassembly, and recovery.

Authors
Created2026-10-05
Requires

Markdown

https://raw.githubusercontent.com/ethereum/EIPs/re...
Pull Request#12423PR open

EIP-GPT summary

Contents
AbstractMotivationSpecificationScope and constantsEncodingCapability and profilesObject kinds and canonical bodiesKind 1: mempool wrapperKind 2: block-proof sidecarKind 3: inclusion-list packageDescriptors and identityChunk commitmentsMessages and request lifecycleAnnounceObjects (0x01)GetObjects (0x02) and Objects (0x03)GetChunks (0x04), Chunk (0x05), and Complete (0x06)Cancel (0x07)GetTransactions (0x08) and Transactions (0x09)Reassembly, resume, and multiple peersValidation and relayScheduling and errorsRationaleBackwards CompatibilityTest CasesSecurity ConsiderationsCopyright

Abstract

This EIP defines the lean/1 devp2p capability for discovering and retrieving EIP-8288 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 and RFC 8174.

Scope and constants

This EIP specifies transport only. EIP-8288 and the activated fork define dependencies, proof statements, transaction admission, block validity and inclusion-list obligations; successful transfer establishes none of them.

NameValueMeaning
CAPABILITYlean/1Capability name and version
MESSAGE_COUNT10Relative message IDs 0x00 through 0x09
CHUNK_BYTES65536Fixed object chunk size
MAX_OBJECT_BYTES67108864Transport object ceiling, 64 MiB
MAX_MESSAGE_BYTES131072Uncompressed capability message-data ceiling
MAX_RLP_DEPTH8Maximum nesting of decoded transport lists
MAX_PROFILES16Profile IDs in Status
MAX_ANNOUNCEMENTS64Descriptors in one announcement
MAX_LOOKUPS16Selectors in one GetObjects request
MAX_CHUNKS_PER_REQUEST32Chunk indices in one GetChunks request
MAX_REQUESTS_PER_PEER4Outstanding requests per peer, per direction
MAX_TXS_PER_OBJECT4096Transaction entries in a kind-1 or kind-3 body
MAX_TXS_PER_REQUEST16Transaction hashes in one GetTransactions request
MAX_TX_RESPONSE_BYTES65536Transactions message ceiling
MAX_METADATA_RESPONSE_BYTES65536Objects message ceiling
MAX_HEADER_SKELETON_BYTES16384Encoded header skeleton ceiling
MAX_HEADER_FIELDS64Fields in a header skeleton
MAX_REQUEST_IDLE_SECONDS30Request expiry without useful progress
MAX_REQUEST_AGE_SECONDS120Absolute request lifetime
MAX_ASSEMBLY_IDLE_SECONDS30Assembly expiry without a newly verified chunk
MAX_ASSEMBLY_AGE_SECONDS300Absolute 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 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:

profile_id = H("lean/1/profile\0" || AGGREGATED_VK)

AGGREGATED_VK is the EIP-8288 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:

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:

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 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

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:

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]:

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, 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

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:

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:

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:

IDMessageDirection
0x00StatusBoth
0x01AnnounceObjectsBoth
0x02GetObjectsRequest
0x03ObjectsResponse
0x04GetChunksRequest
0x05ChunkResponse
0x06CompleteTerminal GetChunks response
0x07CancelRequester to responder
0x08GetTransactionsRequest
0x09TransactionsResponse

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)

[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)

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)

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)

[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)

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 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.

[
  {
    "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:

MessageDecoded valueCanonical hex
GetChunksrequest 7, the three-chunk object above, indices [0, 2]e507a0f0d141fc8ee27459e661bd8459f51199967afdcb76b0155459b660954be94fe2c28002
Cancelrequest 7c107
Kind-1 bodyone full entry with envelope 7f00, mode 0, empty deps and proofscac5c480827f0080c2c0c0

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 and related rights waived via CC0.

EIP.tools

EIP.tools

Search, read, and map Ethereum improvement proposals, ERCs, RIPs, and CAIPs from one focused interface.

Farcaster
by @apoorveth