火爆区块链 | Layer1到Layer2:opstack是如何派生出来的
探讨乐观主义变形器的奥秘:L2派生原理
“L2派生原理:背后的魔法”
作者:joohhnnn
介绍
各位数字资产投资者们,认真听好了,今天我要和大家聊聊opstack是如何从Layer1中派生出来Layer2的这件大事!在开始之前,请务必先阅读一下这篇官方介绍,它会告诉你整个派生过程的细节。
嗯,如果你现在读完这篇文章还有点困惑,别着急,放心这很正常。但是,请你牢记这份感觉,因为当你读完我们的这篇分析后,再回过头来再读一遍,你会发现官方的那篇文章在凝练方面真的做得很牛啊,把所有要点和细节都讲得明明白白!
好了,现在我们正式进入正题。我们都知道,Layer2的运行节点是可以从Layer1(DA层)中获取数据,并且构建出完整的Layer2区块数据的。今天我们就来讲解一下这个过程中是如何在codebase
中实现的。
你有哪些问题?
我们先来思考一下,如果这个系统让你来设计,你会碰到哪些问题呢?
- 当你启动一个新节点的时候,整个系统是如何运行的?
- 你需要一个个去查询所有Layer1的区块数据吗?如何触发查询?
- 当你拿到Layer1区块的数据后,你需要哪些数据?
- 派生过程中,区块的状态是怎么变化的?如何从unsafe变成safe再变成finalized?
- 官方规范中晦涩的数据结构
batch/channel/frame
这些到底是干嘛的?
什么是派生(Derivation)?
在理解派生之前,我们先来聊一聊optimism的基本Rollup机制吧。我们可以简单以一笔L2上的转账交易为例。
你在optimism网络上发起一笔转账交易,这笔交易会被”转发”给sequencer节点,由sequencer进行排序,并封装成一个包含你的交易的区块A,并进行区块的广播(也就是出块)。这时,区块A的状态为unsafe。随后,在一个固定的时间间隔之后(比如4分钟),sequencer的batcher模块将这四分钟内所有收集到的交易(包括你的转账交易)通过一笔交易发送到Layer1,并由Layer1产出区块X。此时,区块A的状态仍然为unsafe。任何一个节点执行完派生部分的程序后,会从Layer1获取区块X的数据,并对本地的unsafe区块A进行更新。这时,区块A的状态变为safe。经过两个Layer1的Epoch(也就是64个区块)后,由Layer2节点将区块A标记为finalized区块。
而派生(Derivation)就是将我们身处于上述例子中的Layer2节点,通过不断并行执行派生程序,将获取到的unsafe区块逐步变为safe区块,并将已经是safe的区块逐步变为finalized的过程。
代码层深潜
好了船长,接下来让我们开始深潜吧!
获取batcher发送的batch transactions的data
我们先来看看当我们知道一个新的Layer1区块时,如何查看区块里面是否有batch transactions的数据。在这里,我们要先梳理一下所需要的模块,然后再针对这些模块进行查看。
- 首先要确定下一个Layer1区块的块号是多少。
- 将下一个区块的数据解析出来。
确定下一个区块的块号
通过查询当前origin.Number + 1的块高来获取最新的Layer1块。如果此块不存在,即匹配error和ethereum.NotFound,那么就代表当前块高即为最新的区块,下一个区块还未在Layer1上产生。如果获取成功,将最新的区块号记录在l1t.block中。
将区块的data解析出来
首先,通过InfoAndTxsByHash将刚才获取的区块的所有transactions拿到,然后调用DataFromEVMTransactions函数,将transactions、batcherAddr和config传入。为什么要传这些参数呢?因为在过滤这些交易时,需要保证batcher地址和接收地址的准确性。DataFromEVMTransactions接收到这些参数后,通过循环对每个交易进行地址的准确性过滤,找到正确的batch transactions。
从data到safeAttributes,使unsafe的区块safe化
在这一部分,首先将解析出来的data解析成frame并添加到FrameQueue的frames数组里面。然后从frames数组中提取一个frame,将frame初始化进一个channel并添加到channelbank中,等待该channel中的frames添加完毕后,从channel中提取batch信息,将batch添加到BatchQueue中,将BatchQueue中的batch添加到AttributesQueue中,用来构造safeAttributes,并将enginequeue中的safeblcok更新。最后通过调用ForkchoiceUpdate函数来完成EL层safeblock的更新。
- 数据 -> 帧(frame)
此函数通过NextData函数获取上一步的data,然后将此data解析后添加到FrameQueue的frames数组里面,并返回在数组中的第一个frame。
- 帧 -> 通道(channel)
NextData函数负责从当前channel bank中读出第一个channel中的raw data并返回,并调用NextFrame获取frame并装载到channel中。
- 通道 -> 批次(batch)
NextBatch函数主要负责将刚才解码出来的raw data,解析成具有批次结构的数据并返回。
- 批次 -> safeAttributes
NextAttributes函数传入当前Layer2的safe区块头后,将块头和上一步获取的批次传递到CreateNextAttributes函数中,构造出safeAttributes。
CreateNextAttributes函数内部调用PreProcessLoadAttributes函数,主要负责锚定交易和deposit交易。最后将批次的交易和PreProcessLoadAttributes函数返回的交易拼接起来后返回。
- safeAttributes -> safe block
在这一步,会先将engine queue中的safehead设置为safe,但这并不代表这个区块是safe的了,还必须通过ForkchoiceUpdate函数来将EL中的区块状态更改为safe。最后,将safehead加入到finalizedL1队列中,以供后续的finalized使用。
将safe区块finalized化
在派生过程中,safe区块并不是真正牢固安全的区块,它还需要进行进一步的最终化确定,即finalized化。当一个区块的状态转变为safe时,从此区块派生的来源(L1(batcher transaction))开始计算,经过两个L1 epoch(64个区块)后,该safe区块可以被更新成finalzied状态。
循环触发
在op-node/rollup/driver/state.go的eventLoop函数中,通过间接执行op-node/rollup/derive/engine_queue.go中的Step函数,实现了整个派生过程的循环执行。
总结
整个派生功能看似非常复杂,但其实只要每个环节都拆解开来看,就不难理解。官方的那篇specs难以理解的原因在于,它的batch,frame,channel等概念容易让人迷茫。所以,如果你在看完这篇文章后,还是感到迷茫,我建议你再回过头去继续阅读我们的03-how-batcher-works。
还有,在阅读完本文之后,如果你对这个话题有什么看法或者疑问,欢迎在下方留言,让我们一起讨论吧!
参考资料: