MPT 默克前缀树原理

MPT是以太坊中一种使用很广泛的数据结构,用来管理以太坊账户的状态、状态的变更、交易和收据等。

在以太坊中MPT有以下几个应用的场景:1、交易树:记录交易的状态和变化。缓存到MTP2、收据树(交易收据):交易收据的存储3、状态树(账户信息):帐户中各种状态的保存。如余额等。4、账户中的Storage树。

这是数据都持有一个接口,即trie  Trie:

type Trie struct {
	db   *Database
	root node
	cachegen, cachelimit uint16
}

Trie结构相当于是一个缓存,最根本的还是以key-Value的形式储存在数据库中,只是在我们运行的时候会从数据库中调用数据重新组装成这个结构,也就是说Trie是当程序运行起来储存在内存中的,而不是在硬盘里的。上面几种树,本质上是一样的,只是value不同而已。交易树的value是交易,收据树的value是receipts,状态树的value是accout{}。

上图是官方对MPT的阐释图,什么意思?这里面有三种节点,扩展节点、分支节点和叶子节点。其中我们最终要存的是【a711355:45.0ETH】、【a77d337:1.00wei】、【a7f9365:1.1ETH】、【a77d397:0.12ETH】这4个键值对:

【叶子节点(leaf)】表示为 [ key,value ] 的一个键值对。key是16进制编码出来的字符串,value是RLP编码的数据;【扩展节点(extension)】也是 [ key,value ] 的一个键值对,但是这里的value是其他节点的hash值,通过hash链接到其他节点;【分支节点(branch)】因为MPT树中的key被编码成一种特殊的16进制的表示,再加上最后的value,所以分支节点是一个长度为17的数组。前16个元素对应着key中的16个可能的十六进制字符,如果某个key在这个节点的共享前缀刚好结束,那么这个键值对的value就存在数组的最后一位。

【prefix】prefix是一个4bit的数值,用来区分扩展节点和叶子节点。因为扩展节点和叶子节点的数据结构类似,所以需要进行区分。其中第三位为节点标记,如果是0则表示Extension Node,如果是1则表示leaf Node。最后一位为奇偶性,如果key-end为奇数,则为1,ken-end为偶数则为0。

所以对照上面的图,奇数extension Node的prefix为00010000,偶数extension Node为00000000,key-end为1355的leaf Node的prefix为00100000,key-end为7的leaf Node的prefix为00110000。

MPT节点种类即储存

那么,上面的结构在数据库中是怎么储存的呢?我们先看下在以太坊中这些节点的结构:

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

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

type nodeFlag struct {
	hash  hashNode // cached hash of the node (may be nil)
	gen   uint16   // cache generation counter
	dirty bool     // whether the node has changes that must be written to the database
}

我们看到这里的fullNode就是图中的分支节点,而shortNode就代表只有一个子节点的节点,也就是叶子节点或扩展节点。叶子节点的子节点是valueNode;而扩展节点的子节点则为一个分支节点,即fullNode。

hashNode与valueNode都是[]byte的别名,长度都是32字节,但储存的内容有很大不同:1、valueNode储存的是真正value值的hash值,hashNode储存的则是fullNode和shortNode的hash值;2、valueNode由叶子节点持有,也可以由fullNode的节点持有,储存在children数组的最后一个位置;3、hashNode由每一个fullNode和shortNode所持有。

最终,value的RLP编码值就以valueNode的[]byte为key储存在数据库中,fullNode和shortNode的RLP编码值也以hashNode为key储存在数据库里。所以所有的节点最终都以[hash,RLPdata]的形式储存在数据库中。

最终的MPT树的根hash以一种类似与merkle树的方式形成,即子节点的hash值组合起来进行hash运算,得到的hash值再与其他同级的hash组合起来再进行hash,以此类推直到最后只有一个hash,即为根hash。通过root hash从数据库中找到root节点,再从root节点恢复出其子节点的hash,再从数据库中取出子节点,以此类推,最终可以恢复整棵树。

