【学习专栏】“共识”以太坊是如何运作的?下

图片
这篇文章的目的是解释以太坊如何在技术层面上运行,而不需要复杂的数学或看起来很吓人的公式。即使你不是程序员,也可以看懂。如果某些部分技术性太强且难以理解,那完全没问题!真的没有必要了解每一个小细节。我建议只专注于在广泛的层面上理解事物。
这篇文章中涵盖的许多主题都是对黄皮书中讨论的概念的细分。我添加了自己的解释和图表,会帮助更好的理解以太坊。那些勇于接受技术挑战的人也可以直接阅读以太坊黄皮书。
上期我们聊到, “为什么我们还要为存储付费?” ,就像计算一样,以太坊网络上的存储是整个网络必须承担的成本。
交易Transaction和消息messages
前文讲过,以太坊是一个基于事务的状态机。换句话说,不同账户之间发生的交易是将以太坊的全球状态从一种状态转移到另一种状态的原因。
从最基本的意义上说,交易是由外部拥有的账户生成、序列化、然后提交到区块链的加密签名指令。
有两种类型的交易:消息调用message calls和合约创建contract creations(即创建新的以太坊合约的交易)。
所有交易都包含以下组件,无论其类型如何:
nonce:发送方发送的交易数量的计数。
gasPrice:发送方愿意为执行交易所需的每单位 gas 支付的 Wei 数量。
gasLimit:发送者愿意为执行此交易支付的最大gas量。在完成任何计算之前,该金额已预先设定并支付。
to:收件人的地址。在创建合约的交易中,合约账户地址尚不存在,因此使用空值。
value:从发送者转移到接收者的 Wei 数量。在创建合约的交易中,该值作为新创建合约账户中的起始余额。
v, r, s:用于生成标识交易发送者的签名。
init(仅存在于创建合约的交易中):用于初始化新合约账户的 EVM 代码片段。init只运行一次就会被丢弃。首次运行init时,它会返回账户代码的主体,这是与合约账户永久关联的一段代码。
data(可选字段,只存在于消息调用中):消息调用的输入数据(即参数)。例如,如果智能合约用作域注册服务,则对该合约的调用可能需要输入字段,例如域和 IP 地址。
图片
我们在“账户Accounts”部分了解到,消息调用和合约创建的交易——总是由外部拥有的账户发起,并提交到区块链。另一种思考方式是,交易是外部世界与以太坊内部状态的桥梁。
图片
但这并不意味着合约不能与其他合约对话。
存在于以太坊状态全局范围内的合约,可以与同一范围内的其他合约进行对话。他们这样做的方式是通过“消息”或“内部交易”发送给其他合约。
我们可以将消息或内部交易视为类似交易,主要区别在于:它们不是由外部拥有的帐户生成的。相反,它们是由合约生成的。它们是虚拟对象,与事务不同,它们没有序列化,只存在于以太坊执行环境中。
当一个合约向另一个合约发送内部交易时,将执行存在于接收合约账户上的相关代码。
图片
需要注意的一件重要事情是:内部事务或消息不包含gasLimit。这是因为gas限制是由原始交易的外部创建者(即一些外部拥有的账户)决定的。外部拥有的账户设置的gasLimit必须足够高,以能保证交易的执行,包括由该交易而引发的任何子交易的执行,例如合约到合约的消息。如果在交易和消息链中,特定的消息执行用尽了 gas,那么该消息的执行将连同由执行触发的任何后续消息一起恢复。但是,父执行不需要还原。
块Blocks
所有交易都被组合成“块”。区块链包含一系列链接在一起的此类块。
在以太坊中,一个块包括:
块头 the block header
有关该块中包含的交易集的信息
当前区块的 ommers 的一组其他区块头。
奥默斯Ommers explained
“ommer”到底是什么?ommer 是一个块,其父级等于当前块的父级的父级。让我们快速深入了解 ommer 的用途以及为什么一个块包含 ommer 的块头。
由于以太坊的构建方式,出块时间(约 15 秒)比比特币等其他区块链(约 10 分钟)要短得多。这可以实现更快的事务处理速度。然而,较短出块时间的缺点之一,是矿工发现了更多竞争的区块解决方案。这些竞争块也被称为“孤立块” “orphaned blocks” (orphaned blocks不会进入主链)。
ommers 的目的是帮助奖励包含这些孤立区块的矿工。矿工包含的 ommers 必须是“有效的”,也就是说它必须在当前区块的第六代或更小的范围内。在六个子代之后,不能再引用陈旧的孤立块(因为包含较旧的交易会使事情变得有点复杂)。
Ommer 块获得的奖励小于完整块。
块头Block header
每个区块都有一个区块“头”,但这到底是什么?
字面意思很清楚,块头是块的一部分,包括:
parentHash : 父区块头的哈希值(这就是使区块设置为“链”的原因)
ommersHash : 当前区块的 ommers 列表的哈希值
受益人:接收该区块挖矿费用的账户地址
stateRoot:状态树的根节点的哈希值(回想一下我们是如何得知状态树存储在标头中的,这使得轻客户端可以轻松地验证有关状态的任何内容)
transactionsRoot:包含此块中列出的所有交易的 trie 根节点的哈希
receiptsRoot收据根:包含此块中列出的所有交易收据的特里树根节点的哈希
logsBloom:由日志信息组成的Bloom过滤器(数据结构)
difficulty难度:这个块的难度级别
number:当前块的数量(创世块的块号为零;每个后续块的块号增加1)
gasLimit : 当前每个区块的 gas 限制
gasUsed:该区块中交易使用的总gas总和
timestamp : 此区块开始的 unix 时间戳
extraData : 与此块相关的额外数据
mixHash : 一个哈希,当与 nonce 结合时,证明这个块已经进行了足够的计算
nonce : 一个哈希,当与 mixHash 结合时,证明这个块已经进行了足够的计算
图片
请注意每个块头如何包含三个 trie 结构:
状态 ( stateRoot )
交易(交易根)
收据(receiptsRoot)
这些 trie 结构只不过是我们之前讨论过的 Merkle Patricia 尝试。
此外,上述描述中有一些术语值得单独拿出来讲一下。让我们来看看。
日志Logs
以太坊允许使用日志来跟踪各种交易和消息。合约可以通过定义要记录的“事件”,来生成日志。
日志条目包含:
记录器的帐户地址,
代表此交易执行的各种事件的一系列主题,以及
与这些事件相关的任何数据。
日志存储在布隆过滤器bloom filter中,它以有效的方式存储无穷的日志数据。
交易收据Transaction receipt
标头中存储的日志来自交易收据中包含的日志信息。就像您购买商品时收到收据一样,以太坊会为每笔交易生成收据。每张收据都包含有关交易的信息。具体来说,收据包括以下项目:
块号
块哈希
交易哈希
当前交易使用的gas
当前事务执行后当前块中使用的累积gas
执行当前事务时创建的日志
..其他事项
块难度Block difficulty
块的“难度”用于在验证块所需的时间内强制执行的一致性。创世区块的难度为131,072,在这之后,使用特殊的公式计算每个区块的难度。如果某个区块的验证速度比前一个区块快,那么以太坊协议会增加该区块的难度。
区块的难度会影响nonce,nonce是在挖掘区块时必须使用工作量证明算法计算的哈希值。
块的难度和随机数之间的关系在数学上形式化为:
图片
其中Hd就是难度。
找到满足难度阈值的随机数的唯一方法是使用工作量证明算法来枚举所有可能性。找到解决方案的预期时间与难度成正比:难度越高,找到 nonce 变得越困难,因此验证区块就越难,这反过来又增加了验证新区块所需的时间。因此,通过调整区块的难度,协议可以调整验证区块所需的时间。
如果验证时间变慢,则协议会降低难度。通过这种方式,验证时间会自我调整,以保持恒定的速率:平均每 15 秒一个块。
交易执行Transaction Execution
这一部分是以太坊协议中最复杂的部分之一。假设您将交易发送到以太坊网络进行处理。将以太坊的状态转换为包含您的交易会发生什么?
图片
首先,所有交易必须满足初始要求才能被执行。包括:
交易必须是格式正确的RLP。“RLP”代表“递归长度前缀Recursive Length Prefix”,是一种用于编码二进制数据嵌套数组的数据格式。RLP 是以太坊用来序列化对象的格式。
有效的交易签名。
有效的交易随机数。回想一下,帐户的 nonce 是从该帐户发送的交易计数。为了有效,交易随机数必须等于发送方帐户的随机数。
交易的gas限制必须等于或大于交易使用的固有gasintrinsic gas。固有gas包括:
执行交易的预定义成本 21,000 gas
与交易一起发送的数据的 gas 费(每个等于 0 的数据或代码字节需要 4 个 gas,每个非零数据或代码字节需要 68 个 gas)
如果交易是创建合约的交易,额外的 32,000 gas
发送者的账户余额必须有足够的以太币来支付发送者必须支付的“前期”gas费用。前期 gas 成本的计算很简单:首先,交易的gas 限额乘以交易的gas 价格来确定最大的 gas 成本。然后,将此最大成本添加到从发送者转移到接收者的总价值中。
如果交易满足上述所有有效性要求,那么可以进入下一步。
首先,我们从发送者的余额中扣除执行的前期成本,并将发送者账户的 nonce 增加 1 以计入当前交易。此时,我们可以将剩余gas计算为交易的总gas限制减去使用的固有gas。
接下来,交易开始执行。在整个交易的执行过程中,以太坊保持对 "子状态 "的跟踪。这个子状态是记录交易过程中积累的信息的一种方式,这些信息在交易完成后将被立即需要。具体来说,它包含:
自毁集Self-destruct set:交易完成后将被丢弃的一组账户(如果有)。
日志系列Log series:虚拟机代码执行的存档和可索引的检查点。
退款余额Refund balance:交易完成后退还至汇款人账户的金额。还记得我们如何提到以太坊中的存储需要花钱,并且发件人会因清理存储而获得退款吗?以太坊使用退款计数器跟踪这一点。退款计数器从零开始,每次合约删除存储中的内容时都会递增。
接下来,谈谈处理事务所需的各种计算。
一旦处理完交易所需的所有步骤,并假设没有无效状态,则通过确定要退还给发送者的,未使用gas的数量,来最终确定状态。除了未使用的gas,发送方还从我们上面描述的“退款余额”中退还一些津贴。
发件人退款后:
Gas费的以太币给了矿工
交易使用的gas被添加到块gas计数器(它跟踪块中所有交易使用的总gas,并且在验证块时很有用)
自毁集中的所有账户(如果有)都被删除
最后,我们留下了新状态和事务创建的一组日志。
现在我们已经介绍了交易执行的基础知识,让我们看看创建合约的交易和消息调用之间的一些区别。
合同创建Contract creation
回忆一下,在以太坊中,有两种类型的账户:合约账户和外部拥有的账户。当我们说交易是“合约创建”时,我们的意思是交易的目的是创建一个新的合约账户。
为了创建一个新的合约账户,我们首先使用一个特殊的公式声明新账户的地址。然后我们通过以下方式初始化新帐户:
将随机数nonce设置为零
如果发件人在交易中发送了一些以太币作为价值,则将账户余额设置为该价值
从发件人的余额中减去添加到此新帐户余额的值
将存储设置为空
将合约的 codeHash 设置为空字符串的哈希
一旦我们初始化了账户,我们就可以使用与交易一起发送的初始化代码来实际创建账户(请参阅“交易和消息”部分以复习了解初始化代码)。在执行此初始化代码期间发生的情况是多种多样的。根据合约的构造函数,它可能会更新账户的存储、创建其他合约账户、进行其他消息调用等。
在执行初始化合约的代码时,它会使用 gas。交易消耗的gas不得超过剩余的gas。如果是这样,执行将遇到gas不足 (OOG) 异常并退出。如果交易由于耗尽gas异常而退出,则状态将恢复到交易之前的点。发件人不会退还用完之前消耗的gas。
但是!
但是,如果发送者在交易中发送了任何以太币值,即使合约创建失败,以太币值也会被退还。
如果初始化代码成功执行,则支付最终的成本(合约创建的成本)。这是一个存储成本,与创建的合约代码的大小成正比(没错,干啥都要花钱!)如果没有足够的 gas 剩余来支付这个最终成本,那么交易再次声明一个 out-of-gas 异常并且中止。
如果一切顺利,那么任何剩余的未使用的 gas 都将退还给交易的原始发送者,并且现在允许更改的状态持续存在!
消息调用Message calls
消息调用的执行与合约创建的执行类似,但有一些区别。
因为没有创建新帐户,消息调用执行不包括任何初始化代码。但是,如果该数据是由交易发送者提供的,它则可以包含输入数据。一旦执行,消息调用还有一个包含输出数据的额外组件,如果后续执行需要此数据,则使用该组件。
与创建合约一样,如果消息调用执行因耗尽 gas 或交易无效(例如堆栈溢出、无效跳转目标或无效指令)而导致退出,那么,所使用的 gas 都不会退还给原始调用者。相反,如果所有剩余的未使用gas都被消耗掉,那么状态会被重置到余额转移之前的点。
在以太坊的最新更新之前,如果不让系统消耗您提供的所有gas,就无法停止或恢复交易的执行。例如,假设您创建的合约在调用者无权执行某些交易时引发错误,在以前的以太坊版本中,剩余的 gas 仍然会被消耗掉,并且不会将 gas 退还给发送者。但拜占庭更新包含一个新的“恢复”代码,允许合约停止执行并恢复状态更改,同时不消耗剩余的gas,并能够返回失败交易的原因。如果交易因还原而退出,则未使用的 gas 将返回给发送者。
执行模型Execution model
到目前为止,我们已经了解了事务从开始到结束必须执行的一系列步骤。现在,我们将看看事务是如何在 VM 中实际执行的。
协议中实际处理交易的部分是以太坊自己的虚拟机,称为以太坊虚拟机(EVM)。
如前所述,EVM 是一个图灵完备的虚拟机。EVM 的唯一限制是是受gas约束的。也就是说,可以完成的计算总量,本质上受到所提供的gas量的限制。
图片
资料来源:CMU
此外,EVM 具有基于堆栈的架构。堆栈机器是使用后进先出堆栈来保存临时值的计算机。
EVM 中每个栈项的大小为 256 位,栈最大为 1024。
EVM 有内存,其中项目存储为字寻址字节数组。内存是易失的,不是永久性的。
EVM 也有存储。与内存不同,存储是非易失性的,并且作为系统状态的一部分进行维护。EVM 将程序代码单独存储在一个只能通过特殊指令访问的虚拟ROM中。通过这种方式,EVM 与典型的冯诺依曼架构不同,后者将程序代码存储在内存或存储器中。
图片
EVM 也有自己的语言:“EVM 字节码”。当程序员编写在以太坊上运行的智能合约时,我们通常会使用更高级别的语言(例如 Solidity)编写代码。然后我们可以将其编译为 EVM 可以理解的 EVM 字节码。
好的,现在开始执行。
在执行特定计算之前,处理器确保以下信息可用且有效:
系统状态
用于计算的剩余gas
拥有正在执行的代码的帐户的地址
发起此次执行的交易的发送方地址
导致代码执行的帐户地址(可能与原始发件人不同)
发起此次执行的交易的 Gas 价格
此执行的输入数据
作为当前执行的一部分传递给此帐户的值(以 Wei 为单位)
要执行的机器代码
当前区块的区块头
当前消息调用或合约创建堆栈的深度
在执行开始时,内存和堆栈为空,程序计数器为零。
然后,EVM 递归地执行事务,计算每个循环的系统状态和机器状态。系统状态就是以太坊的全局状态。机器状态包括:
可用gas
程序计数器
内存内容
内存中的活动字数
堆栈内容。
从系列的最左侧部分添加或删除堆栈项。
在每个循环中,从剩余gas中减少适当的gas量,并且程序计数器递增。
在每个循环结束时,存在三种可能性:
机器达到异常状态(例如,gas 不足、指令无效、堆栈项不足、堆栈项将溢出 1024 以上、无效的 JUMP/JUMPI 目标等),因此必须停止,并丢弃任何更改
序列继续处理进入下一个循环
机器达到受控停止(执行过程结束)
假设执行没有达到异常状态,并达到“受控”或正常停止,机器会生成结果状态、执行后的剩余gas、应计子状态和结果输出。
现在我们通过了以太坊最复杂的部分之一。即使你没有完全理解这部分,也没关系。除非您在非常深的层次上工作,否则您实际上并不需要了解具体的执行细节。
一个区块是如何被确定(finalized)的
最后,让我们看看一个包含许多交易的区块是如何最终确定的。
当我们说“最终确定finalized”时,它可能意味着两种不同的东西,具体取决于块是新的还是现有的。
如果它是一个新块,我们指的是挖掘这个块所需的过程。
如果它是一个现有的块,那么我们正在讨论验证该块的过程。
在任何一种情况下,要“最终确定”一个块有四个要求:
1) 验证(或者,如果是挖矿mining,那就是determine)ommer 块头中的每一个区块必须是一个有效的区块头,并且是在本区块的第六代内。
2) 验证(或者,如果是挖矿,则确定)交易区块上的数量必须等于区块中列出的交易使用的累积gas。(回想一下,在执行交易时,我们会跟踪块gas计数器,它会跟踪块中所有交易使用的总gas)。
3) 应用奖励(仅在挖矿mining时)
受益人地址获得 5 Ether 用于挖掘该区块。(根据以太坊提案EIP-649,这个 5 ETH 的奖励将很快减少到 3 ETH)。此外,对于每个 ommer,当前区块的受益人将额外获得当前区块奖励的 1/32。最后,ommer 区块的受益人也会获得一定的奖励(有一个特殊的计算公式)。
4)验证(或者,如果是挖掘,计算一个有效的)状态和随机数
确保所有的交易和结果状态变化都被应用,然后将新的区块定义为区块奖励应用于最终交易的结果状态之后的状态。验证是通过检查这个最终状态与存储在头中的状态树进行的。
挖矿工作量证明
“区块”部分简要介绍了区块难度的概念。赋予区块难度意义的算法称为工作证明 (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有点复杂,在这里我们不展开讨论。但大致上,它是这样工作的:
为每个块计算一个“种子seed”。这个种子对于每个“纪元epoch”都是不同的,每个纪元有 30,000 个块长。对于第一个纪元epoch,种子是一系列 32 字节零的散列。对于随后的每个 epoch,它是前一个种子散列的散列。使用这个种子,节点可以计算一个伪随机“缓存”。
这个缓存非常有用,因为它启用了我们之前讨论过的“轻节点”的概念。轻节点的目的是让某些节点能够有效地验证交易,而无需存储整个区块链数据集。因为缓存可以重新生成它需要验证的特定块,轻节点可以仅基于此缓存来验证交易的有效性。
使用缓存,节点可以生成 DAG“数据集dataset”,其中。数据集中的每个项目都依赖于缓存中少量伪随机选择的项目。为了成为一名矿工,你必须生成这个完整的数据集;所有完整的客户端和矿工都将存储此数据集,并且数据集随时间线性增长。
然后,矿工可以随机抽取数据集的切片,并通过数学函数将它们散列成“ mixHash”。” 矿工将重复生成一个mixHash,直到输出低于所需的目标随机数nonce。当输出满足这个要求时,这个随机数nonce被认为是有效的,并且可以将块添加到链中。
挖矿Mining的安全机制
总体而言,PoW 的目的,是以加密安全的方式证明已经花费了特定数量的计算来生成一些输出(即随机数nonce)。这是因为,除了枚举所有可能性之外,没有更好的方法来找到低于所需阈值的随机数nonce。重复应用哈希函数的输出具有均匀分布,因此可以确信,平均而言,找到这样一个随机数所需的时间取决于难度阈值。难度越高,解决随机数所需的时间就越长。通过这种方式,PoW 算法赋予了难度概念以意义,用于加强区块链的安全性。
我们所说的区块链安全是什么意思?很简单:我们想创建一个每个人都信任的区块链。正如我们之前在这篇文章中所讨论的,如果存在多个链,用户将失去信任,因为他们将无法合理地确定哪个链是“有效”链。为了让一组用户接受存储在区块链上的底层状态,我们需要一组人相信的单一规范区块链。
这正是 PoW 算法所做的:它确保特定区块链在未来也能保持规范,使攻击者难以创建覆盖历史特定部分的新块(例如,通过擦除交易或创建虚假交易)或维护一个分叉。
为了首先验证他们的区块,攻击者需要始终比网络中的任何其他人更快地解决随机数,以便网络认为他们的链是最重的链(基于我们前面提到的 GHOST 协议的原则)。除非攻击者拥有超过一半的网络挖矿能力,否则这是不可能的,这种情况被称为多数 51% 攻击。
图片
挖矿Mining的财富分配机制
除了提供安全的区块链之外,PoW 还是一种将财富分配给那些为提供这种安全性而花费计算的人的方式。回想一下,矿工因挖出一个区块而获得奖励,包括:
“获胜”区块的静态区块奖励为 5 以太币(即将更改为 3 以太币)
区块中包含的交易在区块内消耗的 gas 成本
将 ommers 包含在区块中的额外奖励
为了确保使用 PoW 共识机制进行安全和财富分配的长期可持续性,以太坊努力灌输这两个属性:
让尽可能多的人可以访问它。换句话说,人们不应该需要专门的或不常见的硬件来运行算法。这样做的目的是使财富分配模型尽可能开放,以便任何人都可以提供任意数量的计算能力来换取以太币。
减少任何单个节点(或小集合)获得不成比例利润的可能性。任何可以赚取不成比例利润的节点都意味着该节点对确定规范区块链有很大的影响。这很麻烦,因为它降低了网络安全性。
在比特币区块链网络中,与上述两个属性相关的一个问题是:PoW 算法是一种 SHA256 哈希函数。此类功能的弱点在于,使用专用硬件(也称为 ASIC)可以更有效地解决它。
为了缓解这个问题,以太坊选择了使其PoW算法(Ethhash)具有顺序性的硬盘内存。这意味着,该算法的设计使计算nonce需要大量的内存和带宽。巨大的内存需求使得计算机很难平行使用其内存来同时发现多个nonce,而高带宽需求使得即使是超级快的计算机也很难同时发现多个nonce。这减少了集中化的风险,为进行验证的节点创造了一个更公平的竞争环境。
需要注意的一点是,以太坊正在从 PoW 共识机制转变为所谓的“股权证明”。这本身就是一个可怕的话题,希望可以在以后的文章中进行探讨。
来源:r/以太坊 作者Preethi Kasireddy