以太坊设计理由

尽管以太坊借用了许多已经在比特币这样的旧加密货币中试用和测试了五年的想法,但是以太网中有许多地方与处理某些协议功能的最常见方式不同,而且还有很多情况。以太坊被迫开发全新的经济方法,因为它提供了其他现有系统无法提供的功能。本文档的目的是详细说明在构建以太坊协议的过程中所做的所有更细微的潜在非显而易见或在某些情况下有争议的决策,以及显示我们的方法和可能的替代方案所涉及的风险。

内容

原则

以太坊协议设计过程遵循以下几个原则:

  1. 三明治复杂性模型:我们认为以太坊的底层架构应该尽可能简单,并且与以太坊的接口(包括开发人员的高级编程语言和用户的用户界面)应该尽可能容易理解。在复杂性不可避免的情况下,它应该被推入协议的“中间层”,这不是核心共识的一部分,但最终用户也看不到 - 高级语言编译器,参数序列化和反序列化脚本,存储数据结构模型,leveldb存储接口和有线协议等。但是,这种偏好并不是绝对的。
  2. 自由:用户不应受限于他们使用以太坊协议的内容,我们不应试图根据其目的的性质优先支持或不赞成某些类型的以太坊合同或交易。这类似于“网络中立”概念背后的指导原则。这个原则的一个例子不是遵循的是比特币交易协议中的情况,其中不鼓励使用区块链用于“标签外”目的(例如,数据存储,元协议),并且在某些情况下明确的准协议改变(例如,OP_RETURN限制为40个字节)试图以“未授权”的方式使用区块链攻击应用程序。在以太坊中,我们强烈倾向于以大致激励兼容的方式设置交易费用,这样使用臃肿生产方式使用区块链的用户将其活动成本内部化(即庇古税收) 。
  3. 泛化:以太坊中的协议特征和操作码应该包含最大程度的低级概念,以便它们可以以任意方式组合,包括今天看起来有用但后来可能变得有用的方式,以及一堆低级概念可以通过在不需要时剥离其部分功能来提高效率。遵循这一原则的一个例子是我们选择LOG操作码作为向(特别是轻客户端)dapps提供信息的方式,而不是像之前在内部建议的那样简单地记录所有事务和消息 - “消息”的概念是实际上是多个概念的集合,包括“函数调用”和“对外部观察者感兴趣的事件”,并且值得将两者分开。
  4. 我们没有特点:作为概括的推论,我们经常拒绝将非常常见的高级用例构建为协议的内在部分,并且理解如果人们真的想要这样做,他们总是可以创建一个子合同中的协议(例如,以太币支持的子货币,比特币/ litecoin / dogecoin sidechain等)。这方面的一个例子是在以太坊缺乏类似比特币的“锁定时间”功能,因为这样的功能可以通过用户发送“签名数据包”的协议进行模拟,并且这些数据包可以被送入处理的专用合同中。如果数据包在某种特定于合同的意义上有效,则执行一些相应的功能。
  5. 非风险厌恶:如果风险增加的变化提供非常大的利益(例如,广义的状态转换,50倍的阻塞时间,共识效率等),我们可以接受更高的风险程度

这些原则都涉及指导以太坊的发展,但它们并非绝对的; 在某些情况下,希望减少开发时间或不尝试同时尝试太多激进的东西,这导致我们推迟某些变化,甚至一些明显有益的变化(例如,以太坊1.1)。

区块链级协议

本节介绍了在以太坊中进行的一些区块链协议更改,包括块和事务的工作方式,数据的序列化和存储方式以及帐户背后的机制。

帐户而不是UTXO

比特币及其许多衍生品在基于花费的交易输出(UTX)的结构中存储有关用户余额的数据:系统的整个状态包括一组“未花费的输出”(想想,“硬币”),这样每个硬币都有一个所有者和一个价值,交易花费一个或多个硬币并创建一个或多个新硬币,但要符合有效性限制:

  1. 每个引用的输入必须有效且尚未使用
  2. 事务必须具有与每个输入的输入所有者匹配的签名
  3. 输入的总值必须等于或大于输出的总值

因此,系统中用户的“余额”是用户具有能够产生有效签名的私钥的硬币组的总值。


