Optimism和OP Stack学习

  • maodaishan
  • 更新于 2024-03-09 11:43
  • 阅读 419

对Optimism , OP Stack的技术做了简要描述。 主要内容来自Optimism的官方文档 (2023.9)

整体流程:

deposit和发交易: <!--StartFragment-->

<!--EndFragment-->

image.png withdraw: <!--StartFragment-->

<!--EndFragment-->

image.png

OP主网安全模型: 目前OP主网的安全性依赖于多签,由几个匿名人士掌握着这些钱包。 在 OP 主网中,区块每两秒生成一次,即使没交易也会生成空区块。每个L2区块都归属于epoch,epoch是指L2区块归属于的L1块,这个L1块一般是几分钟前的某个L1块。epoch的编号等于L1上对应块的号。L2块一般由epoch和它在epoch中的序号标识。 epoch的第一个块中包含了所有从L1发往L2的交易(统称deposit)。如果Sequencer忽略了某个deposit交易,这也算是作恶,会被抓出来。 OP交易处理流程: <!--StartFragment-->

<!--EndFragment-->

image.png 交易压缩: 由op-batcher完成。它有两个功能: ● 将交易压缩成批次。 ● 将这些批次发布到 L1 当通道满了,或者超时时,就会把压缩交易发布到L1。 超时时间: 以L1的区块时间来计算,5个块,5*12=60s 压缩: 有两个参数可以控制压缩: ○ L1上的目标大小。--target-l1-tx-size-bytes ○ 预期的压缩比:--approx-compr-ratio 交易发布 当通道已满时,它会作为单个事务或多个事务(取决于数据大小)发布到 L1。 已处理的 L2 事务存在以下三种状态之一: ● 不安全:交易已被处理,但尚未写入 L1。某些情况可能会导致这些事务被丢弃。 ● 安全:交易已被处理并写入 L1。此时如果L1区块重组,还是有可能会被删除。 ● 最终确定:交易被写入 L1 ,且已经足够老以完成确定。 交易处理 交易处理分为两步:

  1. op-geth处理交易,获得新的世界状态
  2. 由op-proposer将merkle根提交到L1 Deposit <!--StartFragment-->

<!--EndFragment-->

image.png L1上的处理

  1. EOA或者合约,调用L1CrossDomainMessenger的sendMessage,有3个参数(例子) a. _target,L2上的目标地址 b. _message,L2交易的calldata,ABI编码 c. _minGasLimit,这个需要注意,这是最小值。在L2上执行时,gasLimit比这个值只大不小,因为还有一些前置工作要做。
  2. L1CrossDomainMessenger调用自己的_send函数,参数如下: a. _to,目标地址,对deposit来说,始终是0x4200000000000000000000000000000000000007 b. _gasLimit c. _value,发送到L2的ETH,取自msg.value d. _data,要中继的数据
  3. _sendMessage,调用portal的depositTransaction接口。注意其他合约也可以调用depositTransaction,但这样会绕过一些数据审查,所以不推荐。
  4. depositTransaction做一些安全检查,然后发出TransactionDeposited事件。 L2上的处理
  5. op_node检查收到的TransactionDeposited事件,有的话就解析它
  6. op_node将事件depositedTransaction.
  7. 多数情况下,调用 L2CrossDomainMessenger的relayMessage处理
  8. relayMessage做一些安全检查,没问题的话,使用relay的calldata,调用真正的目标合约。 DOS处理 deposit交易是在L1发起,L2执行的。所以L1上的费用由用户支付,L2上的费用由OP节点支付。因此需要引入一个gasLimit来防止DOS攻击。这个gasLimit是在TransactionDeposited事件里写入的,在L1上完成收费。 https://github.com/ethereum-optimism/optimism/blob/62c7f3b05a70027b30054d4c8974f44000606fb7/packages/contracts-bedrock/contracts/L1/ResourceMetering.sol#L162 Withdraw 从L2发往L1的交易统称withdraw。withdraw需要发3笔交易:
  9. 在L2上发起提款交易
  10. 在L1上提交提款证明,这是一个基于L2ToL1MessagePasser的merkle根证明
  11. 挑战期结束后,在L1上执行交易,取走资产。 具体交易流程见https://community.optimism.io/docs/protocol/withdrawal-flow/#withdrawal-initiating-transaction

