Ethereum以太坊源码分析(三)Trie树源码分析(上)

Ethereum以太坊源码分析(三)Trie树源码分析(上)

本文参考:

Github - go-ethereum-code-analysis
Ethereum 黄皮书形式化定义参考
以太坊技术黄皮书学习笔记3:改进的Merkle Patricia 树

go-eth v0.9.39-3798-g6566a0a3b


go-ethereum当中的trie包实现了Merkle Patricia Tries,这里用简称MPT来称呼这种数据结构,这种数据结构实际上是一种Trie树变种,以太坊数据的结构与比特币不同,它是利用key-value的形式来储存世界状态。MPT是以太坊中一种非常重要的数据结构,用来存储用户账户的状态及其变更、交易信息、交易的收据信息。MPT实际上是三种数据结构的组合,分别是Trie树, Patricia Trie,和Merkle树。

下面简单介绍这三种数据结构。详细内容和性能分析可以看我blog中之前的一篇文章以太坊Merkle Patricia Tree全解析

Trie树(引用介绍dongxicheng.org/structu

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 Tree

Merkle Tree,通常也被称作Hash Tree,顾名思义,就是存储hash值的一棵树。Merkle树的叶子是数据块(例如,文件或者文件的集合)的hash值。非叶节点是其对应子节点串联字符串的hash。


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

以太坊的 MPT - Merkle Patricia Tries

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

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

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


编码方式

从编码来说,有三种编码:

  • Raw编码:原生的key编码,是MPT对外提供接口中使用的编码方式,当数据项被插入到树中时,Raw编码被转换成Hex编码;
  • Hex编码:16进制扩展编码,用于对内存中树节点key进行编码,当树节点被持久化到数据库时,Hex编码被转换成HP编码;
  • HP编码:16进制前缀编码,用于对数据库中树节点key进行编码,当树节点被加载到内存时,HP编码被转换成Hex编码;



黄皮书的形式化定义(Hex-Prefix Encoding)–十六进制前缀编码

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

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



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

HP编码例子

如果x中有terminator t!=0, 反之 t=0

case 1:

t=0, x={1,2,3,4,5} 且x属于Y(nibble)
f(t)=0, ||x||=5 => odd
HP(x,t) = {16(0+1)+1, 16*2+3,16*4+5}
		= {0x11, 0x23, 0x45}

case 2:

t=0, x={0,1,2,3,4,5} 且x属于Y(nibble)
f(t)=0, ||x||=6 => even
HP(x,t) = {0, 16*0+1,16*2+3, 16*4+5}
		= {0x00, 0x01, 0x23, 0x45}

case 3:

t!=0, x={15,1,12,11,8,16} (16为结束符,不算数据长度)
HP(x,t) = {16*3+f, 16*1+c, 16*b+8}
		= {0x3f, 0x1c, 0xb8}

case 4:

t!=0, x={0,15,1,12,11,8,16} (16为结束符,不算数据长度)
HP(x,t) = {16*2, 16*0+f, 16*1+c, 16*b+8}
		= {0x20, 0x0f, 0x1c, 0xb8}

黄皮书对Trie的定义

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


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


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


函数y(J)这个公式的作用就是根据规则将key的字节序的每个字节拆成2个半字节。示例如下:

k0 = {0x12, 0x34, 0x56} ||k0|| = 3 i<2||kn||
k0'[0] = [k0[0/2]/16]下取整 i = 0 is even
	= [k0[0/2]/16]下取整 = [0x12/16]下取整 = 1
k0'[1] = k0[[1/2]下取整]	mod 16 = k0[0] mod 16 = 0x12 mod 16 = 2
同理
k0'[2] = 3
k0'[3] = 4
k0'[4] = 5
k0'[5] = 6

k0' = {0x1, 0x2, 0x3, 0x4, 0x5, 0x6} ||k0'|| = 6

接下来的3个公式精确的定义了Trie:

我们定义了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进行序列化的函数。



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

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

叶节点是在很多树形状的数据结构经常用到的内容,扩展节点和分支节点是以太坊为了提高访问速度和节省存储大小对Merkle树进行的改进。接下来我们用一个例子来加深理解:

Trie数 例子

Step 1

只有一个键值对时 J = {“alice1”,”40 eth”}

1.
||J|| = 1
k0 = "alice1" v0 = "40 eth"
y(J) = {(k0', v0)}

2.
a的Hex编码0x61, l的Hex编码0x6C...以此类推
所以 k0 = {0x61, 0x6c, 0x69, 0x63, 0x65, 0x31}
k0' = {6,1,6,12,6,9,6,3,6,5,3,1,16} (16是结束符, 因为Key对应的节点存储的是真实的数据项内容(即该节点是叶子节点),则在末位添加一个ASCII值为16的字符作为terminator;)

3. 
TRIE(J) = KEC(c(J,0)) ||J|| = 1

4. c(J,0) = RLP((HP(I0[0...(||I0||-1)]), true, I1))
I0 = K0, ||I0|| = 12, I1 = v0

5.
HP(I0[0..11], true) = HP(k0', true) t!=0, true!=0 f(t)=2
					= {16f(t), 16*x[0]+x[1], 16*x[2]+x[3],...}
					= {0x20, 0x61, 0x6c, 0x69, 0x63, 0x65, 0x31}

首先根据公式1,我们先描述了输入域的值,只有1个键值对。然后根据公式2在前面讲解过,把属于的key转换成可以用于计算hash树根的格式,即key属于Y:半字节结合。根据J的长度为1这个条件,使用公式3,4,首先计算key的HP运算的结果。在公式4中结束符标志t是true,因此f(t)=2,k’0的长度为12,是个偶数,运用前面的知识, 我们首先计算出key的HP值。

令5的结果为I0'

6.
RLP(I0', I1)
s(x) = RLP(x0):RLP(x1), x0 = I0', x1 = I1. (":"为连接符号)

7.
RLP(x0) = RLP({0x20, 0x61, 0x6c, 0x69, 0x63, 0x65, 0x31})
||x0|| = 7, RLP(x0) = (128+7):x0 = 0x87 : x0
					= {0x87, 0x20, 0x61, 0x6c, 0x69, 0x63, 0x65, 0x31}

8. 
Hex(x1) = Hex("40 eth") = {0x34, 0x30, 0x20, 0x65, 0x74, 0x68}
||x1|| = 6, RLP(x0) = (128+6):x1
					= {0x86, 0x34, 0x30, 0x20, 0x65, 0x74, 0x68}

9.
s(x) = RLP(x0):RLP(x1),  ||s(x)|| = 15 < 56
Root = RLP((I0',I1)) = (192+||s(x)||):s(x)  这里的192+||s(x)||是Head
					 = {Head, I0', I1}
					 = {0xcf, 0x87, 0x20, 0x61, 0x6c, 0x69, 0x63, 0x65, 0x31, 0x86, 0x34, 0x30, 0x20, 0x65, 0x74, 0x68}

KEC(Root) = 0x1b1f569b5f27c6160298a9d56d0084b24424c13622526f7a285644b3f9ca2f67

在计算完key的HP值之后,可以用于RLP的算法,将此此键值对序列化。公式6是上一节的知识,不再详细解释。公式7也是在上一节中也有过详细讲解,这一段主要是将key的HP序列号。公式8是讲value序列化。这些都是上一节的只是,并不需要多解释,需要解释的是,我们的示例是讲字符串键值对进行保存,所以需要根据ASCII码表【6】去查询这些字符串的字节码值。公式9也是上一节的只是,||s(x)||=15,没有超过56,因此根据上一节的知识。将所有结果串联之后得到一串字节码序列.我们发现对于至于1个键值对的情况,这个字节码序列就是我们尽心hash运算的对象了,对得到的结果如公式10.具体值太长

0x1b1f569b5f27c6160298a9d56d0084b24424c13622526f7a285644b3f9ca2f67


Step 2

在Step1的基础上增加一个键值对(“alice9”:”20wei”),

则J的长度变成了2,我们可以理解键值对是一个个增加的,因此我们可以在Step1的基础上执行增加操作,就可以得到新的Merkle树。但是为了方便证明,我们把J的key和value都设计短些,对J重新定义:{(“dog1”,”40 eth”),(“do9”,”20 wei”)}

1.
J = {("dog1","40 eth"),("do9","20 wei")}

2.
y(J) = {((0x06, 0x04, 0x06, 0x0f, 0x06, 0x07, 0x03, 0x01, 0x10), "40 eth"), ((0x06, 0x04, 0x06, 0x0f, 0x03, 0x09, 0x10), "20 wei")} 
(0x10为结束符)

3.
TRIE = KEC(c(J,0)) ||J|| = 2 != 1 不符合
i != j, j = argmaxx : exist II : ||II|| = x : any I in J : I0[0...(x-1)] = II 提取最大相同前缀

4.
exist II = (0x06, 0x04, 0x06, 0x0f), i=0, x=4=j, i!=j

5.
RLP(HP(I0[0...3], false), n(J, 4))

6.
n(J, 4) 根据 n(J, i)的公式,我们可知:
||J|| = 2 != NULL - 排除条件1
||c(J, 4)|| < 32 - 满足条件2, 故我们计算c(J, 4)

对于c(J,4) ||J|| = 2, i = 4, j = 4, 对条件1,2无效

所以我们取 c(j,4) = RLP(u(0), u(1), u(2), u(3),...,u(15), v)

7.
exist I : I in J ^ ||I0|| = i 当前不满足
所以 v = NULL

8.
I: I in J ^ I0[i] = j

当前i=4, j=0...15
k0'[4] = 0x06, k1'[4] = 0x03 其余
u(j) = n({I:I in J ^ I0[i]=j}, i+1)
当 j = 03 和 06的时候 u(j) !=  NULL 其余  u(j) = NULL  

9.
由8,我们需要计算
n((k0', v0), 5) 和 n((k1', v1), 5)
之后的算法类似之前的操作,都是Leaf Node

公式8其公式定义比较复杂,其本质是对键值对中,key不属于交集的部分,逐个进行RLP封装,当逐个封装是,就是公式9,公式9就与step1的操作不收是一样的了,注意公式7会在后面的例子中讲到。

当公式9执行完成之后,将结果返回给公式8,公式8 的结果返回到公式6然后最后求出来的结果就是最新的Merkle树如下图。


其中C函数的条件3对应的是Branch

Node的设计,条件2对应的Extension Node的设计,本例中extension node也是root

node。条件1对应的是leaf Node的设计,上图中给出了key=”do9”的查找路径,找到leaf

node即可访问其value字段值:”20 wei”

Step 3

MPT的生成其实是一种递归的形式,现在我们可以看看MPT如何存储更多的Node。

当J= {(“dog1”,”40 eth”),(“do9”,”20 wei”),(“do91b”,”5 fin”),(“do99c”,”12 sza”)}其Merkle树的结果


其中需要注明的是红色7的位置,这里就是step2中未涉及到的条件7

源码实现

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为偶数个(即完整的字节)的情况下,便于存储

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

// go-ethereum/trie/encoding.go

// Hex编码 => HP编码
func hexToCompact(hex []byte) []byte {
	terminator := byte(0)
	// 判断是否有terminator
	if hasTerm(hex) {
		terminator = 1
		hex = hex[:len(hex)-1]
	}
	buf := make([]byte, len(hex)/2+1)
	buf[0] = terminator << 5 // the flag byte
	// 给Hex编码加上prefix
	// 这里的操作完全和黄皮书的公式一致
	// hex长度是否为odd
	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
}

// HP编码 => Hex编码
func compactToHex(compact []byte) []byte {
	base := keybytesToHex(compact)
	// delete terminator flag
	if base[0] < 2 {
		base = base[:len(base)-1]
	}
	// apply odd flag
	chop := 2 - base[0]&1
	return base[chop:]
}

// 把keybytes转换成Hex半字节(Hex nibble)
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
}

// 把半字节(Hex nibble)转回 keybytes
// 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)/2)
	decodeNibbles(hex, key)
	return key
}

// 拼接nibbles
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]
	}
}

// 求a和b的公共前缀
// prefixLen returns the length of the common prefix of a and b.
func prefixLen(a, b []byte) int {
	var i, length = 0, len(a)
	if len(b) < length {
		length = len(b)
	}
	for ; i < length; i++ {
		if a[i] != b[i] {
			break
		}
	}
	return i
}

// 判断是否有Terminator 0x10(16)
// hasTerm returns whether a hex key has the terminator flag.
func hasTerm(s []byte) bool {
	return len(s) > 0 && s[len(s)-1] == 16
}

这一部分的代码就是黄皮书中编码的实现。相对来说比较简单,可以根据之前的例子验证一下。

数据结构

node的结构,可以看到node分为4种类型, fullNode对应了黄皮书里面的分支节点shortNode对应了黄皮书里面的扩展节点叶子节点(通过shortNode.Val的类型来判断当前节点是叶子节点(shortNode.Val为valueNode)还是拓展节点(通过shortNode.Val指向下一个node))。

// go-ethereum/trie/trie.go

type node interface {
	fstring(string) string
	cache() (hashNode, bool)
	canUnload(cachegen, cachelimit uint16) bool
}

type (
	//branch Node
	fullNode struct {
		Children [17]node // Actual trie node data to encode/decode (needs custom encoder)
		flags    nodeFlag
	}
	//Leaf Node or Extension Node
	shortNode struct {
		Key   []byte
		Val   node
		flags nodeFlag
	}
	hashNode  []byte
	valueNode []byte
)

trie的结构,root包含了当前的root节点,db是后端的KV存储,trie的结构最终都是需要通过KV的形式存储到数据库里面去,然后启动的时候是需要从数据库里面加载的。

originalRoot 启动加载的时候的hash值,通过这个hash值可以在数据库里面恢复出整颗的trie树。每次调用Commit操作的时候,会增加Trie树的cache时代。 cache时代会被附加在node节点上面,如果当前的cache时代 - cachelimit参数大于node的cache时代,那么node会从cache里面卸载,以便节约内存。其实这就是缓存更新的LRU算法,如果一个缓存在多久没有被使用,那么就从缓存里面移除,以节约内存空间。

// go-ethereum/trie/trie.go

// 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   *Database
	root node

	// Cache generation values.
	// cachegen increases by one with each commit operation.
	// new nodes are tagged with the current generation and unloaded
	// when their generation is older than than cachegen-cachelimit.
	cachegen, cachelimit uint16
}

Trie树的插入,查找和删除

Trie树的初始化调用New函数,函数接受一个hash值和一个Database参数,如果hash值不是空值的化,就说明是从数据库加载一个已经存在的Trie树,就调用trei.resolveHash方法来加载整颗Trie树,这个方法后续会介绍。 如果root是空,那么就新建一颗Trie树返回。

// go-ethereum/trie/trie.go

var (
	// emptyRoot is the known root hash of an empty trie.
	emptyRoot = common.HexToHash("56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421")

	// emptyState is the known hash of an empty state trie entry.
	emptyState = crypto.Keccak256Hash(nil)
)

...

// New creates a trie with an existing root node from db.
//
// If root is the zero hash or the sha3 hash of an empty string, the
// trie is initially empty and does not require a database. Otherwise,
// New will panic if db is nil and returns a MissingNodeError if root does
// not exist in the database. Accessing the trie loads nodes from db on demand.
func New(root common.Hash, db *Database) (*Trie, error) {
	if db == nil {
		panic("trie.New called without a database")
	}
	trie := &Trie{
		db: db,
	}
	if root != (common.Hash{}) && root != emptyRoot {
		rootnode, err := trie.resolveHash(root[:], nil)
		if err != nil {
			return nil, err
		}
		trie.root = rootnode
	}
	return trie, nil
}

Trie树的插入,这是一个递归调用的方法,从根节点开始,一直往下找,直到找到可以插入的点,进行插入操作。参数node是当前插入的节点,prefix是当前已经处理完的部分key,key是还没有处理玩的部分key, 完整的key = prefix + key。 value是需要插入的值。 返回值bool是操作是否改变了Trie树(dirty),node是插入完成后的子树的根节点,error是错误信息。

插入的过程如下:

  • 如果节点类型是nil(一颗全新的Trie树的节点就是nil的),这个时候整颗树是空的,直接返回shortNode{key, value, t.newFlag()}, 这个时候整颗树的跟就含有了一个shortNode节点。
  • 如果当前的根节点类型是shortNode(也就是叶子节点),首先计算公共前缀,如果公共前缀就等于key,那么说明这两个key是一样的,如果value也一样的(dirty == false),那么返回错误。 如果没有错误就更新shortNode的值然后返回。如果公共前缀不完全匹配,那么就需要把公共前缀提取出来形成一个独立的节点(扩展节点),扩展节点后面连接一个branch节点,branch节点后面看情况连接两个short节点。首先构建一个branch节点(branch := &fullNode{flags: t.newFlag()}),然后再branch节点的Children位置调用t.insert插入剩下的两个short节点。这里有个小细节,key的编码是HEX encoding,而且末尾带了一个终结符。考虑我们的根节点的key是abc0x16,我们插入的节点的key是ab0x16。下面的branch.Children[key[matchlen]]才可以正常运行,0x16刚好指向了branch节点的第17个孩子。如果匹配的长度是0,那么直接返回这个branch节点,否则返回shortNode节点作为前缀节点。
  • 如果当前的节点是fullNode(也就是branch节点),那么直接往对应的孩子节点调用insert方法,然后把对应的孩子节点只想新生成的节点。
  • 如果当前节点是hashNode, hashNode的意思是当前节点还没有加载到内存里面来,还是存放在数据库里面,那么首先调用 t.resolveHash(n, prefix)来加载到内存,然后对加载出来的节点调用insert方法来进行插入。
// go-ethereum/trie/trie.go

// 输入依次为:当前插入的节点(位置),插入的key的前缀,插入的key,插入的value
// 输出依次为:该树是否dirty,插入完成的子数根节点,错误信息 
func (t *Trie) insert(n node, prefix, key []byte, value node) (bool, node, error) {
	// 插入key长度为0
	if len(key) == 0 {
		if v, ok := n.(valueNode); ok {
			return !bytes.Equal(v, value.(valueNode)), value, nil
		}
		return true, value, nil
	}

	switch n := n.(type) {
	case *shortNode:
		// 取插入值的key和当前node的key对比,取最二者的共同前缀
		// 如果插入key和之前一样,那么只需要更新值
		matchlen := prefixLen(key, n.Key)
		// If the whole key matches, keep this short node as is
		// and only update the value.
		if matchlen == len(n.Key) {
			dirty, nn, err := t.insert(n.Val, append(prefix, key[:matchlen]...), key[matchlen:], value)
			if !dirty || err != nil {
				return false, n, err
			}
			return true, &shortNode{n.Key, nn, t.newFlag()}, nil
		}
		// 构建一个branch节点
		// Otherwise branch out at the index where they differ.
		branch := &fullNode{flags: t.newFlag()}
		var err error
		// 分别把这两个key插入一个branch当中
		_, branch.Children[n.Key[matchlen]], err = t.insert(nil, append(prefix, n.Key[:matchlen+1]...), n.Key[matchlen+1:], n.Val)
		if err != nil {
			return false, nil, err
		}
		_, branch.Children[key[matchlen]], err = t.insert(nil, append(prefix, key[:matchlen+1]...), key[matchlen+1:], value)
		if err != nil {
			return false, nil, err
		}
		// matchlen为0,表明二者没有共同前缀,可以“分叉”,说明该子树肯定为branch节点
		// Replace this shortNode with the branch if it occurs at index 0.
		if matchlen == 0 {
			return true, branch, nil
		}
		// matchlen不为0, 说明有共同前缀,当前子树为Extension Node,该拓展节点指向一个
		// Otherwise, replace it with a short node leading up to the branch.
		return true, &shortNode{key[:matchlen], branch, t.newFlag()}, nil

	case *fullNode:
		// 在branch节点插入
		dirty, nn, err := t.insert(n.Children[key[0]], append(prefix, key[0]), key[1:], value)
		if !dirty || err != nil {
			return false, n, err
		}
		n = n.copy()
		n.flags = t.newFlag()
		n.Children[key[0]] = nn
		return true, n, nil
	// 节点为新的节点,直接返回shortNode,也就是叶子结点
	case nil:
		return true, &shortNode{key, value, t.newFlag()}, nil

	case hashNode:
	    // 如果当前节点是hashNode, hashNode的意思是当前节点还没有加载到内存里面来,还是存放在数据库里面,
	    // 那么首先调用 t.resolveHash(n, prefix)来加载到内存,然后对加载出来的节点调用insert方法来进行插入。
		// We've hit a part of the trie that isn't loaded yet. Load
		// the node and insert into it. This leaves all child nodes on
		// the path to the value in the trie.
		rn, err := t.resolveHash(n, prefix)
		if err != nil {
			return false, nil, err
		}
		dirty, nn, err := t.insert(rn, prefix, key, value)
		if !dirty || err != nil {
			return false, rn, err
		}
		return true, nn, nil

	default:
		panic(fmt.Sprintf("%T: invalid node: %v", n, n))
	}
}

Trie树的Get方法,基本上就是很简单的遍历Trie树,来获取Key的信息。 Get -> TryGet -> tryGet

// go-ethereum/trie/trie.go

// Get returns the value for key stored in the trie.
// The value bytes must not be modified by the caller.
func (t *Trie) Get(key []byte) []byte {
	res, err := t.TryGet(key)
	if err != nil {
		log.Error(fmt.Sprintf("Unhandled trie error: %v", err))
	}
	return res
}

// TryGet returns the value for key stored in the trie.
// The value bytes must not be modified by the caller.
// If a node was not found in the database, a MissingNodeError is returned.
func (t *Trie) TryGet(key []byte) ([]byte, error) {
	key = keybytesToHex(key)
	value, newroot, didResolve, err := t.tryGet(t.root, key, 0)
	if err == nil && didResolve {
		t.root = newroot
	}
	return value, err
}

func (t *Trie) tryGet(origNode node, key []byte, pos int) (value []byte, newnode node, didResolve bool, err error) {
	switch n := (origNode).(type) {
	case nil:
		return nil, nil, false, nil
	case valueNode:
		return n, n, false, nil
	case *shortNode:
		if len(key)-pos < len(n.Key) || !bytes.Equal(n.Key, key[pos:pos+len(n.Key)]) {
			// key not found in trie
			return nil, n, false, nil
		}
		value, newnode, didResolve, err = t.tryGet(n.Val, key, pos+len(n.Key))
		if err == nil && didResolve {
			n = n.copy()
			n.Val = newnode
			n.flags.gen = t.cachegen
		}
		return value, n, didResolve, err
	case *fullNode:
		value, newnode, didResolve, err = t.tryGet(n.Children[key[pos]], key, pos+1)
		if err == nil && didResolve {
			n = n.copy()
			n.flags.gen = t.cachegen
			n.Children[key[pos]] = newnode
		}
		return value, n, didResolve, err
	case hashNode:
		child, err := t.resolveHash(n, key[:pos])
		if err != nil {
			return nil, n, true, err
		}
		value, newnode, _, err := t.tryGet(child, key, pos)
		return value, newnode, true, err
	default:
		panic(fmt.Sprintf("%T: invalid node: %v", origNode, origNode))
	}
}

Trie树的Delete方法,跟插入比较类似。


因为字数有限制,本文的下半部分在这里:

Ethereum以太坊源码分析(三)Trie树源码分析(下)zhuanlan.zhihu.com图标

希望能和对区块链感兴趣的朋友多多交流,我的blog地址:

DinghaoLIdinghaoli.github.io图标

编辑于 2018-11-23

文章被以下专栏收录