(图片来自https://bitcoin.org/en/developer-guide

以太网抛弃了这种方案,采用了一种更简单的方法:状态存储每个帐户都有余额的帐户列表,以及以太坊特定数据(代码和内部存储),如果发送帐户有足够的话,交易有效余额用于支付,在这种情况下,发送帐户将被记入借方,并且接收帐户将记入该值。如果接收帐户具有代码,则代码运行,并且内部存储也可能被更改,或者代码甚至可能向其他帐户创建额外的消息,这导致进一步的借记和贷记。

UTXO的好处是:

  1. 更高程度的隐私:如果用户为他们收到的每笔交易使用新地址,那么通常很难将帐户相互链接。这很大程度上适用于货币,但不太适用于任意dapps,因为任意dapps通常必然涉及跟踪复杂的捆绑用户状态,并且可能不存在如货币那样简单的用户状态分区方案。
  2. 潜在的可扩展性范例:UTXO在理论上与某些类型的可扩展性范例更加兼容,因为我们只能依赖某些硬币的所有者维护Merkle的所有权证明,即使包括所有者在内的每个人都决定忘记该数据,那么只有所有者受到伤害。在帐户范例中,每个人都失去了与帐户相对应的Merkle树的部分,这使得无法以任何方式处理影响该帐户的消息,包括发送给它。但是,确实存在非UTXO依赖的可伸缩性范例。(很难理解这句话。)

帐户的好处是:

  1. 节省大量空间:例如,如果一个帐户有5个UTXO,那么从UTXO模型切换到帐户模型将减少(20 + 32 + 8)* 5 = 300字节的空间要求(地址为20,32为txid和值为8)到20 + 8 + 2 = 30个字节(地址为20,值为8,nonce为2(见下文))。实际上,由于帐户需要存储在Patricia树中(见下文),因此节省的成本并不大,但它们仍然很大。此外,事务可以更小(例如,在以太坊中为100字节而在比特币中为200-250字节)因为每个事务只需要创建一个引用和一个签名并产生一个输出。
  2. 更大的可替代性:因为没有区块链级别的特定硬币来源的概念,在技术上和法律上,制定一个红名单/黑名单计划并根据他们来自哪里来区分硬币变得不太实际从。
  3. 简单性:更容易编码和理解,尤其是一旦涉及更复杂的脚本。尽管可以将任意分散的应用程序变为UTXO范例,但主要是通过让脚本能够限制给定UTXO可以花费的UTXO类型,并且需要花费包括应用程序状态更改的Merkle树证明。 -root脚本评估,这样的范例比仅仅使用帐户更复杂和丑陋。
  4. 恒亮客户端引用:轻客户端可以通过向下扫描特定方向的状态树,随时访问与帐户相关的所有数据。在UTXO范例中,引用随每个事务而变化,这对于尝试使用上述状态根目录在UTXO传播机制的长期运行的dapp来说是一个特别棘手的问题。

我们已经决定,特别是因为我们正在处理包含任意状态和代码的dapps,帐户的好处大大超过了替代方案。此外,根据We We No Features原则,我们注意到如果人们真的关心隐私,那么混合器和coinjoin可以通过签约数据包协议在合同内构建。

帐户范例的一个弱点是,为了防止重放攻击,每个事务必须具有“随机数”,以便帐户跟踪所使用的随机数,并且如果在使用最后一个随机数之后其nonce为1,则仅接受事务。这意味着即使不再使用的帐户也永远不会从帐户状态中删除。解决此问题的一个简单方法是要求事务包含一个块编号,使其在一段时间后不可重放,并在每个周期重置一次。矿工或其他用户需要“ping”未使用的帐户才能将其从州中删除,因为作为区块链协议本身的一部分进行全面扫描太昂贵了。我们没有采用这种机制只是为了加快1.0的开发速度; 1.1及以上可能会使用这样的系统。

Merkle帕特里夏树

Merkle Patricia树/ trie,以前由Alan Reiner设想并在Ripple协议中实现,是以太坊的主要数据结构,用于存储所有帐户状态,以及每个块中的交易和收据。MPT是Merkle树Patricia树的组合,采用两者的元素来创建具有以下两个属性的结构:

  1. 每个唯一的键/值对唯一映射到根哈希,并且不可能欺骗trie中的键/值对的成员资格(除非攻击者具有~2 ^ 128计算能力)
  2. 可以在对数时间内更改,添加或删除键/值对

这为我们提供了一种方法,可以为整个状态树提供高效,易于更新的“指纹”。以太网MPT在这里正式描述:https//github.com/ethereum/wiki/wiki/Patricia-Tree

MPT中的具体设计决策包括:

  1. 有两类节点,kv节点和发散节点(有关详细信息,请参阅MPT规范)。kv节点的存在提高了效率,因为如果树在特定区域中是稀疏的,则kv节点将用作“快捷方式”,从而不需要具有深度为64的树。
  2. 使发散节点成为六进制而非二进制:这样做是为了提高查找效率。我们现在认识到这种选择是不理想的,因为通过存储批处理节点,可以在二进制范例中模拟六叉树的查找效率。但是,由于trie构造很容易错误地实现并且最终导致状态根不匹配,我们决定将这样的重组表直到1.1。
  3. 空值和非成员之间没有区别:这是为了简单起见,因为它适用于以太坊的默认值,未设置的值(例如,余额)通常表示零,空字符串用于表示零。但是,我们注意到它牺牲了一些普遍性,因此略微不理想。
  4. 终止节点和非终止节点之间的区别:从技术上讲,“此节点终止”标志是不必要的,因为以太坊中的所有尝试都用于存储静态密钥长度,但我们无论如何都添加它以增加通用性,希望以太坊MPT实现将由其他加密协议按原样使用。
  5. 使用sha3(k)作为“安全树”中的密钥(用于状态和帐户存储尝试):这使得通过设置64个级别的最大不利的分支节点链并且重复调用来更难以执行该操作SLOAD和SSTORE就在他们身上。请注意,这使得枚举树更加困难; 如果要在客户端中具有枚举功能,最简单的方法是维护数据库映射sha3(k) -> k

RLP

RLP(“递归长度前缀”)编码是以太坊中使用的主要序列化格式,并且在任何地方使用 - 用于块,事务,帐户状态数据和有线协议消息。RLP在这里正式描述:https//github.com/ethereum/wiki/wiki/RLP

RLP旨在成为高度简约的序列化格式; 它的唯一目的是存储嵌套的字节数组。protobufBSON和其他现有解决方案不同,RLP不会尝试定义任何特定的数据类型,如布尔值,浮点数,双精度数甚至整数; 相反,它只是以嵌套数组的形式存储结构,并将其留给协议来确定数组的含义。键/值映射也未明确支持; 支持键/值映射的半官方建议是表示这样的映射,[[k1, v1], [k2, v2], ...]其中k1, k2...使用字符串的标准排序对其进行排序。

RLP的替代方案是使用现有算法,例如protobuf或BSON; 但是,我们更喜欢RLP,因为(1)实现简单,(2)保证绝对字节完美一致性。许多语言中的键/值映射没有明确的排序,浮点格式有许多特殊情况,可能导致相同的数据导致不同的编码,从而产生不同的散列。通过在内部开发协议,我们可以确信它的设计考虑了这些目标(这是一个通用原则,也适用于代码的其他部分,例如VM)。请注意,BitTorrent使用的bencode可能为RLP提供了一个可通行的替代方案,尽管它对长度使用十进制编码使得它与二进制RLP相比略微不理想。

压缩算法

有线协议和数据库都使用自定义压缩算法来存储数据。该算法最好被描述为行程长度编码零并保留其他值,除了一些常见值的特殊情况,例如sha3('')例如:

>>> compress('horse')
'horse'
>>> compress('donkey dragon 1231231243')
'donkey dragon 1231231243'
>>> compress('\xf8\xaf\xf8\xab\xa0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xbe{b\xd5\xcd\x8d\x87\x97')
'\xf8\xaf\xf8\xab\xa0\xfe\x9e\xbe{b\xd5\xcd\x8d\x87\x97'
>>> compress("\xc5\xd2F\x01\x86\xf7#<\x92~}\xb2\xdc\xc7\x03\xc0\xe5\x00\xb6S\xca\x82';{\xfa\xd8\x04]\x85\xa4p")
'\xfe\x01'

在压缩算法存在之前,以太坊协议的许多部分都有许多特殊情况; 例如,sha3经常被覆盖sha3('') = '',因为这样可以节省64个字节而不需要在帐户中存储代码或存储。然而,最近在所有这些特殊情况被删除的情况下进行了更改,默认情况下使以太网数据结构更加庞大,而是将数据保存功能添加到区块链协议之外的层,方法是将其置于有线协议上并将其无缝插入到用户的数据库实现。这增加了模块化,简化了共识层,并且还允许相对容易地(例如,通过网络协议版本)部署对压缩算法的持续升级。

Trie用法

警告:本节假设了解bloom过滤器的工作原理。有关介绍,请参阅http://en.wikipedia.org/wiki/Bloom_filter

以太坊区块链中的每个块头都包含指向三次尝试的指针:状态trie,表示访问块后的整个状态,事务trie,表示由索引键入的块中的所有事务(即,键0:要执行的第一个事务) ,键1:第二个交易等)和收据树,表示与每个交易相对应的“收据”。事务的收据是RLP编码的数据结构:

