本文档介绍了 Flashblock 级别访问列表 (FAL),它是对 EIP-7928 区块级别访问列表 (BAL) 的一种调整,适用于生成 flashblock 的 OP Stack 链。
本文档介绍了 Flashblock 级别访问列表 (FAL),它是 EIP-7928 区块级别访问列表 (BAL) 的一个改编版本,专为生成 flashblock 的 OP Stack 链设计。FAL 记录了在 flashblock 执行期间访问的所有账户和存储位置,以及它们在执行后的值。与 BAL 类似,FAL 支持并行磁盘读取、并行交易验证和无执行的状态更新,但它是专门为 OP Stack 链中使用的 flashblock 架构而设计的。
FAL 将 BAL 规范适配于生成 flashblock 的 OP Stack 链 - 以子区块间隔(例如,对于 2 秒的规范区块时间,每 200 毫秒)生成的增量式 “迷你区块”。虽然 BAL 是为以太坊 L1 规范区块设计的,但 FAL 必须处理:
0:预执行系统合约 + L1 属性交易1 到 n:常规 L2 交易(包括 L1→L2 用户存款)n + 1:后执行系统合约(如果存在)我们向 flashblock 元数据引入一个新字段 flashblock_access_list,其中包含完整的 flashblock 访问列表结构,包括交易索引边界和哈希值。
##[derive(Clone, Debug, Default, Deserialize, Serialize)]
pub struct FlashblocksPayloadV1 {
pub payload_id: PayloadId,
pub index: u64,
pub base: Option<ExecutionPayloadBaseV1>,
pub diff: ExecutionPayloadFlashblockDeltaV1,
pub metadata: Value, // Contains "flashblock_access_list"
}
// The flashblock_access_list field in metadata contains:
##[derive(Clone, Debug, Deserialize, Serialize)]
pub struct FlashblockAccessList {
pub min_tx_index: u64, // Inclusive starting transaction index in the overall block
pub max_tx_index: u64, // Exclusive ending transaction index in the overall block
pub account_changes: Vec<AccountChanges>, // List of all account changes
pub fal_hash: B256, // Keccak-256 hash of RLP-encoded account_changes
}
交易索引边界:
min_tx_index (包含):整体区块中的起始交易索引max_tx_index (排除):整体区块中的结束交易索引这些边界允许每个 flashblock 在整体区块的交易序列中保持其位置。
示例: 如果一个区块有 15 个交易,分布在 3 个 flashblock 中:
min_tx_index=0, max_tx_index=5(包含交易 0-4)min_tx_index=5, max_tx_index=10(包含交易 5-9)min_tx_index=10, max_tx_index=15(包含交易 10-14)当不存在状态变更时,account_changes 是一个空列表,fal_hash 是一个空 RLP 列表的哈希值:0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347,即 keccak256(rlp.encode([]))。
FAL 使用与 BAL 相同的 RLP 编码,遵循以下模式:address -> field -> block_access_index -> change。
## Type aliases for RLP encoding (identical to BAL)
Address = bytes # 20-byte Ethereum address
StorageKey = bytes # 32-byte storage slot key
StorageValue = bytes # 32-byte storage value
CodeData = 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
## Constants (adapted for OP Stack)
MAX_TXS = 30_000
MAX_SLOTS = 300_000
MAX_ACCOUNTS = 300_000
MAX_CODE_SIZE = 24_576
MAX_CODE_CHANGES = 1
## Core change structures (identical to BAL)
StorageChange = [BlockAccessIndex, StorageValue]
BalanceChange = [BlockAccessIndex, Balance]
NonceChange = [BlockAccessIndex, Nonce]
CodeChange = [BlockAccessIndex, CodeData]
SlotChanges = [StorageKey, List[StorageChange]]
## AccountChanges: [address, storage_changes, storage_reads, balance_changes, nonce_changes, code_changes]
AccountChanges = [
Address,
List[SlotChanges], # storage_changes
List[StorageKey], # storage_reads
List[BalanceChange], # balance_changes
List[NonceChange], # nonce_changes
List[CodeChange] # code_changes
]
## FlashblockAccessList: List of AccountChanges
FlashblockAccessList = List[AccountChanges]
FlashblockAccessList 是 flashblock 执行期间访问的所有地址的集合。
它必须包括:
BALANCE、EXTCODESIZE、EXTCODECOPY、EXTCODEHASH 操作码的目标CALL、CALLCODE、DELEGATECALL、STATICCALL 的目标(即使它们回退)CREATE/CREATE2 的目标地址(即使创建失败)SELFDESTRUCT 的受益人地址没有状态更改的地址必须仍然存在,但带有空的更改列表。
来自 EIP-2930 访问列表的条目不得自动包含。仅记录在执行期间实际触及或更改的地址和存储槽。
以下排序规则必须适用:
BlockAccessIndex 值必须按如下方式分配:
0 用于预执行系统合约调用和 L1 属性交易(仅在 min_tx_index = 0 的第一个 flashblock 中)min_tx_index … max_tx_index - 1 用于此 flashblock 中的交易(包括 L1→L2 用户存款和常规 L2 交易)n + 1 用于后执行系统合约调用(如果存在,仅在最后一个 flashblock 中)重要提示: 交易的 block_access_index 使用整体区块交易索引,而不是 flashblock 本地索引。这允许组合来自多个 flashblock 的 FAL,同时保持正确的交易顺序。
写入包括:
读取包括:
SLOAD 访问但未写入的槽SSTORE,也称为“无操作写入”)注意:实现必须检查 pre-transaction 值才能正确区分实际写入和无操作写入。
balance_changes)记录以下对象的交易后余额 (uint256):
value > 0 时)SELFDESTRUCT/SENDALL 受益人零值转账: 不得记录在 balance_changes 中,但相应的地址必须仍然包含在空的 AccountChanges 中。
跟踪已部署或修改合约的 交易后运行时字节码,以及 EIP-7702 中定义的成功委托的 委托指示符。
记录以下对象的交易后 nonce:
CREATE 或 CREATE2 的合约排序器费用金库 (0x4200000000000000000000000000000000000011):
基础费用金库 (0x4200000000000000000000000000000000000019):
L1 费用金库 (0x420000000000000000000000000000000000001a):
L1 属性交易(索引 0 处的已存入交易):
block_access_index = 0basefee (槽 1)blobBaseFee (槽 5)hash (槽 6)number (槽 0)timestamp (槽 2)sequenceNumber (槽 4)batcherHash (槽 3)来自 L1 的用户存款 (例如,通过 OptimismPortal):
block_access_index = 1..n 的常规交易记录环形缓冲区中单个更新的存储槽的系统合约存储差异。
OP Stack 注意: 区块哈希存储可能使用相同的 EIP-2935 机制或修改后的版本。记录实际发生的任何存储更改。
AccountChanges 中,而没有 nonce 或代码的更改。但是,如果该帐户在交易前有正余额,则必须记录余额更改为零。在 self-destructed 合约中修改或读取的存储键必须作为 storage_read 包含。EXTCODEHASH、EXTCODESIZE、BALANCE、STATICCALL 等的目标)。balance_changes 中省略。storage_reads 中。block_access_index = 0。block_access_index = len(transactions) + 1。状态转换函数必须验证提供的 FAL 是否与实际的状态访问匹配:
def validate_flashblock(flashblock):
# 1. Extract FAL from metadata
import rlp
fal = flashblock.metadata['flashblock_access_list']
min_tx_index = fal['min_tx_index']
max_tx_index = fal['max_tx_index']
provided_account_changes = fal['account_changes']
provided_fal_hash = fal['fal_hash']
# 2. Verify provided hash matches account_changes
computed_hash = keccak256(rlp.encode(provided_account_changes))
assert computed_hash == provided_fal_hash
# 3. Execute flashblock and collect actual accesses
actual_account_changes = execute_and_collect_accesses(flashblock, min_tx_index, max_tx_index)
# 4. Verify actual execution matches provided account_changes
actual_fal_hash = keccak256(rlp.encode(actual_account_changes))
assert actual_fal_hash == provided_fal_hash
def execute_and_collect_accesses(flashblock, min_tx_index, max_tx_index):
"""Execute flashblock and collect all state accesses into FAL format
Args:
flashblock: The flashblock to execute
min_tx_index: Starting transaction index (inclusive) in the overall block
max_tx_index: Ending transaction index (exclusive) in the overall block
"""
accesses = {}
# Pre-execution: L1 attributes transaction (block_access_index = 0)
# Only include for first flashblock (min_tx_index == 0)
if min_tx_index == 0:
track_l1_attributes_tx(flashblock, accesses, block_access_index=0)
track_system_contracts_pre(flashblock, accesses, block_access_index=0)
# Execute transactions (block_access_index = min_tx_index..max_tx_index)
# This includes both L1→L2 deposits and regular L2 transactions
for i, tx in enumerate(flashblock.diff.transactions):
tx_index = min_tx_index + i
execute_transaction(tx)
track_state_changes(tx, accesses, block_access_index=tx_index)
track_fee_vault_changes(tx, accesses, block_access_index=tx_index)
# Post-execution system contracts
# Only include for last flashblock (would need to be indicated separately)
# For now, omitted as flashblocks are incremental
# Convert to FAL format and sort
return build_fal(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 track_fee_vault_changes(tx, accesses, block_access_index):
"""Track OP Stack fee vault balance changes after each transaction"""
# Sequencer Fee Vault (priority fees)
SEQUENCER_FEE_VAULT = 0x4200000000000000000000000000000000000011
# Base Fee Vault (base fees)
BASE_FEE_VAULT = 0x4200000000000000000000000000000000000019
# L1 Fee Vault (L1 data fees)
L1_FEE_VAULT = 0x420000000000000000000000000000000000001a
for vault in [SEQUENCER_FEE_VAULT, BASE_FEE_VAULT, L1_FEE_VAULT]:
if vault not in accesses:
accesses[vault] = {
'storage_writes': {},
'storage_reads': set(),
'balance_changes': [],
'nonce_changes': [],
'code_changes': []
}
# Record vault balance after transaction if it changed
if balance_changed(vault):
accesses[vault]['balance_changes'].append((block_access_index, get_balance(vault)))
def track_l1_attributes_tx(flashblock, accesses, block_access_index):
"""Track L1 attributes transaction (deposited tx at index 0)"""
L1_BLOCK_CONTRACT = 0x4200000000000000000000000000000000000015
if L1_BLOCK_CONTRACT not in accesses:
accesses[L1_BLOCK_CONTRACT] = {
'storage_writes': {},
'storage_reads': set(),
'balance_changes': [],
'nonce_changes': [],
'code_changes': []
}
# Track storage updates to L1Block contract
# Slots: number(0), basefee(1), timestamp(2), batcherHash(3),
# sequenceNumber(4), blobBaseFee(5), hash(6)
for slot in [0, 1, 2, 3, 4, 5, 6]:
if slot_changed(L1_BLOCK_CONTRACT, slot):
if slot not in accesses[L1_BLOCK_CONTRACT]['storage_writes']:
accesses[L1_BLOCK_CONTRACT]['storage_writes'][slot] = []
value = get_storage(L1_BLOCK_CONTRACT, slot)
accesses[L1_BLOCK_CONTRACT]['storage_writes'][slot].append((block_access_index, value))
def build_fal(accesses):
"""Convert collected accesses to FAL format"""
fal = []
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]
fal.append([
addr,
storage_changes,
sorted(list(data['storage_reads'])),
sorted(data['balance_changes']),
sorted(data['nonce_changes']),
sorted(data['code_changes'])
])
return fal
FAL 必须完整且准确。缺少或虚假的条目会使 flashblock 无效。
客户端可以通过将执行收集的访问与 FAL 进行比较来验证。
如果任何交易超出声明状态,客户端可以立即失效。
OP Stack 上的示例 flashblock: 预执行 (block_access_index = 0):
交易:
后执行 (block_access_index = 4):
结果 FAL(RLP 结构):
[
# 地址按字典顺序排序
[ # 0x0000F90827F1C53a10cb7A02335B175320002935 的 AccountChanges(区块哈希合约)
0x0000F90827F1C53a10cb7A02335B175320002935,
[ # storage_changes
[b'\\x00...\\x0f\\xa0', [[0, b'...']]] # 槽, [[block_access_index, parent_hash]]
],
[], # storage_reads
[], # balance_changes
[], # nonce_changes
[] # code_changes
],
[ # 0x4200000000000000000000000000000000000011 的 AccountChanges(排序器费用金库)
0x4200000000000000000000000000000000000011,
[], # storage_changes
[], # storage_reads
[[1, 0x...fee1], [2, 0x...fee2], [3, 0x...fee3]], # balance_changes: 每次 tx 之后
[], # nonce_changes
[] # code_changes
],
[ # 0x4200000000000000000000000000000000000015 的 AccountChanges (L1Block 合约)
0x4200000000000000000000000000000000000015,
[ # 来自 L1 属性 tx 的 storage_changes
[b'\\x00...\\x00', [[0, b'...']]], # number
[b'\\x00...\\x01', [[0, b'...']]], # basefee
[b'\\x00...\\x02', [[0, b'...']]], # timestamp
[b'\\x00...\\x03', [[0, b'...']]], # batcherHash
[b'\\x00...\\x04', [[0, b'...']]], # sequenceNumber
[b'\\x00...\\x05', [[0, b'...']]], # blobBaseFee
[b'\\x00...\\x06', [[0, b'...']]] # hash
],
[], # storage_reads
[], # balance_changes
[], # nonce_changes
[] # code_changes
],
[ # 0x4200000000000000000000000000000000000019 的 AccountChanges(基础费用金库)
0x4200000000000000000000000000000000000019,
[], # storage_changes
[], # storage_reads
[[1, 0x...base1], [2, 0x...base2], [3, 0x...base3]], # balance_changes: 每次 tx 之后
[], # nonce_changes
[] # code_changes
],
[ # 0x420000000000000000000000000000000000001a 的 AccountChanges(L1 费用金库)
0x420000000000000000000000000000000000001a,
[], # storage_changes
[], # storage_reads
[[1, 0x...l1fee1], [2, 0x...l1fee2], [3, 0x...l1fee3]], # balance_changes: 每次 tx 之后
[], # nonce_changes
[] # code_changes
],
[ # 0xaaaa... 的 AccountChanges(Alice - 发送者 tx 1)
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
],
[ # 0xbbbb... 的 AccountChanges(Bob - 接收者 tx 1)
0xbbbb...,
[], # storage_changes
[], # storage_reads
[[1, 0x...b9aca00]], # balance_changes: +1 ETH
[], # nonce_changes
[] # code_changes
],
[ # 0xcccc... 的 AccountChanges(Charlie - 发送者 tx 2)
0xcccc...,
[], # storage_changes
[], # storage_reads
[[2, 0x...bc16d67]], # balance_changes: gas 之后
[[2, 5]], # nonce_changes
[] # code_changes
],
[ # 0xdave... 的 AccountChanges(Dave - L1→L2 存款接收者 tx 3)
0xdave...,
[], # storage_changes
[], # storage_reads
[[3, 0x...8ac7230]], # balance_changes: 来自 L1 的 +10 ETH
[], # nonce_changes
[] # code_changes
],
[ # 0xdddd... 的 AccountChanges(已部署合约)
0xdddd...,
[], # storage_changes
[], # storage_reads
[], # balance_changes
[[2, 1]], # nonce_changes: 新合约 nonce
[[2, b'\\x60\\x80\\x60\\x40...']] # code_changes: 已部署字节码
],
[ # 0xffff... 的 AccountChanges(工厂合约)
0xffff...,
[ # storage_changes
[b'\\x00...\\x01', [[2, b'\\x00...\\xdd\\xdd...']]] # 槽 1, 已部署地址
],
[], # storage_reads
[], # balance_changes
[[2, 5]], # nonce_changes: CREATE 之后
[] # code_changes
]
]
RLP 编码和压缩:~500-600 字节(由于 L1Block 合约更新而略大于 BAL)。
Flashblock 大小影响(估计):
block_access_index (例如,-1 或 0xFFFF)fal_hash 是否与 keccak256(rlp.encode(account_changes)) 匹配min_tx_index 和 max_tx_index 执行 flashblock,以分配正确的区块访问索引account_changes 匹配从 flashblock 组装标准区块时:
验证访问列表会增加验证开销,但对于防止接受无效的 flashblock 至关重要。开销与 BAL 验证相当。
增加的 flashblock 大小会影响传播。平均开销(约 45 KiB)对于 200 毫秒的 flashblock 间隔是合理的,并且可以通过并行化实现显著的性能提升。
追踪三个手续费金库而不是一个 COINBASE 会稍微增加 FAL 的大小,但可以完全透明地了解 OP Stack 手续费分配。
通过 CC0 放弃版权和相关权利。
- 原文链接: github.com/base/op-rbuil...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!