深入理解 OVM

为什么需要 OVM ?
我们团队中的许多成员都曾参与过致力于支持智能合约的第一代通用 plasma 网络(plapps)的架构设计。然而,部署 plapps 需要借助一整套新的开发工具(包括功能受限的 “predicate” 合约)。我们很快意识到,人们对于以太坊 Layer 2 的期待远不止此—— 以太坊 L2 不只意味着扩展以太坊的应用,还要扩展以太坊本身。
以上原因促使我们开发了 Optimistic Rollup —— 首个能将以太坊智能合约的全部功能引入扩展层的 L2 架构。
Unipig.exchange 展示了这个前所未见的新功能:这是 Uniswap 第一次部署在 L2 上;不过如果要让 Uniswap 套用 
rollup,仍然要求我们编写特定的 L2 智能合约 。为了创造更好的开发体验,我们还有很长的路要走。
什么是 OVM ?
OVM 是个功能完备、与 EVM 完全兼容的执行环境,适用于 L2 系统。我们可以通过 OVM 在 rollup 链进行在以太坊主链上能做的所有操作 —— 编写 Solidity 智能合约,并通过 Web3 API 与区块链进行交互。
有了 OVM 后,将 dApp 移植到 L2 不再是架构级别的大工程,只剩下简化的部署操作。当然,dApp 的构建仍然要考虑紧耦合( tight coupling )及可组合性( composability ),但只要你需要,新的智能合约随时能够部署到使用 OVM 的链上。换言之,在 L2 打造货币乐高( money lego,泛指 DeFi 产品)依然可以非常方便。
所以,OVM 是怎么办到的?OVM 实现起来为什么这么困难?让我们一探究竟!
问题描述:EVM 中的 EVM

所有乐观的 L2 方案都是围绕着执行结果及其分歧来打造的:从 plasma 到 rollup,其关键都在于 “乐观性执行( optimistic execution )”—— 乐观性执行的意思是:任何人(或群体)都能宣称 “嘿,Layer 1,这些交易的执行结果是 X,不用再执行验证了!”;如果结果不为 X ,(我们假设)会有其他群体愿意支付主链的执行成本,来证明结果 X 是错误的。

理想情况下我们不需要在主链上执行 transaction,这也是为什么乐观性执行能很大程度上提高链的吞吐量。然而考虑到安全性,一旦出现错误的情况(如上图 tx2),我们也需要有 transaction 回溯机制!
Unipig 的自定义代码基本上就是 Unipig 编码形式的 execute_L2_tx(),你也可以称之为 execute_uniswap_tx() !
总的来说,其实我们需要的是 Unipig 编码实现 execute_EVM_tx() —— 一个能够让我们在 L1 transaction 中,嵌套执行任何以太坊 L2 transaction 的函数(以实现 fraud proof “错误性证明” 功能)。但是理想很丰满,现实很骨感,要让以太坊 transaction 嵌套执行本来就非常困难,更何况有些 L2 transaction 根本不适合 L1 !

为什么很难构建 EVM 中的 EVM
在我们深入解释我们所提出的独特解决方案之前,我们先想想 ——为什么这会成为一个问题 ?难道 EVM 不是执行 EVM transaction 的完美环境吗?它可是 EVM 呢!
天真的想法:将 L2 的智能合约重新部署到 L1
EVM 的核心定义了一组计算机指令,以及定义了在 transaction 中每个指令对应地该执行什么。智能合约就像是个巨大又丑陋的指令集合——举个简单的例子,下图是 Solidity 的 SafeMath.sol 库在部署之前的部分编译:

如果我们想要在 L1 上执行 L2 transaction,直观的做法就是获取 L2 使用的代码(智能合约),然后放到 L1;简言之,就是直接在 L1 上部署对应的 L2 智能合约!

然而不能这样做,因为:不同的链,不同的结果
这个方法可能适用于某些情况。比如逻辑非常简单的智能合约—— SafeMath 库,它只执行加、减等数学运算;如果我们将 L2 的 SafeMath 合约部署到 L1 上,则它在 L1 上也能正确执行!毕竟加就是加、减就是减,跟在哪条链执行无关。
但对于其他智能合约来说,事情就变得复杂了。举个简单的例子,下面的智能合约执行后会返回“当前的以太坊(区块)的时间戳 + 42”:
contract TimeShifter {
   function getShiftedTime() returns(uint) {
      return block.timestamp + 42;
   }
}
(在错误性挑战中)把这个合约重新部署到 L1 上之后,还能返回相同的值吗?