[ medstate, gas_used, logbloom, logs ]

哪里:

  • medstate 是处理事务后的状态trie root
  • gas_used 是处理交易后使用的天然气量
  • logs在执行事务期间[address, [topic1, topic2...], data]LOG0... LOG4操作码生成的表单项的列表(包括主调用和子调用)。address是生成日志的合同的地址,主题最多4个32字节值,数据是任意大小的字节数组。
  • logbloom 是一个布隆过滤器,由事务中所有日志的地址和主题组成。

块头中还有一个bloom,它是块中事务的所有Bloom的OR。这种结构的目的是使Ethereum协议在尽可能多的方面对轻客户端友好。有关以太坊轻客户端及其用例的更多详细信息,请参阅轻客户端页面(原则部分)

叔叔的激励

“Greedy Heaviest Observed Subtree”(GHOST)协议是首次引入的创新由Yonatan Sompolinsky和Aviv Zohar于2013年12月完成,并且是第一次尝试解决阻止更快阻塞时间的问题。GHOST背后的动机是具有快速确认时间的区块链目前由于高陈旧率而遭受安全性降低 - 因为如果矿工A挖掘一个区块然后矿工B碰巧挖掘另一个区块,那么区块需要一定时间才能通过网络传播在矿工A的区块传播到B之前,矿工B的区块将最终浪费(“陈旧”)并且不会对网络安全做出贡献。此外,还有一个集中问题:如果矿工A是具有30%哈希值的挖掘池,而B具有10%的哈希值,A将有70%的时间产生陈旧区块的风险(因为其他30%的时间A产生了最后一个区块,因此将立即获得采矿数据)而B将有产生过时区块的风险90%的时间。因此,如果块间隔足够短以使陈旧速率变高,则仅通过其尺寸A将基本上更有效。通过组合这两种效果,快速生成块的区块链很可能导致一个挖掘池具有足够大的网络散列容量百分比,以实际控制挖掘过程。

