你也许不太清楚以太坊具体是什么,但是我相信你一定听过它。它最近出现在很多新闻和杂志中,但如果你不了解以太坊到底是什么,那么阅读相关文章可能会觉得很难懂。从本质上讲,以太坊是一个永久记录并保存数字交易的
你也许不太清楚以太坊具体是什么,但是我相信你一定听过它。它最近出现在很多新闻和杂志中,但如果你不了解以太坊到底是什么,那么阅读相关文章可能会觉得很难懂。
从本质上讲,以太坊是一个永久记录并保存数字交易的公共数据库。 与普通数据库不同的是,该数据库不需要任何中央机构来维护和保护它,它是作为一个“无需信任”的交易系统运行的,个人可以在这个框架中进行点对点交易,而无需信任第三方或彼此。
读完上面的介绍是不是仍然感受到很困惑?这就是为什么我要写这篇文章的原因,我希望从技术层面去解释什么是以太坊,但又不引入复杂的数学公式。即使你不是一个程序员,你也可以通过这篇文章去更好的使用区块链技术。如果本文出现一些太难理解的部分,你可以不用去深究。我希望读者能够对以太坊有个整体的理解就行。
这篇文章里面的很多内容都来自于《以太坊黄皮书》。我只是增加了个人的解释和图表来让你更好的理解以太坊。如果你有足够的勇气,也可以去直接阅读从《以太坊黄皮书》开始。
让我们开始吧!
区块链定义
区块链是一个:具有共享状态的加密安全事务单例机。这概念很难理解对吧?让我们把它拆解一下:
- “加密安全”:意味着数字货币的创造是由复杂的数学算法保护的,这些算法非常难以破解。你可以联想一下各类的防火墙。你在区块链上的交易都不可能欺骗整个系统(例如,创建虚假交易、删除交易等)。
- “交易单例机”:意味着只有一个机器的单一实例,这个单一实例负责系统中创建的所有事务。 换句话说,每个人都相信一个单一的真理。
- “共享状态”:意味着在这台机器上存储的所有状态,都是共享并且开发给所有人的。
以太坊就实现了上面所讲的几点区块链范式。
区块链范式解释
以太坊区块链本质上是一个基于交易的状态机。在计算机科学中,状态机是指能够读取一系列输入,并根据这些输入转移到新的状态的东西。
当以太坊上最初没有任何交易的状态叫「创世态」。你可以把它想象成一张什么都没有的白纸,当交易被执行时,「创世态」转换到某个最终状态。 在任何时间点,这个最终状态都代表了以太坊的当前状态。
现在以太坊的状态中记录有数以亿计的交易,这些交易被分组为“区块”。一个区块包含一系列交易,每个区块都与其前一个区块链接在一起。
要触发从一种状态到下一种状态的转换,这个交易必须得是合法。为了使交易被认为是合法,它必须经过一个我们所熟知的「挖矿」的验证过程。 挖矿是指一组节点(即计算机)消耗它们的计算资源来创建一个合法的区块。
网络上任何自称为矿工的节点都可以尝试创建和验证区块。但同一时间,来自世界各地的许多矿工都在试图创建和验证区块。每个矿工在向区块链提交区块时都会提供一个数学“证明”,这个证明起到了保障作用,如果该证明存在,则该区块即是合法的区块。
对于要添加到主区块链的区块,矿工必须比任何其他竞争矿工更快地证明它。通过让矿工提供数学证明来验证每个区块的过程被称为“工作证明”。
验证新区块的矿工因完成这项工作而获得一定的奖励。那这个奖励是什么呢?以太坊区块链使用一种称为“以太币”的原生数字货币作为奖励。每次矿工证明一个区块时,都会生成并奖励新的以太币。
你可能想知道:是什么能够保证每个人都是在以太坊这一条区块链上?一群矿工难道不会生成区块后,组成一条新的区块链吗?
早些时候,我们将区块链定义为具有共享状态的事务单例机。使用这个定义,我们可以理解当前状态是一个单一的全局事实,每个人都必须接受。如果拥有多个状态(或链),这会破坏整个系统,因为所有节点不能就哪个状态是正确的达成一致。如果链条分叉,你可能在一个链条上拥有 10 个以太币,在另一个链条上拥有 20 个以太币,在另一个链条上拥有 40 个以太币。 在这种情况下,将无法确定哪个链是最“有效的”。
每当生成了不止一个路径时,就会发生“分叉”。我们通常希望避免分叉,因为它们会破坏系统并迫使人们选择他们“相信”的链。
在以太坊中,使用了“幽灵协议”(GHOST protocol)这个机制,来确定哪条路径是有效的, 而且能避免多条链的出现。
幽灵协议:Greedy Heaviest Observed Subtree
简单来说,幽灵协议是指我们必须选择的路径是一条拥有最多计算量的路径。一种确定方式是选择区块块号最大一个区块。这个区块块号代表了当前路径下所有区块的数量(不包括创世区块)。区块的块号越大,那么这条路径上矿工在挖矿中付出的计算量就越大。使用这种方式,就可以对当前状态的版本达成一致。
现在你已经对区块链是什么有了一个大致的概览,让我们来深入了解一下以太坊区块链的主要组成部分:
- 账户(accounts)
- 状态(state)
- gas 和 gas fee
- 交易(transactions)
- 区块(blocks)
- 交易执行(transaction execution)
- 挖矿
- 工作证明
账户
以太坊的全球“共享状态”,是由许多能够通过消息传递框架相互交互的对象(“账户”)组成。每个帐户都有一个与之关联的状态,和一个20字节的地址。也就是说,以太坊中的地址是采用了160位的标识符来标识账户地址(1字节 = 8位)。
在以太坊中,有两种类型的账户:
- 外部账户:私钥控制,没有与之关联的代码。
- 合约账户:合约代码控制,有与之关联的代码。
外部账户 vs 合约账户
理清外部账户和合约账户的区别是非常重要的。外部帐户可以通过使用自己的私钥创建和签署交易,以此向其他外部拥有的帐户或其他合约帐户发送消息。两个外部拥有的账户之间的消息只能是以太币转移的数值。但是从外部账户到合约账户的消息会激活合约账户的代码,允许执行各种操作(例如转移代币、写入内部存储、铸造新代币、执行计算、创建新合约等)。
与外部账户不同,合约账户不能自行发起新交易。相反,合约账户只能响应它们收到的其他交易(来自外部账户或来自另一个合约账户)。我们将在“交易和消息”部分了解有关合约和合约调用的更多信息。
因此,在以太坊区块链上发生的任何行动,都总是由外部账户发起的交易。
账户状态(Accout State)
账户状态由四个部分组成,无论是外部账户或者合约账户都有这四个部分:
- nonce: 如果账户是外部账户,这个数字代表从账户地址发送的交易数量。如果账户是合约账户,nonce就是该账户创建的合约数量。
- balance: 账户地址所拥有的以太币的数量,这里使用wei做单位的,1Ether等于1e+18wei。
- storageRoot:梅克尔帕特里夏树(Merkle Patricia tree)的根节点的hash(我们将会在后面解释什么是梅克尔帕特里夏树)。这棵树编码了账户存储数据的哈希值,默认是空的。
- codeHash:此帐户的 EVM(以太坊虚拟机 — 稍后会详细介绍)代码的哈希值。 对于合约账户,这是经过哈希处理并存储为codeHash的代码。对于外部账户,codeHash字段的哈希值是空字符串。
全局状态
我们知道以太坊的全局状态由帐户地址和帐户状态之间的映射组成。此映射存储在称为梅克尔帕特里夏树(Merkle Patricia tree)的数据结构中。
梅克尔树(Merkle tree,也叫Markle trie,下文统一叫Markle trie)是一种由一组节点组成的二叉树,它由以下部分组成:
- 包含底层数据的,位于树底部的大量叶节点
- 一组中间节点,中间节点的每个节点是其余两个子节点的哈希
- 单个根节点,也由其两个子节点的哈希形成,表示树的顶部
树底部的数据是通过将我们要存储的数据拆分成块,然后把块拆分成桶,最后取每个桶的哈希并重复相同的过程,直到剩余的哈希总数变为只有一个:根哈希(Root hash)。
这棵树需要为每条存储的数据存储一个键值。从树的根节点开始,这个键值决定子节点是哪个,从而得到对应的值,这个值存储在叶子节点中。在以太坊的例子中,状态树的键/值映射在地址和它们关联的账户之间,包括每个账户的balance、nonce、codeHash 和 storageRoot(其中storageRoot本身就是一棵树)。
资源:以太坊白皮书
同样的trie结构也能够存储交易和收据。更具体地说,每个块都有一个“头”,它存储三个不同Merkle trie结构的根节点的哈希,包括:
- State Trie
- Transaction Trie
- Receipts Trie
(译者补充:上图中StateRoot、TransactionsRoot和ReceiptsRoot分别取自上面三个MPT的计算结果:State Trie、Transaction Trie和Recipts Trie的根节点的哈希值)。
对于以太坊中中的“轻客户端”和“轻节点”而言,Merkle trie能够有效的存储所有信息。记住,区块链是由成千上万个节点所维护的,这些节点可以大致分成两类:全节点和轻节点。
全节点会下载创世区块到当前区块的完整链来同步真个区块链,并执行其中包含的所有交易。通常,矿工存储完整的存档节点,因为他们在挖矿过程需要。 当然也可以在不执行每笔交易的情况下下载完整节点。无论如何,任何全节点都包含整个区块链。
但是某些节点其实既不需要执行每一笔交易,也不需要查询历史记录,这样的节点是没有必要保存整个区块链数据的。这样的的节点就是上文提到的轻节点。轻节点不用下载和存储整个链并执行所有交易,它只下载从创世块到当前区块的区块头数据(Block Header),不执行任何交易或检索任何关联状态。但轻节点因为保存了区块头数据,区块头数据又存储了上文提到的三个Merkle trie结构的哈希值。所以轻节点依然能够生成和接受交易、事件和余额等的验证答案。
因为 Merkle 树中的哈希值是向上传播 的,如果恶意用户试图将虚假交易交换到 Merkle 树的底部,这种变化将导致上面节点的哈希值发生变化,从而改变更上面节点的哈希值。依此类推,直到它最终改变树的根。
节点可以使用Merkle证明去验证一条数据。Merkle证明组成如下:
- 待验证的数据块及其哈希
- 树的根哈希
- 分支(区块到根一条上升路径上所有的哈希)
任何使用该证明的人,都可以验证该分支的哈希和树的关系是具有一致性的,因此给定的块实际上是在树中的那个位置。
总之,使用 Merkle Patricia 树的好处是该结构的根节点,在密码学上依赖于存储在树中的数据,因此根节点的哈希可以用作该数据的安全身份。 由于区块头包含状态、交易和收据树的根哈希,因此任何节点都可以验证以太坊的一小部分状态,而无需存储整个状态,因为整个状态这可能是无限大的。
Gas和费用(fee)
以太坊中一个非常重要的概念是费用的概念。由于以太坊网络上的交易而发生的每次计算都会产生费用 ( 天底下没有免费的午餐!), 这笔费用以称为“gas”的面额支付。
Gas 是用于衡量计算所需费用的单位,Gas Price是你愿意在每单位gas上花费的以太币,单位是“gwei”。“wei”是以太币的最小单位,1以太币等于1 x 1018wei。1gwei是1 x 109wei。
对于每笔交易,发件人通常会设置gas limit和gas price。gas price和gas limit相乘,代表发送方愿意为执行这笔交易支付的最大wei的金额。
举个例子,交易发送方将gas limit设置为50,000,gas price设置为20gwei,那着意味着这个发送方愿意最多为这笔交易费用花费:
50,000 x20gwei = 1,000,000,000,000,000wei = 0.001以太币
gas limit代表的是交易发送方愿意为这笔交易花费的最大值,如果账户中有足够的以太币,那么这笔交易就会被成功执行。如果有没有使用的gas,会以原始的以太币价格退还给发送方。
如果发送方没有提供这笔交易足够的gas,那么这笔交易会由于“out of gas”而失效。出现这种情况,这笔交易会被中止,以一条失败的记录记录在以太坊中,同时所有的状态都会回滚到交易开始之前的初始状态。而且由于在“out of gas”前已经投入了计算资源,按理来说,gas已经被消耗了,不会被退还给发送方。
你可能会好奇,gas这笔钱跑到哪里去了呢?答案是这笔钱会被发送到矿工地址上去。因为这笔交易是由矿工们参与了计算,从而验证了交易的合法性。所以gas应该奖励给矿工们。
通常来说,如果发送方付出更高的gas,那么矿工们因为能得到更高的奖励,所以会优选选择验证这笔交易。如果gas费用太低,矿工也可能会主动忽视这笔交易。为了指导发送方设置一个合理的gas价格,矿工们可以选择公布他们将执行交易的最低gas价格。
存储费用
gas并不仅仅是上文提到的,只会在交易计算时消耗,它同时也用于支付存储费用。存储的总费用与使用的 32 字节的最小倍数成正比。
存储费用的计算有较多的细节。比如,增加的存储空间会同时增加以太坊上所有的节点的存储空间,所以努力保持更小的存储空间是更有利的。由于这个原因,如果一个交易有着释放存储空间的操作,那么执行这个操作的计算费用将会被免除,这笔计算将退还并释放存储控件。
费用的作用是什么?
我们知道,在以太坊网络中任何一个交易的执行都会同时影响所有的节点。然而,在EVM上的计算成本是十分高的。所以,以太坊的智能合约理应用来执行一些简单的工作,例如运行一个简单的业务逻辑、验证签名或者其他加密场景,而不应该用来执行像是存储数据、发送邮件,或者是机器学习这样的可能会花费巨大的计算资源,让整个以太坊网络超负荷的工作。费用就是因为这个原因而被设计出来的。
以太坊是一个图灵完备的区块链,要知道一个图灵机是无法判断一段代码是否最终会结束。但是在以太坊中运行的智能合约又能够执行循环操作,所以如果计算没有费用的话,智能合约的开发者可能会写出一个无限循环的代码段,这会拖垮整个以太坊网络。因此,费用的设计能够保证整个网络不会陷入这样的恶意攻击。
您可能还会想:“为什么我们还要为存储付费呢?”。其实就像计算一样,以太坊网络上的存储也是整个网络必须承担的成本。
交易和消息
上文有提到,以太坊是一个基于交易的状态机。在不同账户之间发生交易后,整个以太坊网络会从一个状态转移到另一个状态。
交易本质上是外部账户生成的一条加密签名指令,这条指令序列化后然后提交到区块链。
交易分为两类:消息调用和合约创建。
这两类交易由以下字段组成:
- nonce:发件人发送的交易数量
- gasPrice:发送方想要执行这笔交易,愿意付出的每单位gas价格
- gasLimit:发送方愿意为执行此交易支付的最大 gas 量。 该金额需要在交易执行前预先设定并支付
- to:接收方的地址。在合约创建类型的交易中,合约账户地址还没有生成,这个时候这个字段为空值
- value:发送方项接收方发送的以太币数量(单位Wei)。在合约创建类型的交易中,这个值是新合约地址的余额值。
- v,r,s:用于生成标识交易发送方的签名
- init(仅存在于创建合约的交易中):用于初始化新合约账户的 EVM 代码片段。 init 只运行一次,然后被丢弃。 首次运行 init 时,它会返回账户代码的主体,这是与合约账户永久关联的一段代码
- data(仅存在于消息调用的可选字段):消息调用的输入数据(即参数)。 例如,如果智能合约用作域注册服务,则对该合约的调用可能需要输入字段,例如域名和 IP 地址。
我们在上面“账户”章节了解到,交易 始终是由外部账户发起并提交到区块链,消息调用和合约创建这两个交易分类都是如此。 我们可以说,交易是外部世界与以太坊内部状态的桥梁。
但是这并不意味着一个合约不能和另一个合约对话。一个存在于以太坊状态全局范围内的合约是可以和同一范围内的其他合约对话。这种方式是通过“消息”或“内部交易”的方式实现的的, 你可以把这类实现方式类比为交易,但是区别在于发起方不是外部账户,而是合约账户。不像交易行为,它们没有被序列化且只是以虚拟对象的方式存在于以太坊的执行环境中。
如果一个合约向另一个合约发送了内部交易,此时作为接收方的合约中的代码会被执行。
一个内部交易或者消息并不需要gasl limit,因为gas limit是由原始交易的外部账户决定的。外部账户设置的gas limit需要涵盖这个交易行为衍生的子交易行为,例如合约和合约之间的消息。如果在这个交易的执行链条过程中,有一个特定的消息由于gas耗尽而执行失败,那么所有已经被执行的交易都将回滚回初始状态。唯一的例外情况是如果您在合约中使用原始 CALL 操作码(除非不得不这么做,否则不建议这样做)。在这种情况下,对另一个合约的 CALL 可能会失败,但如果父合约不检查并处理该错误,则执行会正常进行,并且结果可能会显示成功。
区块
所有的交易都会被分组成为区块,一系列的链接在一起的区块组成了区块链。
在以太坊中,一个区块包含:
- 区块头
- 这个区块的交易集数据
- 当前区块的一组叔块(ommer)的区块头
什么是叔块(ommer)?
叔块(ommer)也是一个区块,它的父级是当前块的父级的父级(这就是为什么它被称作叔块的原因,有时也会被称为uncle)。让我们简单了解一下叔块的用途,为什么一个区块块需要包含叔块的块头?
由于以太坊的构建方式,出块时间(约 15 秒)相比于比特币等其他区块链(约 10 分钟)要短得多,这可以实现更快的事务处理。 但是较短的出块时间也是有缺点的,缺点之一是同一时间可能产生多个区块,这些由于竞争时产生的区块也被称为“孤块”(即块不会进入主链的区块)。
叔块的设计目的是用于同时奖励开采出有效孤块的矿工,因为这些矿工也付出了计算工作。
但是叔块收到的奖励是要比打包进入主链的区块要低的。尽管如此,因为有奖励的存在,矿工仍然有动力将这些孤块打包在内并获得奖励。
区块头
让我们回到区块本身,上文中有提到每个区块都有个区块头,那么区块头具体是什么什么呢?
区块头由一下字段组成:
- parentHash:父块的hash值(这也是为什么区块组成了区块链的原因所在)
- ommersHash:多个叔块的hash
- beneficiary:生成出这个区块的矿工地址获得的奖励
- stateRoot: Merkle trie 的根哈希,存储系统的整个状态
- transactionsRoot:此区块中列出的所有交易接收方的 trie 根节点的哈希
- logsBloom:包含日志数据的布隆过滤器(一种数据结构)
- diffculty:这个区块开采的难度
- number:当前块的计数(创世块的块号为零;每个后续块的块号增加 1)
- gasLimit:区块中的交易设置的总 gas limit
- gasUsed:这个区块中的所有交易消耗的总gas
- timestamp:矿工开采该区块的时间戳
- extraData:矿工附加在区块中的任何额外数据
- mixHash:一个hash值,和nonce字段结合时,可以用于证明该区块是由足够计算量产生的
- nonce:一个hash值,和mixHash字段结合时,可以用于证明该区块是由足够计算量产生的
每个区块头包含我们前文中提到的梅克尔帕特里夏树的三种trie结构:
- state(stateRoot)
- transactions(transactionsRoot)
- receipts(receiptsRoot)
上面的几个术语我们会在接下来的篇幅中细讲。
日志
以太坊设计了日志用于追中不同的交易和消息。一个合约如果想要记录日志,可以通过定义一个事件来实现。
一个日志包括:
- 记录日志的账户地址
- 不同交易携带的topics
- 与这个事件相关的任何数据
日志存储在布隆过滤器中,这个数据结构能够高效的存储大量数据。
交易收据
区块头中存储的日志来自交易收据中包含的日志信息。就像你去一个超市购买商品后收到一个收据一样,以太坊也会为每笔交易生成一个收据。收据中都包含这笔交易的信息,包括:
- 区块数(the block number)
- 区块hash(block hash)
- 交易hash(transaction hash)
- 交易使用的gas
- 在当前交易执行后这个区块使用的gas累计值
- 执行当前交易的日志生成量
- 等等..
区块难度
区块的“难度”用来衡量挖出一个区块平均所需要的运算次数,反映了在一定难度下用多长时间才能挖到一定数量的区块。 创世区块的难度为131,072,之后使用特殊的公式计算每个区块的难度。 如果某个区块的验证速度比前一个区块快,则以太坊协议会增加该区块的难度。
难度将会影响nonce,因为这个hash必须通过矿工通过挖矿计算而来,使用工作量证明算法。
区块难度和nonce之间关系可以用下面的数学公式表达(表示区块难度):
找到满足难度阈值的随机数的唯一方法是使用工作量证明算法来枚举所有可能性。 找到解决方案的预期时间与难度成正比 —— 难度越高,找到 nonce 就越难,因此验证区块就越难,这反过来又增加了验证新区块所需的时间。因此,通过调整区块的难度,就可以调整验证区块所需的时间。
另一方面,如果验证时间变慢,则协议会降低难度。 这样,验证时间会自我调整以保持恒定速率 — 平均每 15 秒一个块。
交易执行
我们来到了以太坊协议最复杂的一部分:交易执行。当你在以太坊网络上发送一笔交易,以太坊经历了怎样的一个状态转移?
首先,所有的交易都必须满足一组基本要求才能够被执行:
- 交易必须是格式正确的 RLP。 “RLP”代表“Recursive Length Prifix(递归长度前缀)”,是一种用于编码二进制数据嵌套数组的数据格式。 RLP 是以太坊用来序列化对象的格式。
- 合法的交易签名
- 合法的交易nonce。回想一下我们之前提到的,账户的 nonce 是从该帐户发送的交易计数。所以,交易的noce必须等于交易发送方账户地址的nonce
- 一个交易的gas limit必须大于等于这个交易的固有gas,固有gas包括:执行交易的预定义成本 21,000 gas与交易一起发送的数据的 gas 费(每个等于 0 的数据或代码字节需要 4 个 gas,每个非零数据或代码字节需要 68 个 gas)如果这个交易包括合约创建交易,额外增加32,000gas交易执行过程中的操作步骤消耗的gas
- 发送方的账户余额必须有足够的以太币来支付必须支付的“前期”gas费用。 前期 gas 成本的计算很简单:首先,交易的 gas limit乘以交易的 gas price来确定最大的 gas 成本。 然后,将此最大成本添加到从发送方转移到接收方的总价值中。
如果一笔交易通过上面的验证,那么它将进入到下一阶段:
首先,我们先从发送方余额直接减去刚才的提到的前期gas费用,并在发送方账户的nonce加1。此时,我们可以计算出剩余气体 = 交易的总gas limit - 已使用的固有气体。
下一步,这个交易开始执行。在整个交易执行过程中,以太坊都会跟踪“子状态”。 子状态是一种记录在事务期间累积的信息的方式,这些信息将在交易完成时使用。 它包含:
- 需要销毁的账户集合:交易完成后将被丢弃的一组账户(如果存在的话)。
- 日志系列:虚拟机代码执行的存档和索引的检查点
- 退款余额:交易完成后要退还给发送方账户的金额。 还记得我们如何提到以太坊中的存储需要花钱,并且发件人会因清理存储而获得退款吗? 以太坊使用退款计数器跟踪这一点, 退款计数器从0开始,每次合约删除存储中的内容时都会递增。
接下来,处理交易所需的各种计算。
如果交易所需的所有步骤都处理完成了,而且没有出现异常状态,则通过确定要退还给发送方的没消耗gas的数量来最终确定状态。 除了没消耗的gas,发送方还会从我们上面描述的“退款余额”中得到一些补偿。
一旦发送方收到退款:
- gas的费用会被奖励给矿工
- 使用的gas会被添加记录到区块的gas计数器上(用于记录这个区块消耗的总gas,这个值会在验证区块的时候被使用到)
- 销毁的丢弃的账户(如果存在的话)
最后,这笔交易留下了一个新的状态和一系列的日志。
现在,我们已经大概理解了交易执行过程的全部知识点。接下来我们来看看合约创建和消息调用这两种情况有什么不同之处吧。
合约创建
回想一下上文里面有提到,以太坊中有两种类型的账户:合约账户和外部账户。当我们说一个交易是一个“合约创建”交易的时候,这意味着这笔交易的目的是创建一个合约账户地址。
为了创建一个合约账户的地址,我们需要先用一个特定的方式声明新账户的地址。然后初始化一个新的账户地址:
- 设置合约账户地址的nonce为0
- 如果发送方在这个交易中有发送以太币,那么将这些以太币用作这个账户的余额
- 从发送方的账户上减去要发送的以太币的数值
- 设置存储为空
- 设置这个合约的codeHash为一个空字符串的hash值
一旦我们初始化了账户,我们就可以使用与交易一起发送的初始化代码来创建账户。 在执行此初始化代码期间可能会发生很多情况。根据合约的构造函数,它可能会更新账户的存储、创建其他合约账户、进行其他消息调用等。
在执行初始化合约的代码时,它会使用 gas。 交易消耗的gas不得超过剩余的gas。 如果是这样,执行将遇到gas耗尽 (OOG)的异常并退出,此时状态将恢复到交易之前的状态。发送方不会退还用完之前消耗的gas。
但是如果发送方在交易中发送了任何以太币值,即使合约创建失败,以太币也会被退还。
如果初始化代码成功执行,则支付最终的合约创建成本。 这个成本是一个存储成本,与创建的合约代码的大小成正比(记住天下没有免费的午餐!)。如果没有足够的剩余gas 来支付这个最终的存储成本,那么交易会出现声明一个gas耗尽异常并且被中止。
如果一切顺利的话,那么没有被消耗的 gas 都将退还给交易的发送方。
消息调用
消息调用的执行和合约创建过程基本类似,但也有一些不同点。
消息调用不会包含任何需要初始化的代码,但是它可以输入数据,这个输入数据是由交易的发送方提供的。消息调用还有一个包含输出数据的额外组件,如果后续执行需要这个输出数据,则需要使用该组件。
与创建合约一样,如果消息调用执行过程中出现gas耗尽或交易无效(例如堆栈溢出、无效跳转目标或无效指令)而退出,则已经消耗的 gas 不会退还给原始发送方 。所有剩余的未使用gas都被消耗掉,并且状态被重置到之前的状态。
最初以太坊是不允许在消耗提供的gas前停止或者恢复交易的,例如,如果你的一个合约被一个没有权限的调用者给调用了,那么在之前的以太坊中,剩余的gas将依然被消耗,不会退还给发送方。但在拜占庭更新后,以太坊新增了一个revert的代码,允许合约执行中断或者恢复,并且退还还没有消耗的gas。
执行模式
前文中我们已经看到了一个交易从开始执行到结束的各个步骤。现在,我们来看一下交易具体在EVM(以太坊虚拟机)中是如何执行的。
EVM 是一个图灵完备的虚拟机。 EVM 的唯一限制是 gas,这点在其他的图灵完备的虚拟机上是没有的。所以在EVM上可以完成的计算总量,本质上受到gas提供量的限制。
EVM是基于栈的,一个基于栈的机器是使用后进先出堆栈来保存临时值的计算机。
EVM 中每个栈项的大小为 256 位,栈最大的大小为 1024位。
EVM 是有内存的,其中的数据存储为字寻址字节数组。 内存中存储的数据不是永久性数据。
EVM 也有外存。与内存不同,外存作为系统状态的一部分进行维护,能够存储永久性数据。 EVM 将程序代码单独存储在一个只能通过特殊指令访问的虚拟 ROM 中。通过这种方式,EVM 与典型的冯诺依曼架构不同,后者将程序代码存储在内存或存储器中。
EVM 也有自己的语言:“EVM 字节码”。 当程序员编写在以太坊上运行的智能合约时,我们通常会使用更高级别的语言(例如 Solidity)编写代码,然后将其编译为 EVM 可以理解的 EVM 字节码。
介绍完EVM的基础概念,我们来看看具体的执行环节。
在执行一个计算前,处理器会去报下面的信息是可用和合法的:
- 系统状态
- 计算剩余的gas量
- 合约代码的创建账户地址
- 发起此次执行的交易的发送方地址
- 导致代码执行的帐户地址(可能与原始发送方不同)
- 交易执行的gas pirce
- 输入数据
- 传入的以太币数值(单位wei)
- 被执行的机器码
- 当前区块的区块头
- 当前消息调用或合约创建的栈的深度
当开始执行时,内存和栈都应该是空的,且程序计数器的值为0。
然后,EVM 递归地执行交易,计算每个循环的系统状态和机器状态。 系统状态就是以太坊的全局状态。 机器状态包括:
- 可用的gas
- 程序计数器
- 内存数据
- 内存中的活跃字数
- 栈内容
每次循环,相应的gas都会从剩余的总gas中被减去,程序计数器加1。
- 机器达到异常状态(例如gas 不足、指令无效、栈内存不足、无效的 JUMP/JUMPI 目标等)。此时计算必须停止,而且更改会被作废
- 继续处理进入下一个循环
- 机器达到受控停止(执行过程结束)
假设执行没有遇到异常状态并达到“受控”或正常停止,机器会生成结果状态、执行后的剩余气体、应计子状态和结果输出。
现在,我们一览了以太坊最复杂的部分之一。 即使你没有完全理解这部分,也没关系。 除非你想成为区块链架构师,需要了解很多底层,否则你实际上并不需要了解具体的执行细节。
区块是如何确定的?
最后,让我们来了解一下包含许多交易的区块是如何被确定的?
一个区块的确定可能意味着不同的事情,这取决于这个区块是新产生的还是已经存在的。如果这是一个新的区块,我们指的是产生区块的挖矿过程。如果这是已经存在区块,指的是区块验证过程。这两种情况的区块确定,需要4个要素:
- 验证叔块
块头中的每个叔块必须有着合法的区块头,并且在当前区块的第六代之内。
- 验证交易
区块上的gasUsed的数量,必须等于区块中交易使用的累积gas数量。
- 奖励(只有在挖矿时才有)
矿工地址因开采该区块而获得 5 Ether。(根据以太坊提案 EIP-649,这个 5 ETH 的奖励已减少到 3 ETH)。 此外,对于每个叔块,当前区块的矿工将额外获得当前区块奖励的 1/32。最后,叔块的引用区块也会获得一定的奖励(有一个特殊的计算公式)。
- 验证状态和随机数
在确保所有交易和结果的状态都发生了更改后,区块获得奖励,此时区块的新状态最终被确定并定义。
工作量证明
“区块”部分简要介绍了区块难度的概念。 赋予区块难度的算法称为工作证明 (PoW)。
以太坊的工作量证明算法称为“Ethash”(以前称为 Dagger-Hashimoto)。
该算法的正式定义如下:
m是mixHash,n是nonce。Hn是新产生区块的头部(不包括mixHash和nonce,因为这两部分是需要被计算得出的),Hn是区块头部的nonce,d是DAG,是一个很大的数据集。
在“区块”的章节中,我们有提到过区块头是由几个不同的字段组成的。上面的mixHash和nonce正是区块头中的字段:
- mixHash:一个hash,和nonce一起使用能够证明该区块提供了足够的计算量
- nonce:一个hash,和mixHash一起使用能够证明该区块提供了足够的计算量
工作量证明就是用来计算这两个hash的。
至于计算mixHash和nonce的具体细节是比较复杂的,我们未来可以单独写一篇比较深入的专栏去分析。工作量证明算法大致如下[1]:
- 为每个区块计算一个种子seed
- 根据种子可以计算一个初始大小为 16MB的伪随机缓存cache。轻客户端保存这个 cache,用于辅助校验区块和生成数据集
- 根据 cache, 可以生成一个初始大小为 1GB的DAG数据集。数据集中的每个条目(64字节)仅依赖于 cache 中的一小部分条目。数据集会随时间线性增长,每30000个区块间隔更新一次。数据集仅仅存储在完整客户端和矿工节点,但大多数时间矿工的工作是读取这个数据集,而不是改变它
- 挖矿则是在数据集中选取随机的部分并将他们一起哈希。可以根据 cache 仅生成验证所需的部分,这样就可以使用少量内存完整验证,所以对于验证来讲,仅需要保存 cache 即可。
挖矿的安全机制
总体而言,POW(工作量证明)的目的是以加密安全的方式证明花费了特定数量的计算来生成一些输出(例如nonce)。采用这个方式,是因为找不到比穷举法更好的方法来找到低于所需阈值的随机数。重复利用哈希函数的解在空间中具有均匀分布的特性。因此我们可以确信,平均而言,找到这样一个随机数所需的时间取决于难度阈值。 难度越高,解决随机数所需的时间就越长。 通过这种方式,PoW 算法能通过调整难度,来加强区块链的安全性。
区块链安全是什么意思? 很简单,我们想创建一个每个人都信任的区块链。 正如前文有提到的,如果存在多个链,用户将无法合理地确定哪个链是“有效”链,从而让该链失去用户的信任。 为了让用户接受存储在区块链上的底层状态,我们需要让人相信该区块链是一个遵循单一规范的区块链。
这正是 PoW 算法所做的:它确保特定区块链保持规范,使攻击者难以创建覆盖历史特定部分的新区块(例如,通过清除交易或创建虚假交易) 或维护一个分叉。 为了验证他们的区块,攻击者需要始终比网络中的任何其他人更快地算出nonce,从而让整个网络认为他们的链是最重的链(基于我们前面提到的 GHOST 协议的原则)。 除非攻击者拥有超过一半的网络挖矿能力,否则这是不可能的,这种情况被称为 51% 攻击。
挖矿的财富分配机制
除了提供安全的区块链之外,PoW还是一种财富分配机制,将财富分配给那些为保证区块链安全做出了计算贡献的矿工。上文中有提到,矿工因挖出一个区块而获得奖励,包括:
- 创建新区块奖励的3ETH
- 获取这个区块的交易中的gas
- 叔块奖励
为了确保使用 PoW 共识机制进行安全和财富分配的长期可持续性,以太坊努力实现这两点:
- 让尽可能多的人可以访问区块链。人们不应该需要专门的或专用硬件来运行算法。 这样做的目的是使财富分配模型尽可能开放,让任何人都可以提供任意数量的计算能力来赚取以太币。
- 减少单一节点获取巨额以太币的可能性。如果这样的事情发生了,就说明这个单一节点对区块链的影响很大,这将会降低整个网络的安全性。
在比特币区块链网络中,与上述两点相关的一个问题是 PoW 算法是一种 SHA256 哈希函数。 此算法弱点在于,使用专用硬件(也称为 ASIC)可以更高效地计算出哈希。
为了缓解这个问题,以太坊采用的是Ethash算法,该算法经过精心设计。采用Ethash计算 nonce需要大量内存和带宽。 大内存的要求,让计算机很难有足够大的内存同时发现多个 nonce,而高带宽要求使得即使是超级计算机也难以同时发现多个 nonce。 这降低了中心化的风险,并为进行节点验证创造了一个更公平的竞争环境。
需要注意的一点是,以太坊正在从 PoW 共识机制转变为所谓的“PoS(股权证明)”,预计2022年下半年实现迁移。
总结
这篇文章翻译自《How does Ethereum work, anyway?》。
文章中也许有很多要消化的东西,你可能需要多次阅读才能理解。 本人也是多次阅读以太坊黄皮书、白皮书和代码库的各个部分,然后才写出这篇文章。
尽管如此,我希望您发现此概述对你有所帮助。 如果有任何错误,请在评论区支出。
参考资料
[1] 也许是国内第一篇把以太坊工作量证明从算法层讲清楚的 - Tiny熊
[2] How does Ethereum work, anyway?
原创文章,作者:币圈吴彦祖,如若转载,请注明出处:https://www.kaixuan.pro/news/402814/