区块级访问列表

该EIP提出了区块级访问列表(Block-Level Access Lists, BALs),记录区块执行期间所有账户和存储位置的访问及其执行后值。BALs旨在通过在区块头中包含其哈希值并在执行负载中传输列表,实现并行磁盘读取、并行事务验证、并行状态根计算和无执行状态更新,从而提高以太坊的可扩展性和效率。

摘要

本 EIP 引入了区块级访问列表(Block-Level Access Lists, BALs),记录了区块执行期间访问的所有账户和存储位置,以及它们执行后的值。BALs 能够实现并行磁盘读取、并行交易验证、并行状态根计算和无需执行的状态更新。

动机

在不提前知道将访问哪些地址和存储槽的情况下,交易执行无法并行化。虽然 EIP-2930 引入了可选的交易访问列表,但它们并未强制执行。

本提案在区块级别强制执行访问列表,从而实现:

  • 并行磁盘读取和交易执行
  • 并行后状态根计算
  • 无需执行交易即可重建状态
  • 执行时间缩短至 并行 IO + 并行 EVM

规范

区块结构修改

我们在区块头中引入了一个新字段 block_access_list_hash,其中包含 RLP 编码的区块访问列表的 Keccak-256 哈希值。当没有状态更改时,此字段是空 RLP 列表 0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347 的哈希值,即 keccak256(rlp.encode([]))

class Header:
    # Existing fields
    ...

    block_access_list_hash: Hash32 = keccak256(rlp.encode(block_access_list))

BlockAccessList 不包含在区块体中。EL 单独存储 BALs,并通过 Engine API 将它们作为 ExecutionPayload 中的一个字段进行传输。BAL 被 RLP 编码为 AccountChanges 列表。当没有状态更改时,此字段是空 RLP 列表 0xc0,即 rlp.encode([])

RLP 数据结构

BALs 使用 RLP 编码,遵循以下模式:address -> field -> block_access_index -> change

## Type aliases for RLP encoding
Address = bytes20  # 20-byte Ethereum address
StorageKey = uint256  # Storage slot key
StorageValue = uint256  # Storage value
Bytecode = bytes  # Variable-length contract bytecode
BlockAccessIndex = uint16  # Block access index (0 for pre-execution, 1..n for transactions, n+1 for post-execution)
Balance = uint256  # Post-transaction balance in wei
Nonce = uint64  # Account nonce

## Core change structures (RLP encoded as lists)
## StorageChange: [block_access_index, new_value]
StorageChange = [BlockAccessIndex, StorageValue]

## BalanceChange: [block_access_index, post_balance]
BalanceChange = [BlockAccessIndex, Balance]

## NonceChange: [block_access_index, new_nonce]
NonceChange = [BlockAccessIndex, Nonce]

## CodeChange: [block_access_index, new_code]
CodeChange = [BlockAccessIndex, Bytecode]

## SlotChanges: [slot, [changes]]
## All changes to a single storage slot
SlotChanges = [StorageKey, List[StorageChange]]

## AccountChanges: [address, storage_changes, storage_reads, balance_changes, nonce_changes, code_changes]
## All changes for a single account, grouped by field type
AccountChanges = [
    Address,                    # address
    List[SlotChanges],          # storage_changes (slot -> [block_access_index -> new_value])
    List[StorageKey],           # storage_reads (read-only storage keys)
    List[BalanceChange],        # balance_changes ([block_access_index -> post_balance])
    List[NonceChange],          # nonce_changes ([block_access_index -> new_nonce])
    List[CodeChange]            # code_changes ([block_access_index -> new_code])
]

## BlockAccessList: List of AccountChanges
BlockAccessList = List[AccountChanges]

范围和包含

BlockAccessList 是区块执行期间访问的所有地址的集合。