正如Sompolinsky和Zohar所描述的那样,GHOST通过在计算哪个链“最长”时包含过时块来解决网络安全丢失的第一个问题。也就是说,不仅是一个块的父级和进一步的祖先,而且块的祖先(在以太坊术语中,“叔叔”)的陈旧后代被添加到计算哪个块具有最大的工作支持总证据它。

为了解决集中偏差的第二个问题,我们采用了不同的策略:我们为stales提供块奖励:陈旧的块接收其基本奖励的7/8(87.5%),并且包含陈旧块的侄子接收1/32 (3.125%)基本奖励作为包含奖励。但是,交易费不会授予叔叔或侄子。

在以太坊中,陈旧的块只能作为一个叔叔包含在其直接兄弟之一的第七代后代中,而不是任何具有更远距离关系的块。这样做有几个原因。首先,无限的GHOST将在计算给定块的哪些叔叔有效时包含太多复杂性。其次,以太坊中使用的无限制的叔叔激励消除了矿工在主链上开采的动机,而不是公共攻击者的链条。最后,计算表明,限制到七个级别提供了大部分期望的效果,而没有许多负面后果。

我们的块时间算法中的设计决策包括:

  • 12秒块时间:选择12秒作为尽可能快的时间,但同时显着长于网络延迟。2013纸由德克尔和Wattenhofer苏黎世措施比特币网络等待时间,并且确定12.6秒是它需要一个新的块传播到节点的95%的时间; 然而,该论文还指出,传播时间的大部分与块大小成正比,因此在更快的货币中,我们可以预期传播时间将大幅减少。传播间隔的恒定部分约为2秒; 但是,为了安全起见,我们假设在我们的分析中块需要12秒才能传播。
  • 7块的祖先限制:这是一个设计目标的一部分,希望在少量块之后很快“忘记”块历史,并且已经证明7个块提供了大部分所需的效果
  • 1块后代限制(例如c(c(p(p(p(head))))),其中c = child和p = parent,无效):这是简单设计目标的一部分,上面的模拟器表明它不会造成大的集中风险。
  • Uncle有效性要求:uncles必须是有效的头,而不是有效的块。这样做是为了简化,并将区块链的模型保持为线性数据结构(而不​​是块-DAG,如Sompolinsky和Zohar的新模型)。要求叔叔成为有效的块也是一种有效的方法。
  • 奖励分配:7/8的基础采矿奖励给叔叔,1/32到侄子,0%的交易费用。如果费用占主导地位,那么从集中化的角度来看,这将使叔叔的激励无效; 然而,这就是为什么只要我们继续使用PoW,以太坊就要继续发行以太的原因之一。