不同的是merkle是二叉树,而以太坊中不是二叉树,比如fullNode有可能存在16个子节点,但是处理原理都是讲这些子节点的hash值组合然后进行hash。

MPT的序列化和反序列化

先从Trie树的初始化开始,New()函数接受一个根hash值和一个Database参数,初始化一个trie。函数调用了resolveHash()方法,从数据库中取回了rootnode。

func New(root common.Hash, db *Database) (*Trie, error) {
    if db == nil {
        panic("trie.New called without a database")
    }
    trie := &Trie{
        db:           db,
        originalRoot: root,
    }
    if (root != common.Hash{}) && root != emptyRoot {
        rootnode, err := trie.resolveHash(root[:], nil)
        if err != nil {
            return nil, err
        }
        trie.root = rootnode
    }
    return trie, nil
}

resolveHash()方法旨在通过hash解析出node的RLP值,它在Trie的序列化和反序列化过程中多次使用:1、New():新建一个树时用于获取根节点;2、tryGet():通过给定的key去找对应的value,从根节点递归想想查询,当查到了hashNode的时候将该hashNode对应的Node找出来,基于该Node进一步去查询,直到找到valueNode;3、insert():插入节点遍历到hashNode时,说明该节点还没有被载入内存,此时调用resolveHash取出该Node,进一步执行insert();4、delete():删除节点遍历到hashNode时,用途与insert()类似。

func (t *Trie) resolveHash(n hashNode, prefix []byte) (node, error) {
    cacheMissCounter.Inc(1)

    hash := common.BytesToHash(n)
    //通过hash解析出node的RLP值
    enc, err := t.db.Node(hash)
    if err != nil || enc == nil {
        return nil, &MissingNodeError{NodeHash: hash, Path: prefix}
    }
    return mustDecodeNode(n, enc, t.cachegen), nil
}

下面我们看看MPT如何序列化和反序列化,即如何编码和解码,又或者说如何储存和读取。

trie.Commit()方法进行序列化并得到一个hash值(root),它的主要流程通过hasher.go中的hash()方法来实现。

func (t *Trie) Commit(onleaf LeafCallback) (root common.Hash, err error) {
    if t.db == nil {
        panic("commit called on trie with nil database")
    }
    hash, cached, err := t.hashRoot(t.db, onleaf)
    if err != nil {
        return common.Hash{}, err
    }
    t.root = cached  // 更新了t.root,将节点的hash值放在了root节点的node.flag.hash中
    t.cachegen++
    return common.BytesToHash(hash.(hashNode)), nil
}

// 该函数返回的是根hash和存入了hash值的trie的副本
func (t *Trie) hashRoot(db *Database, onleaf LeafCallback) (node, node, error) {
    if t.root == nil {
        return hashNode(emptyRoot.Bytes()), nil, nil
    }
    h := newHasher(t.cachegen, t.cachelimit, onleaf)
    defer returnHasherToPool(h)
    return h.hash(t.root, db, true)
}

根hash是通过遍历shortNode和fullNode,将其中的子节点rlp编码值转换为hash值,构成了一个全部由hash组成的树;同时将每个节点的hash值存入每个节点的node.flag.hash中,构成了一个包含了hash的副本Trie,并传给cache。hash和cache就是我们所需要的。

这里hash()方法是关键。hash()方法是所谓“折叠”的入口,即将node折叠为hash。它的基本逻辑是:1、从root节点开始,调用hashChildren()将子节点折叠成hashNode,替换到子节点原来所在父节点中的位置,直到所有的子节点都变为hash值;2、上面的过程是一个递归调用,通过hash()调用hashChildren(),hashChildren()判断如果不是valueNode则递归调用hash(),这样使得整个折叠的过程从valueNode开始,最后终结于root。3、调用store()方法对这个全部由子节点hash组成的node进行RLP编码,并进行哈希计算,得到该节点的hashNode;同时以hashNode为key,以本node的RLP编码的值为value存入数据库。