必须包含:

  • 发生状态更改(存储、余额、nonce 或代码)的地址。
  • 未发生状态更改但被访问的地址,包括:

    • BALANCEEXTCODESIZEEXTCODECOPYEXTCODEHASH 操作码的目标
    • CALLCALLCODEDELEGATECALLSTATICCALL 的目标(即使它们回滚;请参阅状态访问前的 Gas 验证以了解包含条件)
    • CREATE/CREATE2 的目标地址,如果目标账户被访问
    • 从带有 initcode 的调用部署到空地址的合约地址(例如,使用 initcode 调用 0x0
    • 交易发送方和接收方地址(即使是零值转账)
    • 如果区块包含交易或提款到 COINBASE 地址,则包含 COINBASE 地址
    • SELFDESTRUCT 的受益人地址
    • 预/后执行期间访问的系统合约地址;系统调用者地址 SYSTEM_ADDRESS (0xfffffffffffffffffffffffffffffffffffffffe) 不得包含,除非它本身经历状态访问
    • 提款接收方地址,无论提款金额是否非零
    • 被调用或访问的预编译合约

未发生状态更改的地址必须仍以空更改列表的形式存在。

EIP-2930 访问列表中的条目不得自动包含。只有在执行期间实际接触或更改的地址和存储槽才会被记录。

区块访问列表大小限制

区块访问列表受区块 Gas 限制而非固定最大项目数的约束。约束定义如下:

bal_items <= block_gas_limit // ITEM_COST

其中:

  • bal_items = storage_keys + addresses
  • ITEM_COST = 2000

storage_keys 是所有账户中存储键的总数,addresses 是区块中访问的唯一地址总数。根据 EIP-7981,向 BAL 添加项目的最便宜方式是冷 SLOAD,成本为 COLD_SLOAD_COST (2100,如 EIP-2929 中定义)。ITEM_COST 被刻意设置低于此最小值,以创建大约 block_gas_limit / 42000 个额外项目的缓冲区,该缓冲区吸收来自系统合约执行(例如,EIP-2935EIP-4788EIP-7002EIP-7251)和提款接收方(EIP-4895)的 BAL 条目,这些条目不消耗区块 Gas。

状态访问前的 Gas 验证

状态访问操作码执行 Gas 验证分两个阶段:

  • 前状态验证:无需状态访问即可确定的 Gas 成本(内存扩展、基本操作码成本、冷/热访问成本)
  • 后状态验证:需要状态访问的 Gas 成本(账户存在性、EIP-7702 委托解析)

前状态验证必须在任何状态访问发生之前通过。如果前状态验证失败,则目标资源(地址或存储槽)永远不会被访问,并且不得包含在 BAL 中。

一旦前状态验证通过,目标就会被访问并包含在 BAL 中。然后计算后状态成本;它们的顺序是实现定义的,因为目标已经已被访问。

下表指定了除基本操作码成本之外的前状态验证成本(Gas 常量如 EIP-2929 中定义):

指令 前状态验证
BALANCE access_cost
SELFBALANCE 无(访问当前合约,始终为热)
EXTCODESIZE access_cost
EXTCODEHASH access_cost
EXTCODECOPY access_cost + memory_expansion
CALL access_cost + memory_expansion + GAS_CALL_VALUE (如果 value > 0)
CALLCODE access_cost + memory_expansion + GAS_CALL_VALUE (如果 value > 0)
DELEGATECALL access_cost + memory_expansion
STATICCALL access_cost + memory_expansion
CREATE memory_expansion + INITCODE_WORD_COST + GAS_CREATE
CREATE2 memory_expansion + INITCODE_WORD_COST + GAS_KECCAK256_WORD + GAS_CREATE
SLOAD access_cost
SSTORE 超过 GAS_CALL_STIPEND 可用
SELFDESTRUCT GAS_SELF_DESTRUCT + access_cost

其中:

  • access_cost:对于账户访问操作码:如果是冷访问则为 COLD_ACCOUNT_ACCESS_COST,如果是热访问则为 WARM_STORAGE_READ_COST。对于存储访问操作码(SLOAD):如果是冷访问则为 COLD_SLOAD_COST,如果是热访问则为 WARM_STORAGE_READ_COST
  • memory_expansion:扩展输入/输出区域内存的 Gas 成本

后状态成本(例如,对空账户调用的 GAS_NEW_ACCOUNT,如果受益人不存在则为 GAS_SELF_DESTRUCT_NEW_ACCOUNT)不影响 BAL 的包含,因为目标已经已被访问。

EIP-7702 委托

当调用目标具有 EIP-7702 委托时,会访问目标以解析委托。如果存在委托,则委托地址在访问之前需要进行自己的 access_cost 检查。如果此检查失败,委托地址不得出现在 BAL 中,但原始调用目标将被包含(因为已访问以解析委托)。

注意:委托账户不能为空,因此在解析委托时 GAS_NEW_ACCOUNT 永远不适用。

SSTORE

SSTORE 会隐式读取当前的存储值以进行 Gas 计算。GAS_CALL_STIPEND 检查可防止在调用津贴内操作时发生此状态访问。如果 SSTORE 未通过此检查,则存储槽不得出现在 storage_readsstorage_changes 中。

排序、唯一性和确定性

必须应用以下排序规则:

  • 账户:按地址的字典序
  • storage_changes:槽位按存储键的字典序;每个槽位内,更改按区块访问索引(升序)
  • storage_reads:按存储键的字典序
  • balance_changes, nonce_changes, code_changes:按区块访问索引(升序)

必须满足以下唯一性约束:

  • 每个地址必须BlockAccessList 中恰好出现一次。
  • 每个存储键必须在每个账户的 storage_changes 中最多出现一次。
  • 每个存储键必须在每个账户的 storage_reads 中最多出现一次。
  • 存储键不得在同一账户的 storage_changesstorage_reads 中同时出现。
  • 每个 block_access_index 必须在每个更改列表(balance_changesnonce_changescode_changes 和每个槽位的 StorageChange 列表)中最多出现一次。

BlockAccessIndex 分配

BlockAccessIndex必须按以下方式分配:

  • 0 用于预执行系统合约调用。
  • 1 … n 用于交易(按区块顺序)。
  • n + 1 用于后执行系统合约调用。

按更改类型记录语义

存储
  • 写入包括

    • 任何值更改(后值 ≠ 前值)。
    • 将槽位归零(前值存在,后值为零)。
  • 读取包括

    • 通过 SLOAD 访问但未写入的槽位。
    • 写入但值未更改的槽位(即,SSTORE 中后值等于前值,也称为“无操作写入”)。

注意:实现必须检查交易前的值,以正确区分实际写入和无操作写入。

余额 (balance_changes)

记录以下账户的交易后余额 (uint256):

  • 交易发送方 (gas + value)。
  • 交易接收方(仅当 value > 0 时)。
  • CALL/CALLCODE 发送方 (value)。
  • CALL/CALLCODE 接收方(仅当 value > 0 时)。
  • CREATE/CREATE2 接收方(仅当 value > 0 时)。
  • COINBASE(奖励 + 费用)。
  • SELFDESTRUCT/SENDALL 受益人。
  • 提款接收方(系统提款,EIP-4895)。

对于未更改的账户余额:

如果账户余额在交易期间发生更改,但其交易后余额等于其交易前余额,则不得将该更改记录在 balance_changes 中。发送方和接收方地址必须包含在 AccountChanges 中。

以下特殊情况要求在没有其他状态更改时包含带有空更改的地址:

  • 零值转账接收方
  • 对交易前余额为零的地址进行同交易 SELFDESTRUCT 调用

零值区块奖励接收方不得在区块访问列表中触发余额更改,也不得导致接收方地址作为读取(例如,无更改)被包含。零值区块奖励接收方必须仅在奖励大于零的区块中包含余额更改。

代码

跟踪已部署或修改合约的交易后运行时字节码,以及 EIP-7702 中定义的成功委托的委托指示符

Nonce

记录以下账户的交易后 nonce

  • EOA 发送方。
  • 执行成功 CREATECREATE2 的合约。
  • 已部署合约。
  • EIP-7702 授权方。

边缘情况 (规范性)

  • COINBASE / 费用接收方:COINBASE 地址必须在发生任何状态更改时被包含。在没有交易的区块中,如果不存在其他状态更改(例如,来自 EIP-4895 提款),则不得包含它。如果 COINBASE 奖励为零,则 COINBASE 地址必须作为读取包含。
  • 预编译合约:预编译合约在被访问时必须包含。如果预编译合约接收值,则会记录余额更改。否则,它将包含空更改列表。
  • SENDALL:对于正值自毁,发送方和受益人将记录余额更改。
  • SELFDESTRUCT (交易内):在交易内销毁的账户必须包含在 AccountChanges 中,不带 nonce 或代码更改。但是,如果账户在交易前有正余额,则必须记录余额更改为零。自毁合约中被修改或读取的存储键必须包含为 storage_reads 条目。
  • 已访问但未更改:包含带有空更改的地址(例如,EXTCODEHASHEXTCODESIZEBALANCESTATICCALL 等的目标)。
  • 零值转账 / 交易中余额未更改:包含地址;从 balance_changes 中省略。
  • Gas 退款:记录每个交易后发送方的最终余额。
  • 区块奖励:记录每个交易后费用接收方的最终余额。
  • 异常中止:记录每个交易后发送方的最终 nonce 和余额,以及费用接收方的最终余额。回滚调用的状态更改将被丢弃,但所有访问的地址必须包含。如果没有剩余更改,地址将包含空列表;如果读取了存储,则相应的键必须出现在 storage_reads 中。
  • 预执行系统合约调用:所有状态更改必须使用 block_access_index = 0
  • 后执行系统合约调用:所有状态更改必须使用 block_access_index = len(transactions) + 1
  • EIP-7702 委托:在任何成功的委托设置、更新或清除之后,授权地址必须包含 nonce 和代码更改。如果在授权地址已加载并添加到 accessed_addresses(根据 EIP-2929)后授权失败,它必须仍以空更改集包含;如果在授权方加载之前授权失败,它不得包含。委托目标不得在委托创建或修改期间包含,并且必须仅在它实际作为执行目标加载时包含(例如,通过 CALL/CALLCODE/DELEGATECALL/STATICCALL 在授权执行下)。
  • EIP-4895 (共识层提款):接收方将记录其提款后的最终余额。
  • EIP-2935 (区块哈希):记录 ring buffer 中单个更新存储槽的系统合约存储差异。
  • EIP-4788 (信标根):记录 ring buffer 中两个更新存储槽的系统合约存储差异。
  • EIP-7002 (提款):在出队调用后,记录存储槽 0-3(4 个槽)的系统合约存储差异。出队还会读取最多 3 × MAX_WITHDRAWAL_REQUESTS_PER_BLOCK 个队列数据槽(从槽 4 开始),这些槽将显示为 storage_reads
  • EIP-7251 (合并):在出队调用后,记录存储槽 0-3(4 个槽)的系统合约存储差异。出队还会读取最多 4 × MAX_CONSOLIDATION_REQUESTS_PER_BLOCK 个队列数据槽(从槽 4 开始),这些槽将显示为 storage_reads

Engine API

Engine API 通过新的结构和方法进行扩展,以支持区块级访问列表:

ExecutionPayloadV4 扩展了 ExecutionPayloadV3,增加了:

  • blockAccessList: RLP 编码的区块访问列表

engine_newPayloadV5 验证执行负载:

  • 接受 ExecutionPayloadV4 结构
  • 验证计算的访问列表是否与提供的 blockAccessList 匹配
  • 如果访问列表格式错误或不匹配,则返回 INVALID

engine_getPayloadV6 构建执行负载:

  • 返回 ExecutionPayloadV4 结构
  • 在交易执行期间收集所有账户访问和状态更改
  • 使用 RLP 编码的访问列表填充 blockAccessList 字段

区块处理流程:

处理区块时:

  1. EL 在 ExecutionPayload 中接收 BAL
  2. EL 计算 block_access_list_hash = keccak256(blockAccessList) 并将其包含在区块头中
  3. EL 执行区块并生成实际的 BAL
  4. 如果生成的 BAL 与提供的 BAL 不匹配,则区块无效(头部中的哈希值将错误)

执行层通过 Engine API 向共识层提供 RLP 编码的 blockAccessList。共识层随后计算用于 ExecutionPayload 中存储的 SSZ hash_tree_root

历史 BALs 的检索方法

  • engine_getPayloadBodiesByHashV2:返回包含交易、提款和 blockAccessListExecutionPayloadBodyV2 对象
  • engine_getPayloadBodiesByRangeV2:返回包含交易、提款和 blockAccessListExecutionPayloadBodyV2 对象

blockAccessList 字段包含 RLP 编码的 BAL,或者对于阿姆斯特丹之前的区块或数据已被修剪时为 null

EL 必须至少保留 BALs 弱主观性周期 (=3533 epochs) 的持续时间,以支持离线时间少于 WSP 后的重新执行同步。

状态转换函数

状态转换函数必须验证提供的 BAL 与实际状态访问相匹配。

实现说明:BAL 本身不需要进入状态转换函数。实现可以通过在执行期间生成一个虚拟 BAL,对其进行哈希,并与头部中的 block_access_list_hash 进行比较来验证。这是 execution-specs 参考实现中采用的方法。

def validate_block(execution_payload, block_header):
    # 1. Compute hash from received BAL and set in header
    block_header.block_access_list_hash = keccak(execution_payload.blockAccessList)

    # 2. Execute block and collect actual accesses
    actual_bal = execute_and_collect_accesses(execution_payload)

    # 3. Verify actual execution matches provided BAL
    # If this fails, the block is invalid (the hash in the header would be wrong)
    assert rlp.encode(actual_bal) == execution_payload.blockAccessList

def execute_and_collect_accesses(block):
    """Execute block and collect all state accesses into BAL format"""
    accesses = {}

    # Pre-execution system contracts (block_access_index = 0)
    track_system_contracts_pre(block, accesses, block_access_index=0)

    # Execute transactions (block_access_index = 1..n)
    for i, tx in enumerate(block.transactions):
        execute_transaction(tx)
        track_state_changes(tx, accesses, block_access_index=i+1)

    # Withdrawals and post-execution (block_access_index = len(txs) + 1)
    post_index = len(block.transactions) + 1
    for withdrawal in block.withdrawals:
        apply_withdrawal(withdrawal)
        track_balance_change(withdrawal.address, accesses, post_index)
    track_system_contracts_post(block, accesses, post_index)

    # Convert to BAL format and sort
    return build_bal(accesses)

def track_state_changes(tx, accesses, block_access_index):
    """Track all state changes from a transaction"""
    for addr in get_touched_addresses(tx):
        if addr not in accesses:
            accesses[addr] = {
                'storage_writes': {},  # slot -> [(index, value)]
                'storage_reads': set(),
                'balance_changes': [],
                'nonce_changes': [],
                'code_changes': []
            }

        # Track storage changes
        for slot, value in get_storage_writes(addr).items():
            if slot not in accesses[addr]['storage_writes']:
                accesses[addr]['storage_writes'][slot] = []
            accesses[addr]['storage_writes'][slot].append((block_access_index, value))

        # Track reads (slots accessed but not written)
        for slot in get_storage_reads(addr):
            if slot not in accesses[addr]['storage_writes']:
                accesses[addr]['storage_reads'].add(slot)

        # Track balance, nonce, code changes
        if balance_changed(addr):
            accesses[addr]['balance_changes'].append((block_access_index, get_balance(addr)))
        if nonce_changed(addr):
            accesses[addr]['nonce_changes'].append((block_access_index, get_nonce(addr)))
        if code_changed(addr):
            accesses[addr]['code_changes'].append((block_access_index, get_code(addr)))

def build_bal(accesses):
    """Convert collected accesses to BAL format"""
    bal = []
    for addr in sorted(accesses.keys()):  # Sort addresses lexicographically
        data = accesses[addr]

        # Format storage changes: [slot, [[index, value], ...]]
        storage_changes = [[slot, sorted(changes)] 
                          for slot, changes in sorted(data['storage_writes'].items())]

        # Account entry: [address, storage_changes, reads, balance_changes, nonce_changes, code_changes]
        bal.append([
            addr,
            storage_changes,
            sorted(list(data['storage_reads'])),
            sorted(data['balance_changes']),
            sorted(data['nonce_changes']),
            sorted(data['code_changes'])
        ])

    return bal

BAL 必须是完整且准确的。缺少或虚假的条目会使区块无效。虚假条目可以通过验证 BAL 索引来检测,这些索引不得高于 len(transactions) + 1

如果任何交易超过声明的状态,客户端可以立即使其无效。

客户端必须将 BALs 与区块分开存储,并通过 Engine API 提供它们。

具体示例

示例区块:

预执行:

  • EIP-2935:在区块哈希合约 (0x0000F90827F1C53a10cb7A02335B175320002935) 存储父哈希
  • EIP-7002:为简化而省略。

交易:

  1. Alice (0xaaaa...) 向Bob (0xbbbb...) 发送 1 ETH,检查 0x2222... 的余额
  2. 查理 (0xcccc...) 调用工厂 (0xffff...) 在 0xdddd... 部署合约

后执行:

  • 向夏娃 (0xabcd...) 提款 100 ETH
  • EIP-7002 和 EIP-7251 为简化而省略。

注意:预执行系统合约使用 block_access_index = 0。后执行提款使用 block_access_index = 3 (len(transactions) + 1)。

生成的 BAL (RLP 结构):

[
    # Addresses are sorted lexicographically
    [ # AccountChanges for 0x0000F90827F1C53a10cb7A02335B175320002935 (Block hash contract)
        0x0000F90827F1C53a10cb7A02335B175320002935,
        [ # storage_changes
            [b'\x00...\x0f\xa0', [[0, b'...']]]  # slot, [[block_access_index, parent_hash]]
        ],
        [],  # storage_reads
        [],  # balance_changes
        [],  # nonce_changes
        []   # code_changes
    ],
    [ # AccountChanges for 0x2222... (Address checked by Alice)
        0x2222...,
        [],  # storage_changes
        [],  # storage_reads
        [],  # balance_changes (no change, just checked)
        [],  # nonce_changes
        []   # code_changes
    ],
    [ # AccountChanges for 0xaaaa... (Alice - sender tx 0)
        0xaaaa...,
        [],  # storage_changes
        [],  # storage_reads
        [[1, 0x...29a241a]],  # balance_changes: [[block_access_index, post_balance]]
        [[1, 10]],  # nonce_changes: [[block_access_index, new_nonce]]
        []  # code_changes
    ],
    [ # AccountChanges for 0xabcd... (Eve - withdrawal recipient)
        0xabcd...,
        [],  # storage_changes
        [],  # storage_reads
        [[3, 0x...5f5e100]],  # balance_changes: 100 ETH withdrawal
        [],  # nonce_changes
        []   # code_changes
    ],
    [ # AccountChanges for 0xbbbb... (Bob - recipient tx 0)
        0xbbbb...,
        [],  # storage_changes
        [],  # storage_reads
        [[1, 0x...b9aca00]],  # balance_changes: +1 ETH
        [],  # nonce_changes
        []   # code_changes
    ],
    [ # AccountChanges for 0xcccc... (Charlie - sender tx 1)
        0xcccc...,
        [],  # storage_changes
        [],  # storage_reads
        [[2, 0x...bc16d67]],  # balance_changes: after gas
        [[2, 5]],  # nonce_changes
        []  # code_changes
    ],
    [ # AccountChanges for 0xdddd... (Deployed contract)
        0xdddd...,
        [],  # storage_changes
        [],  # storage_reads
        [],  # balance_changes
        [[2, 1]],  # nonce_changes: new contract nonce
        [[2, b'\x60\x80\x60\x40...']]  # code_changes: deployed bytecode
    ],
    [ # AccountChanges for 0xeeee... (COINBASE)
        0xeeee...,
        [],  # storage_changes
        [],  # storage_reads
        [[1, 0x...05f5e1], [2, 0x...0bebc2]],  # balance_changes: after tx fees
        [],  # nonce_changes
        []   # code_changes
    ],
    [ # AccountChanges for 0xffff... (Factory contract)
        0xffff...,
        [ # storage_changes
            [b'\x00...\x01', [[2, b'\x00...\xdd\xdd...']]]  # slot 1, deployed address
        ],
        [],  # storage_reads
        [],  # balance_changes
        [[2, 5]],  # nonce_changes: after CREATE
        []  # code_changes
    ]
]

RLP 编码和压缩后:约 400-500 字节。

基本原理

BAL 设计选择

选择此设计变体有几个关键原因:

  1. 大小与并行化:BALs 包含所有访问的地址(即使未更改),以实现完全的并行 IO 和执行。

  2. 写入的存储值:执行后值使得在同步期间无需针对状态根的单独证明即可重建状态。

  3. 开销分析:历史数据显示平均 BAL 大小约为 70 KiB。

  4. 交易独立性:60-80% 的交易访问不相交的存储槽,从而实现有效的并行化。剩余的 20-40% 可以通过交易后状态差异进行并行化。

  5. RLP 编码:以太坊原生编码格式,保持与现有基础设施的兼容性。

BAL 大小考量 (60M 区块 Gas 限制)

平均 BAL 大小:约 72.4 KiB(压缩后)

  • 存储写入:约 29.2 KiB (40.3%)
  • 存储读取:约 18.7 KiB (25.8%)
  • 余额差异:约 6.7 KiB (9.2%)
  • Nonce 差异:约 1.1 KiB (1.5%)
  • 代码差异:约 1.2 KiB (1.6%)
  • 账户地址(带差异):约 7.7 KiB (10.7%)
  • 仅接触的地址:约 3.5 KiB (4.8%)
  • RLP 编码开销:约 4.4 KiB (6.1%)

小于当前最差情况下的 calldata 区块。

已在此处进行经验分析:这里。针对 6000 万 Gas 限制的更新分析可在此处找到:这里

异步验证

BAL 验证与并行 IO 和 EVM 操作同时进行,而不会延迟区块处理。

向后兼容性

本提案要求对区块结构和 Engine API 进行更改,这些更改不具备向后兼容性,并且需要硬分叉。

安全考量

验证开销

验证访问列表和余额差异会增加验证开销,但对于防止接受无效区块至关重要。

区块大小

区块大小增加会影响传播,但开销(平均约 70 KiB)对于性能提升来说是合理的。

恶意 BALs 的早期拒绝

由于 storage_reads 条目未映射到特定的交易索引,因此它们的有效性只能在执行所有交易后才能确认。恶意提议者可以利用这一点,声明从未访问过的幻影存储读取,迫使客户端进行不必要的 I/O 预取和大量数据下载,而区块在完成之前仍无法拒绝。

为了缓解这种情况,客户端应该在交易边界强制执行 Gas 预算可行性检查。令:

  • R_remaining = 尚未访问的已声明存储读取数量
  • G_remaining = 剩余区块 Gas

必须满足以下不变量:

G_remaining >= R_remaining * 2000

其中 2000 是存储读取的最小 Gas 成本(通过 EIP-2930 访问列表:1900 预付 + 100 热读取)。如果此检查失败,则可以立即拒绝该区块为无效,因为剩余的 Gas 不足以访问声明的读取。此检查应该定期执行(例如,每 8 笔交易),以实现早期拒绝,而不会影响并行执行。

版权

通过 CC0 放弃版权及相关权利。

  • 原文链接: github.com/nerolation/EI...
  • 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
Nerolation
Nerolation
江湖只有他的大名,没有他的介绍。