Loading .
Settings
1
1
2
1
2
1
1
1
2
2

New sharding design with tight beacon and shard block integration

Previous data shard construction: Add \(n=64\) data shards to the beacon chain. Each slot, \(n\) proposers independently propose their shard blobs, which are then confirmed by committees. Once a shard blob is confirmed (this can take several slots), it can be referenced by the execution chain.

Here is an alternative proposal: Add a new type of transaction that can contain additional sharded data as calldata. The block is then constructed by a single block builder (which can be different from the proposer using proposer builder separation), and includes normal transactions as well as transactions with sharded calldata. This is efficient because it means that tight integrations between rollups and L1 become possible, and it is expected that this “super-block-builder” strategy will emerge in practice anyway in order to maximize MEV extraction.

Advantages:

Disadvantages

Suggested Implementation

MAX_SHARDS = 2**8 # 256
SHARD_SIZE_FIELD_ELEMENTS = 2**12 # 2**12*31 = 126976 bytes
MAX_BLOCK_SIZE = SHARD_SIZE_FIELD_ELEMENTS * MAX_SHARDS # 2**20 field elements / 32,505,856 bytes
TARGET_BLOCK_SIZE = MAX_BLOCK_SIZE // 2 # EIP1559 for data gas
class ShardedBeaconBlockCommitment(Container):
    sharded_commitments: List[KZGCommitment, 2 * MAX_SHARDS]
    beacon_block_commitments: uint64      # Number of commitments occupied by Beacon block + Execution Payload

    # Aggregate degree proof for all sharded_commitments
    degree_proof: KZGCommitment
class ExecutionPayload(Container):
    # Execution block header fields
    parent_hash: Hash32
    fee_recipient: ExecutionAddress  # 'beneficiary' in the yellow paper
    state_root: Bytes32
    receipt_root: Bytes32  # 'receipts root' in the yellow paper
    logs_bloom: ByteVector[BYTES_PER_LOGS_BLOOM]
    random: Bytes32  # 'difficulty' in the yellow paper
    block_number: uint64  # 'number' in the yellow paper
    gas_limit: uint64
    gas_used: uint64
    timestamp: uint64
    extra_data: ByteList[MAX_EXTRA_DATA_BYTES]
    base_fee_per_gas: uint256

    # Extra payload fields
    block_hash: Hash32  # Hash of execution block
    transactions: List[Transaction, MAX_TRANSACTIONS_PER_PAYLOAD]
    
    # NEW fields for data gas market
    data_gas_limit: uint64
    data_gas_used: uint64
    data_base_fee_per_gas: uint256
@dataclass
class TransactionKZGCalldataPayload:
	chain_id: int = 0
	signer_nonce: int = 0
	max_priority_fee_per_gas: int = 0
	max_fee_per_gas: int = 0

    # New data gas
    max_priority_fee_per_data_gas: int = 0
	max_fee_per_data_gas: int = 0

    gas_limit: int = 0
	destination: int = 0
	amount: int = 0
	payload: bytes = bytes()
    
    # KZG Calldata
    kzg_calldata_commitments: List[KZGCommitment, MAX_SHARDS]
    
	access_list: List[Tuple[int, List[int]]] = field(default_factory=list)
	signature_y_parity: bool = False
	signature_r: int = 0
	signature_s: int = 0

Sampling

Block Builder Costs

Block builders get two new tasks. One is computing the KZG witnesses for the samples, and the other one is seeding the samples in the P2P network.

Sharded mempool

Rationale

The current mempool that most transactions go through on Ethereum does not scale to data transactions. Every node processes the full mempool, but with sharding the capacity of the chain increases to 1.3 MB/s, which is also the lower bound for the expected mempool bandwidth if all transactions go through it.

Clearly it makes no sense to shard data transactions on chain while requiring nodes to process the full mempool. We instead need to do data availability sampling on the mempool as well when data transactions are concerned.

Is a mempool still required?

An alternative to sending transactions to a public mempool is to send them to a block builder, which is effectively already happening now with Flashbots. The advantage is that block builders can promise not to front run transactions. If they do, then users can simply switch to another block builder.

Solana, as an example, already implements this design and by default only sends transactions to the next proposer.

It is therefore quite likely that the mempool will be much smaller in size and not even be required for the chain at all. However, we want to keep a mempool in the design for two reasons:

  1. The new censorship resistance design by Francesco requires a public mempool
    This does not contradict the earlier point that most transactions will be sent directly to a builder first. Basically users will mostly send their transactions to builders, and only if they get censored they will escalate to the mempool; the mempool becomes a “censored pool”
  2. An Ethereum user with a full node should have a way of sending transactions without configuring a central provider.
    I find this argument less convincing as the future will largely consist of rollups on L1, and end users using L2s. So end users should very rarely have to use the L1.

Construction

All transactions are broadcastet via a libp2p pubsub channel. For transaction type 3, the kzg_calldata_commitments are included, but not the actual underlying data.

We construct a second set of 512 pubsub channels that are only for propagating samples of shard data transactions. When someone wants to send a type 3 transaction to the mempool, they send the 512 samples of each kzg_calldata_commitment to these sample channels.