func (h *hasher) hash(n node, db *Database, force bool) (node, node, error) {
    // If we're not storing the node, just hashing, use available cached data
    if hash, dirty := n.cache(); hash != nil {
        if db == nil {
            return hash, n, nil
        }
        if n.canUnload(h.cachegen, h.cachelimit) {
            cacheUnloadCounter.Inc(1)
            return hash, hash, nil
        }
        if !dirty {
            return hash, n, nil
        }
    }
    // 对所有子节点进行处理,将本节点中所有的节点都替换为节点的hash
    collapsed, cached, err := h.hashChildren(n, db)
    if err != nil {
        return hashNode{}, n, err
    }
    // 将hash和rlp编码的节点存入数据库
    hashed, err := h.store(collapsed, db, force)
    if err != nil {
        return hashNode{}, n, err
    }

    cachedHash, _ := hashed.(hashNode)
    switch cn := cached.(type) {
    case *shortNode:
        cn.flags.hash = cachedHash
        if db != nil {
            cn.flags.dirty = false
        }
    case *fullNode:
        cn.flags.hash = cachedHash
        if db != nil {
            cn.flags.dirty = false
        }
    }
    return hashed, cached, nil
}

上面是将Trie存入数据库,下面再看看如何从数据库中取出节点,重构Trie。上面的resolveHash() 方法中通过db.Node(hash)取值指定hash的node的RLP值。

decodeNode方法,这个方法根据rlp的list的长度来判断这个编码到底属于什么节点,如果是2个字段那么就是shortNode节点,如果是17个字段,那么就是fullNode,然后分别调用各自的解析函数。

// decodeNode parses the RLP encoding of a trie node.
func decodeNode(hash, buf []byte, cachegen uint16) (node, error) {
    if len(buf) == 0 {
        return nil, io.ErrUnexpectedEOF
    }
    elems, _, err := rlp.SplitList(buf)
    if err != nil {
        return nil, fmt.Errorf("decode error: %v", err)
    }
    switch c, _ := rlp.CountValues(elems); c {
    case 2:
        n, err := decodeShort(hash, buf, elems, cachegen)
        return n, wrapError(err, "short")
    case 17:
        n, err := decodeFull(hash, buf, elems, cachegen)
        return n, wrapError(err, "full")
    default:
        return nil, fmt.Errorf("invalid number of list elements: %v", c)
    }
}

decodeShort方法,通过key是否有终结符号来判断到底是叶子节点还是扩展节点。如果有终结符那么就是叶子结点,通过SplitString方法解析出来val然后生成一个shortNode。 不过没有终结符,那么说明是扩展节点, 通过decodeRef来解析剩下的节点,然后生成一个shortNode。

func decodeShort(hash, buf, elems []byte, cachegen uint16) (node, error) {
    // kbuf代表compact key
    // rest代表shortnode的value
    kbuf, rest, err := rlp.SplitString(elems)
    if err != nil {
        return nil, err
    }
    flag := nodeFlag{hash: hash, gen: cachegen}
    key := compactToHex(kbuf)
    if hasTerm(key) {
        // value node
        val, _, err := rlp.SplitString(rest)
        if err != nil {
            return nil, fmt.Errorf("invalid value node: %v", err)
        }
        return &shortNode{key, append(valueNode{}, val...), flag}, nil
    }
    r, _, err := decodeRef(rest, cachegen)
    if err != nil {
        return nil, wrapError(err, "val")
    }
    return &shortNode{key, r, flag}, nil
}

decodeRef方法根据数据类型进行解析,如果类型是list,那么有可能是内容<32的值,那么调用decodeNode进行解析。 如果是空节点,那么返回空,如果是hash值,那么构造一个hashNode进行返回,注意的是这里没有继续进行解析,如果需要继续解析这个hashNode,那么需要继续调用resolveHash方法。 到这里decodeShort方法就调用完成了。

