以太坊数据结构
数据结构关系图 以太坊采用账号系统,因而相比比特币,它除了区块数据外还有账号数据。同时它有图灵完备的智能合约虚拟机,因而又多了一个状态数据,同时为了保留执行记录,又多了一个receipt数据Block: 由header和body构成,header里有三个trie的rootHash<hash, receipt>数据构造的receipt trie, head
·
数据结构关系图
以太坊采用账号系统,因而相比比特币,它除了区块数据外还有账号数据。同时它有图灵完备的智能合约虚拟机,因而又多了一个状态数据,同时为了保留执行记录,又多了一个receipt数据
Block:
由header和body构成,header里有三个trie的rootHash
- <hash, receipt>数据构造的receipt trie, header.receiptHash=这个trie的rootHash
- <hash, transaction>数据构造的transaction trie, header.txHahs=这个trie的rootHash
- <address, StateObject>数据构造的state trie, header.root=这个trie的rootHash
StateObject:
是一个账号(地址)的状态信息
- 对于普通账号,这个对象保存了balance, nonce等信息
- 对于智能合约账号,还额外保留了智能合约的状态,这个状态就是智能合约里的定义的各种变量的值。以太坊虚拟机的变量是以<k, v>存储的,所以这个状态就是大量<k, v>对象。
比如一般的ICO智能合约里,都会定义一个balance的变量,用来保存各个地址的代币持有量
mapping(address => uint256) balances;
那这个值就会以<k, v>的形式存储在leveldb中,运行时会以trie的数据结构加载到StateObject对象里
LevelDb:
以太坊所有的数据都是以<k, v>的方式存储,方便检索,最终都是通过LevelDB写入持久化存储的数据库文件。
除了上面提到的<k, v>数据对,还有<trie.nodehash, trie.noderawrpl>, <header.hash, header>等,核心<k ,v>数据对格式详情如下
key | value |
'h' + num + hash | header's RLP raw data |
'h' + num + hash + 't' | td |
'h' + num + 'n' | hash |
'H' + hash | num |
'b' + num + hash | body's RLP raw data |
'r' + num + hash | receipts RLP |
'l' + hash | tx/receipt lookup metadata |
数据结构详细
Header
type
Header
struct
{
ParentHash common.Hash
`json:"parentHash" gencodec:"required"`
UncleHash common.Hash
`json:"sha3Uncles" gencodec:"required"`
Coinbase common.Address
`json:"miner" gencodec:"required"`
Root common.Hash
`json:"stateRoot" gencodec:"required"`
TxHash common.Hash
`json:"transactionsRoot" gencodec:"required"`
ReceiptHash common.Hash
`json:"receiptsRoot" gencodec:"required"`
Bloom Bloom
`json:"logsBloom" gencodec:"required"`
GasLimit
uint64
`json:"gasLimit" gencodec:"required"`
GasUsed
uint64
`json:"gasUsed" gencodec:"required"`
Extra []
byte
`json:"extraData" gencodec:"required"`
MixDigest common.Hash
`json:"mixHash" gencodec:"required"`
Nonce BlockNonce
`json:"nonce" gencodec:"required"`
}
Header是Block的核心,它的成员变量都很重要,需要仔细分析
-
ParentHash:父区块(parentBlock)的hash, 通过这个hash可进一步找到parentBlock。除了创世块(Genesis Block)外,每个区块有且只有一个父区块
-
UncleHash:Block结构体的成员uncles的RLP哈希值。uncles是一个Header数组。以太坊引入叔块还是很合理的,相比比特币10分钟出一个区块,以太坊9s出一个区块,因而区块分叉的概率高很多,从而会出现大量新块不被采纳的情况,为了增加矿工的积极性,系统会将部分无用的区块打包进区块并给这些无用块(叔块)创造者一小份收益。
-
Coinbase:区块的作者的地址。在每次执行交易时系统会给与作者一定的奖励Ether
-
Root:“state Trie”的根节点的RLP哈希值
-
TxHash: “tx Trie”的根节点的RLP哈希值
-
ReceiptHash:"Receipt Trie”的根节点的RLP哈希值。Block的所有Transaction执行完后会生成一个Receipt数组,这个数组中的所有Receipt被逐个插入一个MPT结构中,最后形成"Receipt Trie"
-
Bloom:Bloom过滤器(Filter),用来快速判断一个参数Log对象是否存在于一组已知的Log集合中。
- Difficulty:区块的难度。Block的Difficulty由共识算法基于parentBlock的Time和Difficulty计算得出,并会动态调整
-
Number:区块的高度(即index)。Block的Number等于其父区块Number +1。
- Time:区块“应该”被创建的时间。由共识算法确定,其一般等于parentBlock.Time + 9s,也可能等于当前系统时间
-
GasLimit:区块内所有Gas消耗的上限。该数值在区块创建时赋值,与父区块的数据相关。具体来说,根据父区块的GasUsed同创世块的GasLimit * 2/3的大小关系来计算得出。
- GasUsed:执行区块内所有Transaction实际消耗的Gas总和
- Nonce:一个64bit的哈希数,它被用于POW等挖块算法,暴力碰撞得出。
- mixDigest: 区块头除去Nonce, mixDigest数据的hash+nonce的RLP的hash值
Block
type
Block
struct
{
header *Header
uncles []
*
Header
transactions Transactions
// caches
hash atomic.Value
size atomic.Value
// Td is used by package core to store the total difficulty
// of the chain up to and including the block.
// These fields are used by package eth to track
// inter-peer block relay.
ReceivedAt time.Time
ReceivedFrom
interface
{}
}
Block的大部分功能都由header代劳,只是多一些transaction数据
func
(b
*
Block)
GasLimit
()
uint64
{
return
b.header.GasLimit }
func
(b
*
Block)
GasUsed
()
uint64
{
return
b.header.GasUsed }
func
(b
*
Block)
NumberU64
()
uint64
{
return
b.header.Number.
Uint64
() }
func
(b
*
Block)
MixDigest
() common.Hash {
return
b.header.MixDigest }
func
(b
*
Block)
Nonce
()
uint64
{
return
binary.BigEndian.
Uint64
(b.header.Nonce[:]) }
func
(b
*
Block)
Bloom
() Bloom {
return
b.header.Bloom }
func
(b
*
Block)
Coinbase
() common.Address {
return
b.header.Coinbase }
func
(b
*
Block)
Root
() common.Hash {
return
b.header.Root }
func
(b
*
Block)
ParentHash
() common.Hash {
return
b.header.ParentHash }
func
(b
*
Block)
TxHash
() common.Hash {
return
b.header.TxHash }
func
(b
*
Block)
ReceiptHash
() common.Hash {
return
b.header.ReceiptHash }
func
(b
*
Block)
UncleHash
() common.Hash {
return
b.header.UncleHash }
func
(b
*
Block)
Extra
() []
byte
{
return
common.
CopyBytes
(b.header.Extra) }
那为啥还要设计两个对象,主要是header是轻量级数据,而block数据可能很大,因而对于网络传输的时候,先传输header数据验证重复性及合法性可以节省大量带宽。同时对于SPV的支持也是很有用的。
Block的hash和header的hash是一样,都是header中除nonce和mixDigest数据外的rlp的hash,这样相同的交易内容的相同该区块hash是一样,哪怕是由不同的节点创建出来的。
一个block的还原就是根据H+hash=>header, B+hash=>body,也就是一个block的数据时分为两个<k, v>储存的,尽管hash相同,但是前缀不一样,导致key不一样
StateDB
一个block通过root字段可以构造加载一个StateDB对象,也就是每个block有一个StateDB实例
type
StateDB
struct
{
//leveldb操作接口
db Database
// <address, stateObject>生成的trie
trie Trie
// This map holds 'live' objects, which will get modified while processing a state transition.
// <address, stateObject>数据cache
stateObjects
map
[common.Address]
*
stateObject
stateObjectsDirty
map
[common.Address]
struct
{}
….
}
StateDB保存的是所有的账号信息即stateObject, 因而<address, stateObject>是一个巨量的<k, v>数据集,通过map[common.Address]*stateObject来加载所有的账号信息是不可能的,因而需要“分级”缓存机制:
- 第一缓存stateObjects,这里保留了近期活跃的账号信息
-
第二级缓存trie,以trie的方式维护<address, stateObject>信息,也能快速访问
- 第三级存储leveldb,从leveldb数据库中根据address获取对应的stateObject
我们知道设计缓存的目的是解决速度和容量的平衡。比如上面的应该第一级缓存空间小,速度快,后面的缓存应该空间大,速度小。这几级缓存设计咋一看很合理也没什么疑惑。但是仔细看了代码觉得不对啊。第一级缓存map保存某一block交易涉及到stateObject(数量有限的)。但是map从二级缓存trie读取数据却从来没删除过,这样map和trie的空间是一样的,这样看来trie这级缓存并没有卵用。然而,以太坊发展了这么久,如果这个设计有这么明显的错误肯定应该早就发现了。因此还得从自身的理解找问题,经过仔细的代码阅读和深入的思考,我个人认为trie这一模块不应该被称作为缓存,但是trie是要有的,且大有用处。
- <address, stateObject>数据有分片验证的需求,即需要一个merkle tree, 即检验某一个address的stateObject数据是否真的存在某个block
- 需要实现<address, stateObject>私有,我们知道底层的leveldb的<k, v>是全局共享的,即对一个address,只会保存一个stateObject, 而每个block都需要记录当前区块的所有address的stateObject信息,因而不同区块肯定会存在一个address对应不同stateObject情况。因而需要一种新的编码方式,将<k, v>转成<encode(k+v), v>存储到leveldb中,这样对于相同的address,如果其在不同的block里的stateObject不一样,其在leveldb中对应的key也不一样。
这两个功能的结合就是MPT树,更详细的信息可以查看MPT这篇博文
StateDB的map和trie随着交易的相关操作肯定会不断扩展变大,那这么多的数据何时回收呢?这个由go的自动gc实现。StateDB一般都是在进行block的相关过程中临时创建的,因而很快就被gc释放掉了,对应的map,trie自然也自动回收了
每个block的stateDB都是在parentBlock的stateDB的基础上执行交易更新而来的
stateObject
type
stateObject
struct
{
address common.Address
addrHash common.Hash
// hash of ethereum address of the account
//普通账号的信息
data Account
//leveldb操作接口
db
*
StateDB
// Write caches.
// <k, v>状态数据生成的MPT
trie Trie
// storage trie, which becomes non-nil on first access
code Code
// contract bytecode, which gets set when code is loaded
//最近使用的<k, v>缓存
cachedStorage Storage
// Storage entry cache to avoid duplicate reads
dirtyStorage Storage
// Storage entries that need to be flushed to disk
}
type
Storage
map
[common.Hash]common.Hash
Receipt
type
Receipt
struct
{
// Consensus fields
PostState []
byte
`json:"root"`
Status
uint
`json:"status"`
CumulativeGasUsed
uint64
`json:"cumulativeGasUsed" gencodec:"required"`
Bloom Bloom
`json:"logsBloom" gencodec:"required"`
Logs []*Log `json:"logs" gencodec:"required"`
// Implementation fields (don't reorder!)
TxHash common.Hash
`json:"transactionHash" gencodec:"required"`
ContractAddress common.Address
`json:"contractAddress"`
GasUsed
uint64
`json:"gasUsed" gencodec:"required"`
}
Receipt里的核心数据时Logs,智能合约允许开发人员通过event定义一些事件并广播到全网,这些event就是记录在Logs里面的
原文链接:https://blog.csdn.net/itleaks/article/details/80094294
更多推荐
已为社区贡献3条内容
所有评论(0)