本文深入探讨了Polygon zkEVM交易的生命周期,从交易提交到以太坊L1上通过零知识证明进行验证的全过程。文章详细解释了交易如何被提交、执行、批量处理、排序并最终在L1上实现最终确认,以及Polygon zkEVM如何继承以太坊的安全性。
在我的前一篇文章中,我们讨论了 ZK 证明的工作原理,以及不同的非交互式 ZK 证明如何在 ZK-EVM(如 Polygon zkEVM)中使用。
但这是如何运作的呢? 有哪些信息会被发送回以太坊?这又是如何使 Polygon zkEVM 能够继承以太坊的安全性的?
在这篇文章中,我们将更仔细地 بررسی 到底层发生了什么,包括交易如何:
提交到 Polygon zkEVM。
几乎立即执行。
使用数据加密方法批量处理。
排序并发送到以太坊 L1。
在 L1 上利用 ZK 证明的力量实现整合的最终性。
让我们开始吧!
正如我们在前一篇文章中探讨的那样,用户不断地通过 JSON-RPC 接口 (通常通过像 MetaMask 这样的钱包) 向受信任的排序器的节点提交他们的 L2 交易。
这使得 Polygon zkEVM 在与 dApp 交互时,从用户的角度来看,与以太坊完全一样。示例如下:
https://twitter.com/jarrodWattsDev/status/1686304245539274753
从用户的角度来看,交易几乎立即完成,允许他们在提交交易后立即继续使用 dApp。这种强大的用户体验优势之所以成为可能,是因为 zkEVM 的状态更新无需首先向以太坊 (L1) 发送任何信息。
但是,如果用户想要从 L2 (zkEVM) 桥接资金 到 L1 (以太坊),本质上是执行提款,那么完整的交易生命周期需要完成。这需要 PolygonZkEVM 智能合约:
接收来自排序器的批次(知道要证明哪些交易)。
接收来自聚合器的有效性证明(证明交易)。
从用户角度来看,此时简化的流程如下所示:
提交后,交易将存储在待处理交易池中,等待排序器选择执行或丢弃。
排序器会做一些检查,看看是否可以基于以下内容丢弃交易:
发送者是否有足够的资金来支付交易。
调用的智能合约是否存在并且具有有效的/正确的字节码。
该交易是否不是重复的。
该交易是否不是“双重支付”,以确保发送者的资金尚未在另一笔交易中支出。
一旦该交易被认为是有效的,排序器就会更新 Polygon zkEVM 的状态,此时用户会体验到交易几乎立即完成。
如果我们修改我们的图表以探索底层,那么它是这样的:
此时,用户继续与 L2 的状态进行交互。此之后的每一步都与将交易数据发布回以太坊 L1 相关;这仅与想要将资金返回到以太坊的用户相关。
添加到 L2 状态后,交易将广播到网络上的所有其他 zkEVM 节点,并准备好与其他交易进行批量处理。
要将交易批量处理在一起,它们会以二进制形式连接成一组字节。在 PolygonZkEVM
智能合约上,定义了一个 Solidity struct
,名为BatchData
。
在这个结构体中,定义了一个类型为 bytes
的 transactions
字段,其中包含连接在一起的编码交易批次。
/**
* @notice 将用于调用 sequenceBatches 的结构体
* @param transactions L2 以太坊交易 EIP-155 或带有签名的 pre-EIP-155:
* EIP-155: rlp(nonce, gasprice, gasLimit, to, value, data, chainid, 0, 0,) || v || r || s
* pre-EIP-155: rlp(nonce, gasprice, gasLimit, to, value, data) || v || r || s
* @param globalExitRoot 批次的全局退出根
* @param timestamp 批次的排序时间戳
* @param minForcedTimestamp 强制批处理数据的最小时间戳,非强制批处理时为空
*/
struct BatchData {
bytes transactions;
bytes32 globalExitRoot;
uint64 timestamp;
uint64 minForcedTimestamp;
}
正如你在上面的注释中看到的,交易是使用 "RLP" 进行编码的,可以是 EIP-155 或 pre-EIP-155 交易。
EIP-155 是 Vitalik 于 2016 年提出的一个提案,旨在防止重放攻击。它添加了 chainId
值,以避免用于一个链的交易也可以在其他链上工作。
rlp
代表 Recursive-Length Prefix Serialization。这是一种数据序列化方法,以太坊使用它将任意嵌套的数据数组的结构编码为二进制。
至于其他三个字段:
globalExitRoot
:全局退出 (Merkle) 树的根,该树存储有关 L1 和 L2 之间资产转移的信息。了解更多。
timestamp
:创建批次的时间。
minForcedTimestamp
:仅当用户不使用受信任的排序器时才相关(有助于审查抗性)。通常设置为 0
。了解更多。
要更新我们的图表,让我们缩小范围,看看排序器在做什么:
根据我们将在下一節中定义的,在 L1 智能合约上定义的一些大小规则,创建了多个此类批次。因此,让我们看看接下来这些批次会发生什么。
一旦我们有了交易批次,它们就可以被“排序”了。为此,排序器调用 PolygonZkEVM
智能合约的 sequenceBatches
函数(在 L1 上)并为其提供多个交易批次。
此交易 是以太坊主网上 sequenceBatch
交易的一个示例。通过检查输入数据,我们可以看到在此特定函数调用中包含的 52
个交易批次:
每个批次还包含我们之前看到的 transactions
字段(连接的字节),该字段连接了尽可能多的 RLP 编码交易:
PolygonZkEVM
智能合约具有一个名为 _MAX_TRANSACTIONS_BYTE_LENGTH
的常量值,该值确定可以在该字段中连接多少个交易。源代码:
// 可以在单个批次中添加的最大交易字节数
// 最大 keccaks 电路 = (2**23 / 155286) * 44 = 2376
// 每个 keccak 的字节数 = 136
// 最小静态 keccaks 批次 = 2
// 允许的最大字节数 = (2376 - 2) * 136 = 322864 字节 - 1 字节填充
// 四舍五入为 300000 字节
// 为了处理交易,数据大约哈希两次用于 ecrecover:
// 300000 字节 / 2 = 150000 字节
// 由于 geth 池当前最多只接受 128kb 交易:
// https://github.com/ethereum/go-ethereum/blob/master/core/txpool/txpool.go#L54
// 我们将限制此长度以符合 geth 限制,因为我们的节点将使用它
// 我们保留 8kb 作为安全裕度
uint256 internal constant _MAX_TRANSACTIONS_BYTE_LENGTH = 120000;
同样,可以作为一个交易发送的批次数量也有限制,在一个名为 _MAX_VERIFY_BATCHES
的常量变量中。源代码:
// 一次调用中可以验证的最大批次数量。这取决于我们当前的指标
// 这应该是防止有人试图生成大量无效批次的保护措施,我们在挂起超时到期之前无法证明
uint64 internal constant _MAX_VERIFY_BATCHES = 1000;
这些批次被提供给一个名为 sequenceBatches
的函数,该函数接受:
一个名为 batches
的 BatchData
结构体数组。
一个地址,用于发送排序批次的费用,称为l2Coinbase
。
function sequenceBatches(
BatchData[] calldata batches,
address l2Coinbase
) external ifNotEmergencyState onlyTrustedSequencer {
...
}
这个函数遍历每个批次,确保它们有效,然后在 L1 智能合约的一个名为 sequencedBatches
的 mapping 中更新虚拟状态:
// 定义虚拟状态的批次队列
// SequenceBatchNum --> SequencedBatchData
mapping(uint64 => SequencedBatchData) public sequencedBatches;
在我们的图表中,这是我们所处的位置:
现在是介绍交易在 Polygon zkEVM 中可以处于的不同状态的好时机。交易会经历不同的最终性阶段:
可信状态:L2 上的状态已更新。尚未到达 L1。
虚拟状态:批次已排序,数据可在 L1 上获得。
合并状态:一个 ZK 证明已发布在 L1 上。
到目前为止,我们已经讨论了可信和虚拟状态的过程所以,让我们现在跳到最后阶段,其中计算完整性的 ZK 证明被提交到 L1。
一旦所有排序的批次都到达 L1,最后一步是生成一个 ZK 证明,以验证这些交易的有效性。
聚合器节点获取排序的批次并将它们提供给 ZK prover,它使用 fflonk
协议生成最终的 SNARK。(下面是快速总结):
https://twitter.com/jbaylina/status/1624116186861404188
最终结果是聚合器收到一个 ZK 证明,该证明足够简洁,可以存储在以太坊 L1 上。此流程的简化图如下:
一旦聚合器节点有了证明,它就会调用 PolygonZkEVM 智能合约的 verifyBatchesTrustedAggregator
函数,并将它刚刚收到的证明提供给该函数,以及其他参数,源代码:
/**
* @notice 允许聚合器验证多个批次
* @param pendingStateNum 初始挂起状态,如果使用合并状态则为 0
* @param initNumBatch 聚合器开始验证的批次
* @param finalNewBatch 聚合器打算验证的最后一个批次
* @param newLocalExitRoot 一旦批次被处理,新的本地退出根
* @param newStateRoot 一旦批次被处理,新的状态根
* @param proof fflonk 证明
*/
function verifyBatchesTrustedAggregator(
uint64 pendingStateNum,
uint64 initNumBatch,
uint64 finalNewBatch,
bytes32 newLocalExitRoot,
bytes32 newStateRoot,
bytes calldata proof
) external onlyTrustedAggregator {
...
}
让我们再次检查一个示例交易,看看这在现实世界中是什么样子。
此交易 来自受信任的聚合器,并使用该证明在 PolygonZkEVM 智能合约上调用 verifyBatchesTrustedAggregator
:
有关其他参数的详细信息,请参见 此处 .
在这个函数中,另一个名为 rollupVerifier
的合约有一个函数 verifyProof
会被调用。这个函数被提供了该证明以及一个 inputSnark
;这是一个特定的 L2 状态转换的所有 L2 交易的加密表示。
// 验证证明
if (!rollupVerifier.verifyProof(proof, [inputSnark])) {
revert InvalidProof();
}
如果证明有效,各种状态会更新,例如全局退出根和包含合并的 L2 状态根的 batchNumToStateRoot
映射:
// 状态根映射
// BatchNum --> 状态根
mapping(uint64 => bytes32) public batchNumToStateRoot;
在这个例子中,在 L1 上发布和验证这个证明花费了大约 350K gas:
通过对我们的图表进行最后一次更新,我们在 L1 上实现了合并状态!
此时,交易批次处于最终的“合并”状态,这就是 Polygon zkEVM 如何继承以太坊的安全性的;通过将所有交易数据发布和证明回以太坊 L1。
从用户的角度与 Polygon zkEVM 交互时,交易几乎可以立即确认,同时通过在底层发生的整个过程继承以太坊的安全性。
这种方法提供了两全其美的效果,用户既可以在几秒钟内获得低 gas 费和快速交易速度,也可以使用 ZK 证明的力量在大约 1 小时内将资金桥接回以太坊。
在这篇博客中,我们介绍了从 Polygon zkEVM 交易的完整生命周期,一直到使用以太坊 L1 上的零知识进行证明。
如果你喜欢这篇文章,请考虑在 Twitter 上关注我!
- 原文链接: blog.jarrodwatts.com/how...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!