OVM和欺诈证明

参考:https://medium.com/taipei-ethereum-meetup/optimistic-rollup-%E7%9A%84%E6%8C%91%E6%88%B0%E6%A9%9F%E5%88%B6-%E4%B8%80-optimism-ovm-1-0-2b6a8e9d64cd 这部分是OP的核心,即如何在L1上完成欺诈证明挑战。 Optimism出过两个版本,V1和V2,分别看下。

OVM V1

核心思想是:交易是在L2运行的,状态根提交到了L1上。当有人发现提交到L1的状态根不对(某笔交易的执行结果不对)时,需要在L1上发起挑战。挑战就是在L1上把这笔交易重新执行一遍,看是谁错了,如果挑战成功,就得回滚。 所以这里的重点是:从L1重新部署L2上的合约,然后执行挑战的那笔交易,比较结果。 难点是:在L1上,Context都跟L2上执行时不一样,如何在L1上重现L2的执行环境+上下文?这里上下文包括如:block.time,block.height, msg.sender等,其中最关键的是:SLOAD,也就是执行中用到的状态。这些如何处理? OP修改了编译器,把这些相关的opcode的处理做了定制化。所以同一套solidity代码,在OP上部署时,需要用OVM compiler重新编译,里面所有跟Context相关的都变了。 以SLOAD为例,它会加载storage里的状态,实际上这个状态需要提前在L1上先用一笔交易填上去,然后下次执行挑战交易的时候,SLOAD执行的时候就能读出正确数据了。注意交易里所有SLOAD要用到的值全都得加载进去。 提前加载的交易是否能加载错误数据来影响挑战结果呢?不行。因为这个是有merkle证明的,要符合merkle证明的才能被加载进去。 使用这些被OP改过的opcode,就可以在L1上完美执行来自L2的交易,完成裁决。这种方法也叫EVM in EVM。 这种方案是有很多代价的: ● 需要修改编译器,所以有的工具在OP没法直接用,得重新编写才行。geth等很多东西都得改。 ● 合约大小限制。目前合约限制24kb左右,OP的编译器会导致编译结果变大约15%,所以实际合约大小限制是20.8kb左右。对于本就受此限制影响的合约来说,更难了。 ● 为了防止SLOAD需要在L1加载的数据过多,导致挑战消耗的gas过大,L2上一个交易一个块。

OVM V2

这个新的OVM方案叫做Cannon,文档:https://medium.com/ethereum-optimism/cannon-cannon-cannon-introducing-cannon-4ce0d9245a03 V1的缺点是:需要修改编译器,geth等,导致很多工具都得改,对开发者不友好,维护也很困难。而且一个交易一个块。 所以对以上重新思考后,推出了OVM V2,即Cannon。Cannon的主要价值是:可以在L2上运行跟L1上完全一样的EVM。即实现了EVM等效性。 OVM的出发点还是如何进行作恶挑战。这是个交互式的挑战过程。在V1里,挑战的是一个交易,在V2里,挑战的是一个opcode。因此首先需要考虑要解决哪些问题:

  1. 如何定位到要挑战的opcode。
  2. 要挑战一笔opcode,应该需要哪些数据
  3. 如何在L1挑战一笔opcode。 执行opcode时,有哪些内容会影响到它的执行呢? ● 一些全局变量,如op, pc, gas, depth等 ● stack ● memory ● storage <!--StartFragment-->

<!--EndFragment-->

