This EIP introduces Payload Chunking, a protocol upgrade that restructures Ethereum block propagation into self-contained execution chunks with separated Chunk Access Lists (CALs). The proposer propagates beacon and execution payload headers, followed by CALs and chunks as independent network messages. CALs contain state diffs from chunk execution, enabling parallel chunk validation: chunk N can be validated once CALs 0..N are available. After all chunks validate successfully, the consensus layer finalizes the block. This architecture transforms block validation from a monolithic operation into a streaming pipeline while preserving block atomicity. Chunks exist only as a propagation and validation construct; the canonical chain stores complete execution payloads.
Motivation
As Ethereum’s gas limit increases, block sizes and execution complexity grow correspondingly. The current monolithic block structure requires validators to download and decompress the entire execution payload before beginning execution, creating a bottleneck in the consensus timeline. This sequential dependency—download, decompress, then execute—becomes increasingly problematic as blocks grow larger.
This proposal addresses these constraints by:
Parallel propagation: Nodes propagate parts of the block separately, improving propagation speed through the network
Streaming validation: Execution begins as chunks arrive rather than after full payload receipt
Parallel chunk execution: CALs provide state diffs enabling independent validation of chunks
Bounded resources: Per-chunk gas limits (CHUNK_GAS_LIMIT = 2**24) bound memory and CPU per validation unit
Early rejection: Invalid blocks can be rejected at the first invalid chunk without processing remaining chunks
Proving compatibility: Chunk boundaries create natural proving units for future ZK proving systems
ChunkIndex=uint8# Chunk position within block (0 to MAX_CHUNKS_PER_BLOCK-1)
Root=Bytes32# 32-byte hash (Merkle root or block root)
Data Structures
Execution Chunk
# Chunk header containing execution commitments
classExecutionChunkHeader(Container):index:ChunkIndex# Position in block [0, MAX_CHUNKS_PER_BLOCK)
chunk_access_list_hash:Root# keccak256(RLP(ChunkAccessList)) per EIP-7928
pre_chunk_tx_count:uint32# Cumulative transaction count before this chunk
pre_chunk_gas_used:uint64# Cumulative gas used before this chunk
pre_chunk_blob_gas_used:uint64# Cumulative blob gas used before this chunk
txs_root:Root# Merkle root of chunk transactions
gas_used:uint64# Gas consumed by this chunk
blob_gas_used:uint64# Blob gas consumed by this chunk
withdrawals_root:Root# Merkle root of withdrawals (non-empty only in last chunk)
# Self-contained execution chunk
classExecutionChunk(Container):chunk_header:ExecutionChunkHeadertransactions:List[Transaction,MAX_TRANSACTIONS_PER_CHUNK]withdrawals:List[Withdrawal,MAX_WITHDRAWALS_PER_PAYLOAD]# Only in last chunk
Chunk Access Lists (CALs)
CALs are RLP-encoded structures following the Block Access List format from EIP-7928, scoped to individual chunks. Each CAL records state diffs produced by executing its corresponding chunk.
Type Definitions:
Address=Bytes20# Ethereum address
StorageKey=Bytes32# Storage slot key
StorageValue=Bytes32# Storage slot value
Balance=uint256# Account balance
Nonce=uint64# Account nonce
CodeData=bytes# Contract bytecode
TxIndex=uint16# Transaction index within the block
Change Structures:
StorageChange=[TxIndex,StorageValue]# [tx_index, new_value]
BalanceChange=[TxIndex,Balance]# [tx_index, post_balance]
NonceChange=[TxIndex,Nonce]# [tx_index, new_nonce]
CodeChange=[TxIndex,CodeData]# [tx_index, new_code]
SlotChanges=[StorageKey,List[StorageChange]]# All changes to a single storage slot
AccountChanges=[Address,# Account address
List[SlotChanges],# Storage writes
List[StorageKey],# Storage reads (slots read but not written)
List[BalanceChange],# Balance changes
List[NonceChange],# Nonce changes
List[CodeChange]# Code deployments
]ChunkAccessList=List[AccountChanges]
CAL Properties:
Incremental: Each CAL contains only the state diffs from its chunk. Changes from earlier chunks are not repeated.
Independent: CALs can be validated independently given the pre-state.
System Contract Entries:
First CAL (chunk 0): Includes pre-execution system writes (EIP-2935 block hash history, EIP-4788 beacon root)
Last CAL (chunk N-1): Includes post-execution system operations (EIP-4895 withdrawals, EIP-7002 and EIP-7251 validator operations)
ExecutionPayload and ExecutionPayloadHeader
The ExecutionPayload is no longer used and is replaced by ExecutionPayloadHeader.
The ExecutionPayloadHeader is modified to include number of chunks in a block:
classExecutionPayloadHeader(Container):# ... existing fields ...
chunk_count:uint8# Number of chunks in block
Beacon Block Commitments
The beacon block body replaces execution_payload with execution_payload_header and includes two Merkle roots committing to chunks and CALs:
classBeaconBlockBody(Container):# ... other existing fields ...
# Removed `execution_payload`
execution_payload_header:ExecutionPayloadHeader# Execution payload header
chunk_headers_root:Root# Commitment to all chunk headers
chunk_access_lists_root:Root# Commitment to all CAL hashes
Note: If EIP-7732 (ePBS) is enabled, then execution_payload field of ExecutionPayloadEnvelope is replaced by execution_payload_header in the same way.
Commitment construction:
# Chunk headers commitment (SSZ)
chunk_headers:List[ExecutionChunkHeader,MAX_CHUNKS_PER_BLOCK]chunk_headers_root=hash_tree_root(chunk_headers)# CAL commitment (SSZ hash of RLP-encoded CALs for CL inclusion proofs)
cal_hashes:List[Root,MAX_CHUNKS_PER_BLOCK]# Each Root = hash_tree_root(ByteList(RLP(CAL)))
chunk_access_lists_root=hash_tree_root(cal_hashes)
Note: The EL uses keccak256(RLP(CAL)) in chunk headers per EIP-7928. The CL uses SSZ hash_tree_root for inclusion proofs. Both reference the same RLP-encoded CAL data.
Network Topis
Chunks and CALs propagate as separate network message with SSZ Merkle inclusion proofs against the beacon block header.
classExecutionChunkMessage(Container):chunk:ExecutionChunk# The execution chunk
inclusion_proof:Vector[Bytes32,CHUNK_INCLUSION_PROOF_DEPTH]# SSZ Merkle proof against signed_block_header.message.body_root
signed_block_header:SignedBeaconBlockHeader# Signed beacon block header
classChunkAccessListMessage(Container):chunk_index:ChunkIndex# Chunk index this CAL corresponds to
chunk_access_list:ByteList[MAX_CAL_SIZE]# RLP-encoded chunk access list
inclusion_proof:Vector[Bytes32,CHUNK_INCLUSION_PROOF_DEPTH]# SSZ Merkle proof against signed_block_header.message.body_root
signed_block_header:SignedBeaconBlockHeader# Signed beacon block header
The signed_block_header provides the proposer signature for authentication and the beacon block root for proof verification.
Chunk Construction Rules
Block producers MUST follow these rules when constructing chunks:
Gas Bound: Each chunk MUST satisfy gas_used <= CHUNK_GAS_LIMIT
Minimal Chunking: For any two consecutive chunks i and i+1, their combined gas MUST exceed CHUNK_GAS_LIMIT. This ensures chunks cannot be trivially merged, preventing unnecessary fragmentation.
Transaction Atomicity: Transactions MUST NOT be split across chunks. Each transaction belongs entirely to one chunk.
Withdrawal Placement: Withdrawals MUST appear only in the final chunk. All other chunks have empty withdrawal lists.
Chunk Count Bound: A block MUST contain at most MAX_CHUNKS_PER_BLOCK chunks
Sequential Indexing: Chunks MUST be indexed sequentially from 0 to N-1 where N is the total chunk count
Network Protocol
Gossip Topics
Chunks and CALs propagate on dedicated gossip topics:
Topic
Payload
execution_chunk
ExecutionChunkMessage
chunk_access_list
ChunkAccessListMessage
Message Validation
Nodes MUST validate messages before forwarding:
ExecutionChunkMessage validation:
Verify signed_block_header.signature is valid for the proposer at the given slot
Nodes MUST subscribe to chunk and CAL topics. Messages may be validated immediately using the signed block header without waiting for the full beacon block.
Consensus Layer Changes
The consensus layer orchestrates chunk validation as beacon blocks, chunks, and CALs arrive from the network.
classStore:# ... existing fields ...
# Tracks received CALs per block
chunk_access_lists:Dict[Root,Set[ChunkIndex]]# Tracks received chunk headers per block
chunk_headers:Dict[Root,Dict[ChunkIndex,ExecutionChunkHeader]]# Tracks chunk execution results per block
chunk_execution_statuses:Dict[Root,Dict[ChunkIndex,ChunkExecutionStatus]]# Tracks final block validity
block_valid:Dict[Root,bool]
Three-Phase Validation
Phase 1: CAL Reception
CALs are forwarded to the execution layer upon receipt. The EL caches CALs for use in subsequent chunk validation.
defon_chunk_access_list_received(store:Store,cal_message:ChunkAccessListMessage)->None:# Derive beacon block root from signed header
root=hash_tree_root(cal_message.signed_block_header.message)chunk_index=cal_message.chunk_index# Validate cal message (proposer signature, inclusion proof, index bounds)
assertverify_chunk_access_list_message(cal_message)# Wait for beacon block to get execution payload header
block=wait_for_beacon_block(store,root)# Forward CAL to execution layer for caching
execution_engine.new_chunk_access_list(block.body.execution_payload_header.block_hash,chunk_index,cal_message.chunk_access_list)# Record CAL receipt and notify waiting chunk validations
store.chunk_access_lists[root].add(chunk_index)notify_cal_available(root,chunk_index)
Phase 2: Chunk Validation
Chunks are validated as they arrive, provided all prerequisite CALs (indices 0 through N for chunk N) are available.
defon_chunk_received(store:Store,chunk_message:ExecutionChunkMessage)->None:# Derive beacon block root from signed header
root=hash_tree_root(chunk_message.signed_block_header.message)chunk=chunk_message.chunkchunk_index=chunk.chunk_header.index# Validate chunk message (proposer signature, inclusion proof, index bounds, gas bounds, tx root)
assertverify_chunk_message(chunk_message)# Store chunk header for later finalization
store.chunk_headers[root][chunk_index]=chunk.chunk_header# Wait for all prerequisite CALs [0, chunk_index]
foriinrange(chunk_index+1):wait_for_cal(root,i)# Wait for beacon block to get execution payload header
block=wait_for_beacon_block(store,root)# Execute chunk via EL (EL has cached all required CALs)
execution_status=execution_engine.execute_chunk(block.body.execution_payload_header.block_hash,chunk)store.chunk_execution_statuses[root][chunk_index]=execution_status# Attempt block finalization if all chunks validated
maybe_finalize_block(store,root)
Phase 3: Block Finalization
Once all chunks have been validated, the block is finalized by verifying chunk chaining and aggregate consistency.
defmaybe_finalize_block(store:Store,root:Root)->None:ifrootinstore.block_valid:returnblock=store.blocks[root]payload_header=block.body.execution_payload_headerchunk_headers=store.chunk_headers.get(root,{})chunk_statuses=store.chunk_execution_statuses.get(root,{})# Verify sequential chunk coverage and validity
foriinrange(payload_header.chunk_count):ifinotinchunk_statuses:return# Gap in sequence
ifnotchunk_statuses[i].valid:store.block_valid[root]=Falsereturn# EL verifies chunk header chaining and final state root
finalize_result=execution_engine.finalize_block(payload_header.block_hash)store.block_valid[root]=finalize_result.valid
Attestation Rules
Validators MUST NOT attest to a block until:
All chunks have been received and individually validated (Phase 2 complete)
Block finalization has succeeded (Phase 3 complete)
ePBS Integration (EIP-7732): PTC (Payload Timeliness Committee) members attest to chunk and CAL data availability. PTC attestations are independent of execution validity—they confirm only that all messages were received within the timeliness window.
Execution Layer Changes
Chunk Execution
The execution layer validates chunks independently. For chunk N, the EL applies CALs 0..N-1 to the parent block state to reconstruct the chunk’s pre-state, then executes the chunk transactions. It uses CAL N to execute transactions in parallel (the same as in EIP-7928).
defel_execute_chunk(payload_header:ExecutionPayloadHeader,chunk:ExecutionChunk,cached_cals:Dict[ChunkIndex,ChunkAccessList])->PayloadStatusV1:"""Execute chunk using cached CALs to reconstruct pre-state."""chunk_index=chunk.chunk_header.index# Verify all prerequisite CALs are cached
foriinrange(chunk_index):ifinotincached_cals:returnPayloadStatusV1(status="INSUFFICIENT_INFORMATION",error=f"Missing CAL {i}, have {list(cached_cals.keys())}")# Verify chunk's own CAL is cached
ifchunk_indexnotincached_cals:returnPayloadStatusV1(status="INSUFFICIENT_INFORMATION",error=f"Missing CAL {chunk_index}")cal=cached_cals[chunk_index]# Get parent block state
parent_state=get_state(payload_header.parent_hash)ifparent_stateisNone:returnPayloadStatusV1(status="SYNCING")# Apply CALs 0..N-1 to reconstruct chunk pre-state
pre_state=parent_stateforiinrange(chunk_index):pre_state=apply_state_diffs(pre_state,cached_cals[i])# Verify CAL hash matches chunk header commitment (EIP-7928 format)
cal_hash=keccak256(cal)# CAL is RLP-encoded
ifcal_hash!=chunk.chunk_header.chunk_access_list_hash:returnPayloadStatusV1(status="INVALID",error="CAL hash mismatch")# Execute chunk transactions against pre-state
result=execute_transactions(payload_header,pre_state,chunk.transactions,cal)ifresult.error:returnPayloadStatusV1(status="INVALID",error=result.error)returnPayloadStatusV1(status="VALID")
Engine API
Four new Engine API methods support chunked validation:
engine_newBlockHeaderV1
Replaces engine_newPayloadV5. Informes EL that new block is available and passes block data, except CALs and chunks. Must be called first, as other calls depend on this data.
Parameters:
payload_header: ExecutionPayloadHeader
beaconRoot: Hash32
blobHashes: List[Hash32]
executionRequests: List[Bytes]
Returns: PayloadStatusV1
engine_newChunkAccessListV1
Caches a CAL for subsequent chunk execution. Requires block data to have been sent via engine_newBlockHeaderV1.
Parameters:
blockHash: Hash32 - The block hash
chunkIndex: ChunkIndex - Index of the chunk this CAL corresponds to
chunkAccessList: ChunkAccessList - RLP-encoded chunk access list
Returns:PayloadStatusV1
engine_executeChunkV1
Executes and validates a chunk. Requires block data to have been sent via engine_newBlockHeaderV1, and all prerequisite CALs (indices 0 through N) to have been sent via engine_newChunkAccessListV1.
Parameters:
blockHash: Hash32 - The block hash
chunk: ExecutionChunk - The chunk to execute
Returns:PayloadStatusV1
engine_finalizeBlockV1
Finalizes block validation after all chunks have been executed. Verifies:
Final state root matches payload_header.state_root
All block header fields are valid
Parameters:
blockHash: Hash32 - Hash of the execution payload
Returns:PayloadStatusV1
Response Types
All new Engine API methods return PayloadStatusV1 type, but status field has different meaning and values depending on the method. Following table defines all possible values and when they can be used.
Status
Semantics
newBlockHeader
newChunkAccessList
executeChunk
finalizeBlock
ACCEPTED
BlockHeader/CAL accepted successfully
✅
✅
VALID
Chunk/block executed correctly
✅
✅
INSUFFICIENT_INFORMATION
Missing prerequisite CALs
✅
INVALID
Execution or validation failed
✅
✅
✅
✅
SYNCING
Parent state not available (node syncing)
✅
✅
✅
✅
The error field is present if status is INSUFFICIENT_INFORMATION or INVALID.
Rationale
Incremental CALs
Chunk N applies CALs 0..N-1 to the parent state to reconstruct its pre-state. This incremental approach provides:
Parallel Execution: Multiple chunks can execute concurrently once their prerequisite CALs arrive. Alternative designs where each CAL contains cumulative state diffs would cause CAL sizes to grow linearly with chunk count, creating worst-case bandwidth issues.
Early Rejection: Invalid chunks cause immediate block rejection without processing subsequent chunks.
Bounded Resources: Each chunk validation is independently bounded by CHUNK_GAS_LIMIT.
CL-Driven Architecture
The consensus layer orchestrates chunk execution because: