经典回顾 | 以太坊1.0的设计依据(下)

尽管以太坊借用了许多已经在诸如比特币这样的旧加密货币中试用并测试了五年的想法,但是以太坊中也有许多地方不同于当前处理特定协议特性的常见方式。此外,以太坊不得不开发一种全新的经济方法,以提供其它现有系统无法提供的特性。本文档旨在详细说明在构建以太坊协议的过程中所产生的细微甚至被忽视又或者是在某些情况下有争议的决策,并揭示这些方法和潜在的替代方案所涉及的风险。

尽管以太坊借用了许多已经在诸如比特币这样的旧加密货币中试用并测试了五年的想法,但是以太坊中也有许多地方不同于当前处理特定协议特性的常见方式。此外,以太坊不得不开发一种全新的经济方法,以提供其它现有系统无法提供的特性。本文档旨在详细说明在构建以太坊协议的过程中所产生的细微甚至被忽视又或者是在某些情况下有争议的决策,并揭示这些方法和潜在的替代方案所涉及的风险。

此篇文章的前两部分见:

经典回顾 | 以太坊1.0的设计依据(上)

经典回顾 | 以太坊1.0的设计依据(中)

虚拟机

以太坊虚拟机是执行交易代码的引擎,是以太坊和其它系统之间的核心差异。需要注意的是,虚拟机应该与合约和消息模型分开考虑。举个例子,SIGNEXTEND操作码是VM的一项功能,但“合约可以调用其它合约并指定子调用的gas限制”这一事实是合约和消息模型的一部分。 EVM中的设计目标包括:

  • 简单性:尽可能使用低级的操作码,并且操作码、数据类型和虚拟机层面的结构要尽可能少。

  • 完全确定性:VM规范的任何部分都不应该存在歧义,并且执行结果应该是完全确定的。此外,这里应该存在一个精确的计算步骤概念,我们可以对其进行测量,并计算gas的消耗量。

  • 节省空间:EVM组件应该尽可能紧凑(例如,不接受C语言程序所默认的4000字节的基本大小)

  •  令预期应用变得专业化:能够处理20字节的地址以及拥有32字节的值的自定义加密;能够在自定义加密中使用模数运算;读取区块和交易数据;与状态交互等

  •  简单的安全性:我们应该很容易就能构建出一个与操作相关的gas成本模型,这些操作将保护VM免遭攻击者的利用

  • 优化友好性:易于优化,以便构建JIT编译(即时编译)以及其它加速版本的VM。

我们还作出了一些特殊的设计决策:

  • 临时/永久存储的区别:临时存储存在于VM的每个实例中,并在VM执行完成时消失;永久存储存在于区块链状态,与帐户相关。举个例子,假设我们执行以下执行树(其中,S表示永久存储,M表示临时存储):

       (i)A调用B

       (ii)B设置B.S[0] = 5,B.M[0] = 9

       (iii)B调用C

       (iv)C调用B

此时,如果B尝试读取B.S[0] ,那么它将会接收到先前存储在B中的值5,但如果B尝试读取B.M[0] ,那么其将收到0,因为带有全新临时存储的虚拟机的新实例。如果B现在在这个内部调用中设置B.M[0] = 13和B.S[0] = 17,然后这个内部调用和C的调用都终止,那么执行最终将返回到B的外部调用,随后B读取M将看到B.M[0] = 9(因为这个值上一次被设置的时机就是在同一个VM执行实例中)并且B.S[0] = 17。如果B的外部调用结束,并且A再次调用B,那么B将看到B.M[0] = 0以及B.S[0] = 17。这个区别的目的是(1)分别为每个执行实例分配内存,使其不会因递归调用而遭受破坏,从而让安全编程更容易,以及(2)提供一种可以被非常快速地操作的内存形式,考虑到存储更新需要对树进行修改,这个过程必然很慢。

  • 堆栈/内存模型 :在早期的决策中,计算状态共有三种类型(除了指向下一条指令的程序计数器):

    (i)堆栈(32字节值的标准LIFO堆栈)

      (ii)内存(无限可扩展的临时字节数组)

      (iii)存储(永久存储)

在临时存储方面,堆栈和内存的替代方案是“只使用内存(memory-only)”范例,或者寄存器和内存混合方案(差别不大,因为寄存器基本上也是一种内存)。在这种情况下,每条指令都有三个参数,比如ADD R1 R2 R3:M[R1] = M[R2] + M[R3]。选择堆栈范例的原因很明显,它能够使代码规模缩减四倍。

  • 32字节的单词大小:我们也可以让一个单词的大小为4个或8个字节,或者和大多数架构(比如比特币)一样,不设限制。 4个或8个字节的单词大小对于存储地址和用于加密计算的大数而言限制太多,与此同时,无限制大小又难以构建出安全的 gas 模型。32字节大小比较理想,因为它足够存储在许多加密实现中常见的32字节大小的值和地址(并提供将地址和值打包成单个存储索引作为优化的能力),但又不会大到足以拖低效率。

  • 完全拥有属于我们自己的VM:另一种选择是重用Java,或者Lisp、Lua。我们确信我们更应该拥有专用VM,因为(i)我们的VM规范比其它虚拟机要简单得多,因为其它虚拟机为复杂性所付出的成本很低。而在以太坊中,每一个额外的复杂性单元都将遭遇重重障碍,因为复杂性的增加有可能导致中心化和安全漏洞,比如共识失败,(ii)VM的专用程度更高,比如支持32字节大小的单词,(iii)它使我们能够摆脱复杂的外部依赖,这种依赖可能会加剧安装的难度,以及(iv)考虑到特定的安全需求,我们需要对以太坊进行完整的安全性审查,这意味着对于外部VM,我们同样需要进行相同的工作。因此,开发专属VM跟使用外部VM的工作量相比,差不了太多。

  • 使用可变且可扩展的内存大小:我们认为限制内存大小是不必要的。如果内存大小太小,那么这种限制会带来诸多不便;而如果内存大小太大,内存的成本将过于昂贵。除此之外,在内存大小固定的情况下,系统每次访问内存还需要检查该访问是否超出边界,显然这样做的效率并不高。

  • 拥有1024个调用深度限制:相比内存或计算过载,许多编程语言在栈深度过大时更容易触发中断。所以区块 gas 限制中所隐含的限定是不够的。

  • 没有类型:这是为了遵循简单性。尽管如此,我们可以使用DIV,SDIV,MOD,SMOD的有符号和无符号操作码(事实证明,对于ADD和MUL,有符号和无符号操作码的运作是等效的)。此外,在普遍情况下,定点算术转换(深度高的定点算术是32字节单词所带来的另一个好处)也十分简单。比方说,在32位深度处,a * b -> (a * b) / 2^32, a / b -> a * 2^32 / b,并且 +, - 和 * 依然遵循整数运算中的定义。

在VM中,某些操作码的功能和目的是显而易见的,但其它操作码则不那么明显。这么做的理由是:

  • ADDMOD,MULMOD:在大多数情况下,mulmod(a, b, c) = a * b % c。然而,在许多类型的椭圆曲线加密的特定情况中,其所使用的是32字节模数运算,因此直接执行 a * b % c 运算实际上是在执行((a * b) % 2^256) % c,最终后者给出了一个完全不同的结果。在32字节空间中,计算包含32字节值的公式a * b % c不仅罕见,而且十分繁琐。

  • SIGNEXTEND:SIGNEXTEND的目的是方便较大的有符号整数到较小的有符号整数间的类型转换。小的有符号整数十分有用,因为在将来,JIT编译虚拟机可能能够检测到长时间运行的代码块(这些代码主要处理32字节整数),并极大地提高速度。

  • SHA3:SHA3与以太坊代码的契合度非常高,因为使用存储的安全高强度哈希映射可能需要使用安全的哈希函数以防止恶意冲突,并用于验证默克尔树以及类似于以太坊的数据结构。最关键的一点是,与SHA3类似的哈希函数如SHA256,ECRECOVER 和 RIPEMD160都不是作为操作码,而是作为伪合约被包含在内。这么做的目的是将它们放在一个单独的类别中,如此一来,如果(或“当”)我们后续提出一个适当的“原生扩展”系统,我们可以添加更多类似的合约,而无需扩展操作码的类别。

  • ORIGIN:ORIGIN操作码会提供交易发送方的信息,其主要用途是允许合约退还 gas。

  • COINBASE:COINBASE操作码的主要用途是(i)允许子货币为网络安全做出贡献,以及(ii)允许使用矿工作为去中心化的经济集合以作用于类似于谢林币(Schellingcoin)这种基于子共识的应用。

  • PREVHASH:用作半安全的随机源,并允许合约评估前一个区块中的状态的默克尔树证明,从而避免使用高度复杂的递归“以太坊轻客户端”结构。

  • EXTCODESIZE,EXTCODECOPY:这里的主要用途是允许合约依照模板检查其它合约的代码,甚至在与代码交互之前对其进行模拟。相关应用详情请参阅http://lesswrong.com/lw/aq9/decision_theories_a_less_wrong_primer/。

  • JUMPDEST:当跳转目的地被限制为几个索引时,JIT编译虚拟机的实现会更加容易(可变目的地跳转的计算复杂度是O(log(有效跳转目的地的数量)),而静态跳转的时间总是恒定的)。因此,我们需要(i)对有效可变跳转的目的地进行限制,以及(ii)激励开发者使用静态跳转,少用动态跳转。为了实现这两个目标,我们制定了以下规则:(i)紧接在push操作之后的跳转可以跳到任何地方,但不能跳转到另一个跳转处,以及(ii)其它跳转只能跳转到JUMPDEST。通过对跳转进行限制,我们便可以通过简单地查看代码中的先前操作来确定该跳转到底是动态的还是静态的。也就是说,对执行静态跳转的JUMPDEST操作的需求的缺乏恰恰是使用它们的动机。与此同时,禁止跳转到push数据也会加快JIT虚拟机编译和执行的速度。

  • LOG: LOG 用于记录事件,具体请参阅《经典回顾 | 以太坊1.0的设计依据(上)》中关于树(trie)的内容。

  • CALLCODE:这个操作码的用途是允许合约以存储在其它合约中的代码形式来调用“函数”。该合约使用单独的堆栈和内存,但使用的是合约自己的存储。如此一来,在区块链上可扩展地实现代码“标准库”会更加容易。

  • SELFDESTRUCT:这是一个允许合约在其自身不再需要时快速自删的操作码。SELFDESTRUCT并非立即执行,而是在交易执行结束之后再执行。这么做的原因是,恢复那些已经被执行的SELFDESTRUCT操作码将会大幅增加缓存的复杂度,而缓存在高效的VM实现中是必不可少的 。

  • PC:虽然理论上没有必要,因为PC操作码的所有实例都可以通过简单地将该索引处的实际程序计数器作为push来替换,但在代码中使用PC能够创建出与位置无关的代码(即可被复制/粘贴到其它合约的编译函数,并且如果它们最终出现在不同的索引中,也不会发生中断)。

经典回顾 | 以太坊1.0的设计依据(下)

本文翻译:喏呗尔

原文作者:Vitalik Buterin

原文链接:https://github.com/ethereum/wiki/wiki/Design-Rationale

相关阅读:

Vitalik:以太坊 Serenity 设计依据综述

【文章版权归原作者所有,其内容与观点不代表Unitimes立 场。翻译文章仅为传播更有价值的信息,合作或授权请联系我们】

生成图片
4

发表评论

经典回顾 | 以太坊1.0的设计依据(下)

星期日 2019-02-17 21:58:37

尽管以太坊借用了许多已经在诸如比特币这样的旧加密货币中试用并测试了五年的想法,但是以太坊中也有许多地方不同于当前处理特定协议特性的常见方式。此外,以太坊不得不开发一种全新的经济方法,以提供其它现有系统无法提供的特性。本文档旨在详细说明在构建以太坊协议的过程中所产生的细微甚至被忽视又或者是在某些情况下有争议的决策,并揭示这些方法和潜在的替代方案所涉及的风险。

此篇文章的前两部分见:

经典回顾 | 以太坊1.0的设计依据(上)

经典回顾 | 以太坊1.0的设计依据(中)

虚拟机

以太坊虚拟机是执行交易代码的引擎,是以太坊和其它系统之间的核心差异。需要注意的是,虚拟机应该与合约和消息模型分开考虑。举个例子,SIGNEXTEND操作码是VM的一项功能,但“合约可以调用其它合约并指定子调用的gas限制”这一事实是合约和消息模型的一部分。 EVM中的设计目标包括:

  • 简单性:尽可能使用低级的操作码,并且操作码、数据类型和虚拟机层面的结构要尽可能少。

  • 完全确定性:VM规范的任何部分都不应该存在歧义,并且执行结果应该是完全确定的。此外,这里应该存在一个精确的计算步骤概念,我们可以对其进行测量,并计算gas的消耗量。

  • 节省空间:EVM组件应该尽可能紧凑(例如,不接受C语言程序所默认的4000字节的基本大小)

  •  令预期应用变得专业化:能够处理20字节的地址以及拥有32字节的值的自定义加密;能够在自定义加密中使用模数运算;读取区块和交易数据;与状态交互等

  •  简单的安全性:我们应该很容易就能构建出一个与操作相关的gas成本模型,这些操作将保护VM免遭攻击者的利用

  • 优化友好性:易于优化,以便构建JIT编译(即时编译)以及其它加速版本的VM。

我们还作出了一些特殊的设计决策:

  • 临时/永久存储的区别:临时存储存在于VM的每个实例中,并在VM执行完成时消失;永久存储存在于区块链状态,与帐户相关。举个例子,假设我们执行以下执行树(其中,S表示永久存储,M表示临时存储):

       (i)A调用B

       (ii)B设置B.S[0] = 5,B.M[0] = 9

       (iii)B调用C

       (iv)C调用B

此时,如果B尝试读取B.S[0] ,那么它将会接收到先前存储在B中的值5,但如果B尝试读取B.M[0] ,那么其将收到0,因为带有全新临时存储的虚拟机的新实例。如果B现在在这个内部调用中设置B.M[0] = 13和B.S[0] = 17,然后这个内部调用和C的调用都终止,那么执行最终将返回到B的外部调用,随后B读取M将看到B.M[0] = 9(因为这个值上一次被设置的时机就是在同一个VM执行实例中)并且B.S[0] = 17。如果B的外部调用结束,并且A再次调用B,那么B将看到B.M[0] = 0以及B.S[0] = 17。这个区别的目的是(1)分别为每个执行实例分配内存,使其不会因递归调用而遭受破坏,从而让安全编程更容易,以及(2)提供一种可以被非常快速地操作的内存形式,考虑到存储更新需要对树进行修改,这个过程必然很慢。

  • 堆栈/内存模型 :在早期的决策中,计算状态共有三种类型(除了指向下一条指令的程序计数器):

    (i)堆栈(32字节值的标准LIFO堆栈)

      (ii)内存(无限可扩展的临时字节数组)

      (iii)存储(永久存储)

在临时存储方面,堆栈和内存的替代方案是“只使用内存(memory-only)”范例,或者寄存器和内存混合方案(差别不大,因为寄存器基本上也是一种内存)。在这种情况下,每条指令都有三个参数,比如ADD R1 R2 R3:M[R1] = M[R2] + M[R3]。选择堆栈范例的原因很明显,它能够使代码规模缩减四倍。

  • 32字节的单词大小:我们也可以让一个单词的大小为4个或8个字节,或者和大多数架构(比如比特币)一样,不设限制。 4个或8个字节的单词大小对于存储地址和用于加密计算的大数而言限制太多,与此同时,无限制大小又难以构建出安全的 gas 模型。32字节大小比较理想,因为它足够存储在许多加密实现中常见的32字节大小的值和地址(并提供将地址和值打包成单个存储索引作为优化的能力),但又不会大到足以拖低效率。

  • 完全拥有属于我们自己的VM:另一种选择是重用Java,或者Lisp、Lua。我们确信我们更应该拥有专用VM,因为(i)我们的VM规范比其它虚拟机要简单得多,因为其它虚拟机为复杂性所付出的成本很低。而在以太坊中,每一个额外的复杂性单元都将遭遇重重障碍,因为复杂性的增加有可能导致中心化和安全漏洞,比如共识失败,(ii)VM的专用程度更高,比如支持32字节大小的单词,(iii)它使我们能够摆脱复杂的外部依赖,这种依赖可能会加剧安装的难度,以及(iv)考虑到特定的安全需求,我们需要对以太坊进行完整的安全性审查,这意味着对于外部VM,我们同样需要进行相同的工作。因此,开发专属VM跟使用外部VM的工作量相比,差不了太多。

  • 使用可变且可扩展的内存大小:我们认为限制内存大小是不必要的。如果内存大小太小,那么这种限制会带来诸多不便;而如果内存大小太大,内存的成本将过于昂贵。除此之外,在内存大小固定的情况下,系统每次访问内存还需要检查该访问是否超出边界,显然这样做的效率并不高。

  • 拥有1024个调用深度限制:相比内存或计算过载,许多编程语言在栈深度过大时更容易触发中断。所以区块 gas 限制中所隐含的限定是不够的。

  • 没有类型:这是为了遵循简单性。尽管如此,我们可以使用DIV,SDIV,MOD,SMOD的有符号和无符号操作码(事实证明,对于ADD和MUL,有符号和无符号操作码的运作是等效的)。此外,在普遍情况下,定点算术转换(深度高的定点算术是32字节单词所带来的另一个好处)也十分简单。比方说,在32位深度处,a * b -> (a * b) / 2^32, a / b -> a * 2^32 / b,并且 +, - 和 * 依然遵循整数运算中的定义。

在VM中,某些操作码的功能和目的是显而易见的,但其它操作码则不那么明显。这么做的理由是:

  • ADDMOD,MULMOD:在大多数情况下,mulmod(a, b, c) = a * b % c。然而,在许多类型的椭圆曲线加密的特定情况中,其所使用的是32字节模数运算,因此直接执行 a * b % c 运算实际上是在执行((a * b) % 2^256) % c,最终后者给出了一个完全不同的结果。在32字节空间中,计算包含32字节值的公式a * b % c不仅罕见,而且十分繁琐。

  • SIGNEXTEND:SIGNEXTEND的目的是方便较大的有符号整数到较小的有符号整数间的类型转换。小的有符号整数十分有用,因为在将来,JIT编译虚拟机可能能够检测到长时间运行的代码块(这些代码主要处理32字节整数),并极大地提高速度。

  • SHA3:SHA3与以太坊代码的契合度非常高,因为使用存储的安全高强度哈希映射可能需要使用安全的哈希函数以防止恶意冲突,并用于验证默克尔树以及类似于以太坊的数据结构。最关键的一点是,与SHA3类似的哈希函数如SHA256,ECRECOVER 和 RIPEMD160都不是作为操作码,而是作为伪合约被包含在内。这么做的目的是将它们放在一个单独的类别中,如此一来,如果(或“当”)我们后续提出一个适当的“原生扩展”系统,我们可以添加更多类似的合约,而无需扩展操作码的类别。

  • ORIGIN:ORIGIN操作码会提供交易发送方的信息,其主要用途是允许合约退还 gas。

  • COINBASE:COINBASE操作码的主要用途是(i)允许子货币为网络安全做出贡献,以及(ii)允许使用矿工作为去中心化的经济集合以作用于类似于谢林币(Schellingcoin)这种基于子共识的应用。

  • PREVHASH:用作半安全的随机源,并允许合约评估前一个区块中的状态的默克尔树证明,从而避免使用高度复杂的递归“以太坊轻客户端”结构。

  • EXTCODESIZE,EXTCODECOPY:这里的主要用途是允许合约依照模板检查其它合约的代码,甚至在与代码交互之前对其进行模拟。相关应用详情请参阅http://lesswrong.com/lw/aq9/decision_theories_a_less_wrong_primer/。

  • JUMPDEST:当跳转目的地被限制为几个索引时,JIT编译虚拟机的实现会更加容易(可变目的地跳转的计算复杂度是O(log(有效跳转目的地的数量)),而静态跳转的时间总是恒定的)。因此,我们需要(i)对有效可变跳转的目的地进行限制,以及(ii)激励开发者使用静态跳转,少用动态跳转。为了实现这两个目标,我们制定了以下规则:(i)紧接在push操作之后的跳转可以跳到任何地方,但不能跳转到另一个跳转处,以及(ii)其它跳转只能跳转到JUMPDEST。通过对跳转进行限制,我们便可以通过简单地查看代码中的先前操作来确定该跳转到底是动态的还是静态的。也就是说,对执行静态跳转的JUMPDEST操作的需求的缺乏恰恰是使用它们的动机。与此同时,禁止跳转到push数据也会加快JIT虚拟机编译和执行的速度。

  • LOG: LOG 用于记录事件,具体请参阅《经典回顾 | 以太坊1.0的设计依据(上)》中关于树(trie)的内容。

  • CALLCODE:这个操作码的用途是允许合约以存储在其它合约中的代码形式来调用“函数”。该合约使用单独的堆栈和内存,但使用的是合约自己的存储。如此一来,在区块链上可扩展地实现代码“标准库”会更加容易。

  • SELFDESTRUCT:这是一个允许合约在其自身不再需要时快速自删的操作码。SELFDESTRUCT并非立即执行,而是在交易执行结束之后再执行。这么做的原因是,恢复那些已经被执行的SELFDESTRUCT操作码将会大幅增加缓存的复杂度,而缓存在高效的VM实现中是必不可少的 。

  • PC:虽然理论上没有必要,因为PC操作码的所有实例都可以通过简单地将该索引处的实际程序计数器作为push来替换,但在代码中使用PC能够创建出与位置无关的代码(即可被复制/粘贴到其它合约的编译函数,并且如果它们最终出现在不同的索引中,也不会发生中断)。

经典回顾 | 以太坊1.0的设计依据(下)

本文翻译:喏呗尔

原文作者:Vitalik Buterin

原文链接:https://github.com/ethereum/wiki/wiki/Design-Rationale

相关阅读:

Vitalik:以太坊 Serenity 设计依据综述

【文章版权归原作者所有,其内容与观点不代表Unitimes立 场。翻译文章仅为传播更有价值的信息,合作或授权请联系我们】