难度更新算法

以太坊的难度目前根据以下规则更新:

diff(genesis) = 2^32

diff(block) = diff.block.parent + floor(diff.block.parent / 1024) *
    1 if block.timestamp - block.parent.timestamp < 9 else
    -1 if block.timestamp - block.parent.timestamp >= 9

难度更新规则背后的设计目标是:

  • 快速更新:在增加或减少散列功率的情况下,块之间的时间应该快速重新调整
  • 低波动性:如果哈希值恒定,难度不应过度反弹
  • 简单性:算法应该相对简单实现
  • 低内存:算法不应该依赖于多个历史块,并且应该包括尽可能少的“内存变量”。假设最后十个块,加上放在最后十个块的块头中的所有内存变量,都可供算法使用
  • 不可利用性:算法不应过度鼓励矿工摆弄时间戳,或挖掘池以反复添加和删除哈希值,以试图最大化其收入

我们已经确定我们当前的算法在低波动性和非可利用性方面非常不理想,并且至少我们计划将时间戳切换为父母和祖父母,因此矿工只有动机才能修改时间戳。正在连续开采两个街区。另一个更强大的模拟公式位于https://github.com/ethereum/economic-modeling/blob/master/diffadjust/blkdiff.py(模拟器使用比特币挖掘能力,但使用整个平均值为整个一天;它一度模拟一天95%的崩溃)。

天然气和费用

虽然比特币中的所有交易大致相同,因此它们的网络成本可以建模为单个单元,但以太坊中的交易更复杂,因此交易费系统需要考虑许多因素,包括带宽成本,存储成本和计算成本。特别重要的是,以太坊编程语言是图灵完备的,因此交易可以使用任意数量的带宽,存储和计算,后者可能最终被用于因停止问题甚至无法可靠的数量提前预测。通过无限循环防止拒绝服务攻击是一个关键目标。

