以太坊源码分析(52)trie源码分析

本文深入探讨以太坊源代码中的trie数据结构,解析其工作原理和实现细节,适合区块链开发者和对以太坊底层感兴趣的读者。
摘要由CSDN通过智能技术生成
包trie 实现了Merkle Patricia Tries,这里用简称MPT来称呼这种数据结构,这种数据结构实际上是一种Trie树变种,MPT是以太坊中一种非常重要的数据结构,用来存储用户账户的状态以及状态的变更,用来存储交易信息,用来存储交易的收据信息。MPT实际上是三种数据结构的组合,分别是Trie树, Patricia Trie, 和Merkle树。下面分别介绍这三种数据结构。

## Trie树 (引用介绍 http://dongxicheng.org/structure/trietree/)
Trie树,又称字典树,单词查找树或者前缀树,是一种用于快速检索的多叉树结构,如英文字母的字典树是一个26叉树,数字的字典树是一个10叉树。

Trie树可以利用字符串的公共前缀来节约存储空间。如下图所示,该trie树用10个节点保存了6个字符串tea,ten,to,in,inn,int:


在该trie树中,字符串in,inn和int的公共前缀是“in”,因此可以只存储一份“in”以节省空间。当然,如果系统中存在大量字符串且这些字符串基本没有公共前缀,则相应的trie树将非常消耗内存,这也是trie树的一个缺点。

Trie树的基本性质可以归纳为:

- 根节点不包含字符,除根节点意外每个节点只包含一个字符。
- 从根节点到某一个节点,路径上经过的字符连接起来,为该节点对应的字符串。
- 每个节点的所有子节点包含的字符串不相同。

## Patricia Tries (前缀树)
前缀树根Trie树的不同之处在于Trie树给每一个字符串分配一个节点,这样如果很多很长的,又没有公共节点的字符串就会导致Trie树退化成一个数组。在以太坊里面会由黑客构造很多这种节点造成拒绝服务攻击。前缀树的不同之处在于如果节点公共前缀,那么就使用公共前缀,否则就把剩下的所有节点插入同一个节点。Patricia相对Tire的优化正如下图:


上图存储的8个Key Value对,可以看到前缀树的特点。

|Key | value |
| ------------- | ---: |
|6c0a5c71ec20bq3w|5 |
|6c0a5c71ec20CX7j|27 |
|6c0a5c71781a1FXq|18 |
|6c0a5c71781a9Dog|64 |
|6c0a8f743b95zUfe|30 |
|6c0a8f743b95jx5R|2 |
|6c0a8f740d16y03G|43 |
|6c0a8f740d16vcc1|48 |

## Merkle树 (参考 http://blog.csdn.net/wo541075754/article/details/54632929)
Merkle Tree,通常也被称作Hash Tree,顾名思义,就是存储hash值的一棵树。Merkle树的叶子是数据块(例如,文件或者文件的集合)的hash值。非叶节点是其对应子节点串联字符串的hash。

![ image]( picture/trie_3.png)

Merkle Tree的主要作用是当我拿到Top Hash的时候,这个hash值代表了整颗树的信息摘要,当树里面任何一个数据发生了变动,都会导致Top Hash的值发生变化。 而Top Hash的值是会存储到区块链的区块头里面去的, 区块头是必须经过工作量证明。 这也就是说我只要拿到一个区块头,就可以对区块信息进行验证。 更加详细的信息请参考那个博客。有详细的介绍。


## 以太坊的MPT
每一个以太坊的区块头包含三颗MPT树,分别是

- 交易树
- 收据树(交易执行过程中的一些数据)
- 状态树(账号信息, 合约账户和用户账户)

下图中是两个区块头,其中state root,tx root receipt root分别存储了这三棵树的树根,第二个区块显示了当账号 175的数据变更(27 -> 45)的时候,只需要存储跟这个账号相关的部分数据,而且老的区块中的数据还是可以正常访问。(这个有点类似与函数式编程语言中的不可变的数据结构的实现)
![ image]( picture/trie_4.png)
详细结构为
![ world state trie]( picture/worldstatetrie.png)