image.png 这些在执行时都是确定性的。因此如果我们把一笔交易拆开细化来看,每一个opcode执行完之后,都会有个确定的状态,这个状态可以被提取出来,形成merkle树,用树根就可以表达这个状态。 然后,我们通过比较挑战者和守护者的这个状态树根,用二分法来查找哪个opcode出现分歧。这应该很快,但它是交互式的,所以。。。未必很快。

这里有几个问题:

  1. 用EVM来表达上面这些状态方便吗?不方便的,用go方便,因为是现成的,就是geth。而且用geth的话,升级很方便。
  2. 在L1里,用EVM来模拟EVM,即V1的那种EVM in EVM,是必须的吗?能不能otherthing IN EVM?其实是可以的。 因此,考虑一种新的方案:MIPS in EVM 。所以最终结果是: OP对geth进行了裁剪,只留下了执行opcode相关的部分,称为mini geth。把minigeth编译成了MIPS opcode代码。用合约实现了MIPS opcode(才不到400行)。 烧脑开始: Cannon官方的解释是: <!--StartFragment-->

<!--EndFragment-->

image.png 所以在L2上,是: go代码执行mini geth, mini geth里运行EVM, EVM上运行MIPS的合约,合约里模拟的是mini geth, 上面运行的是EVM。 在L1上,是: EVM上运行MIPS合约,合约里模拟mini geth,上面运行EVM。 这样,在L1和L2上,就统一起来了,从以前的EVM兼容,变成了EVM等效。这样也不必再改编译器那些东西了,L1和L2上运行的是一样的合约,因为这个合约是从相当于一个mini geth模拟器里执行的。 preImage Oracle 但上面的东西只是完成了执行层面的一致性,L1上挑战时,还是得加载L2上的状态值才行。这通过preImage Oracle完成。 <!--StartFragment-->

<!--EndFragment-->

image.png 以太坊里的状态存在一棵树里,其实底层是通过一个个key-value对存储的。 例如要取node4的值,就先拿到root,然后node1,然后node4,是个递归过程。minigeth通过不停询问preImage Oracle最终获得需要的值。这个方式在链上和链下都可以做。 对链上,需要挑战者和被挑战者先把这些需要的值load到oracle里,后续在执行中就会通过oracle获取。在链下时就是通过rpc服务获取。 另外,对一些区块context的oracle可以直接在L1的块头中获得。例如可以把L2的一个epoch的块,其context都设置为一个L1上的块,这样在执行这个epoch内的所有交易时,对Context的获取都可以直接从L1块头中,而不需要用oracle专门写入了。 扩展: 既然EVM能模拟MIPS,那也可以模拟其他指令集,所以L2上就可以执行非EVM的虚拟机了。

OP Stack

OP Stack 是一套为 Optimism 提供支持的软件——即当前 Optimism 主网使用的软件。它最终将以 Optimism 超级链及其治理的形式出现。 随着超级链概念的出现,对于 Optimism 来说,轻松创建可在超级链生态系统中互操作的新链变得越来越重要。因此,OP Stack 主要专注于创建一个共享的、高质量的、完全开源的系统,用于创建新的 L2 区块链。通过协调共享标准 随着Optimism的发展,OP 堆栈也会发展。尽管 OP Stack 目前的核心是运行 L2 区块链的基础设施,但 OP Stack 理论上可以扩展到底层区块链之上的层,包括区块浏览器、消息传递机制、治理系统等工具。 现状 Optimism Bedrock 是 OP Stack的当前迭代。Bedrock 提供了用于启动 Optimistic Rollup 的工具。 今天的 OP Stack是为了支持Optimism Superchain 构建的,这是一个拟议的 L2 网络,共享安全性、通信层和通用开发堆栈(OP Stack本身)。OP Stack 的 Bedrock 版本可以轻松启动 L2,该 L2 启动时将与超级链兼容。OP 堆栈是一个不断发展的概念。它随着Optimism的增长而增长。 架构 <!--StartFragment-->

