以太坊的数据结构(状态树、交易树、收据树)及代码分析
文章目录
- 一、状态树
- 1.1 trie
- 1.2 Patricia tree(trie)
- 1.3 Merkle Patricia tree(trie)
- 1.4 Modified Merkle Patricia tree(trie)
- 1.5 账户状态值存储
- 二、交易树、收据树
- 2.1 概述
- 2.2 Modified Merkle Patricia tree(trie)
- 2.3 布隆过滤器(bloom filter)
- 2.4 总结:交易驱动的状态机
- 三、区块及数据结构代码分析
- 3.1 区块信息结构体
- 3.2 交易树和收据树的创建
- 3.3 bloom filter的创建和查询
一、状态树
以太坊是基于账户的账本,因此需要进行账户地址和账户状态的映射,如下所示:
我们尝试寻找一种合适的数据结构来完成这个需求:
- 如果以哈希表的形式保存状态数据,可以非常有效率地查找、更新账户状态数据,但是由于状态数据只保存在区块体中,轻节点难以进行Merkle Proof,那么接下来考虑构建Merkle tree;
- 如果将账户数据简单组织成Merkle tree,不进行排序,就需要发布所有账户到区块中,保证根哈希一致,但是数量级太大,不可行;如果只发布状态变化的账户,就会导致所有节点的根哈希不一致,无法共识;
- 如果使用排序的Merkle tree,各个节点的根哈希就会相同,但是增加账户时,需要重构Merkle tree,代价太大。另外Merkle tree不能够快速查找、更新状态数据。以太坊中使用的是一种新的数据结构Merkle Patricia trie。
1.1 trie
trie是一种字典前缀树,信息检索较为方便。如果有General、Genesis、Go、God、Good这几个单词,组成trie如下所示:
- 每个节点的分支取决于元素范围,上面例子中为26个英文字母和结束标志,最多27个分叉。在以太坊中使用16进制(0~F)表示账户,加上结束标志,最多17个分支;
- 查找效率取决于key值长度,键值越长,查找访问次数越多。以太坊中账户为40位16进制数,因此查找长度固定是40;
- 不会出现碰撞,哈希表则有碰撞的问题;
- 给定一组输入,构成的trie一致;
- 更新数据非常容易,只需访问局部分支。
1.2 Patricia tree(trie)
Patricia tree称为路径压缩前缀树,可以节省存储空间,同时还可以降低了查找访问次数,提高查找效率。例如将上例中的trie改进为Patricia tree,如下图所示:
Patricia tree适合键值分布比较稀疏的数据,压缩效果比较明显,如下图所示:
在以太坊中,为防止碰撞,使用了160bit长的账户,非常稀疏,因此适合使用Patricia tree数据结构。
1.3 Merkle Patricia tree(trie)
将Patricia tree的指针全部换成哈希指针,就构建成了Merkle Patricia tree,可以计算出根哈希值,保存在区块头中。
- 通过根哈希保证树不被篡改,每个账户的状态都是不可篡改的;
- 通过Merkle proof,可以用来证明任意一个账户的状态,比如账户余额;
- 通过Merkle proof,可以证明某个账户不存在。
1.4 Modified Merkle Patricia tree(trie)
以太坊使用的是修改版的Merkle Patricia tree,与Merkle Patricia tree没有本质区别。例如下图中有4个7位的地址,保存账户余额信息(value),树中有3种节点,每个节点存储关联节点的哈希值:
- Extension Node:扩展节点,保存路径压缩压缩数据,即shared nibbles中保存的16进制数据;
- Branch Node:分支节点,无法压缩;
- Leaf Node:叶子节点,保存账户状态数据;
新发布一个区块的时候,某些账户的状态会发生变化,新区快中会为变化的账户重新建立分支,大部分不变的数据则指向历史区块中的分支,因此区块间会共享大部分不变的状态分支。如下图所示:
保留历史状态的好处:
未胜出的临时性分叉需要回滚才能继续出块,由于智能合约的执行不易反推执行,保留起始与结束的记录,回滚才比较方便。
账户树中保存全部账户信息的原因:
查找某个账户更快速,如果区块内只保存区块内交易的相关账户信息,查询某个很久没有交易的账户就要花费很长时间,最坏的情况是如果转账给一个从未进行交易的账户,就必须追溯到创世区块,最后有可能发现区块内没有该账户的信息。
1.5 账户状态值存储
账户状态数据经过RLP(Recursive Length Prefix)序列化后存储,RLP序列化方式相比protobuf较为简单,只支持字符嵌套数组(nested array of bytes),实现起来比较容易。
二、交易树、收据树
2.1 概述
区块内的交易列表组成交易树,与比特币的Merkle tree作用类似。
每个交易执行完会有一个收据,记录这个交易的相关信息,与交易树上的节点一一对应。以太坊中以太坊的智能合约执行过程比较复杂,增加收据树,有助于快速查询一些交易执行结果。
2.2 Modified Merkle Patricia tree(trie)
交易树与收据树同样采用的是MPT数据结构,这样三棵树的代码更加统一,便于管理。同时MPT具有良好的数据查询性能。
与状态树不同的是,交易树和收据树都只把当前区块发布的交易包含进来,独立于其他区块,没有共享分支。
MPT的作用:
MPT可以给轻节点提供Merkle proof,交易树可以证明某个交易确实属于某个区块,收据树同样可以证明某个交易结果。
2.3 布隆过滤器(bloom filter)
布隆过滤器给包含很多元素的集合计算出一个很紧凑的摘要,用较少的空间来表示较大集合的存在关系,可以高效地查找某个元素是否在一个大的集合里。
对于给定输入集合,用Hash函数给集合中元素分别计算出地址,分别在位串的这些地址上标记为1。在查找时,进行同样的计算过程,并查看位串上的元素,如果是 1,则说明较大概率是存在该输入。如下图所示:
通常用多个哈希函数来防止哈希碰撞,可以降低误判率,仍存在着误报(FalsePositive)的情况,但绝对不会漏报(False Negative)。
布隆过滤器的作用:
每个交易执行完会形成一个收据,收据里面包含bloom filter,记录交易类型、地址等信息,发布的区块头里面会有一个总的bloom filter,是区块内所有交易的bloom filter的并集,根据区块头里的bloom filter,即使是轻节点就可以过滤掉大部分区块,然后向全节点请求候选区块体数据,查询区块内每个交易的bloom filter,得知哪些区块有想要的交易,当然也有可能某些区块是误报,里面没有想要的交易。
如上所述,交易树通过布隆过滤器可以进行更加复杂的查询,比如查询过去10天跟某个智能合约所有有关的交易,或者查询过去10天内某种类型(众筹、发行新币等)的事件。
2.4 总结:交易驱动的状态机
以太坊是一个交易驱动的状态机(transaction-driven state machine),状态指的是所有账户的状态,即状态树包含的内容,交易指的是区块内包含的那些交易,通过执行这些交易会驱动系统从当前状态转移到下一个状态,且状态的转移具有确定性。
比特币同样可以认为是一个交易驱动的状态机,比特币的状态是UTXO,每次发布的区块内的交易会驱动状态机从当前状态确定地转移到下一个状态。
三、区块及数据结构代码分析
3.1 区块信息结构体
区块头的结构定义如下所示:
ParentHash表示父区块的哈希,UncleHash是叔父区块哈希,Coinbase是示矿工账户地址,Root是状态树的根哈希,TxHash是交易树的根哈希,ReceiptHash是收据树的根哈希,Bloom是块头的bloom filter,Difficulty是挖矿难度(可根据需要调整),GasLimit和GasUsed与汽油费相关,Time是区块大致产生时间,MixDigest与挖矿过程相关,从Nonce经过一些列计算而来,Nonce是挖矿的谜底随机数。
// Header represents a block header in the Ethereum blockchain.
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"`Difficulty *big.Int `json:"difficulty" gencodec:"required"`Number *big.Int `json:"number" gencodec:"required"`GasLimit uint64 `json:"gasLimit" gencodec:"required"`GasUsed uint64 `json:"gasUsed" gencodec:"required"`Time uint64 `json:"timestamp" gencodec:"required"`Extra []byte `json:"extraData" gencodec:"required"`MixDigest common.Hash `json:"mixHash"`Nonce BlockNonce `json:"nonce"`
}
区块结构如下所示:
header为指向区块头(Header)的指针,uncles为指向叔父区块头的指针,transactions为区块内的交易列表。
// Block represents an entire block in the Ethereum blockchain.
type Block struct {header *Headeruncles []*Headertransactions Transactions// cacheshash atomic.Valuesize atomic.Value// Td is used by package core to store the total difficulty// of the chain up to and including the block.td *big.Int// These fields are used by package eth to track// inter-peer block relay.ReceivedAt time.TimeReceivedFrom interface{}
}
发布的区块信息:
extblock为发布到网络中的区块信息,包括区块头、交易列表、叔父区块头。
// "external" block encoding. used for eth protocol, etc.
type extblock struct {Header *HeaderTxs []*TransactionUncles []*Header
}
3.2 交易树和收据树的创建
NewBlock函数里包含交易树、收据树的创建以及叔父区块处理。
创建交易树的步骤:
- 判断交易列表是否为空,如果为空,那么块头的根哈希就是空哈希值
- 如果不为空,调用DeriveSha函数得到交易树的根哈希值
- 然后创建区块的交易列表
创建收据树的步骤:
- 判断收据列表是否为空,如果为空,那么根哈希就是空哈希值
- 如果不为空,调用DeriveSha函数得到收据树的根哈希值
- 然后调用CreateBloom函数创建块头里的bloom filter
处理叔父区块:
- 判断叔父区块列表是否为空,如果为空,那么块头的叔父区块哈希值就是空的哈希值
- 如果不为空,调用CalcUncleHash计算哈希值
- 通过一个循环构建区块里的叔父数组
如下所示:
// NewBlock creates a new block. The input data is copied,
// changes to header and to the field values will not affect the
// block.
//
// The values of TxHash, UncleHash, ReceiptHash and Bloom in header
// are ignored and set to values derived from the given txs, uncles
// and receipts.
func NewBlock(header *Header, txs []*Transaction, uncles []*Header, receipts []*Receipt) *Block {b := &Block{header: CopyHeader(header), td: new(big.Int)}// TODO: panic if len(txs) != len(receipts)if len(txs) == 0 {b.header.TxHash = EmptyRootHash} else {b.header.TxHash = DeriveSha(Transactions(txs))b.transactions = make(Transactions, len(txs))copy(b.transactions, txs)}if len(receipts) == 0 {b.header.ReceiptHash = EmptyRootHash} else {b.header.ReceiptHash = DeriveSha(Receipts(receipts))b.header.Bloom = CreateBloom(receipts)}if len(uncles) == 0 {b.header.UncleHash = EmptyUncleHash} else {b.header.UncleHash = CalcUncleHash(uncles)b.uncles = make([]*Header, len(uncles))for i := range uncles {b.uncles[i] = CopyHeader(uncles[i])}}return b
}
DeriveSha函数如下所示,创建了trie结构体:
func DeriveSha(list DerivableList) common.Hash {keybuf := new(bytes.Buffer)trie := new(trie.Trie)for i := 0; i < list.Len(); i++ {keybuf.Reset()rlp.Encode(keybuf, uint(i))trie.Update(keybuf.Bytes(), list.GetRlp(i))}return trie.Hash()
}
Trie是一个Merkle Patricia Trie,如下所示:
// Trie is a Merkle Patricia Trie.
// The zero value is an empty trie with no database.
// Use New to create a trie that sits on top of a database.
//
// Trie is not safe for concurrent use.
type Trie struct {db *Databaseroot node
}
3.3 bloom filter的创建和查询
每个交易执行完,形成一个收据,记录交易执行结果,其中Status为交易执行情况。如下所示,Bloom域即布隆过滤器,每个收据可以包含多个Log,Bloom就是根据Log产生的:
// Receipt represents the results of a transaction.
type Receipt struct {// Consensus fields: These fields are defined by the Yellow PaperPostState []byte `json:"root"`Status uint64 `json:"status"`CumulativeGasUsed uint64 `json:"cumulativeGasUsed" gencodec:"required"`Bloom Bloom `json:"logsBloom" gencodec:"required"`Logs []*Log `json:"logs" gencodec:"required"`// Implementation fields: These fields are added by geth when processing a transaction.// They are stored in the chain database.TxHash common.Hash `json:"transactionHash" gencodec:"required"`ContractAddress common.Address `json:"contractAddress"`GasUsed uint64 `json:"gasUsed" gencodec:"required"`// Inclusion information: These fields provide information about the inclusion of the// transaction corresponding to this receipt.BlockHash common.Hash `json:"blockHash,omitempty"`BlockNumber *big.Int `json:"blockNumber,omitempty"`TransactionIndex uint `json:"transactionIndex"`
}
块头里Bloom域就是通过CreateBloom函数创建的,由这个块中所有receipts的Bloom Filter组合得到。
CreateBloom的参数是区块的所有收据,通过for循环对每个收据调用LogsBloom函数来生成收据的Bloom Filter,通过Or函数合并这些Bloom Filter,得到整个区块的Bloom Filter。
LogsBloom函数的功能是生成收据的Bloom Filter,参数为Receipt结构体里的Log数组,外层循环对log数组里的每个log进行处理,把log的地址取哈希后加到Bloom Filter里,内层循环把log包含的每个topic包含到Bloom Filter里。这样就得到了收据的Bloom Filter。
bloom9函数是Bloom Filter中使用的哈希函数,把输入映射到digest的3个位置,这3个位置的值置为1。首先生成参数的32字节哈希值,然后是3轮循环,取前6字节,每2个字节一组,拼接在一起,和2047进行“与运算”(相当于对2048取余),得到2047区间内的数,之所以这样做是因为以太坊Bloom Filter的长度是2048,然后将数字1左移这么多的长度,然后合并到上一轮得到的Bloom Filter,3轮循环结束后,就得到了3个位置被置为1的Bloom Filter。
BloomLookup函数的功能是查询Bloom Filter中是否有感兴趣的topic,首先调用bloom9函数将topic转换成bit数据,然后和Bloom Filter进行“与运算”,这样Bloom Filter有其他的topic也不会有影响,然后再跟自身比较,如果相等,说明Bloom Filter中对应位置确实是1,即该Bloom Filter中包含感兴趣的topic。
func CreateBloom(receipts Receipts) Bloom {bin := new(big.Int)for _, receipt := range receipts {bin.Or(bin, LogsBloom(receipt.Logs))}return BytesToBloom(bin.Bytes())
}func LogsBloom(logs []*Log) *big.Int {bin := new(big.Int)for _, log := range logs {bin.Or(bin, bloom9(log.Address.Bytes()))for _, b := range log.Topics {bin.Or(bin, bloom9(b[:]))}}return bin
}func bloom9(b []byte) *big.Int {b = crypto.Keccak256(b)r := new(big.Int)for i := 0; i < 6; i += 2 {t := big.NewInt(1)b := (uint(b[i+1]) + (uint(b[i]) << 8)) & 2047r.Or(r, t.Lsh(t, b))}return r
}var Bloom9 = bloom9func BloomLookup(bin Bloom, topic bytesBacked) bool {bloom := bin.Big()cmp := bloom9(topic.Bytes())return bloom.And(bloom, cmp).Cmp(cmp) == 0
}
以太坊的数据结构(状态树、交易树、收据树)及代码分析相关推荐
- V神最新演讲:以太坊2.0的分片交易
V神最新演讲:以太坊2.0的分片交易 以太坊创始人Vitalik Buterin 文/Vitalik Buterin 2019年6月29日,由CSDN.灵钛科技主办的"2019第二届以太坊技 ...
- 以太坊中的账户、交易、Gas和区块Gas Limit等基本概念
本篇文章作为科普文章,汇总整理了以太坊中的账户.交易.Gas和区块Gas Limit等相关概念,以便大家在实践中更好的与具体业务相结合. 什么是账户 以太坊账户与我们所知的账户概念有一定相似之处,却又 ...
- 【以太坊】如何获取内部交易
技术改变生活,技术视角 : 区块链专栏 文章目录 内部交易是什么? 内部交易怎么获取 [以太坊]如何获取内部交易 内部交易是什么? 简单来说,内部交易就是合约内部又调用了其他合约或者说产生新的交易 内 ...
- pythonxy是什么东西_无状态以太坊:二进制状态树实验
什么是 "无状态以太坊"? 如果您已经了解什么是 "无状态以太坊" 以及 "区块见证数据",可以跳过这一段. 为执行交易及验证区块,以太坊网 ...
- 以太坊MPT数据结构
Trie树 Trie,又称为字典树或者前缀树 (prefix tree),属于查找树的一种.它与平衡二叉树的主要不同点包括: 每个节点数据所携带的 key 不会存储在 Trie 的节点中,而是通过该节 ...
- 【以太坊】javascript控制台完整交易流程
一.前言 在前面几章,我们简单的在本地搭建了以太坊的测试网络.需要的小伙伴们可以看一下之前的那篇:搭建测试网络之ubuntu系统安装geth客户端以及同步区块数据,现在的问题是搭建完之后,该如何进行操 ...
- 解析以太坊区块链跨链交易,并爬虫爬取更新于数据库
1.Execute函数执行 调用解析区块函数 errcode_rpc, trans_list, block_list, new_height, max_height = self.parallel[i ...
- 以太坊PHP离线交易签名生成,以太坊web3.sendRawTransaction离线签名交易
工作中需要复现短地址攻击和thedao重入攻击,重入攻击可以直接通过eth.sendTransaction和remix来发送交易,但是短地址攻击由于钱包和remix这些都对input做了长度检测,无法 ...
- 北京大学肖臻老师《区块链技术与应用》公开课笔记:以太坊原理(一):以太坊概述、账户、状态树、交易树和收据树
1.ETH-以太坊概述 比特币和以太坊是两种最主要的加密货币,比特币被称为区块链1.0,以太坊被称为区块链2.0 以太坊在系统设计上针对比特币运行过程中出现的问题进行了改进,比如: 出块时间,比特币的 ...
最新文章
- 红帽启动apache服务器_redhat 启动ssh服务器
- WordPress强制跳转https教程
- 网络转型临界点 带你看瞻博网络的创新步伐
- Windows Intellij环境下Gradle的 “Could not determine Java version from ‘9.0.1’”的解决方式...
- 【SRX】折腾了半天终于我的那对SRX210 升级到了 12.1R1.9
- mysql数据库with ur_Python使用MySQL数据库(新)
- 获取天气html,使用htmlparser获取sohu的天气预报
- Trick (八)——list 模拟 stack(Python)
- WARNING: Ignoring invalid distribution -ip (e:\python\lib\site-packages)
- 台服海岛纪元显示无法连接服务器,打开海岛纪元提示网络异常或者连接不上
- LOJ10102旅游航道
- 使用Labwindows开发DAQmx
- org.apache.jasper.JasperException: java.lang.NullPointerException 解决办法
- 如何做好互联网广告销售
- Jetson nano安装Google拼音输入法
- 求某年某月1日是星期几C语言,用c语言调用函数编程,1990年1月1日是星期一 要求输入某年某月某日,输出它是星期几...
- 使用Servlet和JSP开发Java应用程序 ----错误处理
- Scanner的基本用法
- 2021年中式烹调师(中级)考试题库及中式烹调师(中级)操作证考试
- RuntimeError: Legacy autograd function with non-static forward method is deprecated.