交易费用的基本机制如下:

  • 每笔交易必须指定其愿意消费(称为startgas的“天然气”数量,以及它愿意为每单位天然气支付的费用(gasprice)。在执行开始时,startgas * gasprice以太网将从交易发件人的帐户中删除。
  • 事务执行期间的所有操作,包括数据库读取和写入,消息以及虚拟机采取的每个计算步骤都会消耗一定量的气体。
  • 如果交易执行完全处理,消耗的气体少于其指定的限制,比如gas_rem剩余的气体,则交易正常执行,并且在执行结束时交易发送者收到退款gas_rem * gasprice并且块的矿工获得奖励(startgas - gas_rem) * gasprice
  • 如果交易在执行中“耗尽”,那么所有执行都会恢复,但交易仍然有效,并且交易的唯一影响是将全部金额转移startgas * gasprice给矿工。
  • 当合同向另一个合同发送消息时,它还可以选择设置特定于该消息产生的子执行的气体限制。如果子执行耗尽气体,那么子执行被还原,但是仍然消耗了气体。

上述每个组件都是必需的。例如:

  • 如果事务不需要指定气体限制,那么恶意用户可以发送一个产生数十亿循环的事务,并且没有人能够处理它,因为处理这样​​的事务需要比块间隔更长的时间,但是矿工们无法事先说出来,导致拒绝服务的漏洞。
  • 严格的气体计数,时间限制的替代方案不起作用,因为它太主观(一些机器比其他机器更快,甚至在相同的机器中,关闭调用将始终存在)
  • 整个价值startgas * gasprice必须在开始时作为存款取出,这样就不会出现账户在执行中“破坏”并且无法支付其燃气费用的情况。请注意,余额检查还不够,因为帐户可以在其他地方发送余额。
  • 如果在气体错误不足的情况下执行没有恢复,那么合同将需要采取强有力和困难的安全措施,以防止自己被中途提供足够气体的交易或消息利用,从而导致一些变化在合同执行中执行而不是其他人。
  • 如果不存在子限制,那么恶意帐户可以通过与他们签订协议,然后在计算开始时插入无限循环,以便受害者合同的任何尝试,来对其他合同实施拒绝服务攻击。补偿攻击合同或向其发送消息会使整个事务执行变得饥饿。
  • 要求交易发送者支付天然气而不是合同,这大大增加了开发人员的可用性。非常早期版本的以太坊有合同支付天然气,但这导致了一个相当丑陋的问题,每个合同必须实施“守卫”代码,以确保每一个传入的消息补偿合同与足够的以太来支付它的气体消耗。

请注意燃气成本中的以下特性:

  • 任何交易都会收取21000燃气作为“基本费用”。这包括椭圆曲线操作的成本,以从签名中恢复发送方地址以及存储事务的磁盘和带宽空间。
  • 事务可以包括无限量的“数据”,并且在虚拟机中存在操作码,其允许接收事务的合同访问该数据。数据的“固有气体”费用是每零字节4气体和每非零字节68气体。之所以出现这个公式是因为我们看到用户编写的合同中的大多数事务数据被组织成一系列32字节的参数,其中大多数都有许多前导零字节,并且考虑到这样的结构似乎效率低但实际上由于压缩算法而有效,我们希望鼓励使用它们来代替更复杂的机制,这些机制会尝试根据预期的字节数紧密地打包参数,从而导致编译器级别的复杂性增加。这是三明治复杂性模型的一个例外,
  • 设置帐户存储值的SSTORE操作码的成本是:(i)将零值更改为非零值时为20000气体,(ii)将零值更改为零值或非零值时为5000气体非零值,或(iii)将非零值更改为零值时的5000气体,以及在成功交易执行结束时给出的15000气体退款(即不执行导致气体不足的情况)例外)。退款的上限为交易所用总气量的50%。这为清理存储提供了一个小动力,因为我们注意到缺乏这种激励,许多合同会使存储闲置,导致迅速增加膨胀,为存储提供“收取租金”的大部分好处,而不会失去保证的成本。一旦放置合同将永远存在。延迟退款机制对于防止拒绝服务攻击是必要的,其中攻击者发送具有少量气体的交易,该交易反复清除大量存储槽作为长时间循环的一部分,然后耗尽气体,消耗大量验证者的计算能力,而无需实际清理存储或消耗大量气体。需要50%上限以确保给定具有一定量气体的交易的矿工仍然可以确定执行交易的计算时间的上限。计算能力,而无需实际清理存储或消耗大量气体。需要50%上限以确保给定具有一定量气体的交易的矿工仍然可以确定执行交易的计算时间的上限。计算能力,而无需实际清理存储或消耗大量气体。需要50%上限以确保给定具有一定量气体的交易的矿工仍然可以确定执行交易的计算时间的上限。
  • 合同提供的消息中的数据没有气体成本。这是因为在消息调用期间不需要实际“复制”任何数据,因为调用数据可以简单地被视为指向父契约的内存的指针,该内存在子执行正在进行时不会改变。
  • 内存是一个无限可扩展的数组。但是,每32字节内存扩展的气体成本为1,四舍五入。
  • 一些操作码,其计算时间高度依赖于参数,具有可变的气体成本。例如,EXP的气体成本在指数中为每字节10 + 10(即x ^ 0 = 1气体,x ^ 1 ... x ^ 255 = 2气体,x ^ 256 ... x ^ 65535 = 3气体等),复制操作码(CALLDATACOPY,CODECOPY,EXTCODECOPY)的气体成本是每32字节拷贝1 + 1,向上舍入(LOG也有类似的规则)。内存膨胀气体成本不足以弥补这一点,因为它开启了二次攻击(50000气体的50000气体〜= 50000 ^ 2计算工作量,但在引入可变气体成本之前只有约50000气体)
  • 如果值非零,则CALL操作码(和对称的CALLCODE)会额外花费9000个气体。这是因为任何值传输都会导致存档节点的历史存储显着膨胀。请注意,实际收取的费用是6700; 除此之外,我们还添加了一个强制性的2300气体最小值,自动提供给接收者。这是为了确保接收交易的钱包至少有足够的气体来记录交易。

天然气机制的另一个重要部分是天然气价格本身的经济性。在比特币中使用的默认方法是纯粹自愿收费,依靠矿工充当守门人并设定动态最小值; 以太坊中的等价物将允许交易发送者设定任意的天然气成本。这种方法在比特币社区中得到了非常好的支持,特别是因为它是“基于市场的”,允许矿工和交易发送者之间的供需来确定价格。然而,这种推理的问题在于交易处理不是市场; 虽然将交易处理解释为矿工向发件人提供的服务直觉上具有吸引力,实际上,矿工包含的每笔交易都需要由网络中的每个节点处理,因此交易处理的绝大部分成本由第三方承担,而不是决定是否包括在内的矿工它。因此,很可能发生悲剧性的公地问题。

目前,由于缺乏有关矿工如何在现实中表现的明确信息,我们采用了一种相当简单的方法:投票系统。矿工有权将当前区块的气体限制设置在最后一个区块的气体限制的~0.0975%(1/1024)之内,因此产生的气体限制应该是矿工偏好的中位数。希望将来我们能够将其软分解为更精确的算法。

虚拟机