<!--EndFragment-->

image.png 并不是所有东西都已经是生产状态。上图中的部分内容不是最新的。例如cannon已经被部署了。 Derivation Derivation层用来对DA层的数据进行解释,将其翻译为可以直接用标准以太坊API作为执行输入的参数。Derivation层一般与DA层紧密耦合,因为它们配合在一起工作。 结算层 结算层用于在外部链(如L1 以太坊)上建立对OP链的视图(view),从而了解当前OP链的状态情况。结算层对外部链来说是只读的,可以让外部链通过这个view来做出决定,如从L2到L1的提款请求。

超级链

一个去中心化的区块链平台,由许多共享安全性和技术堆栈(OP Stack)的链组成。互操作性和标准化使工具和钱包能够同等对待各个链。 超级链是一个 L2 链网络,其中每条链称为 OP 链,它们共享安全性、通信层和开源技术堆栈。然而,与多链设计不同,这些链是标准化的,旨在用作可互换的资源。这使得开发人员能够构建以整个超级链为目标的应用程序,并抽象出应用程序运行的底层链。

<!--StartFragment-->

<!--EndFragment-->

image.png

<!--StartFragment-->

<!--EndFragment-->

image.png

将OP升级为超级链需要做哪些事(概述)

以下内容,需要逐一确认是否已经ready,可能已经好了,但是文档没更新。 将BedRock桥升级成为链工厂 BedRock在L1上部署合约用来定义L2链。包括链id,key定义,gas Limit等。 (这应该是待实现的)一旦链的定义数据在链上了,我们就可以创建一个工厂合约,为每条链来部署链配置,以及其他所需的合约。用CREATE2,就可以在有了链配置后,就把链桥的地址确定下来。这可以让链继承标准安全性。 使用链工厂导出OP Chain的数据 通过L1链,可以获得所有标准OP Chain的数据。OP节点应该可以在给定单个 L1 地址和到 L1 的连接的情况下确定性地同步任何OP 链。 当OP链同步时,链状态在本地计算。这意味着确定 OP 链的状态是完全无需许可且安全的。链推导不需要证明系统,因为所有无效交易都会被节点执行的本地计算过程忽略。然而,仍然需要一个证明系统来启用超级链withdraw。 用来withdraw的无需许可的证明系统 在Bedrock中,有一个permissioned的提款交易提交者,并且用户提款需要在L1上规定时间内提交特定交易。在未来,这将被修改。特定permissioned的提款交易提交者将被消除,任何人都可以提交提款申请。 每个链可配置的Sequencer 所有链共享升级路径 为了使初始超级链对安全性和去中心化充满信心,引入去中心化的安全委员会来管理升级。安全委员会应该能够更新链证明者集,延迟启动合约升级,并按下紧急桥暂停按钮,这也会取消待处理的升级。 在紧急情况下暂停桥梁的能力意味着,在最坏的情况下,即安理会参与者的私钥泄露的必要阈值,结果将是无限期暂停提款,桥梁升级将永久取消。换句话说,L1资金将被冻结。

Bedrock

组成:

