https://liaoxuefeng.com/books/blockchain/ethereum/block/index.html
比特币的区块链是由PoW保证每个区块都指向前一个区块,而在每一个区块内部,由一个独立的Merkle Tree来保证所有交易的不可篡改。用户的比特币是以UTXO的方式存储的,因此,比特币的交易就是不断地消耗现有的UTXO,并产生新的UTXO。
而以太坊采用的是账户模型,如果小明的账户在某个区块的资产是1 ETH,当小明给小红转账0.2 ETH后,刨除手续费,他的账户还剩下约0.8 ETH。由于小明的账户地址不变,所以,以太坊的区块结构必须能在每个区块持续地跟踪并记录小明的账户余额变动。因此,和比特币相比,以太坊的区块数据结构更加复杂。
Merkle Patricia Tree
以太坊存储账户数据的数据结构是MPT:Merkle Patricia Tree,它是一种改进的Merkle Tree。当MPT的每个叶子结点的值确定后,计算出的Root Hash就是完全确定的。例如,在第一个区块中,4个账户的余额确定后,即可确定Root1
:
┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐
Root1
│ ┌───┐ │
│ │
│ └───┘ │
│
│ ┌─────┴─────┐ │
│ │
│ ┌───┐ ┌───┐ │
│ │ │ │
│ └───┘ └───┘ │
│ │
│ ┌──┴──┐ ┌──┴──┐ │
│ │ │ │
│ ┌───┐ ┌───┐ ┌───┐ ┌───┐ │
│5.5│ │0.2│ │1.7│ │9.0│
│ └───┘ └───┘ └───┘ └───┘ │
─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
在第二个区块中,如果发生了转账,将计算出一个新的Root2
:
┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐
Root2
│ ┌───┐ │
│ │
│ └───┘ │
│
│ ┌─────┴─────┐ │
│ │
│ ┌───┐ ┌───┐ │
│ │ │ │
│ └───┘ └───┘ │
│ │
│ ┌──┴──┐ ┌──┴──┐ │
│ │ │ │
│ ┌───┐ ┌───┐ ┌───┐ ┌───┐ │
│5.5│ │0.1│ │1.8│ │9.0│
│ └───┘ └───┘ └───┘ └───┘ │
─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
每一个区块通过Root Hash将完全确定所有账户的状态,所以,从全局看,以太坊就是一个状态机,每个区块通过记录一个stateRoot
来表示一个新状态。如果给定某个区块的stateRoot
,我们肯定能完全确定所有账户的所有余额等信息。因此,stateRoot
就被称为当前的世界状态。
有的同学可能会思考,如果第一个区块只有几个账户,随着账户的增加,如果有数百万个账户,到后面岂不是区块存储的数据量会越来越大?
实际上,每个区块的stateRoot
表示的是一个完全状态的逻辑树,但每个区块记录的数据只包括修改的部分,如果我们观察第二个区块的树,它实际上只记录修改的两个账户,以及两个账户因修改后导致的上层路径的Hash发生的变化:
┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐ ┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐
Root1 Root2
│ ┌───┐ │ │ ┌───┐ │
│ │ │ │
│ └───┘ │ │ └───┘ │
│ │
│ ┌─────┴─────┐ │ │ ┌─────┴─────┐ │
│ │ │ │
│ ┌───┐ ┌───┐ │ │ ┌───┐ ┌───┐ │
│ │ │ │ │ │ │ │
│ └───┘ └───┘ │ │ └───┘ └───┘ │
│ │ │ │
│ ┌──┴──┐ ┌──┴──┐ │ │ └──┐ ┌──┘ │
│ │ │ │ │ │
│ ┌───┐ ┌───┐ ┌───┐ ┌───┐ │ │ ┌───┐ ┌───┐ │
│5.5│ │0.2│ │1.7│ │9.0│ │0.1│ │1.8│
│ └───┘ └───┘ └───┘ └───┘ │ │ └───┘ └───┘ │
─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
想要将一个有几百万节点的树完整地放入内存需要消耗大量的内存,而以太坊全节点也并不会将整颗逻辑树放入内存。实际上,每个节点的数据被存放到LevelDB中,节点仅在内存中存储当前活动的一些账户信息。如果需要操作某个不在内存的账户,则会将其从LevelDB加载到内存。如果内存不够,也会将长期不活动的节点从内存中移除,因为将来可以通过节点的路径再次从LevelDB加载。
账户数据
一个以太坊账户由4部分数据构成:
nonce
balance
storageRoot
codeHash
其中,nonce
是一个递增的整数,每发送一次交易,nonce
递增1
,因此,nonce
记录的就是交易次数。
balance
记录的就是账户余额,以wei
为单位,1 Ether等于1018wei。
如果一个账户是合约账户,则storageRoot
存储合约相关的状态数据,codeHash
存储合约代码的Hash。对于外部账户,这两部分数据都是空。
区块数据
一个以太坊区块由区块头和一系列交易构成。区块头除了记录parentHash(上一个区块的Hash)、stateRoot(世界状态)外,还包括:
sha3Uncles:记录引用的叔块;
transactionRoot:记录当前区块所有交易的Root Hash;
receiptsRoot:记录当前区块所有交易回执的Root Hash;
logsBloom:一个Bloom Filter,用于快速查找Log;
difficulty:挖矿难度值;
number:区块高度,严格递增的整数;
timestamp:区块的时间戳(以秒为单位);
……
transactionRoot
和receiptsRoot
也是两个MPT树,但他两和stateRoot
不同,他们仅表示当前区块的两棵树,与前面的区块状态无关。
叔块
以太坊采用和比特币类似的PoW挖矿,只是算法为改进的Ethash算法。PoW挖矿肯定会产生分叉,但由于最长链共识,最终某个分叉将胜出:
┌───┐
┌─│ 4 │
┌───┐ ┌───┐ ┌───┐ ┌───┐◀┘ └───┘
│ 0 │◀──│ 1 │◀──│ 2 │◀──│ 3 │
└───┘ └───┘ └───┘ └───┘◀┐ ┌───┐ ┌───┐
└─│ 4 │◀──│ 5 │
└───┘ └───┘
但是和比特币不同的是,虽然#4
的竞争导致一个胜出另一个失败,但以太坊鼓励后续的#5
区块引用另一个废弃的#4
块,这种引用的废弃块被称为叔块(Uncle Block):
┌───┐
│ 4 │◀┐
└───┘ │
┌───┐ ┌───┐ ┌───┐ ┌───┐ ┌───┐ └─┌───┐
│ 0 │◀──│ 1 │◀──│ 2 │◀──│ 3 │◀──│ 4 │◀──│ 5 │
└───┘ └───┘ └───┘ └───┘ └───┘ └───┘
区块头记录的sha3Uncles
就是叔块,一个区块可引用0~2个叔块,且叔块高度必须在前7层之内。
叔块的目的是给予竞争失败的矿工部分奖励,避免出现较长的分叉。
小结
以太坊的核心数据结构是以Merkle Patricia Tree记录的世界状态,每个区块通过打包新的交易,从而导致世界状态的变化。