纯干货|深度解析以太坊(4):区块与交易执行
1 区块
所有的交易都被分组为“区块”。区块链包含一系列链接在一 起的这样的块。
在以太坊,一个区块包括:
区块头
关于包含在此区块中交易集的信息
与当前块的ommers相关的一系列其他区块头
1.1 Ommers解释
什么是“ommer”?一个ommer是一个块,其父区块等于当前块的父区块的父区块。我们来快速浏览一下什么是ommers,以及为什么一个块包含了ommers的块头。
由于以太坊的建立方式,阻塞时间比其他区块链(比如比特币(~10分钟))要低得多(~15秒)。这使得交易处理更快。然而,较短的封锁时间的缺点之一是矿工发现了更多的竞争性块解决方案。这些竞争区块也被称为“孤立区块”(即开采区块不会成为主链)。
ommers的目的是帮助奖励矿工包括这些孤区块。矿工包含的ommers必须是有效的,也就是ommers必须在父区块的第6个子区块之内或更小范围内。在第6个子区块之后,陈旧的孤区块将不会再被引用(因为包含老旧的交易会使事情变得复杂一点)。
Ommer区块会收到比全区块少一点的奖励。不管怎样,依然存在激励来让矿工们纳入孤区块并能从中获得一些报酬。
1.2 区块头
让我们暂时回到区块上。我们之前提到过,每个块都有一个块“区块头”,但究竟是什么呢?
区块头是一个区块的一部分,包含了:
parentHash:父区块头的Hash值(这也是使得区块变成区块链的原因)
ommerHash:当前区块ommers列表的Hash值
beneficiary:接收挖此区块费用的账户地址
stateRoot:状态树根节点的Hash值(回忆一下我们之前所说的保存在头中的状态树以及它使得轻客户端认证任何关于状态的事情都变得非常简单)
transactionsRoot:包含此区块所列的所有交易的树的根节点Hash值
receiptsRoot:包含此区块所列的所有交易收据的树的根节点Hash值
logsBloom:由日志信息组成的一个Bloom过滤器 (数据结构)
difficulty: 此区块的难度级别
number:当前区块的计数(创世纪块的区块序号为0,对于每个后续区块,区块序号都增加1)
gasLimit:每个区块的当前gas limit
gasUsed: 此区块中交易所用的总gas量
timestamp:此区块成立时的unix的时间戳
extraData:与此区块相关的附加数据
mixHash:一个Hash值,当与nonce组合时,证明此区块已经执行了足够的计算
nonce:一个Hash值,当与mixHash组合时,证明此区块已经执行了足够的计算
注意每个块头包含三个trie结构:
状态(stateRoot)
交易(transactionsRoot)
收据(receipttsRoot)
这三个树结构就是我们前面讨论的Merkle Patricia树。
另外,上面描述的有几个术语值得说明一下,下面来看一下。
1.3 日志
以太坊允许日志可以跟踪各种交易和消息。合约可以通过定义想要记录的“事件”来明确地生成日志。
日志条目包含:
1.记录器的帐户地址,
2.一系列代表这次交易所进行的各种事件的话题
3.任何与这些事件相关的数据。
日志存储在boom过滤器中,以高效的方式存储无尽的日志数据。
1.4 交易收据
自于被包含在交易收据中的日志信息存储在头中。正如您在商店买东西时收到收据一样,以太坊会为每笔交易生成收据。就像你所期望的那样,每个收据都包含有关交易的某些信息。这个收据包括如下项目:
区块序号
区块Hash
交易Hash
当前交易使用了的gas
在当前交易执行完之后当前块使用的累计gas
执行当前交易时创建的日志
….
1.5 区块难度
区块的难度是被用来在验证区块时加强一致性。创世纪区块的难度是131,072,有一个特殊的公式用来计算之后的每个块的难度。如果某个区块比前一个区块验证的更快,以太坊协议就会增加区块的难度。
区块的难度影响nonce,它是在挖矿时必须要使用proof-of-work算法来计算的一个hash值。
区块难度和nonce之间的关系用数学形式表达就是:
Hd代表的是难度。
找到符合难度阈值的nonce唯一方法就是使用proof-of-work算法来列举所有的可能性。找到解决方案预期时间与难度成正比—难度越高,找到nonce就越困难,因此验证一个区块也就越难,这又相应地增加了验证新块所需的时间。所以,通过调整区块难度,协议可以调整验证区块所需的时间。
另一方面,如果验证时间变的越来越慢,协议就会降低难度。这样的话,验证时间自我调节以保持恒定的速率—平均每15s一个块。
2 交易执行
我们来到以太坊协议中最复杂的部分之一:交易的执行。假设您将交易发送到以太坊网络进行处理。将以太坊的状态过渡到包括交易在内,会发生什么?
首先,所有的交易必须满足一系列的要求才能被执行。这些包括:
1.交易必须是格式正确的RLP。“RLP”代表“递归长度前缀”,并且是用于对二进制数据的嵌套数组进行编码的数据格式。RLP是以太坊用来序列化对象的格式。
2.有效的交易签名。
2.有效的交易序号。回忆一下账户中的nonce就是从此账户发送出去交易的计数。如果有效,那么交易序号一定等于发送账户中的nonce。
3.交易的gas limit 一定要等于或者大于交易使用的intrinsic gas,intrinsic gas包括:
—— 为执行交易预定义21,000Gas的成本
—— 与交易一起发送的数据的gas费用(对于每个字节的数据或代码,等于零的gas为4gas,对于每个非零字节的数据或代码为68gas)
—— 如果交易是创建合约的交易,则另外增加32,000个Gas
发送者的账户余额必须有足够的以太网来支付发送者必须支付的前期Gas成本。前期Gas成本的计算很简单:首先,将交易的Gas限额乘以交易的Gas价格,以确定最大Gas成本。然后,这个最大成本被加到从发送者到接收者的总值上。
如果交易满足上述所有有效性要求,那么我们就进入下一步。
首先,我们从发送者的余额中扣除执行的前期成本,并将发送者帐户的现时值增加1以计入当前交易。此时,我们可以计算剩余的gas量作为交易的总气量限制减去所使用的固有gas。
接下来,交易开始执行。在交易执行过程中,以太坊会跟踪“子状态”。这个子状态是一种记录交易过程中产生的信息的方法,交易完成后立即需要这些信息。具体来说,它包含:
1.自毁集:一组帐户(如果有的话)在交易完成后将被丢弃。
2.日志系列:虚拟机代码执行的归档和可索引检查点。
3.退款余额:交易后退还给发送者账户的金额。请记住我们提到以太坊的存储费用,发送者要是清理了内存就会有退款。以太坊使用退款柜台跟踪此事。退款计数器从零开始,每当合约删除存储内容时递增。
接下来,处理交易所需的各种计算。
一旦交易所需的所有步骤都得到处理,并且假定没有无效状态,则通过确定将退还给发送方的未使用Gas的数量来确定状态。除了未使用的Gas外,发送者还会得到上面所说的“退款余额”中退还的一些津贴。
一旦发送者得到退款之后:
gas的Ether归矿工
交易使用的gas会被添加到区块的gas计数中(计数一直记录当前区块中所有交易使用的gas总量,这对于验证区块时是非常有用的)
所有在自毁集中的账户(如果存在的话)都会被删除
最后,我们留下了新的状态和交易创建的一组日志。
现在我们已经介绍了交易执行的基础知识,让我们来看看合约创建交易和消息通信之间的一些区别。
2.1 合约创建
回想一下,在以太坊,有两种类型的帐户:合约帐户和外部拥有的帐户。当我们说交易是“合约创造”时,我们的意思是交易的目的是创建一个新的合约账户。
为了创建一个新的合约账户,我们首先使用一个特殊的公式来声明新账户的地址。然后我们通过以下方式初始化新帐户:
将nonce设置为零
如果发送者将一定数量的以太币作为交易的价值,则将帐户余额设置为该值
从发送者的余额中扣除添加到此新帐户余额中的值
将存储设置为空
将合约的codeHash设置为空字符串的散列
一旦我们初始化了账户,我们实际上可以使用与交易一起发送的init代码来创建账户(请参阅“交易和消息”一节以获得有关init代码的更新)。这个init代码在执行过程中会发生什么变化。根据合约的构造函数,它可能会更新帐户的存储空间,创建其他合约帐户,进行其他消息呼叫等。
随着初始化合约的代码被执行,会使用gas。交易不允许用尽比其余的Gas更多的Gas。如果是这样,执行会遇到一个gas(OOG)异常并退出。如果由于Gas异常而导致交易退出,则状态将立即恢复到交易之前的状态。发送者不退还用完之前所用的gas。
Boo hoo。
但是,如果发送方在交易中发送了Ether,则即使合约创建失败,Ether也将被退还。
如果初始化代码执行成功,则支付最终的合约创建成本。这是一个存储成本,并与创建的合约代码的大小成比例(没有免费的午餐!)如果没有足够的剩余gas来支付最后的花费,那么交易就会再次宣布gas不足异常并中断退出。
如果一切顺利,我们无一例外地做到这一点,那么任何剩余的未使用的gas将退还给交易的原始发送者,现在允许改变的状态持续下如果所有的都正常进行没有任何异常出现,那么任何剩余的未使用gas都会被退回给原始的交易发送者,现在改变的状态才被允许永久保存。
2.2 消息通信
消息通信的执行与创建合约类似,但有一些不同之处。
执行消息通信不包括任何初始化代码,因为没有新的帐户正在创建。但是,如果此数据由交易发送者提供,则可以包含输入数据。一旦执行,消息通信还会有一个包含输出数据的额外组件,如果后续执行需要此数据,则会使用该组件。
与创建合约一样,如果消息呼叫执行由于耗尽或交易无效(例如堆栈溢出,无效跳转目标或无效指令)而退出,则所有使用的gas都不会退还给原始呼叫者。相反,所有剩余的未使用的Gas都被消耗掉了,状态被重新设定为即将进行余额转移之前的一刻。
直到最近更新的以太坊,没有办法停止或恢复一个交易的执行,而没有让系统消耗你提供的所有gas。例如,假设你创建了一个当调用者没有被授权执行一些交易时抛出错误的合约。在之前版本的以太坊中,剩余的Gas仍然会被消耗,没有Gas会被退还给发送者。但拜占庭的更新包括一个新的“回复”代码,允许合约停止执行和恢复状态的变化,而不消耗剩余的gas,并有能力返回失败的交易的原因。如果交易因回复而退出,则未使用的Gas将返还给发送者。
2.3 执行模式
到目前为止,我们已经了解了交易从头到尾要执行的一系列步骤。现在我们来看看这个交易是如何在虚拟机中执行的。
实际处理交易处理的协议部分是以太坊自己的虚拟机,即以太坊虚拟机(Ethereum Virtual Machine,简称EVM)。
EVM是一个图灵完备的虚拟机,如前所述。EVM唯一的限制就是典型的图灵整机没有EVM本质上受Gas约束。因此,可以完成的总计算量本质上受提供gas量的限制。
而且,EVM具有基于堆栈的架构。一个堆栈机是采用后进先出栈来保存临时值的计算机。
EVM中每个堆栈项目的大小是256位,堆栈的最大大小是1024。
EVM具有内存,其中项目以字寻址字节数组的形式存储。内存是不稳定的,这意味着它不是永久的。
EVM也有存储。与内存不同,存储是非易失性的,并作为系统状态的一部分进行维护。EVM将程序代码分开存储在只能通过特殊指令访问的虚拟ROM中。通过这种方式,EVM与典型的冯诺伊曼架构不同,程序代码存储在存储器或存储器中。
EVM也有自己的语言:“EVM字节码”。当像您或我这样的程序员编写在以太坊上运行的智能合约时,我们通常使用高级语言(如Solidity)编写代码。然后,我们可以将其编译为EVM能够理解的EVM字节码。
好吧,现在执行。
在执行特定计算之前,处理器确保以下信息可用且有效:
系统状态
用于计算的剩余gas
拥有执行代码的账户地址
原始触发此次执行的交易发送者的地址
触发代码执行的账户地址(可能与原始发送者不同)
触发此次执行的交易gas价格
此次执行的输入数据
Value(单位为Wei)作为当前执行的一部分传递给该账户
待执行的机器码
当前区块的区块头
当前消息通信或合约创建堆栈的深度
在执行开始时,内存和堆栈是空的,程序计数器是零。
EVM然后递归执行交易,为每个循环计算系统状态和机器状态。系统状态只是以太坊的全球状态。机器状态包括:
可获取的gas
程序计数器
内存的内容
内存中字的活跃数
堆栈的内容
堆栈项目被添加到系列的最左边部分或从中删除。
在每个循环中,从剩余的gas中减少适当的gas量,程序计数器递增。
在每个循环结束时,有三种可能性:
机器达到异常状态(例如气量不足,指令无效,堆栈项目不足,堆栈项目溢出1024以上,无效的JUMP / JUMPI目标等),因此必须暂停,丢弃任何更改
序列继续处理到下一个循环
机器达到受控停机状态(执行过程结束)
假设执行没有达到异常状态并达到“受控”或正常停止状态,机器将生成结果状态,执行后的剩余gas,累计子状态以及结果输出。
唷。我们通过了以太坊其中一个最复杂的部分。即使你没有完全理解这个部分,那也没关系。你并不真正需要了解的细节问题执行细节,除非你是在一个非常深层次的合作。
2.3 一个块如何完成
最后,让我们看看如何完成一个交易块。
1.验证(或者挖矿)ommers块头
每个ommer块必须是有效头并且在当前的6代块以内。
2.确认(或者挖矿)交易
区块中的gasUsed数量必须与区块中所列交易使用的累积gas量相等。(回忆一下,当执行一个交易的时候,我们会跟踪区块的gas计数器,也就跟踪了区块中所有交易使用的gas总数量)
3.申请奖励(只有在挖矿时)
受益人地址被授予5个以太坊用于开采区块。(在Ethereum提案EIP-649中,这5个ETH的奖励将很快减少到3个ETH)。此外,对于每一位ommers,当前区块的受益者都会获得当前区块奖励的1/32。最后,ommer区块的受益人也获得一定的奖金(这是如何计算的特殊公式)。
4.验证(或者挖矿,计算一个有效的)状态和hash
确保应用所有交易和结果状态更改,然后将块奖励应用于最终交易的结果状态后,将新块定义为状态。通过检查存储在标题中的状态树检查该最终状态来进行验证。
布尼区块链
拥抱区块链未来
打造最有价值的区块链学习&交流社群