Taiko 如何验证 L2 区块
本文档详细介绍了 Taiko 如何通过协议、客户端和电路协同工作来验证 L2 区块。文章深入探讨了交易列表的有效性证明、锚定交易(Anchor Transactions)的验证、块元数据(Block Metadata)的一致性、全局变量的正确性验证、块头的完整性验证以及跨链信号存储的验证,确保 L2 区块的有效性和安全性。
Taiko 如何证明 L2 区块
本文档详细说明了 Taiko 协议、客户端和电路如何协同工作以证明 L2 区块。
数据
交易列表
Layer 2 (L2) 区块的交易列表 txList 最终将成为 Layer 1 (L1) 共识层 (CL) 中的一个 blob 的一部分 - 目前它位于 calldata 中。
它应该是一个 RLP 编码的 L2 交易列表,只有其长度和 commitment 对 L1 执行层 (EL) 可用。长度在提议区块时进行检查,但我们需要使用 ZKP 来证明 txList 是否有效。
一个有效的 txList (直到 issue #13724 被启用和实现):
- 其字节大小小于协议常量
blockMaxTxListBytes(也在合约中强制执行); - 可以 RLP 解码为交易列表,且没有尾随空格;
- 包含的交易 (有效和无效) 不超过协议常量
blockMaxTransactions; - 所有有效交易的总 gas limit 不超过协议常量
blockMaxGasLimit;
ZKP 必须证明 txList 是否有效。对于无效的 txList,相应的 L2 区块将只有一个锚定交易。
对于有效的 txList,ZKP 还必须证明其中包含的每笔交易是否有效。所有有效交易将成为实际 L2 区块的一部分,其中锚定交易作为第一笔交易。
一笔有效的交易 (在以太坊黄皮书中定义):
- 具有有效的交易签名。
- 具有有效的交易 nonce (等同于发送方账户当前的 nonce)。
- 发送方账户上没有部署合约代码 (参见 Feist 等人的EIP-3607 [2021])。
- gas limit 不小于交易使用的内在 gas
g0。 - 发送方账户余额至少包含前期支付所需的成本
v0。 - 交易的 gas limit 小于或等于区块中剩余的 gas 量 (区块 gas limit 为协议常量
blockMaxGasLimit)。 - 交易的 base fee 大于或等于区块的 base fee。
切片和一致性
请注意,txList 的数据不能假定为可直接为 L1 合约访问,因此 ZKP 应进一步证明所选的 txList 是给定 blob 数据的一个切片。当 EIP-4844 合并到以太坊时,blobHash 操作码将可用。
锚定交易
每个 L2 区块都只有一个锚定函数调用作为其第一笔交易。
ZKP 必须证明 TaikoL2.anchor(...) 是区块中的第一笔交易,具有正确的输入参数和 gas limit,由所谓的 golden-touch 地址签名,并且该交易已成功执行。
- 锚定交易的
to地址必须是已注册的taiko_l2地址,该地址被哈希到 ZKPinstance中。并且tx.origin必须是golden touch 地址。 - 锚定交易的 ABI 必须是:
function anchor(
bytes32 l1Hash,
bytes32 l1SignalRoot,
uint64 l1Height,
uint64 parentGasUsed
) external;
- 一个电路将验证
l1Hash、l1SignalRoot和l1SignalServiceAddress之间的一致性。 l1SignalServiceAddress、l2SignalServiceAddress和parentGasUsed直接被哈希到 ZKP 的 instance 中。l1Height和l1Hash都是区块元数据 (meta.l1Height和meta.l1Hash) 的一部分,metaHash用于计算 ZKP instance。l1SignalRoot是证据的一部分,也用于计算 ZKP instance。- 交易的状态码为 1 (成功)。
- 交易的
tx.origin和msg.sender必须是LibAnchorSignature.K_GOLDEN_TOUCH_ADDRESS。 - 交易的签名必须与
LibAnchorSignature.signTransaction(...)相同。 - 交易费用必须为 0。
请注意,锚定交易会发出一个 Anchored 事件,这可能有助于 ZKP 验证区块变量。见下文。
锚定签名
ZKP 还应检查锚定交易的签名:
- 签名者必须是
TaikoL2.GOLDEN_TOUCH_ADDRESS。 - 如果计算出的
r不是0,则签名必须使用1作为k值,否则,k必须是2。请参阅 LibL2Signer.sol。
区块元数据
这个 struct 代表了一个提议的 L2 区块。这个数据将被哈希,并成为电路公共输入的一部分。
struct BlockMetadata {
uint64 id;
uint64 timestamp;
uint64 l1Height;
bytes32 l1Hash;
bytes32 mixHash;
bytes32 blobHash;
uint24 txListByteStart;
uint24 txListByteEnd;
uint32 gasLimit;
address proposer;
address treasury;
TaikoData.EthDeposit[] depositsProcessed;
}
id: 表示 L2 中的区块高度。timestamp: L2 中的区块时间戳。l1Height: L1 中的实际区块高度。l1Hash: L1 中的实际区块哈希。mixHash: 用于容纳多个 L2 区块的加盐随机数,这些 L2 区块可以放入一个 L1 区块中。blobHash: L2 中交易列表的哈希。txListByteStart: L2 中交易列表的字节起始位置。txListByteEnd: L2 中交易列表的字节结束位置。gasLimit: L2 区块的 Gas limit。proposer: L2 中提议者的地址。treasury: L2 中 base fee 的去向地址。depositsProcessed: 构成 depositRoot 的已启动的 L1->L2 以太坊存款。
全局变量
以下 区块级别变量 可供 EVM 访问,但它们的值不是 MPT 的一部分,因此我们需要另一种方法来验证其正确性。
blockhash(uint blockNumber) returns (bytes32): 给定区块的哈希,当blocknumber是 256 个最近的区块之一时; 否则返回零block.basefee(uint): 当前区块的 base fee (EIP-3198 和 修改后的 EIP-1559)block.chainid(uint): 当前链 IDblock.coinbase(address payable): 当前区块矿工的地址block.prevrandao(uint):block.prevrandao的别名 (EIP-4399)block.gaslimit(uint): 当前区块 Gas limitblock.number(uint): 当前区块号block.prevrandao(uint): 由信标链提供的随机数block.timestamp(uint): 自 Unix epoch 以来,当前区块时间戳 (以秒为单位)
我们需要验证当在 EVM 中访问这些变量时,它们的值与当前世界状态、区块的元数据和实际 L2 区块的区块头是否一致:
blockhash: EVM 允许访问最近 256 个区块的哈希值。所有这些哈希值都可以在 plonk 查找表中找到。ZKP 必须证明查找表与 L2 的历史值一致。blockhash(block.number - 1),具有与区块头中相同的值,并且也是 L2 上父区块哈希的相同值。- 其他 255 个哈希值,
blockhash(block.number - 256)到blockhash(block.number - 2)在锚定交易中进行检查,以简化电路。因此,只要锚定交易经过 ZK 证明,这些 255 个祖先哈希值就会被间接证明。 block.basefee: 经验证是在锚定交易中的正确值。block.chainid: 此字段也由锚定交易检查,因此不需要额外的 ZKP 电路。block.coinbase: ZKP 必须验证该值必须与meta.proposer相同。同样,元数据哈希是 ZK instance 的一部分。block.prevrandao: 现在与block.prevrandao相同,因此我们只检查block.prevrandao。block.gaslimit: ZKP 必须验证此值必须等于meta.gasLimit。block.number: 必须对照区块头和meta.id检查此值。block.prevrandao: 必须对照 L2 区块头中的mixHash字段和meta.mixHash检查此值。block.timestamp: 必须对照 L2 区块头中的timestamp字段和meta.proposedAt检查此值。
区块头
并非所有区块头数据都可在 L1 合约中使用; 因此,ZKP 必须验证区块头的完整性,并确保区块头哈希到与 evidence.blockHash 相同的值,evidence.blockHash 是 ZK instance 输入的一部分。
struct BlockHeader {
bytes32 parentHash;
bytes32 ommersHash;
address proposer;
bytes32 stateRoot;
bytes32 transactionsRoot;
bytes32 receiptsRoot;
bytes32[8] logsBloom;
uint256 difficulty;
uint128 height;
uint64 gasLimit;
uint64 gasUsed;
uint64 timestamp;
bytes extraData;
bytes32 mixHash;
uint64 nonce;
uint256 baseFeePerGas;
bytes32 withdrawalsRoot;
}
此外,ZKP 还必须证明以下几点:
parentHash必须与evidence.parentHash相同。ommersHash必须是[]的 keccak256,或0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347。proposer必须是meta.proposer(如上所述,重复)。logsBloom必须是一个包含所有零的bytes32[8]。difficulty== 0。height必须是meta.id。gasLimit==meta.gasLimit。gasUsed必须是区块中所有有效交易使用的所有 gas 的总和。timestamp==meta.proposedAt。extraData== ""。mixHash==meta.mixHash。nonce== 0。baseFeePerGas==block.basefee
请注意,上面的一些标头字段检查是全局变量部分中完成的检查的重复项。
信号存储
ZKP 还需要证明跨链信号服务的存储根具有正确的值。
-
对于 L2 信号服务: 信号服务的 L1 存储根是锚定交易中的第二个参数。ZKP 应通过使用 MPT 证明,根据
meta.l1Hash中存储的状态根,针对l1SignalServiceAddress帐户,来验证 L1 信号服务地址的存储根是否具有给定的值。此 MPT 证明必须由 L2 客户端从 L1 节点查询。 -
对于 L1 信号服务: L2 存储根验证将在电路中使用 MPT 证明,根据
l2SignalServiceAddress帐户的后区块状态根来完成。
EIP-1559
在 Taiko L2 协议中,basefee 不是被烧毁,而是转移到指定的 treasury 地址。为了确保此过程的完整性,ZKP 需要验证 Taiko L1 合约指定的 treasury 地址是否确实是预期的接收者。
LibProving 验证
公共输入参数的实际值必须与用于哈希 ZKP instance 的值一致 (参见 LibProving)。
电路中的数据交叉验证
为了帮助人们可视化上述所有元素。 这是一个图表:
%%{init: {'theme': 'base', 'themeVariables': { 'fontSize': '13px'}}}%%
graph LR
classDef default stroke-width:4px,stroke:#EA27C2,fill:#EA27C2,color:#FFF;
classDef transition stroke-width:4px,stroke:#FF715B,fill:#FF715B,color:#FFF;
classDef otherCircuits stroke-width:4px,stroke:#6ECEB0,fill:#6ECEB0,color:#FFF;
classDef constant stroke-width:4px,stroke:#323745,fill:#323745,color:#FFF;
classDef group stroke-width:2px,stroke:#EA27C2,fill:#FFD2F630;
m_id --- h_height --- v_block_number;
m_h1_height --- a_l1_height;
m_gas_limit --- h_gas_limit --- v_block_gaslimit;
m_timestamp --- h_timestamp --- v_block_timestamp;
m_txlist_first ---|<=| m_txlist_last --- |<= len| tx_list;
m_h1_hash --- a_l1_hash;
m_mix_hash --- h_mix_hash --- v_block_prevrando;
tx_list -.->|keccak| m_txlist_hash;
m_proposer --- h_proposer;
h_parent_hash --- v_blockhash_1 & e_parent_hash;
l2_treasury -.-> m_treasury;
v_block_chainid -.-> dot1;
v_blockhash_others -.-> dot1 -.->|keccak| s_public_input_hash;
v_block_basefee -.-> h_basefee;
v_block_gaslimit -.-> dot2;
v_block_timestamp -.-> dot2;
s_parent_timestamp -.-> dot2;
s_gas_excess -.-> dot2 ---|calcBasefee| v_block_basefee;
m_processed_deposits -.->|keccak| dot4;
b_signal_root --- a_l1_signal_root;
h_gas_used --- e_gas_used;
BlockMetadata -.-> dot4((" ")) --- |keccak| e_meta_hash -.-> dot3((" ")) -.->|keccak| zk_instance;
e_parent_hash & e_block_hash & e_signal_root & e_graffiti & e_prover & e_parent_gas_used & e_gas_used -.-> dot3;
b_l1_signal_service_addr -.-> dot3;
b_l2_signal_service_addr -.-> dot3;
b_l1_taiko_addr -.-> dot3;
e_signal_root --- s_signal_root
e_parent_gas_used --- a_parent_gas_used
h_gas_limit ---|>=| h_gas_used
BlockHeader -.->|abiencode & keccak| dot5((" ")) o--- e_block_hash
BlockEvidence ~~~ L1Storage;
subgraph BlockMetadata[Block Metadata]
m_id(id)
m_gas_limit(gasLimit)
m_timestamp(timestamp)
m_h1_height(h1Height)
m_h1_hash(h1Hash)
m_mix_hash(mixHash)
m_txlist_hash(blobHash)
m_txlist_first(txListByteStart)
m_txlist_last(txListByteEnd)
m_treasury(treasury)
m_proposer(proposer)
l2_treasury("L2 basefee goes to treasury"):::constant;
tx_list("txList\n(blob or calldata)"):::constant;
m_processed_deposits("ethDepositsProcessed"):::constant
end
BlockMetadata:::group
subgraph BlockHeader[Block Header]
h_height(height)
h_gas_limit(gasLimit)
h_gas_used(gasUsed)
h_timestamp(timestamp)
h_mix_hash(mixHash)
h_proposer(proposer)
h_parent_hash(parentHash)
h_ommers_hash("ommersHash = keccak([])")
h_state_root(stateRoot)
h_transactions_root(transactionsRoot)
h_receipts_root(receiptsRoot)
h_logs_bloom("logsBloom = []")
h_difficulty("difficulty = 0")
h_extra_data("extraData = ''")
h_nonce("nonce = 0")
h_basefee(basefee)
end
BlockHeader:::group
subgraph GlobalVariables[Global Variables]
v_block_number(block.number)
v_block_gaslimit(block.gaslimit)
v_block_timestamp(block.timestamp)
v_block_prevrando(block.prevrando)
v_blockhash_1("blockhash(1)")
v_blockhash_others("blockhash(2..256)")
v_block_chainid("block.chainid")
v_block_basefee("block.basefee")
dot1((" "))
dot2((" "))
end
GlobalVariables:::group
subgraph Anchor [Anchor Tx]
a_l1_height(l1Height)
a_l1_hash(l1Hash)
a_parent_gas_used(parentGasUsed)
a_l1_signal_root(l1SignalRoot)
end
Anchor:::group
subgraph L1Storage[L1 Storage]
b_l1_taiko_addr[/taikoL1Address/]
b_l1_signal_service_addr[/L1 signalServiceAddress/]
b_l2_signal_service_addr[/L2 signalServiceAddress/]
b_signal_root[/stateRoot/]
end
L1Storage:::group
subgraph L2Storage[L2 Storage]
s_public_input_hash[/publicInputHash/]
s_parent_timestamp[/parentTimestamp/]
s_gas_excess[/gasExcess/]
s_signal_root[/stateRoot/]
end
L2Storage:::group
subgraph BlockEvidence
e_meta_hash(metaHash)
e_parent_hash(parentHash):::transition
e_block_hash(blockHash)
e_signal_root(stateRoot)
e_graffiti(graffiti)
e_prover(prover)
e_parent_gas_used(parentGasUsed):::transition
e_gas_used(gasUsed)
end
BlockEvidence:::group
zk_instance(zkInstance)
- 原文链接: github.com/taikoxyz/taik...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~