以太坊原理研读笔记

https://ethfans.org/posts/ethereum-whitepaper 以太坊白皮书

Merkle Tree

默克尔帕树(Merkle Patricia tree/trie)

  1. 由Alan Reiner提出设想,并在瑞波协议中得到实现,是以太坊的主要数据结构,用于存储所有账户状态,以及每个区块中的交易和收据数据。MPT是默克尔树和帕特里夏树的结合缩写;
  2. 每个唯一键值对唯一映射到根的hash值;在MPT中,不可能仅用一个键值对来欺骗成员(除非攻击者有~2^128 的算力);
  3. 增、删、改键值对的时间复杂度是对数级别。

MPT的具体设计决策

  1. 有两类节点KV节点和离散节点 KV节点的存在提高了效率,因为如果在特定区域树是稀疏的,KV节点可作为一个“快捷方式”,代替深度为64的树。

  2. 离散节点是16进制,不是二进制
    这样让查找更有效率,我们现在认识到这种选择并不理想,因为16进制树的查找效率在二进制中可以通过批次存储节点来模拟。MPT树的结构实现是非常容易出错的,最终至少会造成状态根不匹配。

  3. 空值(empty value)与非会员(non-membership)之间没有区别
    这样做是为了简化逻辑,以太坊中未设置的值默认为0,空字符串也用0表示。然而,需要强调的是,这样做牺牲了一些通用性,因而也不是最优的。

  4. 终节点和非终节点的区别
    技术上,标识一个节点“是否是终节点”是没必要的,因为以太坊中所有的树都被用于存储静态秘钥长度,但为了增加通用性,我们还是会添加这个标识,以期望以太坊的MPT的实现方式能够被其他加密货币原样采纳。

  5. 在安全树中采用SHA3(k)作为秘钥(在状态树和账户存储树中使用)
    使用SHA3(k),通过设置离散节点的链的深度为64,以及反复调用SLOAD 和 SSTORE指令,使那些企图对树采取Dos攻击的行为变得非常困难。不过,这也让穷举树的节点变得困难,如果你希望你的客户端有穷举的能力,最简单的方法是维持一个数据库映射:sha3(k) -> k

RLP

RLP(recursive length prefix)###

  • 递归长度前缀 RLP编码是以太坊中主要的序列化格式,它的使用无处不在:区块、交易、账户状态以及线路协议消息,旨在成为高度简化的序列化格,。
  • RLP唯一的目的是存储嵌套的字节数组 不同于protobuf、BSON等现有的解决方案,RLP并不定义任何指定的数据类型,如Boolean、floa、double或者integer。它仅仅是以嵌套数组的形式存储结构,并将其留给协议来确定数组的含义。
  • RLP也没有明确支持map集合 半官方的建议是采用 [[k1, v1], [k2, v2], ...] 的嵌套数组来表示键值对集合,k1,k2 ... 按照字符串的标准排序。

Trie 树的使用

状态树、交易树、收据树

  1. 以太坊区块链中每个区块头都包含指向三个树的指针:状态树、交易树、收据树。
  2. 状态树代表访问区块后的整个状态;
  3. 交易树代表区块中所有交易,这些交易由index索引作为key;(例如,k0:第一个执行的交易,k1:第二个执行的交易)
  4. 收据树代表每笔交易相应的收据。交易的收据是一个RLP编码的数据结构:
[ medstate, gas_used, logbloom, logs ]

其中:

  • medstate:交易处理后,树的根的状态;
  • gas_used:交易处理后,gas的使用量;
  • logbloom:交易中所有logs的address和topic组成的布隆过滤器(一种二进制向量数据结构,具有很好的空间和时间效率,被用来检测一个元素是不是集合中的一个成员)
  • logs:是表格[address, [topic1, topic2...], data]元素的列表。表格由交易执行期间调用的操作码LOG0 ... LOG4 生成(包含主调用和子调用);address 是生成日志的合约的地址topics是最多4个32字节的值;data 是任意字节大小的数组;

过时区块处理

问题背景

  • 区块在网络中传播需要一定时间,如果矿工A挖到一个区块并向全网广播,在广播的路上,B也挖出了区块,那么B的区块是过时的,且B的本次挖矿对网络的安全没有贡献。GHOST的目的正是要解决挖矿过时造成的安全性降低的问题。
  • 中心化问题:如果A是一个矿池,有30%的算力,B有10%的算力。A有70%的时间产生过时的区块(因为另外的30%时间会产生最新区块,所以它会立即挖到数据),而B有90%的时间产生过时区块。如果区块的产出时间间隔很短,那么过时率就会变高,则A凭借其更大的算力使挖矿效率也更高。所以,区块生成过快,容易导致网络算力大的矿池控制挖矿过程。

GHOST协议

  • 哪个链是最长的链,GHOST解决了在计算的过程中,因产生过时区块而造成的网络安全性下降的问题。不仅是父区块和更早的区块,同时Uncle区块⑥也被添加到计算哪个块具有最大的工作量证明中去。
  • 中心化问题,我们采用不同的策略:对过时区块也提供区块奖励:挖到过时区块的奖励是该区块基础奖励的7/8;挖到包含过时区块的nephew区块将收到1/32的基础奖励作为赏金。但是,交易费并不奖励给Uncle区块或nephew区块。
  • 区块时间算法的设计决策包括
  1. 区块时间12s 选择12s是因为这已经是最快的时间了,基本上比网络延迟更长。在2013年的一份关于测量比特币网络延迟的论文中,确定了12.6秒是新产生的区块传播到95%节点的时间;
  2. 7个祖先块的限制 这样设计的目的是希望只保留少量区块,而将更早之前的区块清除。已经证明7个区块可以提供大部分所需的效果。
  3. 1个后裔区块的限制 如c(c(p(p(p(head))))) c=child, p = parent,就不合法,因为它有两个后裔区块。这样设计的目的是为了简单,上面的模拟结果显示它并没有构成大的中心化风险。
  4. uncle块要求具有有效性 uncle块必须是有效的header,而不是有效的区块。这样做也是为了简化,将区块链模型保持为线性数据结构。不过,要求uncle块是有效的区块也是合法的方法。
  5. 奖金分配 7/8的挖矿基础奖励分配给uncle块,1/32分给nephew块,它们交易费用都是0%。如果费用占多数,从中心化的角度看,这会使uncle块激励机制无效;然而,这也是为什么只要我们继续使用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

难度更新规则的设计目标如下:

  • 快速更新:区块间的时间应该随着hash算力的增减而快速调整;

  • 低波动性:如果Hash算力恒定,那么难度不应剧烈波动;

  • 简单:算法的实现应相对简单;

  • 低内存:算法不应依赖于过多的历史区块,要尽可能少的使用”内存变量“。假设有最新的十个区块,将存储在这十个区块头部的内存变量相加,这些区块都可用于算法的计算;

  • 非开发性:算法不应让矿工有过多篡改时间戳或者矿池、反复添加或删除算力的能力,以使他们的收益最大化。

Gas与费用

以太坊交易费用的基本机制

交易费用需要考虑到账户的许多方面,包括宽带费用,存储费用和计算费用。尤其重要的是,以太坊编程语言是图灵完备的,所以交易会使用任意数量的宽带、存储和计算成本。这就可能会导致在计算成本过程中,突遭停电而计算被迫中止。

  • 每笔交易必须指明一定数量的gas(即指定startgas的值),以及支付每单元gas所需费用(即gasPrice),在交易执行开始时,gasLimit * gasPrice 价值的以太币会从发送者账户中扣除;

  • 交易执行期间的所有操作,包括读写数据库、发送消息以及每一步的计算都会消耗一定数量的gas;

  • 如果交易执行完毕,消耗的gas值小于指定的限制值,则交易执行正常,并将剩余的gas值赋予变量gas_rem ; 在交易完成后,发送者会收到返回的gas_rem * gasPrice 价值的以太币,而给矿工的奖励是(gasLimit - gas_rem)* gasprice价值的以太币;

  • 如果交易执行中,gas消耗殆尽,则所有的执行恢复原样,但交易仍然有效,只是交易的唯一结果是将 startgas * gasprice 价值的以太币支付给矿工,其他不变;

  • 当一个合约发送消息给另一个合约,可以对这个消息引起的子执行设置一个gas限制。如果子执行耗尽了gas,则子执行恢复原样,但gas仍然消耗。

  • 注意,扣除费用时,仅仅检查账户余额是不够的,因为账户可以在其他地方发送余额。

gas消耗计算的特点

  • 椭圆算法消耗 对于任何交易,都将收取21000gas的基本费用。这些费用可用于支付运行椭圆曲线算法所需的费用。该算法旨在从签名中恢复发送者的地址以及存储交易所花费的硬盘和带宽空间。
  • 不限制大小的内存 内存是一个可以无限扩展的数组,然而,每扩展32字节的内存就会消耗1gas的成本,不足32字节以32字节计。
  • 鼓励清楚存储器与延迟退款 用于设置账户存储器的操作码SSTORE的消耗是:1.将零值改为非零值时,消耗20000gas;2.将零值变成零值,或非零值变非零值,消耗5000gas;3.将非零值变成零值,消耗5000gas,加上交易执行成功后退回的20000gas。退款金额上限是交易消耗gas总额的50%。这样设置会激励人们清除存储器。我们注意到,正因为缺乏这样的激励,许多合约造成了存储空间没有被有效使用,从而导致了存储快速膨胀;为存储收取费用提供了很多好处,同时不会失去合约一旦确立就可以永久存在的保证。延迟退款机制是必要的,因为可以阻止拒绝服务攻击。攻击者发送一笔含有少量gas的交易,循环清除大量的存储,直到用光gas,这样消耗了大量的验证算力,但实际并没有真正清除存储或消耗大量gas。50%的上限的是为了确保获得了一定交易gas的旷工依然能够确定执行交易的计算时间的上限。
  • 消息调用的数据为指针 合约提供的消息的数据是没有成本的。因为在消息调用期间不需要实质复制任何数据,调用数据可以简单地视为指向父合约内存的指针,该指针在子进程执行时不会改变。
  • call操作额外消耗9000gas 如果值不是零,操作码CALL会额外消耗9000gas。这是因为任何值传输都会引起归档节点的历史存储显著增大。请注意,实际消耗是6700,在此基础上,我们强制增加了一个自动给予接受者的gas值,这个值最小2300。这样做是为了让接受交易的钱包至少有足够的gas来记录交易。
  • 可包括无限量的“数据” 虚拟机中的某些操作码,可以让合约允许交易对这些数据的访问。数据的固定消耗计算是:每个零字节4gas,非零字节68gas。这个公式的产生是因为合约中大部分的交易数据由一些列的32字节的参数组成,其中多数参数具有许多前导零字节。