func decodeRef(buf []byte, cachegen uint16) (node, []byte, error) {
    kind, val, rest, err := rlp.Split(buf)
    if err != nil {
        return nil, buf, err
    }
    switch {
    case kind == rlp.List:
        // 'embedded' node reference. The encoding must be smaller
        // than a hash in order to be valid.
        // len(buf) - len(rest)得到的结果应该是类型加内容的长度
        if size := len(buf) - len(rest); size > hashLen {
            err := fmt.Errorf("oversized embedded node (size is %d bytes, want size < %d)", size, hashLen)
            return nil, buf, err
        }
        n, err := decodeNode(nil, buf, cachegen)
        return n, rest, err
    case kind == rlp.String && len(val) == 0:
        // empty node
        return nil, rest, nil
    case kind == rlp.String && len(val) == 32:
        return append(hashNode{}, val...), rest, nil
    default:
        return nil, nil, fmt.Errorf("invalid RLP string size %d (want 0 or 32)", len(val))
    }
}

decodeFull方法。根decodeShort方法的流程差不多。

func decodeFull(hash, buf, elems []byte, cachegen uint16) (*fullNode, error) {
    n := &fullNode{flags: nodeFlag{hash: hash, gen: cachegen}}
    for i := 0; i < 16; i++ {
        cld, rest, err := decodeRef(elems, cachegen)
        if err != nil {
            return n, wrapError(err, fmt.Sprintf("[%d]", i))
        }
        n.Children[i], elems = cld, rest
    }
    val, _, err := rlp.SplitString(elems)
    if err != nil {
        return n, err
    }
    if len(val) > 0 {
        n.Children[16] = append(valueNode{}, val...)
    }
    return n, nil
}

MPT的增删查

insert()delete()tryGet()

insert()

Trie树的插入,这是一个递归调用的方法,从根节点开始,一直往下找,直到找到可以插入的点,进行插入操作。

参数:1、node是当前插入的节点;2、prefix是当前已经处理完的部分key3、key是还没有处理玩的部分key, 完整的key = prefix + key;4、value是需要插入的值。

返回值:1、bool是操作是否改变了Trie树(dirty);2、node是插入完成后的子树的根节点;3、error是错误信息;

流程:1、如果节点类型是nil(一颗全新的Trie树的节点就是nil的),这个时候整颗树是空的,直接返回shortNode{key, value, t.newFlag()}, 这个时候整颗树的跟就含有了一个shortNode节点。

2、如果当前的根节点类型是shortNode(也就是叶子节点),    a. 计算公共前缀,如果公共前缀就等于key,那么说明这两个key是一样的,如果value也一样的(dirty == false),那么返回错误;    b. 如果没有错误就更新shortNode的值然后返回。如果公共前缀不完全匹配,那么就需要把公共前缀提取出来形成一个独立的节点(扩展节点),扩展节点后面连接一个branch节点,branch节点后面看情况连接两个short节点。首先构建一个branch节点(branch := &fullNode{flags: t.newFlag()}),然后再branch节点的Children位置调用t.insert插入剩下的两个short节点。

3、如果当前的节点是fullNode(也就是branch节点),那么直接往对应的孩子节点调用insert方法,然后把对应的孩子节点只想新生成的节点。

4、如果当前节点是hashNode, hashNode的意思是当前节点还没有加载到内存里面来,还是存放在数据库里面,那么首先调用 t.resolveHash(n, prefix)来加载到内存,然后对加载出来的节点调用insert方法来进行插入。

func (t *Trie) insert(n node, prefix, key []byte, value node) (bool, node, error) {
	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:
		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
		}
		// Otherwise branch out at the index where they differ.
		branch := &fullNode{flags: t.newFlag()}
		var err error
		_, 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
		}
		// Replace this shortNode with the branch if it occurs at index 0.
		if matchlen == 0 {
			return true, branch, nil
		}
		// Otherwise, replace it with a short node leading up to the branch.
		return true, &shortNode{key[:matchlen], branch, t.newFlag()}, nil

	case *fullNode:
		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

	case nil:
		return true, &shortNode{key, value, t.newFlag()}, nil

	case hashNode:
		// 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的信息。

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))
	}
}

下一章:以太坊源码解读(23)stateDB

前面介绍了以太坊MPT树的结构和方法,而stateDB对象就是对以太坊状态MPT进行管理的对象。其管理功能包括: 1、初始化:New 2、增加:StateDB.createObject 3、删除:Sta ...