明显不行!(重部署在当前块 “之后的区块”,返回结果肯定不同。)即使是在同一条 L1 上,如果将智能合约重部署在不同的两个区块,返回值也不一样 —— 因为重部署的合约会获取 L1 的时间戳,而正确执行 execute_l2_tx 则应该返回 L2 的时间戳。
如果你深入思考,你会发现这个问题几乎会发生在所有智能合约上。比如对于某个 ERC 20 智能合约来说,你将合约重部署在 L1 上之后,你要怎么设置 L2 上的余额呢?诸如此类,不可胜数。
解决之道:OVM
过去曾出现过两种解决 “ EVM 中的 EVM ” 问题的办法:要么是对 EVM 进行分叉,要么是硬着头皮用 Solidity 重新实现整个 EVM ;OVM 是一种全新的方法,对于当前的以太坊 1.0 有着更好的性能和灵活性,而且不需要分叉!
容器化:执行管理器

OVM 能够解决问题的最重要原因是,它引入了一个全新的智能合约(Execution Manager,执行管理器)—— 作为 OVM 智能合约的虚拟容器。执行管理器会虚拟化所有可能导致 L1、L2 出现不同结果的执行,包括:
· 智能合约存储内容
· 交易内容 —— 如区块高度、时间戳、tx.origin (Solidity 的一个全局变量,它遍历整个调用栈并返回最初发送调用(或事务)的帐户的地址),等等。
· 跨合约信息的路由
基本上,对于可能导致 L1 、 L2 出现不同结果的 EVM 功能, 执行管理器都提供了保证其结果一致的函数。
举例来说,我们构造一个容器来解决上述提到的时间戳不一致的问题:
contract TimestampManager {
    uint storage ovmTimestamp;
    function setOvmTimestamp(number: uint) {
        ovmTimestamp = number;
    }
    function getOvmTimestamp() public returns(uint) {
        return ovmTimestamp;
    }
}
现在我们重部署上面的合约,这回我们使用虚拟容器:
contract OvmTimeShifter {
   function getShiftedTime() returns(uint) {
      return timestampManager.getOvmTimestamp() + 42;
     }
}
如此一来,我们就能够在验证 fraud proof 的时候,设置 L1 容器中的 “虚拟区块高度”,来保证正确的返回值!

这就是 ” EVM 中的 EVM ” —— OVM 的核心概念:虚拟化所有可能在不同链上返回不同结果的 EVM 组件。具体点来说,约有 15 条以太坊指令需要被虚拟化,你可以从以下入口查看真正的执行管理器长啥样。
安全性:容器纯度检查
当然我们还需要稍微修改上面的合约,才能真正调用 timestamp 容器而不是拿到错误的 block.timestamp。
虽然我们解决了结果差异性的问题,但这只作用于该智能合约而已。因此,为了保障 L2 的安全性,我们需要确保 L2 上的所有合约都使用了 timestamp 容器,没有错误使用 block.timestamp 的漏网之智能合约。

OVM 提供了 “容器纯度检查” 的服务 —— 检查目标智能合约 “是否只通过执行管理器来调用虚拟化指令”,而不允许像是 block.timestamp 这样的操作!不论有没有其他智能合约调用了目标合约,只要(目标)合约未通过检查,就无法部署到 OVM。这样就能保证 L2 的安全性。
开发体验:转译器
要让智能合约只通过执行管理器来调用某些指令,还有一个问题就是开发体验 —— 如果开发者需要遍历整份智能合约,然后把所有 block.timestamp 替换为 getOvmTimestamp(),这种费力不讨好的活肯定没人愿意做。
为了解决这个问题,我们搭了一个转译器 —— 输入普通 EVM 字节码,然后转译器会输出使用上述容器的 OVM 字节码。对于使用转译器的开发者来说,完全不需要和 OVM 直接打交道 —— 只需要在 Waffle 、Truffle 等你喜欢的测试套件中加入我们的 solc-transpiler 包。
展望
我们认为 OVM 的出现代表着以太坊 L2 的飞跃,因为它不同于变着招 使用 以太坊,它就是以太坊本身的进步。只要加上几行代码,就能够实现快速且低成本的 Solidity 智能合约迁移,这也是当前关于以太坊扩展方面最令我们兴奋的 topic 。如果你想要自行体验一把,可以关注我们最近的 OVM 测试——在标准的以太坊工具中(如 Graph 和 Burner 钱包),实时运行部分的 Synthetix 复杂交易合约(见此处)。