以太坊是如何工作的?
英文原文链接:https://medium.com/@preethikasireddy/how-does-ethereum-work-anyway-22d1df506369
How does Ethereum work, anyway?
Introduction
你很可能听说过以太坊区块链,不管你是否知道它是什么。它最近出现在很多新闻中,包括一些主要杂志的封面,但如果你不了解以太坊到底是什么,那么阅读这些文章可能就像胡言乱语。那是什么?从本质上讲,这是一个保存数字交易永久记录的公共数据库。重要的是,该数据库不需要任何中央机构来维护和保护它。相反,它作为一个“无需信任”的交易系统运行——一种框架,个人可以在其中进行点对点交易,而无需信任第三方或彼此。
还在迷茫吗?这就是这篇文章的用武之地。我的目的是解释以太坊如何在技术层面上运行,而不需要复杂的数学或看起来可怕的公式。即使你不是程序员,我希望你至少能更好地掌握技术。如果某些部分技术性太强且难以理解,那完全没问题!真的没有必要了解每一个小细节。我建议只专注于在广泛的层面上理解事物。
这篇文章中涵盖的许多主题都是对黄皮书中讨论的概念的细分。我添加了自己的解释和图表,以使理解以太坊更容易。那些勇于接受技术挑战的人也可以阅读以太坊黄皮书。
让我们开始吧!
Blockchain definition
区块链是“cryptographically secure transactional singleton machine with shared-state”。 [1] 这是意味深长的,不是吗?让我们分解一下。
- “Cryptographically secure” :意味着数字货币的创造是由难以破解的复杂数学算法保护的。想想各种防火墙。它们几乎不可能欺骗系统(例如,创建虚假交易、删除交易等)
- “Transactional singleton machine” :意味着有一个机器的单一规范实例负责系统中创建的所有交易。换句话说,每个人都相信一个单一的全体真理。
- “With shared-state”:意味着存储在这台机器上的状态是共享的,对所有人开放。
以太坊实现了这种区块链范式。
The Ethereum blockchain paradigm explained
以太坊区块链本质上是一个基于交易的状态机。在计算机科学中,状态机是指将读取一系列输入并根据这些输入转换到新状态的东西。
使用以太坊的状态机,我们从“创世状态”开始。这类似于在网络上发生任何交易之前的一张白纸。当交易被执行时,这个创世状态转换到某个最终状态。在任何时间点,这个最终状态都代表了以太坊的当前状态。
以太坊的状态有数百万笔交易。这些交易被分组为“块”。一个区块包含一系列交易,每个区块都与其前一个区块链接在一起。
要引起从一种状态到下一种状态的转换,交易必须是有效的。为了使交易被认为是有效的,它必须经过一个称为 mining 的验证过程。挖矿是指一组节点(即计算机)消耗它们的计算资源来创建一个有效交易块。
网络上任何自称为矿工的节点都可以尝试创建和验证区块。来自世界各地的许多矿工试图同时创建和验证区块。每个矿工在向区块链提交区块时都会提供一个数学“证明”,这个证明起到了保证的作用:如果证明存在,则该区块必须是有效的。
对于要添加到主区块链的区块,矿工必须比任何其他竞争矿工更快地证明它。通过让矿工提供数学证明来验证每个区块的过程被称为“proof of work”。
验证新区块的矿工因完成这项工作而获得一定的价值奖励。那价值是什么?以太坊区块链使用一种称为“以太”的内在数字代币。每次矿工证明一个区块时,都会生成并奖励新的以太币。
你可能想知道:什么保证每个人都坚持一个区块链?我们如何确定不存在决定创建自己的区块链的矿工子集?
早些时候,我们将区块链定义为具有共享状态的事务单例机器。使用这个定义,我们可以理解正确的当前状态是一个单一的全体真理,每个人都必须接受。拥有多个状态(或链)会破坏整个系统,因为不可能就哪个状态是正确的达成一致。如果链条分叉,你可能在一个链条上拥有 10 个硬币,在另一个链条上拥有 20 个硬币,在另一个链条上拥有 40 个硬币。在这种情况下,将无法确定哪个链是最“有效的”。
每当生成多个路径时,就会发生“分叉”。我们通常希望避免分叉,因为它们会破坏系统并迫使人们选择他们“相信”的链。
为了确定哪条路径最有效并防止出现多条链,以太坊使用了一种称为“GHOST 协议”的机制。
“GHOST” = “Greedy Heaviest Observed Subtree”
简单来说,GHOST 协议说我们必须选择在其上完成最多计算的路径。确定该路径的一种方法是使用最新的区块(“叶块”)的块号,它表示当前路径中的块总数(不包括创世块)。块数越高,路径越长,到达叶子节点的挖矿工作量就越大。使用这种推理,我们可以就当前状态的规范版本达成一致。
现在您已经了解了 10,000 英尺的区块链是什么,让我们更深入地了解以太坊系统的主要组成部分:
- accounts
- state
- gas and fees
- transactions
- blocks
- transaction execution
- mining
- proof of work
开始前的注意事项:每当我说 X 的“散列”时,我指的是以太坊使用的 KECCAK-256 散列。
Accounts
以太坊的全体“共享状态”由许多能够通过消息传递框架相互交互的小的对象(“账户”)组成。每个帐户都有一个与之关联的状态和一个 20 字节的地址。以太坊中的地址是一个 160 位的标识符,用于识别任何帐户。
有两种类型的帐户:
- 外部帐户,由私钥控制并且没有与之关联的代码。
- 合约账户,由其合约代码控制,并具有与之关联的代码。
Externally owned accounts vs. contract accounts
了解外部账户和合同账户之间的根本区别很重要。外部账户可以通过使用其私钥创建和签署交易来向其他外部账户或其他合约账户发送消息。两个外部账户之间的消息只是价值转移。但是从外部账户到合约账户的消息会激活合约账户的代码,允许其执行各种操作(例如转移 tokens、写入内部存储、铸造新代币、执行一些计算、创建新合约等)。
与外部账户不同,合约账户不能自行发起新交易。相反,合约账户只能触发交易以响应他们收到的其他交易(来自外部账户或来自另一个合约账户)。我们将在“交易和消息”部分了解有关合约到合约调用的更多信息。
因此,在以太坊区块链上发生的任何行动总是由外部账户发起的交易启动。
Account state
帐户状态由四个组件组成,无论帐户类型如何,它们都存在:
- nonce:如果该帐户是外部帐户,则此数字表示从该帐户地址发送的交易数量。如果账户是合约账户,nonce 就是该账户创建的合约数量。
- balance:此地址拥有的 Wei 数量。每个以太币有 1e+18 Wei。
- storageRoot:Merkle Patricia tree 的根节点的哈希(我们稍后会解释 Merkle 树)。该树对该账户的存储内容进行哈希编码,默认为空。
- codeHash:此帐户的 EVM(以太坊虚拟机 - 稍后会详细介绍)代码的哈希值。对于合约账户,这是经过哈希处理并存储为 codeHash 的代码。对于外部账户,codeHash 字段是空字符串的哈希值。
World state
好的,所以我们知道以太坊的全局状态由帐户地址和帐户状态之间的映射组成。此映射存储在称为 Merkle Patricia tree 的数据结构中。
Merkle 树(或也称为“Merkle trie”)是一种由一组节点组成的二叉树,其中:
- 包含底层数据的位于树底部的大量叶节点
- 一组中间节点,其中每个节点是其两个子节点的哈希
- 单个根节点,也由其两个子节点的哈希形成,表示树的顶部
树底部的数据是通过将我们要存储的数据拆分成块,然后将块拆分成桶,然后取每个桶的哈希并重复相同的过程,直到剩余的哈希总数变为只有一个:根哈希。
这棵树需要为其中存储的每个值都有一个key。从树的根节点开始,key 应该告诉你要跟随哪个子节点来获取对应的值,该值存储在叶子节点中。在以太坊的例子中,状态树的键/值映射在地址和它们关联的账户之间,包括每个账户的 balance、nonce、codeHash 和 storageRoot(其中 storageRoot 本身就是一棵树)。
同样的 trie 结构也用于存储交易和收据。更具体地说,每个区块都有一个“块头”,它存储三个不同 Merkle trie 结构的根节点的哈希,包括:
- State trie
- Transactions trie
- Receipts trie
在 Merkle tries 中有效存储所有这些信息的能力在以太坊中对于我们所说的“轻客户端”或“轻节点”非常有用。请记住,区块链由一堆节点维护。从广义上讲,有两种类型的节点:全节点和轻节点。
完整存档节点通过下载从创世块到当前区块的完整链来同步区块链,执行其中包含的所有交易。通常,矿工存储完整的存档节点,因为他们需要在 mining 过程中这样做。也可以在不执行每笔交易的情况下下载完整节点。无论如何,任何完整节点都包含整个链。
但除非节点需要执行每笔交易或轻松查询历史数据,否则实际上没有必要存储整个链。这就是轻节点概念的用武之地。轻节点不是下载和存储整个链并执行所有交易,而是仅下载从创世块到当前头的头链,而不执行任何交易或检索任何关联的状态。因为轻节点可以访问包含三个 trie 哈希的块头,所以它们仍然可以轻松生成和接收有关交易、事件、余额等的可验证答案。
这样做的原因是因为 Merkle 树中的哈希向上传播——如果恶意用户试图将虚假交易交换到 Merkle 树的底部,这种变化将导致上面节点的哈希发生变化,这将改变上面节点的哈希值,依此类推,直到它最终改变树的根。
任何想要验证一段数据的节点都可以使用称为“Merkle proof”的东西来做到这一点。Merkle proof 包括:
- 待验证的数据块及其哈希
- 树的根哈希
- “分支”(所有伙伴哈希沿着从块到根的路径上升)
任何读取证明的人都可以验证该分支的散列在树的整个过程中是一致的,因此给定的块实际上是在树中的那个位置。
总之,使用 Merkle Patricia tree 的好处是该结构的根节点在密码学上依赖于存储在树中的数据,因此根节点的哈希可以用作该数据的安全身份。由于区块头包含状态、交易和收据树的根哈希,因此任何节点都可以验证以太坊的一小部分状态,而无需存储可能是无限大小的整个状态。
Gas and payment
以太坊中一个非常重要的概念是费用的概念。由于以太坊网络上的交易而发生的每次计算都会产生费用——没有免费的午餐!这笔费用以称为“gas”的面额支付。
Gas 是用于衡量特定计算所需费用的单位。 Gas price 是您愿意在每单位 Gas 上花费的 Ether 数量,以“gwei”为单位。 “Wei”是 Ether 的最小单位,其中 1⁰¹⁸ Wei 代表 1 Ether。一 gwei 就是 1,000,000,000 Wei。
对于每笔交易,发件人都会设置 gas limit 和 gas price。 gas price 和 gas limit 的乘积代表发送方愿意为执行交易支付的最大 Wei 金额。
例如,假设发送者将 gas limit 设置为 50,000,将 gas price 设置为 20 gwei。这意味着发送者愿意花费最多 50,000 x 20 gwei = 1,000,000,000,000,000 Wei = 0.001 Ether 来执行该交易。
请记住,gas limit 代表发送者愿意花钱的最大 gas。如果他们的账户余额中有足够的以太币来支付这个最大值,他们就可以走了。在交易结束时,发件人将获得任何未使用的 gas 的退款,并以原始汇率进行兑换。
如果发件人没有提供执行交易所需的gas,则交易会“out of gas”并被视为无效。在这种情况下,交易处理中止并且发生的任何状态更改都被逆转,这样我们最终会回到交易之前的以太坊状态。此外,还会记录交易失败的记录,显示尝试了哪些交易以及失败的位置。而且由于机器在用完 gas 之前已经花费了精力来运行计算,因此从逻辑上讲,没有任何 gas 会退还给发送者。
这些gas钱到底去哪儿了?发件人花在 gas 上的所有钱都会发送到“受益人”地址,该地址通常是矿工的地址。由于矿工正在花费精力来运行计算和验证交易,因此矿工会收到 gas fee 作为奖励。
通常,发送者愿意支付的 gas price 越高,矿工从交易中获得的价值就越大。因此,矿工更有可能选择它。通过这种方式,矿工可以自由选择他们想要验证或忽略哪些交易。为了指导发送者设置什么 gas price,矿工可以选择公布他们将执行交易的最低 gas price。
There are fees for storage, too
gas 不仅用于支付计算步骤,还用于支付存储使用费用。存储的总费用与使用的 32 字节的最小倍数成正比。
存储费用有一些微妙的方面。例如,由于增加的存储会增加所有节点上的以太坊状态数据库的大小,因此有动力保持较小的数据存储量。出于这个原因,如果交易有清除存储中的条目的步骤,则免除执行该操作的费用,并退款以释放存储空间。
What's the purpose of fees?
以太坊工作方式的一个重要方面是网络执行的每一个操作都同时受到每个完整节点的影响。然而,以太坊虚拟机上的计算步骤非常昂贵。因此,以太坊智能合约最适合用于简单的任务,例如运行简单的业务逻辑或验证签名和其他加密对象,而不是更复杂的用途,例如文件存储、电子邮件或机器学习,这会给网络带来压力。征收费用可以防止用户对网络过度征税。
以太坊是图灵完备的语言。 (简而言之,图灵机是一种可以模拟任何计算机算法的机器(对于那些不熟悉图灵机的人,看看这个和这个)。这允许循环并使以太坊容易受到停机问题的影响,在这个问题中你无法确定程序是否会无限运行。如果没有费用,恶意行为者可以很容易地尝试通过在交易中执行无限循环来破坏网络,而不会产生任何影响。因此,费用可以保护网络免受蓄意攻击。
您可能会想,“为什么我们还要为存储付费?”嗯,就像计算一样,以太坊网络上的存储是整个网络必须承担的成本。
Transaction and messages
我们之前注意到以太坊是一个基于交易的状态机。换句话说,不同账户之间发生的交易是将以太坊的全体状态从一种状态转移到另一种状态的原因。
从最基本的意义上说,交易是由外部账户生成、序列化、然后提交到区块链的加密签名指令。
有两种类型的交易:消息调用和合约创建(即创建新的以太坊合约的交易)。
所有交易都包含以下组件,无论其类型如何:
- nonce:发件人发送的交易数量的计数。
- gasPrice:发送方愿意为执行交易所需的每单位 gas 支付的 Wei 数量。
- gasLimit:发送者愿意为执行此交易支付的最大 gas 量。在完成任何计算之前,该金额已预先设定并支付。
- to:收件人的地址。在创建合约的交易中,合约账户地址尚不存在,因此使用空值。
- value:从发送方转移到接收方的 Wei 金额。在创建合约的交易中,该值作为新创建合约账户中的起始余额。
- v, r, s:用于生成交易发送者身份的签名。
- init(仅适用于创建合约的交易):用于初始化新合约账户的 EVM 代码片段。 init 只运行一次,然后被丢弃。首次运行 init 时,它会返回账户代码的主体,这是与合约账户永久关联的一段代码。
- data (仅存在于消息调用的可选字段):消息调用的输入数据(即参数)。例如,如果智能合约用作域注册服务,则对该合约的调用可能需要输入字段,例如域和 IP 地址。
我们在“账户”部分了解到,交易——消息调用和合约创建——总是由外部账户发起并提交到区块链。另一种思考方式是,交易是外部世界与以太坊内部状态的桥梁。
但这并不意味着合约不能与其他合约对话。存在于以太坊状态全局范围内的合约可以与同一范围内的其他合约进行对话。他们这样做的方式是通过“消息”或“内部交易”发送给其他合约。我们可以将消息或内部交易视为类似于交易,主要区别在于它们不是由外部帐户生成的。相反,它们是由合约生成的。它们是虚拟对象,与交易不同,它们没有序列化,只存在于以太坊执行环境中。
当一个合约向另一个合约发送内部交易时,将执行存在于接收合约账户上的相关代码。
需要注意的一件重要事情是内部交易或消息不包含 gasLimit。这是因为 gas limit 是由原始交易的外部创建者(即一些外部账户)决定的。外部账户设置的 gas limit 必须足够高以执行交易,包括由于该交易而发生的任何子执行,例如合约到合约的消息。如果在交易和消息链中,特定的消息执行用尽了 gas,那么该消息的执行将连同由执行触发的任何后续消息一起恢复。但是,父执行不需要还原。
Blocks
所有交易都被组合成“区块”。区块链包含一系列链接在一起的此类区块。
在以太坊中,一个块包括:
- 区块头
- 有关该块中包含的交易集的信息
- 当前区块的 ommers 的一组其他区块头。
Ommers explained
“ommer”到底是什么? ommer 是一个块,其父级等于当前块的父级的父级。让我们快速了解一下 ommer 的用途以及为什么一个块包含 ommer 的块头。
由于以太坊的构建方式,出块时间(约 15 秒)比比特币等其他区块链(约 10 分钟)要短得多。这可以实现更快的交易处理。然而,较短的出块时间的缺点之一是矿工发现了更多竞争的区块解决方案。这些竞争块也被称为“孤立块”(即开采的块不会进入主链)。
ommers 的目的是帮助奖励包含这些孤立区块的矿工。矿工包含的 ommers 必须是“有效的”,这意味着在当前区块的第六代或更小的范围内。在六个孩子之后,不能再引用陈旧的孤立块(因为包含较旧的交易会使事情变得有点复杂)。
Ommer 块获得的奖励小于完整块。尽管如此,矿工仍然有一些动力将这些孤块包括在内并获得奖励。
Block header
让我们暂时回到块。我们之前提到过,每个区块都有一个区块“头”,但这到底是什么?
块头是块的一部分,包括:
- parentHash:父块头的哈希(这就是使块设置为“链”的原因)
- ommersHash:当前区块的 ommers 列表的哈希
- beneficiary:收取该区块挖矿费用的账户地址
- stateRoot:状态树的根节点的哈希(回想一下我们是如何得知状态树存储在块头中的,这使得轻客户端可以轻松地验证有关状态的任何内容)
- transactionsRoot:包含此块中列出的所有交易的 trie 根节点的哈希
- receiptsRoot:包含此块中列出的所有交易收据的 trie 根节点的哈希
- logsBloom:由日志信息组成的 Bloom filter(数据结构)
- difficulty:该区块的难度级别
- number:当前块的计数(创世块的块号为零;每个后续块的块号增加 1)
- gasLimit:当前每个区块的 gas limit
- gasUsed:此区块中交易使用的总气体总和
- timestamp:该区块开始的unix时间戳
- extraData:与此块相关的额外数据
- mixHash:一个哈希,当与 nonce 结合时,证明这个块已经进行了足够的计算
- nonce:一个哈希,当与 mixHash 结合时,证明这个块已经进行了足够的计算
请注意每个块头如何包含三个 trie 结构:
- state (stateRoot)
- transactions (transactionsRoot)
- receipts (receiptsRoot)
Logs
以太坊允许使用日志来跟踪各种交易和消息。合约可以通过定义要记录的“events”来显式生成日志。
日志条目包含:
- 记录者的帐户地址,
- 代表此交易执行的各种事件的一系列主题,以及
- 与这些事件相关的任何数据。
日志存储在布隆过滤器中,它以有效的方式存储无穷无尽的日志数据。
Transaction receipt
区块头中存储的日志来自交易收据中包含的日志信息。就像您在商店购买商品时收到收据一样,以太坊会为每笔交易生成收据。如您所料,每张收据都包含有关交易的某些信息。此收据包括以下项目:
- the block number(区块数)
- block hash(区块hash)
- transaction hash(交易hash)
- gas used by the current transaction(当前交易使用的gas)
- cumulative gas used in the current block after the current transaction has executed(当前交易执行后当前块中使用的累积气体)
- logs created when executing the current transaction(执行当前交易时创建的日志)
- ..and so on
Block difficulty
区块的“难度”用于在验证块所需的时间内强制执行一致性。创世区块的难度为131,072,之后使用特殊的公式计算每个区块的难度。如果某个区块的验证速度比前一个区块快,则以太坊协议会增加该区块的难度。
区块的难度会影响 nonce,nonce 是在挖掘区块时必须使用工作量证明算法计算的哈希值。
区块难度和 nonce 之间的关系在数学上形式化为:
其中 $H_d$ 是难度。
找到满足难度阈值的随机数的唯一方法是使用工作量证明算法来枚举所有可能性。找到解决方案的预期时间与难度成正比——难度越高,找到 nonce 变得越困难,因此验证区块就越难,这反过来又增加了验证新区块所需的时间堵塞。因此,通过调整区块的难度,协议可以调整验证区块所需的时间。
另一方面,如果验证时间变慢,则协议会降低难度。通过这种方式,验证时间会自我调整以保持恒定的速率——平均每 15 秒一个块。
Transaction Execution
我们已经来到了以太坊协议中最复杂的部分之一:交易的执行。假设您将交易发送到以太坊网络进行处理。将以太坊的状态转换为包含您的交易会发生什么?
首先,所有交易必须满足一组初始要求才能执行。这些包括:
- 交易必须是格式正确的 RLP。 “RLP”代表“递归长度前缀”,是一种用于编码二进制数据嵌套数组的数据格式。 RLP 是以太坊用来序列化对象的格式。
- 有效的交易签名。
- 有效的交易 nonce 。回想一下,帐户的 nonce 是从该帐户发送的交易计数。为了有效,交易随机数必须等于发送方帐户的随机数。
- 交易的 gas limit 必须等于或大于交易使用的 intrinsic gas。
intrinsic gas 包括:
- 执行交易的预定义成本 21,000 gas。
- 与交易一起发送的数据的 gas 费(每个等于 0 的数据或代码字节需要 4 个 gas,每个非零数据或代码字节需要 68 个 gas)
- 如果交易是创建合约的交易,额外的 32,000 gas
- 发件人的账户余额必须有足够的以太币来支付发件人必须支付的“前期”gas费用。前期 gas 成本的计算很简单:首先,交易的 gas limit 乘以交易的 gas price 来确定最大的 gas 成本。然后,将此最大成本添加到从发送者转移到接收者的总价值中。
如果交易满足上述所有有效性要求,那么我们进入下一步。
首先,我们从发送者的余额中扣除执行的前期成本,并将发送者账户的 nonce 增加 1 以计入当前交易。此时,我们可以将剩余 gas 计算为交易的总 gas 限制减去使用的固有气体。
接下来,交易开始执行。在整个交易执行过程中,以太坊都会跟踪“子状态”。此子状态是一种记录在交易期间累积的信息的方式,这些信息将在交易完成后立即需要。具体来说,它包含:
- Self-destruct set:交易完成后将被丢弃的一组帐户(如果有)。
- Log series: 虚拟机代码执行的存档和可索引检查点。
- Refund balance: 交易后退还给发送方账户的金额。还记得我们如何提到以太坊中的存储需要花钱,并且发件人会因清理存储而获得退款吗?以太坊使用退款计数器跟踪这一点。退款计数器从零开始,每次合约删除存储中的内容时都会递增。
接下来,处理交易所需的各种计算。
一旦处理完交易所需的所有步骤,并假设没有无效状态,则通过确定要退还给发送者的未使用 gas 的数量来最终确定状态。除了未使用的gas,发送方还从我们上面描述的“退款余额”中退还一些津贴。
发件人退款后:
- 以太币 gas 给了矿工
- 交易使用的 gas 被添加到块 gas 计数器(它跟踪块中所有交易使用的总气体,并且在验证块时很有用)
- 自毁集中的所有账户(如果有)都被删除
最后,我们留下了新的状态和交易创建的一组日志。
现在我们已经介绍了交易执行的基础知识,让我们看看创建合约的交易和消息调用之间的一些区别。
Contract creation
回想一下,在以太坊中,有两种类型的账户:合约账户和外部账户。当我们说交易是“合约创建”时,我们的意思是交易的目的是创建一个新的合约账户。
为了创建一个新的合约账户,我们首先使用一个特殊的公式声明新账户的地址。然后我们通过以下方式初始化新帐户:
- 将 nonce 设置为零
- 如果发件人在交易中发送了一些以太币作为价值,则将账户余额设置为该价值
- 从发件人的余额中减去添加到此新帐户余额的值
- 将存储设置为空
- 将合约的 codeHash 设置为空字符串的哈希
一旦我们初始化了账户,我们就可以使用与交易一起发送的初始化代码来实际创建账户(参见“交易和消息”部分以了解初始化代码的复习)。在执行此初始化代码期间发生的情况是多种多样的。根据合约的构造函数,它可能会更新账户的存储、创建其他合约账户、进行其他消息调用等。
在执行初始化合约的代码时,它会使用 gas。交易消耗的gas不得超过剩余的gas。如果是这样,执行将遇到气体不足 (OOG) 异常并退出。如果交易由于耗尽气体异常而退出,则状态将恢复到交易之前的点。发件人不会退还用完之前消耗的gas。
Boo hoo.
但是,如果发送者在交易中发送了任何以太币值,即使合约创建失败,以太币值也会被退还。呸!
如果初始化代码成功执行,则支付最终的合约创建成本。这是一个存储成本,与创建的合约代码的大小成正比(同样,没有免费的午餐!)如果没有足够的 gas 剩余来支付这个最终成本,那么交易再次声明一个 out-of-gas 异常并且中止。
如果一切顺利并且我们无一例外地做到了这一点,那么任何剩余的未使用的 gas 都将退还给交易的原始发送者,并且现在允许更改的状态持续存在!
Hooray!
Message calls
消息调用的执行与合约创建的执行类似,但有一些区别。
消息调用执行不包括任何 init 代码,因为没有创建新帐户。但是,它可以包含输入数据,如果该数据是由交易发送者提供的。一旦执行,消息调用还有一个包含输出数据的额外组件,如果后续执行需要此数据,则使用该组件。
与创建合约一样,如果消息调用执行因耗尽 gas 或交易无效(例如堆栈溢出、无效跳转目标或无效指令)而退出,则使用的 gas 不会退还给原始调用者。相反,所有剩余的未使用气体都被消耗掉,并且状态被重置到余额转移之前的点。
在以太坊的最新更新之前,如果不让系统消耗您提供的所有气体,就无法停止或恢复交易的执行。例如,假设您创建的合约在调用者无权执行某些交易时引发错误。在以前的以太坊版本中,剩余的 gas 仍然会被消耗掉,并且不会将 gas 退还给发送者。但 Byzantium 更新包含一个新的“恢复”代码,允许合约停止执行并恢复状态更改,而不消耗剩余的气体,并能够返回失败交易的原因。如果交易因还原而退出,则未使用的 gas 将返回给发送者。
Execution model
到目前为止,我们已经了解了交易从开始到结束必须执行的一系列步骤。现在,我们将看看交易是如何在 VM 中实际执行的。
实际处理交易的协议部分是以太坊自己的虚拟机,称为以太坊虚拟机(EVM)。
如前所述,EVM 是一个图灵完备的虚拟机。 EVM 的唯一限制是典型的图灵完备机不是EVM本质上受 gas 限制。因此,可以完成的计算总量本质上受到所提供的 gas 量的限制。
此外,EVM 具有基于堆栈的架构。堆栈机器是使用后进先出堆栈来保存临时值的计算机。
EVM 中每个栈项的大小为 256 位,栈最大大小为 1024。
EVM 具有内存,其中项目存储为字寻址字节数组。内存是易失的,这意味着它不是永久性的。
EVM 也有存储。与内存不同,存储是非易失性的,并且作为系统状态的一部分进行维护。 EVM 将程序代码单独存储在一个只能通过特殊指令访问的虚拟 ROM 中。通过这种方式,EVM 与典型的冯诺依曼架构不同,后者将程序代码存储在内存或存储器中。
EVM 也有自己的语言:“EVM 字节码”。当像你我这样的程序员编写在以太坊上运行的智能合约时,我们通常会使用更高级别的语言(例如 Solidity)编写代码。然后我们可以将其编译为 EVM 可以理解的 EVM 字节码。
好的,现在开始执行。
在执行特定计算之前,处理器确保以下信息可用且有效:
- System state(系统状态)
- Remaining gas for computation(用于计算的剩余气体)
- Address of the account that owns the code that is executing(拥有正在执行的代码的帐户的地址)
- Address of the sender of the transaction that originated this execution(发起此次执行的交易的发送方地址)
- Address of the account that caused the code to execute (could be different from the original sender)(导致代码执行的帐户地址(可能与原始发件人不同))
- Gas price of the transaction that originated this execution(发起此次执行的交易的 Gas 价格)
- Input data for this execution(此执行的输入数据)
- Value (in Wei) passed to this account as part of the current execution(作为当前执行的一部分传递给此帐户的值(以 Wei 为单位))
- Machine code to be executed(要执行的机器代码)
- Block header of the current block(当前区块的区块头)
- Depth of the present message call or contract creation stack(当前消息调用或合约创建堆栈的深度)
在执行开始时,内存和堆栈为空,程序计数器为零。
PC: 0 STACK: [] MEM: [], STORAGE: {}
然后,EVM 递归地执行交易,计算每个循环的系统状态和机器状态。系统状态就是以太坊的全局状态。机器状态包括:
- gas available(可用的 gas)
- program counter(程式计数器)
- memory contents(内存内容)
- active number of words in memory(内存中的活动字数)
- stack contents(栈内容)
从系列的最左侧部分添加或删除堆栈项。
在每个循环中,从剩余气体中减少适当的 gas 量,并且程序计数器递增。
在每个循环结束时,存在三种可能性:
- 机器达到异常状态(例如,gas 不足、指令无效、堆栈项不足、堆栈项将在 1024 以上溢出、无效的 JUMP/JUMPI 目标等),因此必须停止,并丢弃任何更改
- 序列继续处理进入下一个循环
- 机器达到受控停止(执行过程结束)
假设执行没有遇到异常状态并达到“受控”或正常停止,机器会生成结果状态、执行后的剩余气体、应计子状态和结果输出。
呸。我们通过了以太坊最复杂的部分之一。即使你没有完全理解这部分,也没关系。除非您在非常深的层次上工作,否则您实际上并不需要了解具体的执行细节。
How a block gets finalized
最后,让我们看看一个包含许多交易的区块是如何最终确定的。
当我们说“最终确定”时,它可能意味着两种不同的东西,具体取决于块是新的还是现有的。如果它是一个新块,我们指的是挖掘这个块所需的过程。如果它是一个现有的块,那么我们正在讨论验证该块的过程。在任何一种情况下,要“最终确定”一个区块都有四个要求:
1) Validate (or, if mining, determine) ommers
块头中的每个 ommer 块必须是有效的头,并且在当前块的第六代之内。
2) Validate (or, if mining, determine) transactions
区块上的 gasUsed 数量必须等于区块中列出的交易使用的累积gas。 (回想一下,在执行交易时,我们会跟踪块气体计数器,它会跟踪块中所有交易使用的总气体)。
3) Apply rewards (only if mining)
受益人地址因开采该区块而获得 5 Ether。 (根据以太坊提案 EIP-649,这个 5 ETH 的奖励将很快减少到 3 ETH)。此外,对于每个 ommer,当前区块的受益人将额外获得当前区块奖励的 1/32。最后,ommer 区块的受益人也会获得一定的奖励(有一个特殊的计算公式)。
4) Verify (or, if mining, compute a valid) state and nonce
确保应用所有交易和结果状态更改,然后将新块定义为在将块奖励应用于最终交易的结果状态后的状态。通过对照存储在区块头中的状态树检查这个最终状态来进行验证。
Mining proof of work
“区块”部分简要介绍了区块难度的概念。赋予区块难度意义的算法称为工作证明 (PoW)。
以太坊的工作量证明算法称为“Ethash”(以前称为 Dagger-Hashimoto)。
该算法正式定义为:
其中 m 是 mixHash,n 是 nonce,Hn 是新区块的头(不包括要计算的 nonce 和 mixHash 分量),Hn 是区块头的 nonce,d 是 DAG,它是大数据集。
在“区块”部分,我们讨论了区块头中存在的各种项目。其中两个组件被称为 mixHash 和 nonce。您可能还记得:
- mixHash:是一个哈希,当与 nonce 结合时,证明这个块已经进行了足够的计算
- nonce:是一个哈希,当与 mixHash 结合时,证明这个块已经进行了足够的计算
PoW 函数用于评估这两项。
如何使用 PoW 函数精确计算 mixHash 和 nonce 有点复杂,我们可以在另一篇文章中深入探讨。但在高层次上,它是这样工作的:
为每个块计算一个“种子”。这个种子对于每个“epoch”都是不同的,每个 epoch 有 30,000 个块长。对于第一个 epoch,种子是一系列 32 字节零的散列。对于随后的每个 epoch,它是前一个种子散列的散列。使用这个种子,节点可以计算一个伪随机“cache”。
这个缓存非常有用,因为它启用了我们之前在本文中讨论过的“轻节点”的概念。轻节点的目的是让某些节点能够有效地验证交易,而无需存储整个区块链数据集。轻节点可以仅基于此缓存来验证交易的有效性,因为缓存可以重新生成它需要验证的特定块。
使用缓存,节点可以生成 DAG“数据集”,其中数据集中的每个项目都依赖于缓存中少量伪随机选择的项目。为了成为一名矿工,你必须生成这个完整的数据集;所有完整的客户端和矿工都存储此数据集,并且数据集随时间线性增长。
然后,矿工可以随机抽取数据集,并通过数学函数将它们散列成“mixHash”。矿工将重复生成一个 mixHash,直到输出低于所需的目标随机数。当输出满足这个要求时,这个 nonce 被认为是有效的,并且可以将块添加到链中。
Mining as a security mechanism
总体而言,PoW 的目的是以加密安全的方式证明已经花费了特定数量的计算来生成一些输出(即随机数)。这是因为除了枚举所有可能性之外,没有更好的方法来找到低于所需阈值的随机数。重复应用哈希函数的输出具有均匀分布,因此我们可以确信,平均而言,找到这样一个随机数所需的时间取决于难度阈值。难度越高,解决随机数所需的时间就越长。通过这种方式,PoW 算法赋予了难度概念以意义,用于加强区块链的安全性。
我们所说的区块链安全是什么意思?很简单:我们想创建一个每个人都信任的区块链。正如我们之前在这篇文章中所讨论的,如果存在多个链,用户将失去信任,因为他们将无法合理地确定哪个链是“有效”链。为了让一组用户接受存储在区块链上的底层状态,我们需要一组人相信的单一规范区块链。
这正是 PoW 算法所做的:它确保特定区块链在未来保持规范,使攻击者难以创建覆盖历史特定部分的新块(例如,通过擦除交易或创建虚假交易)或维护一个分叉。为了首先验证他们的区块,攻击者需要始终比网络中的任何其他人更快地解决随机数,以便网络认为他们的链是最重的链(基于我们前面提到的 GHOST 协议的原则)。除非攻击者拥有超过一半的网络挖矿能力,否则这是不可能的,这种情况被称为多数 51% 攻击。
Mining as a wealth distribution mechanism
除了提供安全的区块链之外,PoW 还是一种将财富分配给那些为提供这种安全性而花费计算的人的方式。回想一下,矿工因挖出一个区块而获得奖励,包括:
- “获胜”区块的静态区块奖励为 5 以太币(即将更改为 3 以太币)
- 区块中包含的交易在区块内消耗的 gas 成本
- 将 ommers 包含在区块中的额外奖励
为了确保使用 PoW 共识机制进行安全和财富分配的长期可持续性,以太坊努力灌输这两个属性:
- 让尽可能多的人可以访问它。换句话说,人们不应该需要专门的或不常见的硬件来运行算法。这样做的目的是使财富分配模型尽可能开放,以便任何人都可以提供任意数量的计算能力来换取以太币。
- 减少任何单个节点(或小集合)获得不成比例利润的可能性。任何可以赚取不成比例利润的节点都意味着该节点对确定规范区块链有很大的影响。这很麻烦,因为它降低了网络安全性。
在比特币区块链网络中,与上述两个属性相关的一个问题是 PoW 算法是一种 SHA256 哈希函数。此类功能的弱点在于,使用专用硬件(也称为 ASIC)可以更有效地解决它。
为了缓解这个问题,以太坊选择使其 PoW 算法(Ethhash)顺序记忆困难。这意味着该算法经过精心设计,因此计算 nonce 需要大量内存和带宽。大内存要求使得计算机难以并行使用其内存同时发现多个 nonce,而高带宽要求使得即使是超快速计算机也难以同时发现多个 nonce。这降低了中心化的风险,并为进行验证的节点创造了一个更公平的竞争环境。
需要注意的一点是,以太坊正在从 PoW 共识机制转变为所谓的“股权证明”。这本身就是一个可怕的话题,我们希望可以在以后的文章中进行探讨。 ☺️