## 黄皮书形式化定义(Appendix D. Modified Merkle Patricia Tree)

正式地,我们假设输入值J,包含Key Value对的集合(Key Value都是字节数组):


当处理这样一个集合的时候,我们使用下面的这样标识表示数据的 Key和Value(对于J集合中的任意一个I, I0表示Key, I1表示Value)


对于任何特定的字节,我们可以表示为对应的半字节(nibble),其中Y集合在Hex-Prefix Encoding中有说明,意为半字节(4bit)集合(之所以采用半字节,其与后续说明的分支节点branch node结构以及key中编码flag有关)



我们定义了TRIE函数,用来表示树根的HASH值(其中c函数的第二个参数,意为构建完成后树的层数。root的值为0)



我们还定义一个函数n,这个trie的节点函数。 当组成节点时,我们使用RLP对结构进行编码。 作为降低存储复杂度的手段,对于RLP少于32字节的节点,我们直接存储其RLP值, 对于那些较大的,我们存储其HASH节点。
我们用c来定义节点组成函数:



以类似于基数树的方式,当Trie树从根遍历到叶时,可以构建单个键值对。 Key通过遍历累积,从每个分支节点获取单个半字节(与基数树一样)。 与基数树不同,在共享相同前缀的多个Key的情况下,或者在具有唯一后缀的单个Key的情况下,提供两个优化节点。的情况下,或者在具有唯一后缀的单个密钥的情况下,提供两个优化节点。 因此,当遍历时,可能从其他两个节点类型,扩展和叶中的每一个潜在地获取多个半字节。在Trie树中有三种节点:

- **叶子节点(Leaf):** 叶子节点包含两个字段, 第一个字段是剩下的Key的半字节编码,而且半字节编码方法的第二个参数为true, 第二个字段是Value
- **扩展节点(Extention):** 扩展节点也包含两个字段, 第一个字段是剩下的Key的可以至少被两个剩下节点共享的部分的半字节编码,第二个字段是n(J,j)
- **分支节点(Branch):** 分支节点包含了17个字段,其前16个项目对应于这些点在其遍历中的键的十六个可能的半字节值中的每一个。第17个字段是存储那些在当前结点结束了的节点(例如, 有三个key,分别是 (abc ,abd, ab) 第17个字段储存了ab节点的值)

分支节点只有在需要的时候使用, 对于一个只有一个非空 key value对的Trie树,可能不存在分支节点。 如果使用公式来定义这三种节点, 那么公式如下:
图中的HP函数代表Hex-Prefix Encoding,是一种半字节编码格式,RLP是使用RLP进行序列化的函数。

![ image]( picture/trie_10.png)

对于上图的三种情况的解释

- 如果当前需要编码的KV集合只剩下一条数据,那么这条数据按照第一条规则进行编码。
- 如果当前需要编码的KV集合有公共前缀,那么提取最大公共前缀并使用第二条规则进行处理。
- 如果不是上面两种情况,那么使用分支节点进行集合切分,因为key是使用HP进行编码的,所以可能的分支只有0-15这16个分支。可以看到u的值由n进行递归定义,而如果有节点刚好在这里完结了,那么第17个元素v就是为这种情况准备的。

对于数据应该如何存储和不应该如何存储, 黄皮书中说明没有显示的定义。所以这是一个实现上的问题。我们简单的定义了一个函数来把J映射为一个Hash。 我们认为对于任意一个J,只存在唯一一个Hash值。

### 黄皮书的形式化定义(Hex-Prefix Encoding)--十六进制前缀编码
十六进制前缀编码是将任意数量的半字节编码为字节数组的有效方法。它能够存储附加标志,当在Trie树中使用时(唯一会使用的地方),会在节点类型之间消除歧义。

它被定义为从一系列半字节(由集合Y表示)与布尔值一起映射到字节序列(由集合B表示)的函数HP:



因此,第一个字节的高半字节包含两个标志; 最低bit位编码了长度的奇偶位,第二低的bit位编码了flag的值。 在偶数个半字节的情况下,第一个字节的低半字节为零,在奇数的情况下为第一个半字节。 所有剩余的半字节(现在是偶数)适合其余的字节。

## 源码实现
### trie/encoding.go
encoding.go主要处理trie树中的三种编码格式的相互转换的工作。 三种编码格式分别为下面的三种编码格式。

- **KEYBYTES encoding**这种编码格式就是原生的key字节数组,大部分的Trie的API都是使用这边编码格式
- **HEX encoding** 这种编码格式每一个字节包含了Key的一个半字节,尾部接上一个可选的'终结符','终结符'代表这个节点到底是叶子节点还是扩展节点。当节点被加载到内存里面的时候使用的是这种节点,因为它的方便访问。
- **COMPACT encoding** 这种编码格式就是上面黄皮书里面说到的Hex-Prefix Encoding,这种编码格式可以看成是*HEX encoding**这种编码格式的另外一种版本,可以在存储到数据库的时候节约磁盘空间。

简单的理解为:将普通的字节序列keybytes编码为带有t标志与奇数个半字节nibble标志位的keybytes
- keybytes为按完整字节(8bit)存储的正常信息
- hex为按照半字节nibble(4bit)储存信息的格式。供compact使用
- 为了便于作黄皮书中Modified Merkle Patricia Tree的节点的key,编码为偶数字节长度的hex格式。其第一个半字节nibble会在低的2个bit位中,由高到低分别存放t标志与奇数标志。经compact编码的keybytes,在增加了hex的t标志与半字节nibble为偶数个(即完整的字节)的情况下,便于存储

代码实现,主要是实现了这三种编码的相互转换,以及一个求取公共前缀的方法。

    func hexToCompact(hex []byte) []byte {
        terminator := byte(0)
        if hasTerm(hex) {
            terminator = 1
            hex = hex[:len(hex)-1]
        }
        buf := make([]byte, len(hex)/2+1)
        buf[0] = terminator << 5 // the flag byte
        if len(hex)&1 == 1 {
            buf[0] |= 1 << 4 // odd flag
            buf[0] |= hex[0] // first nibble is contained in the first byte
            hex = hex[1:]
        }
        decodeNibbles(hex, buf[1:])
        return buf
    }
    
    func compactToHex(compact []byte) []byte {
        base := keybytesToHex(compact)
        base = base[:len(base)-1]
        // apply terminator flag
        if base[0] >= 2 { // TODO 先将keybytesToHex输出的末尾结束标志删除后,再通过判断头半个字节的标志位t加回去。操作冗余
            base = append(base, 16)
        }
        // apply odd flag
        chop := 2 - base[0]&1
        return base[chop:]
    }
    
    func keybytesToHex(str []byte) []byte {
        l := len(str)*2 + 1
        var nibbles = make([]byte, l)
        for i, b := range str {
            nibbles[i*2] = b / 16
            nibbles[i*2+1] = b % 16
        }
        nibbles[l-1] = 16
        return nibbles
    }
    
    // hexToKeybytes turns hex nibbles into key bytes.
    // This can only be used for keys of even length.
    func hexToKeybytes(hex []byte) []byte {
        if hasTerm(hex) {
            hex = hex[:len(hex)-1]
        }
        if len(hex)&1 != 0 {
            panic("can't convert hex key of odd length")
        }
        key := make([]byte, (len(hex)+1)/2) // TODO 对于一个已经判断为偶数的len(hex)在整除2的同时加1,为无效的+1逻辑
        decodeNibbles(hex, key)
        return key
    }
    
    func decodeNibbles(nibbles []byte, bytes []byte) {
        for bi, ni := 0, 0; ni < len(nibbles); bi, ni = bi+1, ni+2 {
            bytes[bi] = nibbles[ni]<<4 | nibbles[ni+1]
        }
    }
    
    // pr
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

尹成

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值