以太坊虚拟机是执行事务代码的引擎,是以太坊和其他系统之间的核心差异化功能。请注意,虚拟机应与合同和消息模型分开考虑- 例如,SIGNEXTEND操作码是VM的一项功能,但合同可以调用其他合同并指定子呼叫的气体限制这一事实是合同和消息模型。EVM中的设计目标包括:

  • 简单性:尽可能少的和低级操作码,尽可能少的数据类型和尽可能少的虚拟机级构造
  • 总确定性:VM规范的任何部分都应该没有任何歧义的空间,结果应该是完全确定的。此外,应该有一个精确的计算步骤概念,可以对其进行测量以计算气体消耗量。
  • 节省空间:EVM组装应尽可能紧凑(例如,不接受默认C程序的4000字节基本大小)
  • 对预期应用程序的专业化:能够处理20字节地址和使用32字节值的自定义加密,自定义加密中使用的模块化算法,读取块和事务数据,与状态交互等
  • 简单的安全性:应该很容易为操作提供燃气成本模型,使VM不可利用
  • 优化友好性:应该很容易应用优化,以便可以构建JIT编译和其他加速版本的VM。

做出了一些特殊的设计决策:

  • 临时/永久存储区别 - 临时存储(存在于VM的每个实例中并在VM执行完成时消失)和永久存储(存在于区块链状态级别上的每个帐户)之间存在区别。例如,假定下面的树的执行发生(使用S代表永久存储和M为临时的):(ⅰ)A呼叫B,(ⅱ)乙集B.S[0] = 5B.M[0] = 9,(ⅲ)B调用C,(ⅳ)C呼叫B此时,如果B尝试读取B.S[0],它将接收早先存储在B中的值5,但如果B尝试读取B.M[0]它将收到0,因为它是具有新临时存储的虚拟机的新实例。如果B现在设置B.M[0] = 13B.S[0] = 17在这个内部调用中,然后这个内部调用和C的调用终止,将执行返回到B的外部调用,然后B读取M将看到B.M[0] = 9(因为上次设置该值是在同一个VM执行实例中)和B.S[0] = 17如果B的外部呼叫结束而A再次呼叫B,则B将看到B.M[0] = 0B.S[0] = 17这种区别的目的是(1)为每个执行实例提供自己的内存,这些内存不会受到递归调用的破坏,使安全编程更容易,以及(2)提供一种可以非常快速地操作的内存形式,由于需要修改trie,因此存储更新必然很慢。
  • 堆栈/内存模型 - 早期决定有三种类型的计算状态(除了指向下一条指令的程序计数器):堆栈(32字节值的标准LIFO堆栈),内存(无限可扩展)临时字节数组)和存储(永久存储)。在临时存储方面,堆栈和内存的替代方案是仅存储器的范例,或寄存器和存储器的一些混合(不是很不同,因为寄存器基本上是一种存储器)。在这种情况下,每条指令都有三个参数,例如。ADD R1 R2 R3: M[R1] = M[R2] + M[R3]选择堆栈范例的原因很明显,它使代码变小了四倍。
  • 32字节字大小 - 替代方案是4或8字节字,与大多数其他架构一样,或无限制,如比特币。4或8字节字对于存储地址和加密计算的大值来说限制太多,并且无限制的值太难以制作安全的气体模型。32字节是理想的,因为它足够大,可以存储许多加密实现中常见的32字节值,以及地址(并提供将地址和值打包成单个存储索引作为优化的能力),但不是很大效率极低。
  • 拥有我们自己的VM - 另一种选择是重用Java,或者一些Lisp方言,或者Lua。我们决定使用专用VM是合适的,因为(i)我们的VM规范比许多其他虚拟机简单得多,因为其他虚拟机必须为复杂性支付更低的成本,而在我们的情况下,每个额外的复杂单元都是迈向高门槛,创造发展集中化和安全漏洞的可能性,包括共识失败,(ii)它允许我们更多地专门化VM,例如。通过具有32字节字大小,(iii)它允许我们不具有可能导致安装困难的非常复杂的外部依赖性,以及(iv)针对我们特定安全需求的以太坊的完整安全性审查将需要安全性审查无论如何,外部VM都是如此,因此节省的努力并不是那么大。
  • 使用可变的可扩展内存大小 - 如果大小很小,我们认为固定内存大小不必要地限制,如果大小很大,我们认为不必要的昂贵,并注意到在任何情况下都需要内存访问语句来检查越界访问,因此固定大小甚至不会使执行更有效。
  • 具有1024个调用深度限制 - 许多编程语言在高堆栈深度处破坏的速度比在高内存使用量或计算负载下突破的速度快得多,因此阻塞气体限制的隐含限制可能不够。
  • 没有类型 - 为简单而做。相反,使用DIV,SDIV,MOD,SMOD的有符号和无符号操作码(事实证明,对于ADD和MUL,有符号和无符号操作码的行为是等效的),以及定点算术的转换(高深度固定 - 点算术是32字节字的另一个好处)在所有情况下都很简单,例如。在深度的32位,a * b -> (a * b) / 2^32a / b -> a * 2^32 / b,和+, -和*从整数的情况下保持不变。

