Q:老哥,我终于对以太坊有点兴趣了,你能给我讲讲它的工作原理吗?
网上有那么多资料,还有那么多书,自己看去呗。
Q:老哥,我就是为了省时间才找你的嘛,上次你写的那个Web3,我网上看了半年材料,也不如看你那篇文章收获大,帮帮我呗。
那你想听到个什么程度?
Q:我要求不高,只要能让我恍然大悟,感觉思路很清晰就可以了。
这要求还不高啊,这得给你讲多明白才行。
Q:帮帮忙嘛,老哥。
行,不过你最好能耐住性子,我能给你讲明白,前提是你真能看完。
Q:太棒了,您写的东西我肯定看完,看您文章几小时,顶我几个月啊。
那你基础怎么样,有没有一点基本认识?
Q:我学IT的,算有基础吗?而且我还知道一点比特币。
那就好多了,至少我不用给你解释什么是16进制,也不用给你解释什么是虚拟机了,更不用给你解释一个字节是8位了。
Q:老哥,先问一个啊,以太坊和比特币技术有什么本质不同吗?
都是区块链。没有太多本质上的不同,主要是以太坊可以执行任意程序(所谓图灵完备
),比特币只能执行很受限的一些指令。
当然,比特币这么做,是因为它只想当货币,没想干别的。
Q:您能用几句话说明以太坊是什么吗?
以太坊上有很多账户
,每个账户都有其ETH余额,而ETH是一种类似BTC的加密货币。以太坊上还有很多智能合约
,其实就是一些程序,能运行,能记录程序产生的一些数据。以太坊和比特币一样,很多节点一起工作,定期产生区块,通过密码学算法,可以防篡改。
Q:那,您能再多说几句,讲讲以太坊是怎么运转的吗?
互联网上有若干个运行着以太坊软件的计算机(即节点
),他们干的活都一模一样:相互发送和接受交易
(这些交易就是账户间的ETH转账或者触发智能合约部署、运行)
。每隔大约十几秒,就有一个率先挖矿
成功的节点,把这一时间段内的交易打包起来形成一个区块
,然后向所有节点广播。节点收到一个区块后,先验证该区块及其中包含的交易是否满足规范要求,如果无误,就执行其中的交易,更新所涉及的账户余额和合约数据,如果这些数据也检验无误,就将此区块链接到前面所有已接受的区块上,形成区块链。
这些节点就一直这样进行着交易收发、挖矿、区块广播、区块上链
工作。
Q:有点感觉了,您能再说一遍吗,我再体会体会?
若干个节点
(计算机)通过互联网组成一个网络,这些节点运行着实现以太坊协议的程序。在这个网络上,由于外部的需求触发,某个节点发出交易,该交易被广播,每个节点都验证交易的正确性。同时,很多节点会抓紧挖矿
(即PoW计算),首先挖矿成功的节点会打包出一个区块然后广播,其他所有节点会按照协议验证该区块的正确性,如果验证正确,会处理(执行)该区块中的交易,改变节点数据库中所存储的以太坊状态
,这样,各节点保证了数据的一致性,保证了步调一致,在整体上像一个大计算机。
由于各个节点都是对等的,没有哪个节点是特殊的,所以系统是分布式的、去中心化的,很稳健,也很容易扩展。由于挖矿机制的存在,导致对其数据的篡改非常困难,攻击者需要和“好人”节点有同等的算力才行,但通常还是好人多。
你会注意到,同一个概念,我可能会重复说,不同深度地说,目的是让你更好理解。
Q:大概意思是明白了,我想知道些细节的内容,最好能直观点好吗。
先给你看看区块链的样子吧:
以太坊区块链示意,图来自1
你看,就是这样的,区块
(Block)们一个接一个形成链,每个block有一个头部
(Header),还有头部后面的若干个交易
(Transactions)。
链接方法:每个区块的头部,有一个parentHash,里存放着上一个区块头部的hash
值,任何人可以通过这个hash值,验证上一个区块头的内容是否正确无误,而区块头中还有其他几个hash值,可以验证整个区块其他数据是否正确无误。这样,可以从任意一个区块,检验前面一个区块,依此类推,一直检验到第一个区块(创世区块)。(创世区块的ParentHash为0)
以太坊说的Hash, 指的是Keccak256这个hash算法。如果不了解什么是hash,可以看看我的这篇文章:《弄明白HASH,你就弄明白区块链的一大半》2。
Keccak256是从SHA3规范演化出来的。
Q:那交易到底是什么呢?
交易(transaction)就是一些任务,需要以太坊处理的任务。比如ETH转账、智能合约部署、智能合约函数执行等等。这些交易其实就是人们通过以太坊客户端
发出的消息,消息通过RLP编码,做成一个IP包在网络上发送。
智能合约部署,用人话说,就是把写好的程序放到以太坊上,以后就可以调用执行啦!
RLP(Recursive length prefix encoding,递归长度前缀编码)是一种序列化方式,就是把一个结构化的数据弄成一长串16进制数,需要了解的话可以上网去查。
什么是以太坊客户端?就是便于人们和以太坊交互的东西。像Geth、Parity这类全节点软件、metamask这类钱包、Infura这类provider,都是客户端,都能对用户提供Web3 API或JSON RPC API或直接的GUI,使得用户可以获取以太坊信息,可以发送交易等。
以太坊各节点是如何执行交易的:节点验证交易的签名无误后(表明账户主人确实要干这事),而且交易指定的gas额度也够(不够就不执行,或者执行到一半发现不够则回退),就执行交易里面的要求,该转账转账,该部署合约就部署合约,该调用合约就调用合约。
注意,每个有完整功能的节点(全节点
)都运行着一台以太坊虚拟机
(即EVM),都有着自己的数据库(比如底层使用LevelDB),都可以执行智能合约,存储合约相关数据等。
Q:以太坊这么多节点,是怎么协调工作的呢。
比如说,全球现在(2022.05)有2200个左右的节点3在共同维护以太坊运转。
节点间通过广播来通信。平均而言,每一个节点大约维护了至少13个4直接相连的节点(即邻居节点),每一个邻居节点都会对收到的交易进行验证,如果无误,就会将该交易广播给与它们相连的邻居。因此,交易就像是水中的波纹一样,在整个网络中迅速传播开,短短几秒钟之内(通常是6s),一个交易就能到达全球范围内所有节点。
每个开启了挖矿功能的节点,都收集着尚未被区块包含
的交易,他们挖矿,打包交易,试图产生一个新的区块。
注意“
包含
”(include)这个词,一个被挖出的区块中包含一个交易,就代表这个交易上链
了。
以后也许不需要挖矿,以太坊目前(2022.05)的共识机制是PoW,以后(也许很快)
会采用权益证明(PoS)机制。PoW需要付出大量的计算(外加一些运气)
,暴力求解一个难题,谁先算出来,谁就可以做出一个区块并广播出去。其他节点收到后,验证这个区块是否正确,如果确实解决了难题,就接受它,让自己存储的区块链延长一个块。如果自己也正在挖这个块,就承认失败,赶紧挖下一块。
当然,挖矿成功是有奖励的,以前是奖励5个,后来是3个,现在是2个。
如果同时有两个节点(甚至更多节点)都挖出块了,并同时在网络上传播,这就形成了分叉。每个节点会根据相同的算法选择接受哪个区块(也即以哪条链为主链),最终只有一个会成为主链上的块,其他的称为叔块(英文以前叫uncle block,现在叫ommer block)。
区块中的内容(主要是交易),会让所有节点都同步到一个状态上,这个后面慢慢讲。
Q:我还有一点比较好奇,合约的代码都存储在哪里?合约中需要保存的数据又存储在哪里?都在区块里面吗?
这个问题问得好,我当年也为此困惑过。
简单地回答,合约和合约里的数据,都不在区块里,而是在每个节点的数据库里。
区块里面,只有区块头、交易(多个)、叔块的头(最多两个)。
合约是通过交易被部署的,之后就在各节点的内存和数据库里了。
每个在以太坊上部署成功的合约,都是以太坊上的一个账户。
Q:那账户一共有几种啊,长什么样子啊?
账户一共就两种:个人账户和合约账户。
每个账户有一个以太坊地址。
-
个人账户:标准的说法是“
外部拥有账户
”,或者简称“外部账户
”。由于太抽象了,所以本文就叫个人账户了(我的叫法)
。 -
合约账户:就是智能合约账户,合约里面会有一些需要持久保存的数据(而不是内存里的数据)。合约地址上也可以有钱。
每个账户上都可以有钱(其实就是ETH)
,你要给他打钱,就把ETH发送到他的地址上。
每个个人账户都有一个私钥(主人要保存好!别让别人知道了!)。合约账户没有私钥,所以,并没有人真的拥有合约。程序中会有定义出来的owner,那是程序员自己写的。
账户地址长度为20个字节,比如某人的账户长得这个样子:
0xC9dE9C4b93fDBb599693cb1Ee4Af008227B6d204
对应的私钥为:
0xa894566d422cfa3359dcf7a7e25d3d9fd13ef9b839e15739adb7307a71b97327
一定注意,这只是个示例,不要拿这个私钥去用。因为这个私钥我已经知道,你也知道,每个看到这篇文章的人都知道,而且还有很多网络爬虫,天天在网上找私钥,你如果往这个地址打入ETH,很快就会被人盗走。
Q:两种账户在使用上有什么区别吗?
个人地址可以给个人地址发送交易,这就是个人账户间的ETH转账。
个人地址可以给合约地址发送交易,附带一些参数,就可以调用智能合约的相应函数。调用时还可以带钱,相当于把钱转给合约账户。
个人地址可以给0x0这个地址发送交易,并将编译后的智能合约字节码放在交易的data域里,这就是用来部署智能合约的交易。
合约账户不会主动发起交易。但是,合约在执行时,可以给另外一个合约发送消息
,这就是合约间的调用。
Q:哦,有点明白了。我听说账户有状态,这是个啥。
账户的状态就是这个账户的数据。
“状态”是一个比较抽象的说法,听起来比较高大上而已。
每个账户的状态,都可以用4个数据来体现。
这4个数据是:
-
最主要的,是有多少钱!balance,这个数据表明了该地址拥有Wei的数量,1个以太币=10^18 Wei
-
用于确定交易顺序的nonce。
-
codeHash,这个就是合约代码的hash。对于个人账户,codeHash域是一个空字符串的hash值。
-
storageRoot,这个用于合约账户。个人账户此值为0。
这些数据,存储在一种叫做MPT的数据结构中,这是一个树状结构,每个账户占据其中一个节点,存储这4个数。
MPT(Merkle Patricia tree/trie)最早是由Alan Reiner提出并于用Ripple协议的,它结合了Merkle树和Patricia两种树的优点。
Q:上面的那个nonce是个啥啊?
如果是个人账户,nonce值代表从此账户地址发出交易的序号。因为网络中会因为路径延迟问题不能保证数据包总是按照发送的顺序收到,同时为了抵御重放攻击,以太坊中的交易都会带有一个nonce,而且要求这个nonce是递增的(如果上次是22,这次就是23)。
比特币没有账户的概念,只有UTXO,所以不需要nonce。以太坊使用账户来记账,所以需要nonce。我个人认为,这是以太坊不如比特币精妙的地方。
合约账户的nonce值代表此账户创建的合约的序号。(是的,合约也可以部署合约5,而且也要花部署合约的gas费。)
nonce还会带来一个问题:前面nonce交易如果不成功,后面的交易就不会执行,矿工总是按nonce顺序来包含交易的,所以,当交易因gas价较低被堵住了的时候,重新发一个交易,带同样的nonce和更高的gas price,矿工可能就会接受这个交易,前面那个堵住的交易,因为有相同的nonce,就会被抛弃掉。
Q:我还是不太明白什么是gas费,能不能简单给我讲讲?
每个节点上都运行着一个以太坊虚拟机,它运行EVM字节码。
程序员一般用高级语言如Solidity来编写智能合约,通过编译器,将它编译成EVM字节码,然后通过交易部署到以太坊各节点上。
如果运行一个含有死循环的智能合约(比如是一个bug,终于被触发了),那么执行该交易的节点,就会停不下来,这该是多么头疼的情况!
所以运行代码要花钱,钱花完了,不管运行到哪,都停下来。这还可以防范有人搞DDoS攻击。
在以太坊的设计中,任何交易,只要消耗节点的资源,不管是计算还是存储,每个指令都需要花钱,这就是”gas”费,需要的gas数,从几万个(最基本的转账需要21000个gas)到几十万、几百万个(比如部署一个合约)不等。
gas是有价格的,比如每个gas大约需要几十个Gwei(这就是钱了),现阶段(2022.05),20~30Gwei就是比较便宜的情况了。
wei是ETH的最小单位
1ETH = 10^18Wei
1Gwei = 10^9Wei = 1,000,000,000 Wei
对于每个交易,发送者设置gasLimit和gasPrice,gasLimit是发送者愿意花的最大gas数量(主要防止各种意外导致的预期外资金损失)
,gasPrice是愿意给每个gas的价格。注意gasPrice并不是以太坊统一规定的,而是交易发送者自己报的价。矿工们会优先打包gasPrice较高的交易。
“伦敦”升级后,新的规则有所不同,本文无意多说,还是按照gasLimit和gasPrice来理解好了(新规则看我的写的那篇《弄懂gas,读这篇就够了》6即可,主要变化是gas费会有一部分烧掉。)
假设发送者设置gasLimit为50,000,gasPrice为20Gwei。这就表示发送者为了这个交易,愿意最多支付0.001ETH来执行此交易。
50,000*20Gwei = 1,000,000,000,000,000Wei = 0.001ETH
当然,用户要确实有这么多余额,以太坊在执行交易时,会先扣下这个值,然后花多少是多少,多余的费用会被退回(所以gasLimit设高点没事),但gasLimit要是设低了,可能就会出问题:如果交易执行到半截,发现“gas不足”,交易处理就会被终止,所有已改变的状态将会被恢复(revert),账户状态就会回到交易前的状态:就像这笔交易从来没有发生。因为以太坊付出了计算上的消耗,所以,已经花掉的gas不会返回给发送者。
gasLimit设为250个,花了80,退回170
由于gas费比较贵,ETH得来又不容易,因此,以太坊上的智能合约通常都比较短小精悍,仅仅用来执行最基本的任务,比较复杂的操作一般都放到链下。
比如NFT,在链上主要就是记录一下作品的链接、拥有者地址和作品编号的关联,其他内容基本都在链下。
Q:链上、链下是啥啊,链上又存在哪啊?
链上就是区块链上,可能是在区块里面,也可能是从区块里面数据算出来的,存在节点数据库里。链上的数据是由区块链保障其安全性的。
链下,就是和区块链没有一点关系。
每个合约账户都会存储一些数据,这就是合约的storage。比如一个NFT合约,里面就要记录作品的链接、以及每个作品的拥有者地址等等,这些数据,要么是部署时被赋值了,要么是通过交易改变了初始值。
每个合约,都会把这些storage,存在自己的MPT上(最终存到硬盘上的数据库中),storageRoot就是这棵MPT树的根节点hash,树中任何数据有变化,这个值都会改变。
个人账户并没有storage MPT,因为个人账户的数据,就是balance和nonce,没有别的。
当然,如果合约账户还没有产生,就不用给它分配MPT。合约中所有数据的默认值为0,还未分配数值的数据也不用专门存储。
MPT是以太坊中最主要的数据结构。由于MPT结合了Merkle树的特点,任何节点值的修改都会影响到根节点的Hash值,可以方便地实现完整性校验。
在MPT中,节点增删改查、存储、验证的效率很高。本文无意介绍更多,有几篇文章不错,可以看看:7、8、9、10、11。
下面三个图很经典,可以大致感受一下MPT。
在MPT中,通过各节点的hash算出根hash
MPT的三类节点及其作用示例
使用MPT树,更新数据和重新计算根是很容易的,如下图所示。
账户状态更新在MPT中可以很容易做到
上图中,在一个新的区块中,某个账户的状态发生了改变,只需增加少量节点,并做简单的计算,就可以完成MPT更新并计算出区块头中新的stateRoot,其他节点都不需要动。
Q:哦,现在明白什么是账户状态了,那么以太坊状态是个啥?它又存在哪?
以太坊的状态,就是以太坊中所有账户的状态的集合。
以太坊中最重要的一个MPT树,就是账户状态MPT,在此树上,每个账户用一个节点保存自己的状态。这个MPT的根结点hash(这个hash可以简称为根),就是stateRoot,存在于每个区块的区块头中。
stateRoot就可以代表以太坊状态。根据Merkle树的特点,任何账户有数据变化,这个stateRoot都会改变。
由于hash是单向函数不可逆,所以,两棵树的根如果一样,那么两棵树一定是一模一样的。
前面还介绍过,每个合约账户,有自己的storage MPT,它的根就是storageRoot,存在于账户状态中。
仔细看这个图,会有助于你深入理解:
区块、账户状态、合约数据的关系示意
以太坊数据库要存储的东西很多,但人们尽可能地用一些聪明的方法保存它。比如Geth这个以太坊实现,完整保存历史上所有状态,需要大约10T,如果把一些旧的状态去掉,一个节点目前需要大概700GB左右同步状态数据(2022.0512)。
观察以太坊区块头中的stateRoot,会发现每个区块的都不一样。现在有一个问题:是什么导致以太坊状态的改变?
如果没有交易,账户状态就不会发生变化。所以让以太坊状态发生变化的,只有一种东西:交易。
Q:那给我讲讲交易的细节呗,最好给看一个交易的实例?
一个交易,就是指个人账户发出的一条经过其签名的指令。
上图是传统意义上的签名,以太坊中用的是数字签名
交易的目的:转账,部署合约,调用合约。
交易主要包含以下要素:
-
nonce:这个已经讲过了。
-
gasPrice,gasLimit:这个也讲过了。
-
from:交易发送者的地址。从签名中也能算出公钥和这个地址。
-
to:接收者的地址。如果是部署合约,此值设为0。
-
value:从发送者转移到接收者的ETH数量(单位为wei)。如果是合约创建交易,value作为新建合约账户的初始余额。
-
v,r,s:发送者对交易的签名(由3个部分组成),从签名中可以算出发送者地址from。
-
data:对于转账交易,这里的内容为空;对于部署合约交易,内容是合约被编译后的字节码和构造函数参数;对于调用合约的交易,内容是函数的签名和参数。
-
init:这个参数不常见,仅在合约部署时用到这个字段,用于初始化合约账户,它本身是一段代码,并在内存中返回智能合约的字节码,具体可以看这篇文章13。
如果你调用客户端API发送交易,通常只需要指定from、to、value、data等少数参数即可,客户端会自动帮你计算其他值(比如gas和签名),然后编码为RLP格式的包,广播发出。
现在看一个交易的实例,这个交易是我随手找的,上链时间是Apr-19-2022 12:20:46 AM +UTC,根据etherscan.io估算,这个交易从被节点看见到上链,大概花了7秒。
使用Web3 API的get_transaction方法14,把交易hash作为参数,可以获取这个交易的具体信息(以下为节选):
'chainId': '0x1', //这就是以太坊链
'from': '0x7eE1d1d9D959Bb516Cb6D22BF38847Bb1FEfC33E',
'to': '0xfd440152723c374F9a153959AA59602B4B09d2fb',
'gas': 129523, //实际花掉的gas数
'gasPrice': 39098326037,
'hash': '0xe0a8a7efb9553ad756e57029b674794cc2515ffc91bc10f6e7cc4515dd305176', //该交易的ID,根据交易数据hash出来的。
'input':'0xa0712d680000000000000000000000000000000000000000000000000000000000000002', //也就是data,里面含函数的签名0xa0712d68,以及一个参数值2
'nonce': 333,
'r': '0xaae4a59a6c7ed91ee01ce9200ec695d0a42610d1448344f8461315b1b41922c9',
's': '0x7a46ec0091587c58285d4ad96f639db536c396f15b90add3f2d8f52785137d7a',
'v': 0,
'value': 20000000000000000 // 0.02ETH
如果你想看看RLP编码后的交易,可以在etherscan.io查看该hash的交易,然后在右上角三个小点点的地方点击选择“Get Raw Transaction Hex”,得到RLP编码的交易。
对于上面这个交易,它的RLP编码为:
0x02f8990182014d843b9aca0085116910ee6c8301f9f394fd440152723c374f9a153959aa59602b4b09d2fb87470de4df820000a4a0712d680000000000000000000000000000000000000000000000000000000000000002c080a0aae4a59a6c7ed91ee01ce9200ec695d0a42610d1448344f8461315b1b41922c9a07a46ec0091587c58285d4ad96f639db536c396f15b90add3f2d8f52785137d7a
若果你对解读上面这个RLP编码有兴趣,可以看一下这篇文章:15。
Q:老哥,现在我对交易有点直观印象了,您能讲讲一个节点收到交易后,都做些什么吗?
节点收到广播来的交易后,先做一些合规性检查,检查通过后,就将交易放入该节点维护的交易池中。
合规性检查主要有:
-
检查交易Hash是否重复。如果在交易池中已经存在相同的交易Hash,则表明该交易是重复交易,丢弃之。
-
验证交易长度(最大长度为32KB)、转账金额(要看账户是否还有这么多钱)、交易签名、nonce、gasLimit等值是否合理,如果不合理则丢弃。
-
如果一个账户的两个交易有相同的nonce值,那么gasPrice高的留下(有个门槛,高于10%才这样),低的抛弃。
具体可以参考文献16。
交易被放到交易池后,节点就会把它广播出去。
Q:那然后呢?干些啥事?
现在,节点的交易池里有交易了。要看他挖不挖矿了。
挖矿的节点(有些节点并不挖矿)
会从自己的交易池中,选取交易打包成候选区块,一旦挖矿成功,就会广播这个区块。
不挖矿的节点,收到广播的区块后,检验这个区块以及区块中每个交易是否正确。如果正确,节点执行交易,改变各账户的状态,更新账户MPT根,得出新的以太坊状态stateRoot,看这个值和区块头部中记录的stateRoot是否一致。如果一致,其他方面也没有问题,就接受这个区块,上链。如果不一致,会拒绝这个区块。
如果接受该区块,会把区块中包含的交易从自己的交易池中删除。如果由于分叉等情况又要丢弃该区块,则需要将丢弃区块中的交易重新放入交易池17。
挖矿的节点,也会收到广播的区块,如果发现人家的块比自己挖得早(自己还没挖出来)
,或是挖得好(自己挖的成为叔块)
,就自叹弗如,接受并开始挖下一块。
Q:刚才说的都是个人发出的交易,那合约能发出交易吗?
合约不能主动发出交易。
合约可以被触发调用另一个合约,它们之间通过“消息
”(messages)或称“内部交易
”(internal transactions)进行通信。
消息与前面说的交易不同,它们并不从外部发起,它们只存在于以太坊内部执行环境。
下图的示意中,最左边的Externally owned accout就是个人账户,个人账户通过交易激发一个合约执行,合约再通过内部交易调用另一个合约。
合约通过“内部交易”调用另一个合约
需要注意的是:“内部交易”没有gasLimit。在个人账户发出的交易中,gasLimit已经设定好了,这个gasLimit必须要高到足够将交易以及所有触发的“内部交易”都执行完,如果一个“内部交易”不能执行完,该内部交易的执行会被还原(revert),但父执行则未必被还原(以太坊白皮书18是这么说的)。
Q:关于以太坊状态这块,能举个例子吗?我还是想有个直观的认识。
下面是以太坊白皮书给出的一个简单示例,仅仅是一种示意,而非真实情况。
以太坊状态转换示意
我前面说过,以太坊的状态就是各账户状态的集合。
这个示例中,假设以太坊中一共就4个账户,每个账户地址用后4位区分的话,就是f8ba、a980、f92f、ad65。第一个和最后一个是个人账户,中间两个是合约账户。
图例是什么意思呢,左边,是以太坊现在的状态,称之为State,执行完一个交易后,状态变成了State‘。
这个交易,由f8ba发出,发给a980,转账10个以太币。同时发送了两个数据:2和“charlie”。
这个消息会触发a980中的代码:
If !contract.storage [tx.data [0] ]:
contract.storage [tx.data [0]] = tx.data[1]
tx.data[0],就是交易中的第1个数据:2,tx.data[1]就是交易中第2个数据:‘charlie’。
这个过程中,两个账户的状态改变了:
f8ba账户:少了10个以太币,以及可能的gas费(这个图例中没有展示)。
a980账户:增加10个以太币,存储索引为2的地方,以前是0,现在是CHARLIE。
你看,执行了一个交易,以太坊状态就从state,变成了state’。
通常,我们用区块头中的stateRoot来指代以太坊状态,因为它是账户状态MPT的根hash(可简称为“根”),所有账户的状态都存在这个MPT中,任何一个账户的任何一个状态有变化,这个根都会变化。
比如,以太坊第14658154号区块中,stateRoot的值为:
0xfb75c5ee4a69c77493e24ab39d9bb73fb716dda68d3a49dfd699034dbf987e74
到了第14658155号区块,执行完其中294个交易之后,状态根变为:
0xedf3d6be3eb33a2a364966c7c0cd40848beea4a51eaf91cb0d17c83da3b14e67
Q:有点感觉了,区块、区块头、交易、stateRoot,以及MPT,能不能给我图示看看?
我再强调一次:在以太坊中,每个区块包含区块头和区块体,区块头里面有15个数据,区块体里面主要是交易(0个或多个),此外还可能有叔块头(0个或多个)。
以太坊区块、MPT、状态示意,图来自:19
注意看上图左边,就是一个区块的结构,上面是区块头,里面有15个数据,下面是区块体,里面有交易列表、叔块头列表。
往右看,能看到3棵MPT叔,分别是状态树、收据树、消息树。这三棵树的根hash记录在区块头中。
最右边,每个合约账户有一棵自己的存储树,用来存储自己需要保存的数据。
再看一张图感受一下:
以太坊区块、MPT、状态示意,图来自:20
上面这个图是需要说明一下,图中画出了交易列表,画出了Merkle树,但没有画MPT树,图中的Merkle Root其实指的是transactionRoot。
另外,还记得比特币区块的样子吗?
比特币区块结构示意图
Q:您能不能仔细讲一下区块头中那15个数据都有啥?
这15个数据是这样的:
3个根:
-
stateRoot:状态树的根hash。(整个以太坊,只有这么一棵状态树,存所有账户的状态)
-
transactionsRoot:交易树的根hash。(每个区块有一棵交易树,存本区块的交易)
-
receiptsRoot:收据树的根hash。(每个区块有一棵收据树,存本区块交易的收据)
两个Hash:
-
parentHash:父区块头部的hash值。创世块的parentHash是0。
-
ommerHash(又名Sha3Uncles):当前区块中所包含的所有叔块头部的hash。
区块的hash就是区块头部的hash,是区块头部数据按照RLP编码后的hash值(Keccak256算法),具体做法见21。
区块本身相关:
-
number:区块序号。创世块序号为0,之后每个区块序号都增加1,比如:14589807。
-
timestamp:区块被挖出来的时间戳,比如:Apr-15-2022 11:58:09 AM +UTC。
日志相关:
-
logsBloom:由本区块内交易日志信息生成的Bloom Filter,用于快速判断所希望查询的日志是否在本区块中。
gas相关:
-
gasLimit:区块内所有交易所使用的gas上限,注意这个gasLimit不是交易中的那个,而是区块用的。目前(2022.05)上限是30M个gas。
-
gasUsed: 区块中交易实际使用的总gas量。比如第14589807号区块,使用了6,923,108个gas。
挖矿相关:
-
beneficiary(又名Coinbase、miner):接收奖励的账户地址,其实就是挖到此区块的矿工的地址。一开始,挖出一个块会获得5ETH,后来变成了3ETH、2ETH。
-
extraData:附加数据,可以由矿工随意填写,也可以作为预留字段以备它用。
-
difficulty:指示挖矿难度的值,后面再讲。
-
mixHash、nonce:这两个值用来证明矿工已经执行了足够的计算,符合挖矿要求。和比特币类似,矿工不断尝试改变nonce,通过Ethash计算,计算得到中间值mixHash和结果Result,使得Result小于(2^256)/difficulty。(注意区块头里的nonce和账户的nonce不是一回事)
有人会问:既然mixHash能用nonce算出来,为什么头部还要放mixHash呢,有nonce验证Result不就行了吗。这是因为,在验证PoW时会用到mixHash,而计算mixHash仍需较多的工作量,为了防止节点因计算此值而被DDoS攻击,干脆就把这个值也放在区块头中。这样,验证更容易,而攻击者想要开展这类攻击,自己先得算好久22。
下面这个图其实前面已经见过类似的,这里加深一下印象:
以太坊区块头和4种MPT树
若要直观地看一看区块头,可以使用web3 API中的eth.getBlock,比如可以获取14589807号区块的头部信息为:
'difficulty': 12932623899912014,
'extraData': '0x617369612d65617374322d34',
'gasLimit': 30058619,
'gasUsed': 6923108,
'logsBloom':'0x153660a048141a02090a302ce4118104501000610a5080de4100a0031000a43120012051003801510130708208858100024088080808d00401130850227668a9108014440042000948091c3801402b68408184308744506c08823811a08505344a2000020650d5214044100a80201982005a0652180184449403189800180c9e013007044030075408280200506085225c405901050a029a92008544425030000210404981522c02989169c2250998404004cf1b0001224004154e74d381048444c1002a0c5001024022088020044f5000a420142118009316003d12463260120018300800c0404001ad008a1924088004420202e28010482043080380cea4e3',
'miner': '0xEA674fdDe714fd979de3EdF0F56AA9716B898ec8', //挖出此块的矿工地址
'mixHash': '0x4c555411eaca0ca60b408f87fd661d926b0feaf2431852d941608d73b2b67f10',
'nonce': '0xf8980562840be27d',
'number': 14589807,
'parentHash': '0x784a8dc9e9d79954222bd2e384f4a3b736f3dfa1ed7e398f4a4f4a4cec25c6ce',
'receiptsRoot': '0x2205169fcc0bad3f7be5fac8205e1869af7830f0c77a174728cef357c2a16f32',
'sha3Uncles': '0x2972ac0aa31b00741bf17e125a3513b59cbcdc6af6c4d395868e2e0455dd6a89',
'stateRoot': '0x22669a870d805f99996cf3605ab058c4178a444abf917a32a467abdd2d03596d',
'transactionsRoot':'0x6646c38bda83612a386115f14c887cfb036a161aa1b51b041dc9da24da4955b0',
'timestamp': 1650023889 //Apr-15-2022 11:58:09 AM +UTC)
这里面,有些你大概是看不懂的,比如logsBloom,这个后面再讲。
Q:节点收到一个区块后,是怎么验证的呢?
简单地说,有这些工作:(见白皮书)
-
检查区块头中用parentHash引用的上一个区块是否存在、有效。
-
检查区块的时间戳是否比引用的上一个区块大,而且小于15分钟。
-
检查区块序号、难度值、交易根,收据根、叔块根,区块gas限额等是否正确。
-
检查区块的挖矿工作量证明是否有效。
-
对区块体中的交易列表(比如有n个交易),逐一执行,从上一个区块的stateRoot开始,每执行完一个交易,以太坊的状态都会有所改变,执行完区块中的所有交易后,再将矿工账户的balance做改动,就得到本区块的状态S_FINAL。(如果在执行过程中,发现区块gasLimit超出,则表明该区块无效。)
-
检查S_FINAL是否与本区块的stateRoot相同。如果相同,区块是有效的。否则,区块是无效的。
其中的收据根、难度、挖矿,叔块根,这些概念后面再说。
其他部分你应该都能看懂吧。
Q:那么,区块中每个交易是如何执行和验证的?
刚才说到,为了同步到最新状态,每个收到区块的节点,都要执行其中的交易。
执行一个交易的步骤是:
-
从签名中计算出发送者的地址,并从发送者的账户中扣除交易中gasLimit、gasprice这两个值相乘所得的交易费(如果执行用不了这么多,会退还),如果账户余额不足,返回错误。
-
将发送者账户的nonce加1,记录在该账户的状态中。
-
根据交易的所消耗的计算和存储,逐步核减gas值。
-
把发送者在消息value字段里面填的金额转移给接收者。如果接收账户还不存在,创建此账户。
-
如果接收地址是一个合约地址,除了上面说的价值转移,还会根据消息data字段里面的内容运行合约。这会产生合约相关数据的变化,变更该合约的storageRoot MPT以及相应的storageRoot,也就变更了合约账户的状态。
-
如果因为发送者账户没有足够的钱或者gas耗尽,则撤销所有的动作,仍然保持这交易之前的状态,但已经花费的gas不退。
-
合约执行完毕后,将剩余的gas费归还给发送者,消耗掉的gas归挖块成功的矿工。
对交易列表中的每个交易,都这么做。
Q:现在更清楚了。还有一点不明白,区块头中有个收据根,这是什么?
这是以太坊里面才有的,比特币没有收据
(receipt)概念。
作为一个智能合约平台,每一笔交易执行后(不管成功还是失败)
,以太坊均会返回一个交易回执信息(Receipt),这就是收据。类似在银行转账后,可以获得关于这笔转账的电子回单。
比如下面就是一个银行的电子回单(网上我随便找的)
:
银行给客户的收据:电子回单
以太坊也一样,交易无非就是转账、部署合约、合约执行这些嘛,那就给客户一个回应,告诉他办得怎么样。
用户通过客户端发送交易,上链以后,客户端从以太坊获取收据,然后返回给用户。收据里包括很多信息:交易执行成功与否,交易打包的区块高度、gas消耗量等,如果是一个部署合约交易,还有新合约的地址(对部署合约者而言,这个信息很重要)。
以太坊为每笔交易都产生一个收据,收据里面主要有:
-
Status:交易成功与否,1表示成功,0表示失败。
-
CumulativeGasUsed: 到此交易为止(包含本交易),本区块已累计消耗的Gas。
-
Logs: 本交易产生的日志列表。(在solidity语言中,用event定义一个事件,用emit触发它,就可以产生一个日志,一个交易可以产生多个日志。)
-
Bloom:日志布隆过滤器,用于快速检测某个事件日志是否存在于本交易的Logs中。
-
TxHash:本交易的哈希。
-
ContractAddress: 如果这笔交易是部署新合约,这里是新合约的地址。
-
GasUsed:本交易执行所消耗的Gas数。
-
BlockHash:本交易所在区块的哈希。
-
BlockNumber:本交易所在区块的高度。
-
TransactionIndex:本交易在区块中的序号(一个区块中有多个交易)。
回执存在哪里?在区块中吗?当然不是,也是存在节点的数据库中。
每个区块有一棵收据MPT树,专门存储该区块所有交易的回执。该树根就是区块头中的receiptsRoot。从代码看23,每个收据在MPT中只存最重要的一些数据(并不是上面所有),容易计算的数据就不存了,毕竟存储空间很宝贵。
从Geth的实现看,账户、区块、交易、收据这些信息都是读取MTP树后以Map结构放在缓存中的,MPT树则是读取数据库内容后在内存中建立。树上每个节点的数据,最终都是以key-value的形式存入硬盘上的LevelDB数据库。
如果想了解更多,可以看一下24、25。
Q:能给我看一个真实收据的样子吗?
使用eth.getTransactionReceipt这个Web3 API,可以获取某个交易的收据,下面是交易0x9dc1……5007
的收据:
'blockHash': '0x09f3ed2e02f8304b6cd31227100d29ff87b26920cd12fd16166ee892655a71eb',
'blockNumber': 14602453, //该交易位于第14602453区块中。
'contractAddress': None,
'cumulativeGasUsed': 21779267,
'effectiveGasPrice': 13873651479,
'from': '0x71338F15943632Ee9eB07d0bc72dFfcDb1B62401', //交易的发起者
'gasUsed': 54317,
'logs': [……这里是该交易产生的log列表,在此省略……],
'logsBloom': '0x00000000000000000000000000000000000002000000000000000000800000000800000000000000000000000000000000400000000000000000000000000000000000000004000000000008000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000010000000000000000000000000400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000000000000000', //本收据所有log的bloom filter。
'status': 1, //交易成功
'to': '0x0cb42d5C9c4661fee5D2ecfcBAD5BCcefa0727a9', //该交易的目的地址:这是个合约地址
'transactionHash': '0x9dc13c86dd554b2afbce0f890318cd9a4e8b0bdefdde6e0823de3541d7115007', //本交易的ID
'transactionIndex': 185, //这个交易在区块中的位置,该区块一共186个交易,这是最后一个(0到185)
'type': '0x2' // 交易类型,2代表EIP-1559类型的交易,EIP-1559是伦敦升级的一部分。
上面的日志部分,我给省略了,因为下面要专门讲。
Q:什么是日志(log)?日志和收据的关系?
上面的收据示例中,我把logs中的内容给省略了,它的完整内容是这样的:
'logs': [
{'address': '0x0cb42d5C9c4661fee5D2ecfcBAD5BCcefa0727a9', //本log是哪个智能合约产生的
'blockHash': '0x09f3ed2e02f8304b6cd31227100d29ff87b26920cd12fd16166ee892655a71eb', //本log所在区块的hash
'blockNumber': 14602453, ////本log所在区块的序号
'logIndex': 398, //本log是所在区块的第399个log(从0到398)
'removed': False, //如果这个区块最终成为了叔块,此值会变为Ture
'topics': ['0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef',
'0x00000000000000000000000071338f15943632ee9eb07d0bc72dffcdb1b62401',
'0x000000000000000000000000ef6ae800987d334ea1df460ec25d06ea943a5a45'],//本log的topics列表
'transactionHash':
'data': '0x0000000000000000000000000000000000000000000000000de0b6b3a7640000', //不是topic的事件参数,都放这
'0x9dc13c86dd554b2afbce0f890318cd9a4e8b0bdefdde6e0823de3541d7115007', //本log所属交易的ID
'transactionIndex': 185} //本log所属交易在区块中的序号
],
这个交易只有一个log,如果有多个,则会在[]中显示多段这样的内容。
日志和收据的关系:日志只是收据的一部分。
日志和交易的关系:每个交易可能产生多个日志。单纯的eth转账交易没有日志;智能合约中触发了事件函数,才能有日志。
收据和交易的关系:每个交易有一个收据。
收据和区块的关系:每个区块有多个交易。这些交易产生的收据构成一颗树,树根存在区块头中。
收据里面还有一个用于日志查询的Bloom过滤器,交易触发的每个事件的Topic都会计入其中。
Q:啊,上面这句怎么完全看不懂?
你看不懂,主要是不知道什么是事件(event),什么是Topic,什么是Bloom,下面我简单说一下前两者。
一个程序跑的时候,对发生的某些事情,需要记一些日志,便于日后追查。当然,什么时候记日志,记录什么内容,是程序员决定的,也是写在程序里的。
在Solidity语言中,可以定义事件(event)函数,调用这个函数就会产生一个日志。调用的方法是关键字emit。
不讲究的话,可以认为“事件”就是“日志”。
前面的示例中,涉及到一个智能合约,下面是它的片段:
/*
Copyright 2020 DODO ZOO.
SPDX-License-Identifier: Apache-2.0
*/
contract InitializableERC20 {
……
//下面这里定义了事件函数
event Transfer(address indexed from, address indexed to, uint256 amount);
……
function transfer(address to, uint256 amount) public returns (bool) {
……
//下面这句触发事件产生log,其中记录了ERC20 代币的转账人,收账人,金额。
emit Transfer(msg.sender, to, amount);
return true;
}
}
上面这个智能合约是实现ERC20代币的,每次发生转账时,智能合约都会emit一个event,产生一个日志,供客户端查询使用。
注意:日志是智能合约执行的产物,以太坊上原生的ETH转账是不会生成log的。
为了便于日志查询,以太坊引入了topic概念。
一个事件最多有4个topic,第一个topic是“事件签名”(即函数原型的hash),其余的topic是事件函数中带indexed修饰的参数(规定带indexed的参数最3个)。
对于上面所述的Transfer事件,topics[0]就是事件签名,topics[1] 是from参数在该事件中的值,topics[2] 是to参数在该事件中的值。
“事件签名”的制作方法:对事件函数的原型做hash。比如:
keccak('Transfer(address,address,uint256)') = 0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef
那些没有indexed标记的参数(如上例中的amount)的值,会放在log的“data”字段中。
注意:日志里放的是参数的值,而不是参数的名字。
这些日志存在哪里呢?
日志是收据的一部分,收据存在哪里,日志就在哪里。
感兴趣可以阅读文章26、27、28、29。
Q:那说说Bloom Filter吧,我对此可是毫无概念。
这是我学比特币的时候,才知道存在的东西。
简而言之,Bloom Filter是一个很大的数,一开始是0。另有一个数据集,里面有很多数据,每个数据都使用特定算法在Bloom Filter中登记(加入),如果有人要查一个数据是否在数据集中,可以使用Bloom Filter做快速判断,而无需在数据集中一一查找。
用比喻来说明一下,一个小区有100栋楼,每幢楼里住有若干人(比如可以多达数百人),张三要在这个小区找他认识的李四,但不知道李四在哪,只知道李四的手机号(13816679849),但不能打电话找他。请问如何快速的找到李四?敲开每个房间去找?(本质上,这个题是看张三能否快速在成千上万的手机号中找到一个特定的手机号)
现在有一个线索可以帮助张三:每个楼门口贴了一张纸,上面是一串数,这个数正是该楼内所有人手机号计算得出的Bloom filter(简称Bloom)。张三有了这个,可以极大缩短找到李四的时间。
比如1号楼的Bloom是这个样子:
263780a048141a02090a302ce4118104501000610a5080de4100a0031000a43120012051003801510130708208858100024088080808d00401130850227668a9108014440042000948091c3801402b68408184308744506c08823811a08505344a2000020650d5214044100a80201982005a0652180184449403189800180c89
这是一个256字节的大数,一共是1024位。
在我们的示例算法中,手机号加入Bloom的方法:
将手机号的各位相乘,乘积每三位一组,得到三个数,用这三个数置位Bloom(初始值为全0)。
比如李四手机号为13816679849,这11位数相乘以后得到15,676,416。然后把这个乘积每三位一组,得到3个置位数:15、676、416,就将Bloom的第15、676、416位置1。
同样的,比如第二个人,手机号为13978945678,乘积为91,445,760,将Bloom的第91、445、760位置1。
如法炮制,将这个楼内所有人的手机号都按照这样的算法置位此Bloom(如果一个位已经置过1,再置还是1,也即“或”运算),就得到这个楼的Bloom。
张三在某楼门口看到Bloom,计算李四手机号的乘积,看Bloom的第15、676、416位是否为1,如果不全为0,就可以肯定李四不在此楼中。
如果这三位都为1,也不能肯定李四就在里面,因为可能其他人的手机号正好将这3位给置位了。
比如对于1号楼,这个Bloom的第15位为1,第676位为0,第416位为0,所以,李四肯定不在1号楼。
张三就去下一个楼找,如果某楼的Bloom说有,则需要在此楼内进一步查询。如果楼内每层也按这样的方法各有一个Bloom,张三就可以迅速的排除某些楼层,然后去找那些李四可能在的楼层。
这就是bloom的基本原理,它说某个数据不在,那就肯定不在,但它说在,可不一定在。
以太坊的Bloom算法,当然不是比喻中的“乘积”,而是hash算法,Bloom的大小也不是1024位,而是2048位;如果要将某个数据加入Bloom,先对其做hash,然后将hash值的前6个字节,每两个字节组成一个数,得到3个数,这3个数分别对2048求余数,用余数对Bloom置位,就完成加入。
以上就是以太坊Bloom Filter的工作原理。具体可看这篇文章30。
Q:以太坊中,会把哪些数据加入日志Bloom?
一个日志的所有topic都会加入Bloom。
根据前面讲的,那就是事件的签名、事件的每个indexed的参数都会加入。
一个交易假如有5个日志,每个日志有3个topic,则这15个topic都会加入这个收据的logsBloom。
区块头中有一个总的logsBloom,是区块中所有交易收据的logsBloom的并集。
在以太坊中,张三就是客户端,李四就是某个事件的日志,区块就是某幢楼,一个交易就是一个楼层,客户端要想在若干个区块中查找某个日志,首先在区块头中的logsBloom中查找,查找该区块中是否存在关于某个Topic(比如某个事件签名、某个账户地址等)的交易,如果区块头中没有找到,就在下一个区块中找;如果查找存在,在进一步在每个收据的Bloom Filter中查找,如果找到,则读取该交易的所有log,看是否有自己想要的。
如果Bloom说有,但并没有找到,这也不稀奇,找下一个即是。
综上,通过Bloom Filter,可以快速排除掉无关的区块和收据,极大提高了日志查询效率。
如果你想了解更多,这些文章值得一读:31、32、33。
Q:那我怎么应用呢,有没有简便现成的方法?
作为一个很简便的方法,你可以使用Web3 API来监听(订阅)日志,类似这样:
Web3. Eth. Subscribe ("logs", {address: "0x0cb42d5C9c4661fee5D2ecfcBAD5BCcefa0727a9",
topics:[web3. Utils. Sha3 ("Transfer (address, address, uint256)" ) ] } );
其中,address是要监听合约的地址,topics是你想监听的内容(如果是多个topic,则这些topic都出现在log中才匹配上34),上例只有一个事件签名topic,监听该合约发生的所有Transfer事件(不论什么地址,多少金额)。
Q:好家伙,终于把区块头快弄明白了,就剩下一小块了,那个叔块根(ommerHash)是什么?
ommerHash就是区块中ommer区块头列表的hash,用来验证列表数据的完整性。
前面简单说过的,ommer区块就是叔块。
以太坊设计大约每15秒就产生一个区块,和比特币每10分钟一个区块相比,一个节点更有可能收到不同矿工挖出来的编号相同的两个甚至是多个区块,此时,区块链就出现了分叉,节点就需要选择在哪个链条上延续。现在的算法是:选择使得所有区块难度
总和最大的那条链。(后面会介绍什么是难度)
以太坊白皮书中称使用GHOST算法35选取主链,但后来为简单起见,放弃了这个算法36。(这给人的启示是,设计精巧而复杂的协议,往往干不过简单而能用的协议。除非必要,没有人喜欢复杂的东西。)
节点选择一个分叉后,就会抛弃其他分叉上的区块,那些被抛弃的区块就是叔块(就是父区块的兄弟,没当成父皇,成了皇叔)
,本来就叫uncle块,后来改叫ommer块,因为有人觉得区块不应该有性别,就找了这么个生僻的词(似乎是丹麦语中的“失败然后重来”)
。
考虑到鼓励人们挖矿会提高安全性(分析见以太坊白皮书和GHOST算法)
,以太坊让那些挖到叔块的矿工也得到奖励(但挖到叔块的儿子不会再奖励了)
。如果一个区块里包含了叔块头,挖出该区块的矿工和挖出叔块的矿工都会得到奖励。
一个区块包含的叔块(可以最多包含2个)必须是往上数6代(含)之内的叔块,更陈旧的叔块将不会被奖励,而且叔块只能被包含一次。
挖出叔块的矿工会得到奖励,奖励的算法为:37。
叔块奖励 = ( 叔块高度 + 8 - 包含叔块的区块的高度 ) * 正常挖块奖励 / 8
上面算法中的“正常挖块奖励”,最初为5ETH,2017年10月拜占庭升级后将奖励由5ETH降为3ETH,2019年3月君士坦丁堡升级后将奖励由3ETH降为现在的2ETH。
举例:比如在高度为第14625416的区块中,包含了两个高度为14625415的叔块。每个叔块得到的奖励为:( 14625415 + 8 – 14625416 ) * 2 / 8 = 1.75ETH,两个叔块一共得到3.5ETH。
而包含叔块的区块也会得到额外的奖励,每包含一个就会得到区块奖励的1/32,包含两个叔块,得到的奖励就是:2 * ( 1 / 32 ) * 2 = 0.125ETH
这些都可以在etherscan.io上查到。
具体可以看这篇文章38。
Q:我还听说过一种叫轻节点的,这是什么?
轻节点(light clients或light nodes)是和全节点对应的,全节点需要同步所有的区块(从创世区块一直到最新的区块),需要执行这些区块里面的所有交易,在内部建立起数据库,记录所有账号的状态,记录收据数据等等,我们前面讲的节点就是全节点。
全节点中,有些会挖矿,有些也不挖。
轻节点仅仅下载区块的头部,并不保存整个区块内容,也不执行里面的交易,也不挖矿,仅仅是向全节点查询账户余额,生成和发送交易等。
轻节点和全节点通信,用来获取自己所关心账户的数据。为了验证全节点提供数据的正确性,全节点需要提供相关数据的merkle路径数据,轻节点用这些数据计算根,看和区块头中记录的是否一样。
注意:很多钱包并不是轻节点,它们并不下载区块头,它们是通过Web3 Provider和以太坊交互的。
Q:挖矿你还没讲呢,和比特币有什么不一样吗?
和比特币一样,以太坊挖矿也是PoW(工作量证明),但在具体算法上,略有不同。
以太坊使用Ethash算法39(之前用的是Dagger-Hashimoto,后来不断改进成这个算法)
挖矿,矿工试图暴力寻找一个随机数nonce,使得按照Ethash算法得到的结果Result,低于特定的一个值(和难度相关)。找到合适的nonce值很难,全网大约花费15秒才能找到一个,但验证这个nonce是否满足要求则很容易(即便对轻节点而言)。
有人会问,如果全网算力不断变强,那会不会挖矿越来越快?事实上,以太坊是自动调整头部的难度值来控制出块时间的(保持在15s左右),每个节点都按照一致的算法调整难度,简单地说,如果最近的两个区块间隔较短,则会导致难度值增加;如果最近的两个区块间隔过长,则难度值调小。
比特币是每隔2016个区块(大约两周)调整一下挖矿难度,目标是维持出块时间在10分钟左右。以太坊是每个区块都计算新的难度(和父区块难度、两个区块的时间戳、有没有叔块都有关系),计算的算法比较复杂,已经变更了很多次,具体可以看黄皮书40和这篇文章:41。
Ethash算法需要一个占用几个G字节内存大小的DAG(Directed Acyclic Graph)数据集,每隔30000个区块(大约5.2天)更新一次。这个数据集一开始是1G大小,但随着时间线性增长,现在是4.8G左右42(意味着4G显卡不再能挖矿)
。该算法使得挖矿的效率基本与CPU无关(这和比特币用的SHA256不一样),但和内存大小、内存带宽正相关,和比特币相比,以太坊更能抵御专用ASIC矿机的计算垄断。
轻节点无需挖矿,所以无需保存整个DAG,保存16M的cache即可(一开始这么大,后面也会增加)。
Q:那具体怎么个挖法?
如果你知道比特币挖矿的话,应该知道挖矿方法是不断改变区块的头部数据(如nonce值),目的是使区块头部的hash值小于一个难度目标(所以能看到比特币区块hash的前面若干位为0)
。以太坊用的Ethash算法也是一种hash算法,目的也是求出一个nonce,使得结果值小于目标值:
(2^256)/Hd
其中Hd就是区块头中的那个难度值difficulty,Hd是分母,难度越大,目标值越小。
具体的计算有点复杂43、44,这里简单讲一下大致流程:
挖矿计算过程示意
-
节点根据父区块头和新区块头计算出Hd。(难度是动态变化的)
-
给nonce赋一个随机数。(注意这个nonce值并不是账户的nonce值,它们是两码事)
-
将nonce作为输入送给Ethash,算出mixHash和Result两个值。
-
如果Result小于(2^256)/Hd,就表明挖矿成功,将mixHash和nonce记入区块头。
-
否则的话,将nonce加1,继续穷举查找。
有一点需要注意一下,和比特币不一样,这个Ethash的结果值,并不是区块头部的区块hash,那个hash是Keccak256算法做出来的,所以看上去不像比特币的区块hash那样好看。
不过,也许很快(有人说是2022年内),以太坊的共识机制就会从PoW改为PoS了,那时,就不用挖矿了。
Q:太棒了,我感觉似乎已经抓住它了,不过我再奢求一点,你能不能用最简单的语言,给我说说以太坊layer 2的本质到底是什么?
layer 2(第二层)技术有很多种,目的都是为了提高每秒可以处理的二层交易数,并获得和以太坊一样的安全性。以太坊本身是第一层,目前每秒只能处理大约15笔交易。
本质原理是:二层技术在二层处理用户在二层发出的交易,更新用户在二层的状态,并定期和一层交互(通过一层交易调用二层技术在一层的合约),将二层多个交易的相关数据(如二层的状态根)作为一层合约数据保存在一层,以获得一层提供的安全性和去中心化特点。
由于二层交易速度又快,费用又低,很多原先在一层的dapp已经部署在二层,人们在二层玩就好了。人们可以把一层的资产(比如ETH)迁移到二层,在二层像一层那样玩(比如DeFi),需要的时候,再迁移回去。
以上内容,一遍是肯定看不懂的 ,要反复读多遍,并结合具体的二层技术和其他文章来看。
下面这张关于二层的图,画得简直绝了,好好看看,能看懂多少是多少。
一图说明layer 2,来自45
Q:还有什么要告诉我的吗?
你要知道,以太坊经常升级,而且它有自己的路线图,有自己的升级计划,而且你要知道,和所有事情一样,它也未必真按计划来。
所以,你在这里看到的,并不是最初的样子,也不代表以后还是这样。
只不过,很多基础的东西是不太会变的。
Q:真是太感谢老哥了!
Finally,你终于坚持到最后了。恭喜你,如果你都看懂了,你就超过了99%的人。
这篇文章中有很多地方需要消化。可能你还需要反复看好几遍,这完全正常。我看了很多文章和代码才总结出这些。
无论如何,希望你觉得这篇文章对你有帮助。
文|卫剑钒
参考文献:
-
How does Ethereum work, anyway?(https://preethikasireddy.medium.com/how-does-ethereum-work-anyway-22d1df506369)
-
Ethereum Node Tracker(https://etherscan.io/nodetracker)
-
(希)安德烈亚斯·M.安东波罗斯. 《精通以太坊:开发智能合约和去中心化应用》
-
Creating a contract with a smart contract(https://medium.com/upstate-interactive/creating-a-contract-with-a-smart-contract-bdb67c5c8595)
-
以太坊MPT原理,你最值得看的一篇(https://blog.csdn.net/ITleaks/article/details/79992072)
-
死磕以太坊源码分析之MPT树(https://www.jianshu.com/p/8a8fd32d7d44)
-
以太坊源码阅读3——MPT原理(https://blog.csdn.net/qq_50665031/article/details/123486449)
-
基于比特币的新特性,Merkle Patricia tree/trie(MPT树)(https://blog.csdn.net/z714405489/article/details/83934618)
-
以太坊中的状态树(https://www.jianshu.com/p/8c9a1c20b5ad)
-
Ethereum Full Node Sync (Default) Chart(https://etherscan.io/chartsync/chaindefault)
-
https://leftasexercise.com/2021/09/05/a-deep-dive-into-solidity-contract-creation-and-the-init-code/
-
web3.eth API(https://web3py.readthedocs.io/en/stable/web3.eth.html)
-
深入Ethereum Raw Transaction(https://medium.com/taipei-ethereum-meetup/深入ethereum-raw-transaction-5c33d9aa4d7b)
-
以太坊设计与实现:交易入队列(https://learnblockchain.cn/books/geth/part2/txpool/txaddtx.html)
-
王欣,史钦锋,程杰. 《深入理解以太坊》
-
Ethereum Whitepaper(https://ethereum.org/en/whitepaper/)
-
https://dzone.com/articles/ethereum-yellow-paper-walkthrough-27
-
以太坊基本数据结构分析(https://blog.csdn.net/zengqiang1/article/details/109726500)
-
Block header hash calculation(https://ethereum.stackexchange.com/questions/67280/block-header-hash-calculation)
-
https://ethereum.stackexchange.com/questions/5833/why-do-we-need-both-nonce-and-mixhash-values-in-a-block
-
https://github.com/ethereum/go-ethereum/blob/master/core/types/receipt.go
-
以太坊区块数据结构及以太坊的4棵数(https://learnblockchain.cn/article/472)
-
以太坊交易回执-Receipt(https://learnblockchain.cn/article/324)
-
理解以太坊上的事件日志(https://learnblockchain.cn/article/1870)
-
solidity之Indexed属性(https://blog.csdn.net/weixin_43343144/article/details/91502148)
-
Deep Dive into eth_getLogs(https://medium.com/alchemy-api/deep-dive-into-eth-getlogs-5faf6a66fd81)
-
以太坊的交易树与收据树(https://www.cnblogs.com/klgzyq/p/15544059.html)
-
以太坊日志的bloom过滤器(https://www.jianshu.com/p/a5289b828854)
-
以太坊中交易树和收据树介绍(https://zhuanlan.zhihu.com/p/134157692)
-
Ethereum: events, logs, and bloom filters(https://blog.katastros.com/a?ID=00850-9d3e45b4-b4a7-4f25-853b-419d2c753dc3)
-
Go Ethereum Logs Retrieval and Storage Analysis(https://hackmd.io/@yueqiao/BJys6E5PY)
-
web3.js – Ethereum JavaScript API(https://web3js.readthedocs.io/en/v1.7.3/web3-eth-subscribe.html?highlight=filter)
-
Secure High-Rate Transaction Processing in Bitcoin(https://eprint.iacr.org/2013/881.pdf)
-
(https://ethereum.stackexchange.com/questions/13378/what-is-the-exact-longest-chain-rule-implemented-in-the-ethereum-homestead-p)
-
以太坊中Ghost协议详解(https://zhuanlan.zhihu.com/p/135297442)
-
以太坊(Ethereum ETH)的奖励机制(https://blog.csdn.net/cherisegege/article/details/80146273)
-
Ethash(https://eth.wiki/en/concepts/ethash/ethash)
-
Ethereum: A Secure Decentralised Generalised Transaction Ledger(https://ethereum.github.io/yellowpaper/paper.pdf)
-
以太坊难度调整算法(https://www.cnblogs.com/klgzyq/p/15544066.html)
-
DAG size calculator(https://minerstat.com/dag-size-calculator)
-
也许是国内第一篇把以太坊工作量证明从算法层讲清楚的(https://learnblockchain.cn/article/906)
-
以太坊中基于Ethash共识算法详解(https://zhuanlan.zhihu.com/p/136159331)
-
Graphical Depiction of Ethereum Scaling Solutions(https://samlaf.github.io/blockchain/graphical-depiction-of-ethereum-scaling-solutions.html)
原文始发于微信公众号(卫sir说):Hi,哥们,给我讲讲以太坊呗?