以太坊原始碼分析--MPT樹
MPT(Merkle Patricia Tries)是以太坊中儲存區塊資料的核心資料結構,它Merkle Tree和Patricia
Tree融合一個樹形結構,理解MPT結構對之後學習以太坊區塊header以及智慧合約狀態儲存結構的模組原始碼很有幫助。
首先來看下Merkle樹:
它的葉子是資料塊的hash,從圖中可以看出非葉子節點是其子節點串聯字串的hash,底層資料的任何變動都會影響父節點,這棵樹的Merkle Root代表對底層所有資料的“摘要”。
這樣的樹有一個很大的好處,比如我們把交易資訊寫入這樣的樹形結構,當需要證明一個交易是否存在這顆樹中的時候,就不需要重新計算所有交易的hash值。比如證明圖中Hash 1-1,我們可以藉助Hash 1-0重新計算出Hash 1,然後再借助Hash 0重新計算出Top Hash,這樣就可以根據算出來的Top Hash和原來的Top Hash是否一樣,如果一樣的話那麼Hash 1-1就屬於這棵樹。
所以想象一下,我們將這個Top Hash儲存在區塊頭中,那麼有了區塊頭就可以對區塊資訊進行驗證了。同時 Hash 計算的過程可以十分快速,預處理可以在短時間內完成。利用Merkle樹結構能帶來巨大的比較效能提升。
再來看下Patricia樹:
從它的名字壓縮字首樹再結合上圖就可以猜出來Patricia樹的特點了,這種樹形結構比將每一個字元作為一個節點的普通trie樹形結構,它的鍵值可以使用多個字元,降低了樹的高度,也節省了空間,再看個例子:
圖中可以很容易看出數中所儲存的鍵值對:
6c0a5c71ec20bq3w => 5
6c0a5c71ec20CX7j => 27
6c0a5c71781a1FXq => 18
6c0a5c71781a9Dog => 64
6c0a8f743b95zUfe => 30
6c0a8f743b95jx5R => 2
6c0a8f740d16y03G => 43
6c0a8f740d16vcc1 => 48
以太坊中的MPT:
在以太坊中MPT的節點的規格主要有一下幾個:
NULL 空節點,簡單的表示空,在程式碼中是一個空串
Nibble 它是key的基本單元,是一個四元組(四個bit位的組合例如二進位制表達的0010就是一個四元組)
Extension 擴充套件節點有兩個元素,一個是key值,還有一個是hash值,這個hash值指向下一個節點
Branch 分支節點有17個元素,回到Nibble,四元組是key的基本單元,四元組最多有16個值。所以前16個必將落入到在其遍歷中的鍵的十六個可能的半位元組值中的每一個。第17個是儲存那些在當前結點結束了的節點(例如, 有三個key,分別是 (abc ,abd, ab) 第17個欄位儲存了ab節點的值)
Leaf 葉子節點只有兩個元素,分別為key和value
這裡還有一些知識點需要了解的,為了將MPT樹儲存到資料庫中,同時還可以把MPT樹從資料庫中恢復出來,對於Extension和Leaf的節點型別做了特殊的定義:如果是一個擴充套件節點,那麼字首為0,這個0加在key前面。如果是一個葉子節點,那麼字首就是1。同時對key的長度就奇偶型別也做了設定,如果是奇數長度則標示1,如果是偶數長度則標示0。
以太坊中主要有一下幾個地方用了MPT樹形結構:
State Trie 區塊頭中的狀態樹
key => sha3(以太坊賬戶地址address)
value => rlp(賬號內容資訊account)
Transactions Trie 區塊頭中的交易樹
key => rlp(交易的偏移量 transaction index)
每個塊都有各自的交易樹,且不可更改
Receipts Trie 區塊頭中的收據樹
key = rlp(交易的偏移量 transaction index)
每個塊都有各自的交易樹,且不可更改
Storage Trie 儲存樹
儲存只能合約狀態
每個賬號有自己的Storage Trie
這兩個區塊頭中,state root,tx root receipt root分別儲存了這三棵樹的樹根,第二個區塊顯示了當賬號175的資料變更(27 -> 45)的時候,只需要儲存跟這個賬號相關的部分資料,而且老的區塊中的資料還是可以正常訪問。
MPT樹種還有一個重要的概念一個特殊的十六進位制字首(hex-prefix, HP)編碼來對key編碼,我們先來了解一下編碼定義規則,原始碼實現後面再分析:
RAW 原始編碼,對輸入不做任何變更
HEX 十六進位制編碼
RAW編碼輸入的每個字元分解為高4位和低4位
如果是葉子節點,則在最後加上Hex值0x10表示結束
如果是分支節點不附加任何Hex值
比如key=>”bob”,b的ASCII十六進位制編碼為0x62,o的ASCII十六進位制編碼為0x6f,分解成高四位和第四位,16表示終結
0x10,最終編碼結果為[6 2 6 15 6 2 16],
HEX-Prefix 十六進位制字首編碼
輸入key結尾為0x10,則去掉這個終止符
key之前補一個四元組這個Byte第0位區分奇偶資訊,第1位區分節點型別
如果輸入key的長度是偶數,則再新增一個四元組0x0在flag四元組後
將原來的key內容壓縮,將分離的兩個byte以高四位低四位進行合併
十六進位制字首編碼相當於一個逆向的過程,比如輸入的是[6 2 6 15 6 2
16],根據第一個規則去掉終止符16。根據第二個規則key前補一個四元組,從右往左第一位為1表示葉子節點,從右往左第0位如果後面key的長度為偶數設定為0,奇數長度設定為1,那麼四元組0010就是2。根據第三個規則,新增一個全0的補在後面,那麼就是20.根據第三個規則內容壓縮合並,那麼結果就是[0x20
0x62 0x6f 0x62]
官方有一個詳細的結構的示例:
下面再用一個影象化的示例來加深一下對上面的MPT規則的理解
key的16進位制 | key | value |
---|---|---|
<64 6f> | do | verb |
<64 6f 67> | dog | puppy |
<64 6f 67 65> | doge | coin |
<68 6f 72 73 65> | horse | stallion |
三種編碼格式互相轉換的程式碼實現
Compact就是上面說的HEX-Prefix,keybytes為按完整位元組(8bit)儲存的正常資訊,hex為按照半位元組nibble(4bit)儲存資訊的格式。
go-ethereum/trie/encoding:
func hexToCompact(hex []byte) []byte {
terminator := byte(0)
if hasTerm(hex) { //檢查是否有結尾為0x10 => 16
terminator = 1 //有結束標記16說明是葉子節點
hex = hex[:len(hex)-1] //去除尾部標記
}
buf := make([]byte, len(hex)/2+1) // 位元組陣列
buf[0] = terminator << 5 // 標誌byte為00000000或者00100000
//如果長度為奇數,新增奇數位標誌1,並把第一個nibble位元組放入buf[0]的低四位
if len(hex)&1 == 1 {
buf[0] |= 1 << 4 // 奇數標誌 00110000
buf[0] |= hex[0] // 第一個nibble包含在第一個位元組中 0011xxxx
hex = hex[1:]
}
//將兩個nibble位元組合併成一個位元組
decodeNibbles(hex, buf[1:])
return buf
}
//compact編碼轉化為Hex編碼
func compactToHex(compact []byte) []byte {
base := keybytesToHex(compact)
base = base[:len(base)-1]
// apply terminator flag
// base[0]包括四種情況
// 00000000 擴充套件節點偶數位
// 00000001 擴充套件節點奇數位
// 00000010 葉子節點偶數位
// 00000011 葉子節點奇數位
// apply terminator flag
if base[0] >= 2 {
//如果是葉子節點,末尾新增Hex標誌位16
base = append(base, 16)
}
// apply odd flag
//如果是偶數位,chop等於2,否則等於1
chop := 2 - base[0]&1
return base[chop:]
}
// 將keybytes 轉成十六進位制
func keybytesToHex(str []byte) []byte {
l := len(str)*2 + 1
//將一個keybyte轉化成兩個位元組
var nibbles = make([]byte, l)
for i, b := range str {
nibbles[i*2] = b / 16
nibbles[i*2+1] = b % 16
}
//末尾加入Hex標誌位16
nibbles[l-1] = 16
return nibbles
}
// 將十六進位制的bibbles轉成key bytes,這隻能用於偶數長度的key
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)
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]
}
}
// 返回a和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
}
// 十六進位制key是否有結束標誌符
func hasTerm(s []byte) bool {
return len(s) > 0 && s[len(s)-1] == 16
}
以太坊中MTP資料結構
上面已經分析了以太坊的key的編碼方式,接下來我們來看以太坊中MPT樹的資料結構,在分析trie的資料結構前,我們先來了解一下node的定義:
trie/node.go
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
)
上面程式碼中定義了四個struct,就是node的四種類型:
fullNode -> 分支節點,它有一個容量為17的node陣列成員變數Children,陣列中前16個空位分別對應16進位制(hex)下的0-9a-f,這樣對於每個子節點,根據其key值16進位制形式下的第一位的值,就可掛載到Children陣列的某個位置,fullNode本身不再需要額外key變數;Children陣列的第17位,留給該fullNode的資料部分。這和我們上面說的Branch 分支節點的規格一致的。
shortNode,key是一個任意長度的字串(位元組陣列[]byte),體現了PatriciaTrie的特點,通過合併只有一個子節點的父節點和其子節點來縮短trie的深度,結果就是有些節點會有長度更長的key。
-> 擴充套件節點,Val
指向分支節點或者葉子節點
-> 葉子節點,Val
為rlp編碼資料,key為該資料的hash
valueNode -> MPT的葉子節點。位元組陣列[]byte的一個別名,不帶子節點。使用中valueNode就是所攜帶資料部分的RLP雜湊值,長度32byte,資料的RLP編碼值作為valueNode的匹配項儲存在資料庫裡。
hashNode -> 字元陣列[]byte的一個別名,存放32byte的雜湊值,他是fullNode或者shortNode物件的RLP雜湊值
來看下trie的結構以及對trie的操作可能對上面各個node的型別使用可能會更清晰一點,我們來接著看下trie的結構定義
trie/trie.go:
db *Database // 用levelDB做KV儲存
root node //當前根節點
originalRoot common.Hash //啟動載入時候的hash,可以從db中恢復出整個trie
cachegen, cachelimit uint16 // cachegen 快取生成值,每次Commit會+1
}
這裡的cachegen快取生成值會被附加在node節點上面,如果當前的cachegen-cachelimit引數大於node的快取生成,那麼node會從cache裡面解除安裝,以便節約記憶體。一個快取多久沒被時候用就會被從快取中移除,看起來和redis等一些LRU演算法的cache db很像。
Trie的初始化:
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 {
// 如果hash不是空值,從資料庫中載入一個已經存在的樹
rootnode, err := trie.resolveHash(root[:], nil)
if err != nil {
return nil, err
}
trie.root = rootnode //根節點為找到的trie
}
//否則返回新建一個樹
return trie, nil
}
這裡的trie.resolveHash
就是載入整課樹的方法,還有傳入的root common.Hash hash
是一個將hex編碼轉為原始hash的32位byte[] (common.HexToHash()),來看下如何通過這個hash來找到整個trie的:
func (t *Trie) resolveHash(n hashNode, prefix []byte) (node, error) {
cacheMissCounter.Inc(1) //沒執行一次計數器+1
//上面說過了,n是一個32位byte[]
hash := common.BytesToHash(n)
//通過hash從db中取出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
}
這裡的trie.resolveHash就是載入整課樹的方法,還有傳入的root common.Hash hash是一個將hex編碼轉為原始hash的32位byte[] (common.HexToHash()),來看下如何通過這個hash來找到整個trie的:
func (t *Trie) resolveHash(n hashNode, prefix []byte) (node, error) {
cacheMissCounter.Inc(1) //沒執行一次計數器+1
//上面說過了,n是一個32位byte[]
hash := common.BytesToHash(n)
//通過hash從db中取出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
}
mustDecodeNode中呼叫了decodeNode,這個方法通過RLP的list長度來判斷該編碼內容屬於上面節點,如果是兩個欄位則為shortNode,如果是17個欄位則為fullNode,然後再呼叫各自的decode解析函式
func decodeNode(hash, buf []byte, cachegen uint16) (node, error) {
if len(buf) == 0 {
return nil, io.ErrUnexpectedEOF
}
elems, _, err := rlp.SplitList(buf) //將buf拆分為列表的內容以及列表後的任何剩餘位元組。
if err != nil {
return nil, fmt.Errorf("decode error: %v", err)
}
switch c, _ := rlp.CountValues(elems); c {
case 2:
n, err := decodeShort(hash, elems, cachegen) //decode shortNode
return n, wrapError(err, "short")
case 17:
n, err := decodeFull(hash, elems, cachegen) //decode fullNode
return n, wrapError(err, "full")
default:
return nil, fmt.Errorf("invalid number of list elements: %v", c)
}
}
decodeShort函式中通過key是否含有結束識別符號來判斷是葉子節點還是擴充套件節點,這個我們在上面的編碼部分已經講過,有結束標示符則是葉子節點,再通過rlp.SplitString解析出val生成一個葉子節點shortNode返回。沒有結束標誌符則為擴充套件節點,通過decodeRef解析並生成一個shortNode返回。
func decodeShort(hash, elems []byte, cachegen uint16) (node, error) {
kbuf, rest, err := rlp.SplitString(elems) //將elems填入RLP字串的內容以及字串後的任何剩餘位元組。
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主要做了啥操作:
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.
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))
}
}
這段程式碼比較清晰,通過rlp.Split後返回的型別做不同的處理,如果是list,呼叫decodeNode解析,如果是空節點返回空,如果是一個32位hash值返回hashNode,decodeFull:
func decodeFull(hash, 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
}
再回到Trie結構體中的cachegen, cachelimit,Trie樹每次Commit時cachegen都會+1,這兩個引數是cache的控制引數,為了弄清楚Trie的快取機制,我們來看下Commit具體是幹嘛的:
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.cachegen++
return common.BytesToHash(hash.(hashNode)), nil //返回所指向的node的未編碼的hash
}
//返回trie.root所指向的node的hash以及每個節點都帶有各自hash的trie樹的root。
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
}
Commit目的,是將trie樹中的key轉為Compact編碼,為每個節點生成一個hash,它就是為了確保後續能正常將變動的資料提交到db.
那麼這個cachegen是怎麼放到該節點中的,當trie樹在節點插入的時候,會把當前trie的cachegen放入到該節點中,看下trie的insert方法:
//n -> trie當前插入節點
//prefix -> 當前匹配到的key的公共字首
//key -> 待插入資料當前key中剩餘未匹配的部分,完整的key=prefix+key
//value -> 待插入資料本身
//返回 -> 是否改變樹,插入完成後子樹根節點,error
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
}
//如果key長度為0,那麼說明當前節點中新增加的節點和當前節點資料一樣,認為已經新增過了就直接返回
return true, value, nil
}
switch n := n.(type) {
case *shortNode:
matchlen := prefixLen(key, n.Key) // 返回公共字首長度
if matchlen == len(n.Key) {
//如果整個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 := &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
}
//如果它在索引0處出現則用該branch替換shortNode
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:
//在空trie中新增一個節點,就是葉子節點,返回shortNode。
return true, &shortNode{key, value, t.newFlag()}, nil
case hashNode:
rn, err := t.resolveHash(n, prefix)//恢復一個儲存在db中的node
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樹的插入,這是一個遞迴呼叫的方法,從根節點開始,一直往下找,直到找到可以插入的點,進行插入操作。
如果當前的根節點葉子節點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節點作為字首節點。
如果節點型別是nil(一顆全新的Trie樹的節點就是nil的),這個時候整顆樹是空的,直接返回shortNode{key, value, t.newFlag()}, 這個時候整顆樹的跟就含有了一個shortNode節點。
如果當前的節點是fullNode(也就是branch節點),那麼直接往對應的孩子節點呼叫insert方法,然後把對應的孩子節點指向新生成的節點。
如果當前節點是hashNode, hashNode的意思是當前節點還沒有載入到記憶體裡面來,還是存放在資料庫裡面,那麼首先呼叫 t.resolveHash(n, prefix)來載入到記憶體,然後對加載出來的節點呼叫insert方法來進行插入。
接下來看如何遍歷Trie樹從Trie中獲取資料,根據key獲取的value過程:
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在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:
// hashNodes時候需要去db中獲取
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))
}
}
tryGet(origNode node, key []byte, pos int)方法提供三個引數,起始的node,hash key,還有當前hash匹配的位置,didResolve用來判斷trie樹是否發生變化,根據hashNode去db中獲取該node值,獲取到後,需要更新現有的trie,didResolve就會發生變化。
關於Trie的Update和Delete就不分析了,在trie包中還有其他的功能,我們來大略看下主要是幹嘛的不做詳細解讀了:
databases.go trie資料結構和磁碟資料庫之間的一個寫入層,方便trie中節點的插入刪除操作
iterator.go 遍歷Trie的鍵值迭代器
proof.go Trie樹的默克爾證明,Prove方法獲取指定Key的proof證明, proof證明是從根節點到葉子節點的所有節點的hash值列表。 VerifyProof方法,接受一個roothash值和proof證明和key來驗證key是否存在。
security_trie.go 加密了的trie實現
感謝作者Ryan是菜鳥的分享:https://www.yuanxuxu.com