gas机制的经济学原理

gas机制的另一个重要部分是gas价格本身体现出的经济学原理。

  • 比特币中,默认的方法是采取纯粹自愿的收费方式,矿工扮演守门人的角色并且动态设置收费的最小值。
  • 以太坊中允许交易发送者设置任意数目的gas。这种方式在比特币社区非常受欢迎,因为它是“市场经济”的体现:允许矿工和交易者之间依据供需关系来决定价格。然而,这种方式的问题是,交易处理并不遵循市场原则。尽管可以将交易处理看作是矿工向发送者提供的服务(这听起来很直观),但实际上矿工所处理的每个交易都必须由网络中的每个节点处理,所以交易处理的大部分成本都由第三方机构承担,而不是决定是否处理它的矿工。

投票系统设定gas值

  • 当前,因为缺乏矿工在实际中的行为的明确信息,所以我们将采取一个非常简单公平的方法:投票系统,来设定gas限定值。矿工有权将当前区块的gas限定值设定在最后区块的gas限定值的0.0975% (1/1024)内。所以最终的gas限定值应该是矿工们设置的中间值。我们希望将来能够采用软分叉的方法来使用更加精确的算法。

虚拟机EVM

EVM的设计

临时/永久存储的区别
我们先来看看什么是临时存储和永久存储。
临时存储:存在于VM的每个实例中,并在VM执行结束后消失;
永久存储:存在于区块链状态层。
假设执行下面的树(S代表永久存储,M代表临时存储):

  1. A调用B;
  2. B设置B.S[0]=5,B.M[0]=9 ;
  3. B调用C;
  4. C调用B。

此时,如果B试图读取B.S[0],它将得到B前面存入的数据,也就是5;但如果B试图读取B.M[0],它将得到0,因为B.M是临时存储,读取它的时候是虚拟机的一个新的实例。
在一个内部调用中,如果设置B.M[0] = 13和 B.S[0] = 17 ,然后内部调用和C的调用都终止,再执行B的外部调用,此时读取M,将会看到B.M[0] = 9(此值在上一次同一VM执行实例中设置的),B.S[0] = 17。如果B的外部调用结束,然后A再次调用B,将看到B.M[0] = 0,B.S[0] = 17。
这个区别的目的是:

  1. 每个执行实例都分配有内存空间,不会因为循环调用而减损,这让安全编程更加容易。
  2. 提供一个能够快速操作的内存形式:因为需要修改树,所以存储更新必然很慢。

栈/内存模式
早期,计算状态有三种:栈(stack,一个32字节标准的LIFO),内存(memory,可无限扩展的临时字节数组),存储(storage,永久存储)。在临时存储端,栈和内存的替代方案是memory-only范式,或者是寄存器和内存的混合体(两者区别不大,寄存器本质上也是一种内存)。在这种情况下,每个指令都有三个参数,例如:ADD R1 R2 R3: M[R1] = M[R2] + M[R3] 。选择栈范式的原因很明显,它使代码缩小了4倍。
单词大小32字节
在大多数结构中,如比特币,单词大小是4或8字节。4或8字节对存储地址或加密计算来说局限性太大了。而太大的值又很难建立相应安全的gas模型。32字节是一个理想大小,因为它足够存储下许多加密算法的实现以及地址,又不会因为太大而导致效率低下。

使用了可变、可扩展的内存大小
固定内存的大小是不必要的限制,太小或太大都不合适。如果内存大小是固定的,每次访问内存都需要检查访问是否超出边界,显然这样的效率并不高。

栈大小没有限制
没什么特别理由!许多情况下,该设计不是绝对必要的;因为,gas的开销和区块层gas的限制总是会充当每种资源消耗的上限。

1024调用深度限制
许多编程语言在栈的深度过大时触发中断比在内存过载时触发中断的策略要快的多。所以区块中gas限制所隐含的限制是不够的。

SIGNEXTEND
SIGNEXTEND操作码的作用是为了方便从大的有符号整数到小的有符号整数的类型转换。小的有符号整数是很有用的,因为未来的即时编译虚拟机可能会检测长时间运行的代码块,小的有符号整数能加快处理。

以太坊社区知识库

https://ethfans.org/wikis/%E4%BB%A5%E5%A4%AA%E5%9D%8A%E8%AE%BE%E8%AE%A1%E5%8E%9F%E7%90%86

你可能感兴趣的:(以太坊原理研读笔记)