● op-node ● op-geth ● op-batcher ● op-proposer ● contracts-bedrock ● fault-detector ● sdk ● chain-mon 数据可用性来自两个来源:

  1. Sequencer发布到L1上的rollup数据
  2. L1上调用deposit发往L2的交易。 协议包括以下主要部分: ● deposit通过直接与 L1 上的智能合约交互而写入canonical L2 链。 ● withdraw是写入canonical L2 链,隐式触发与 L1 上的合约和账户的交互。 ● Batches是与汇总上的批次相对应的数据写入。 ● 区块Derive是如何解释 L1 上的数据以理解canonical L2 链。 ● 证明系统定义了L1 上发布的输出根的最终性,以便它们可以被执行(例如,执行withdraw)。 BlockDerivation 协议保证在L1上deposit的时间顺序,在L2上也能被重现。 在L2上,每个epoch的开头第一个块必须包含从L1上发来的deposit交易。L2区块的交易时间戳与L1的保持一致。 RollUp Node BedRock上没有共识。共识是由Derivation完成的。执行客户端连接到定义block derivation的客户端,称为Rollup node。 rollup node是没有状态的。它负责通过从L1上读取data和deposite交易来导出系统状态。在bedrock里,rollupnode既可以为用户或其他rollupnode发来的交易排序,也可以通过单独依赖 L1 来验证在 L1 上发布的已确认交易。 下面是它的用途: 验证L2 canonical chain: 运行rollup node的最简单方式是仅follow L2 canonical chain。此方式下不需要连接到其他的rollup node,仅仅从L1读取calldata,并且通过derivation规则来解析数据。 此类节点的目的是为了验证L1上的输出根是否正确。 看起来这像是L2的轻节点。 参与L2网络 最常见的用法是参与L2的网络,跟踪 L2 的进度和状态。 在此模式下,汇总节点不仅从 L1 读取数据和deposite交易,并将其解释为块;而且接受其他rollup node网络的用户和peer的交易。 此网络的roll up 节点使用安全或者不安全的块头。安全的块头指来自L1的块头,无论它是否已终结;不安全的块头是指还没有提交到L1的块头,它来自sequencer或其他rollup node。当出现分歧的时候,总是相信L1的安全块头。对大部分dapp来说,可能它们用的是不安全的块头,因为这些块头更新。 L2交易排序 Rollup node的第三个用法是对L2的交易排序。也就是在不安全的块头上出新块,即Sequencer。注意当前OP网络里只有一个Sequencer。Sequencer也负责把L2块rollup到L1。 Batcher Batcher和rollup node都是sequencer的组件,Batcher从rollup node读取tx数据并将其解释为要写入 L1 的批处理事务。 批处理程序组件负责读取由排序器运行的rollup node的不安全 L2 头、创建批处理程序事务并将其写入 L1。 Standard Bridge Contracts 就是L1和L2之间的桥,负责ETH和ERC20代币的转移。一边是原生代币,另一边是包装币。

BedRock和L1的差异

<!--StartFragment-->

<!--EndFragment-->

image.png 如果在L2想获得最近的L1的数据(也就是下次打包用的L1的Context,即这些L2交易被“认为打入”的L1的Context),可以用getter函数访问合约L1Block。是个预置合约,0x4200000000000000000000000000000000000015 ● number: The latest L1 block number known to L2 ● timestamp: The timestamp of the latest L1 block ● basefee: The base fee of the latest L1 block ● hash: The hash of the latest L1 block ● sequenceNumber: The number of the L2 block within the epoch (the epoch changes when there is a new L1 block 地址别名 由于CREATE操作码的特性,用户可以在L1和L2上创建相同地址的合约,这可能会引入作恶行为。因此对tx.origin和msg.sender的行为,在L1和L2上有一定差异。 <!--StartFragment-->

<!--EndFragment-->

image.png 即,如果是EOA调用的话,在L2上跟L1是一样的。但是如果是L1的合约调用的话(即用L1合约调用deposite),tx.origin是L1合约地址加一个固定字符串。 msg.sender在第一层调用时,总是等于tx.origin,所以也会受上面规定的影响。一般来说,不能用tx.origin作为授权的检测。 块生产: <!--StartFragment-->

<!--EndFragment-->

image.png 其他: ● JSON RPC:OP的JSON RPC除了完全支持以太坊部分外,还引入了一些针对OP的特定接口。 ● EIP-155:在EIP-155之前的交易不支持链id,这可能引入重放攻击,所以OP默认不支持EIP155之前的交易。 ● 交易成本:OP的执行成本包括在L2上的执行成本和L1的数据成本。

点赞 1
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
maodaishan
maodaishan
0xee37...1912
江湖只有他的大名,没有他的介绍。