火爆区块链 | Layer1到Layer2:opstack是如何派生出来的

探讨乐观主义变形器的奥秘:L2派生原理

“L2派生原理:背后的魔法”

作者:joohhnnn

介绍

各位数字资产投资者们,认真听好了,今天我要和大家聊聊opstack是如何从Layer1中派生出来Layer2的这件大事!在开始之前,请务必先阅读一下这篇官方介绍,它会告诉你整个派生过程的细节。

嗯,如果你现在读完这篇文章还有点困惑,别着急,放心这很正常。但是,请你牢记这份感觉,因为当你读完我们的这篇分析后,再回过头来再读一遍,你会发现官方的那篇文章在凝练方面真的做得很牛啊,把所有要点和细节都讲得明明白白!

好了,现在我们正式进入正题。我们都知道,Layer2的运行节点是可以从Layer1(DA层)中获取数据,并且构建出完整的Layer2区块数据的。今天我们就来讲解一下这个过程中是如何在codebase中实现的。

你有哪些问题?

我们先来思考一下,如果这个系统让你来设计,你会碰到哪些问题呢?

  1. 当你启动一个新节点的时候,整个系统是如何运行的?
  2. 你需要一个个去查询所有Layer1的区块数据吗?如何触发查询?
  3. 当你拿到Layer1区块的数据后,你需要哪些数据?
  4. 派生过程中,区块的状态是怎么变化的?如何从unsafe变成safe再变成finalized
  5. 官方规范中晦涩的数据结构batch/channel/frame这些到底是干嘛的?

什么是派生(Derivation)?

在理解派生之前,我们先来聊一聊optimism的基本Rollup机制吧。我们可以简单以一笔L2上的转账交易为例。

你在optimism网络上发起一笔转账交易,这笔交易会被”转发”给sequencer节点,由sequencer进行排序,并封装成一个包含你的交易的区块A,并进行区块的广播(也就是出块)。这时,区块A的状态为unsafe。随后,在一个固定的时间间隔之后(比如4分钟),sequencerbatcher模块将这四分钟内所有收集到的交易(包括你的转账交易)通过一笔交易发送到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的数据。在这里,我们要先梳理一下所需要的模块,然后再针对这些模块进行查看。

  1. 首先要确定下一个Layer1区块的块号是多少。
  2. 将下一个区块的数据解析出来。

确定下一个区块的块号

通过查询当前origin.Number + 1的块高来获取最新的Layer1块。如果此块不存在,即匹配errorethereum.NotFound,那么就代表当前块高即为最新的区块,下一个区块还未在Layer1上产生。如果获取成功,将最新的区块号记录在l1t.block中。

将区块的data解析出来

首先,通过InfoAndTxsByHash将刚才获取的区块的所有transactions拿到,然后调用DataFromEVMTransactions函数,将transactionsbatcherAddrconfig传入。为什么要传这些参数呢?因为在过滤这些交易时,需要保证batcher地址和接收地址的准确性。DataFromEVMTransactions接收到这些参数后,通过循环对每个交易进行地址的准确性过滤,找到正确的batch transactions

从data到safeAttributes,使unsafe的区块safe化

在这一部分,首先将解析出来的data解析成frame并添加到FrameQueueframes数组里面。然后从frames数组中提取一个frame,将frame初始化进一个channel并添加到channelbank中,等待该channel中的frames添加完毕后,从channel中提取batch信息,将batch添加到BatchQueue中,将BatchQueue中的batch添加到AttributesQueue中,用来构造safeAttributes,并将enginequeue中的safeblcok更新。最后通过调用ForkchoiceUpdate函数来完成EL层safeblock的更新。

  • 数据 -> 帧(frame)

此函数通过NextData函数获取上一步的data,然后将此data解析后添加到FrameQueueframes数组里面,并返回在数组中的第一个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.goeventLoop函数中,通过间接执行op-node/rollup/derive/engine_queue.go中的Step函数,实现了整个派生过程的循环执行。

总结

整个派生功能看似非常复杂,但其实只要每个环节都拆解开来看,就不难理解。官方的那篇specs难以理解的原因在于,它的batchframechannel等概念容易让人迷茫。所以,如果你在看完这篇文章后,还是感到迷茫,我建议你再回过头去继续阅读我们的03-how-batcher-works

还有,在阅读完本文之后,如果你对这个话题有什么看法或者疑问,欢迎在下方留言,让我们一起讨论吧!

参考资料:

  1. joohhnnn
  2. source
  3. 第一章
  4. 第二章
  5. 第三章
  6. 第四章
  7. 第五章