VM中某些操作码的功能和目的是显而易见的,但其他操作码则不那么明显。下面给出了一些特别的理由:

  • ADDMODMULMOD:在大多数情况下,mulmod(a, b, c) = a * b % c然而,在许多类椭圆曲线密码术的特定情况下,使用32字节模运算,a * b % c因此直接进行实际操作((a * b) % 2^256) % c,这给出了完全不同的结果。a * b % c在32字节的空间中使用32字节值计算的公式相当重要且笨重。
  • SIGNEXTENDSIGNEXTEND的目的是促进从较大的有符号整数到较小的有符号整数进行类型转换。小型有符号整数很有用,因为JIT编译的虚拟机将来可能能够检测到长时间运行的代码块,这些代码主要处理32字节整数并大大加快速度。
  • SHA3:SHA3在以太坊代码中非常适用,因为使用存储的安全无限大小哈希映射可能需要使用安全哈希函数以防止恶意冲突,以及验证Merkle树甚至验证以太坊类数据结构。关键的一点是,它的同伴SHA256ECRECOVERRIPEMD160包括没有操作码,但作为伪合同。这样做的目的是将它们放在一个单独的类别中,这样,如果/当我们稍后提出一个适当的“原生扩展”系统时,可以添加更多此类合同而不填写操作码空间。
  • 原产地:提供交易发件人的ORIGIN操作码的主要用途是允许合同为天然气支付退款。
  • COINBASE:COINBASE操作码的主要用途是(i)允许子货币在他们选择的情况下为网络安全做出贡献,以及(ii)开放使用矿工作为基于分享共识的应用的分散经济集合像Schellingcoin。
  • PREVHASH:用作半安全的随机源,并允许合同在前一个块中评估Merkle树状态证明,而不需要高度复杂的递归“以太坊中的以太坊光客户端”构造。
  • EXTCODESIZEEXTCODECOPY:这里的主要用途是允许合同在与模板交互之前根据模板检查其他合同的代码,甚至模拟它们。有关应用程序,请参阅http://lesswrong.com/lw/aq9/decision_theories_a_less_wrong_primer/
  • JUMPDEST:当跳转目标被限制为几个索引时,JIT编译的虚拟机变得更容易实现(具体地,变量目标跳转的计算复杂度大致为O(log(有效跳转目标的数量)),尽管静态跳转是总是恒定的时间)。因此,我们需要(i)对有效变量跳转目的地的限制,以及(ii)使用静态动态跳跃的动机。为了实现这两个目标,我们有这样的规则:(i)跳转之前紧接着跳跃可以跳到任何地方而不是另一个跳跃,以及(ii)其他跳跃只能跳转到JUMPDEST。需要限制跳跃跳跃,以便可以通过简单地查看代码中的先前操作来确定跳跃是动态还是静态的问题。缺乏对静态跳转的JUMPDEST操作是使用它们的动机。禁止跳转到推送数据也加速了JIT VM的编译和执行。
  • 日志:LOG用于记录事件,请参阅上面的trie用法部分。
  • CALLCODE:这样做的目的是允许合约以存储在其他合同中的代码形式调用“函数”,使用单独的堆栈和内存,但使用合同自己的存储。这使得在区块链上可伸缩地实现代码的“标准库”变得更加容易。
  • SELFDESTRUCT:一个操作码,允许合同在不再需要时快速删除。SELFDESTRUCTs在事务执行结束时处理而不是立即处理的事实是由于能够恢复已经执行的SELFDESTRUCT,这将大大增加高效VM实现中所需的高速缓存的复杂性。 。
  • PC:虽然理论上没有必要,因为PC操作码的所有实例都可以通过简单地将该索引处的实际程序计数器作为推送来替换,使用代码中的PC允许创建与位置无关的代码(即编译的函数)可以复制/粘贴到其他合同中,如果它们最终出现在不同的索引中,则不会中断)。
posted @ 2019-02-17 19:51  林锅  阅读(365)  评论(0编辑  收藏  举报