[]通过 EVM 代码默克尔化缩减见证数据大小( 二 )


  • 每个 basic block 都无法更改控制流(例如没有 jump 操作码) 。 因此 , 我们可以确定一旦开始执行代码 , 只会存在两种情况:正确执行结束 , 或是 gas 耗尽 。 虽然还没有和其他方案进行比较 , 我们先假设这么执行是相对更有效率的 。
  • 出于效率考量 , 我们合并相邻块 , 直到每个代码块都至少有 128 字节(可自行设置)为止 。 接着以第一个字节作为 key , 将这些合并后的代码块插进 Trie(树状数据结构) 。 最后 , 客户端将此 Trie 的根作为该合约账户的新记录存储下来 。 如下图所示 , 记录代码的 Trie 会成为状态树的子树(类似于 “存储树”) 。
    []通过 EVM 代码默克尔化缩减见证数据大小
    本文插图

    - 代码默克尔化之后 , 会成为状态树(state trie)的子树( sub-tree ) 。 为了简化 , 上图我用了二进制树 (而非以太坊使用的默克尔-帕特里夏树) , 同时树的路径也不准确 , 不能完整表示真实的 key -
    为了测试部署的合约 , 我们试着发起一笔调用该合约的交易 。 矿工会执行这笔交易 , 并标记执行过程中触及的每个 chunk(例子里假设触及 chunk#1 和 chunk#3 ) 。 当要发布区块的时候 , 矿工会将合约状态的证明 , 以及触及哪些代码 chunk 的 turboproof 证明 , 一起打包在区块内 。
    []通过 EVM 代码默克尔化缩减见证数据大小
    本文插图

    - 交易所触及的所有 chunk 和验证 codeRoot 所需的哈希值 ,都会以 turboproof 证明的形式发送出去-
    收到这个区块后 , 无状态客户端就能验证合约是否属于区块状态的一部分 , 也能验证合约的余额、nonce 、状态根、 codeRoot 等其他参数 。 这些信息足以让客户端从 chunk 中重构部分字节码 , 同时清空其他不需要的 chunk。 因为 chunk 算法的设计 , 所以客户端知道所有的 chunk(除了首个 chunk )都是从 JUMPDEST 开始 , 因此能够安全地进行jump操作 。
    []通过 EVM 代码默克尔化缩减见证数据大小
    本文插图

    - 我们可以通过 turboproof 重构字节码;对于交易不需要的 chunk 则设为 0 -
    实验
    为了验证 , 我们编写了一份测试原型 , 该原型可以从 Geth 客户端的 RPC 端口获取主网的区块和过去的状态 , 然后模拟执行交易 。 每当执行过程中遇到新的合约 , 就将合约拆分为多个 chunk , 并标记执行交易时触及的 chunk。 当区块中的交易全部执行完毕后 , 会为所触及的 chunk 生成证明 —— turboproof。
    接着重置状态 , 用 turboproof 重构出来的代码 , 替换掉原本的合约代码 , 然后再次执行刚才的交易 。 为了检查执行的正确性 , 我们比较前后两次消耗的 gas 量和区块的 bloom 过滤器 。
    对最近的 50 个区块执行此过程 , 我们可以看到合约代码量减少了40% ~ 60% 。
    []通过 EVM 代码默克尔化缩减见证数据大小
    本文插图

    提醒:上图的数据结果似乎令人充满希望 , 但请记住 , 我们还需要成千上万个区块中的数据 , 才能得出令人信服的实验结论;目前原型处于早期阶段 , 一切结论都还为时尚早!
    后续发展
    你应该还记得 , 每个代码块的最小长度是可设置的参数(文中取的是 128 字节) , 修改这个参数会在截然不同的两个方面影响见证的大小 。 假设我们将参数设为 32 字节 , 则 chunk 的粒度变得更小 , 要传递的代码量也就变得更少 。 但是这样一来 ,Trie 的深度就必须增加;换句话说 , 为了生成 chunks 的证明 , 我们需要进行更多次哈希运算 。
    所以下一步 , 我们将会深入分析——究竟要将区块最小长度设为多少 , 才能获得最优解 。 当然不论如何 , 只要将 hexary 字典树结构二进制 Trie, 我们就能减少 3/4 的哈希运算(详见此处) , 从而降低见证数据的大小 。


    推荐阅读