forked from TrueCloudLab/neoneo-go
mpt: implement MPT trie
MPT is a trie with a branching factor = 16, i.e. it consists of sequences in 16-element alphabet. Signed-off-by: Evgenii Stratonikov <evgeniy@nspcc.ru>
This commit is contained in:
parent
82692d8d21
commit
dc6741bce7
11 changed files with 1240 additions and 0 deletions
78
pkg/core/mpt/branch.go
Normal file
78
pkg/core/mpt/branch.go
Normal file
|
@ -0,0 +1,78 @@
|
|||
package mpt
|
||||
|
||||
import (
|
||||
"github.com/nspcc-dev/neo-go/pkg/crypto/hash"
|
||||
"github.com/nspcc-dev/neo-go/pkg/io"
|
||||
"github.com/nspcc-dev/neo-go/pkg/util"
|
||||
)
|
||||
|
||||
const (
|
||||
// childrenCount represents a number of children of a branch node.
|
||||
childrenCount = 17
|
||||
// lastChild is the index of the last child.
|
||||
lastChild = childrenCount - 1
|
||||
)
|
||||
|
||||
// BranchNode represents MPT's branch node.
|
||||
type BranchNode struct {
|
||||
hash util.Uint256
|
||||
valid bool
|
||||
|
||||
Children [childrenCount]Node
|
||||
}
|
||||
|
||||
var _ Node = (*BranchNode)(nil)
|
||||
|
||||
// NewBranchNode returns new branch node.
|
||||
func NewBranchNode() *BranchNode {
|
||||
b := new(BranchNode)
|
||||
for i := 0; i < childrenCount; i++ {
|
||||
b.Children[i] = new(HashNode)
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
// Type implements Node interface.
|
||||
func (b *BranchNode) Type() NodeType { return BranchT }
|
||||
|
||||
// Hash implements Node interface.
|
||||
func (b *BranchNode) Hash() util.Uint256 {
|
||||
if !b.valid {
|
||||
b.hash = hash.DoubleSha256(toBytes(b))
|
||||
b.valid = true
|
||||
}
|
||||
return b.hash
|
||||
}
|
||||
|
||||
// invalidateHash invalidates node hash.
|
||||
func (b *BranchNode) invalidateHash() {
|
||||
b.valid = false
|
||||
}
|
||||
|
||||
// EncodeBinary implements io.Serializable.
|
||||
func (b *BranchNode) EncodeBinary(w *io.BinWriter) {
|
||||
for i := 0; i < childrenCount; i++ {
|
||||
if hn, ok := b.Children[i].(*HashNode); ok {
|
||||
hn.EncodeBinary(w)
|
||||
continue
|
||||
}
|
||||
n := NewHashNode(b.Children[i].Hash())
|
||||
n.EncodeBinary(w)
|
||||
}
|
||||
}
|
||||
|
||||
// DecodeBinary implements io.Serializable.
|
||||
func (b *BranchNode) DecodeBinary(r *io.BinReader) {
|
||||
for i := 0; i < childrenCount; i++ {
|
||||
b.Children[i] = new(HashNode)
|
||||
b.Children[i].DecodeBinary(r)
|
||||
}
|
||||
}
|
||||
|
||||
// splitPath splits path for a branch node.
|
||||
func splitPath(path []byte) (byte, []byte) {
|
||||
if len(path) != 0 {
|
||||
return path[0], path[1:]
|
||||
}
|
||||
return lastChild, path
|
||||
}
|
45
pkg/core/mpt/doc.go
Normal file
45
pkg/core/mpt/doc.go
Normal file
|
@ -0,0 +1,45 @@
|
|||
/*
|
||||
Package mpt implements MPT (Merkle-Patricia Tree).
|
||||
|
||||
MPT stores key-value pairs and is a trie over 16-symbol alphabet. https://en.wikipedia.org/wiki/Trie
|
||||
Trie is a tree where values are stored in leafs and keys are paths from root to the leaf node.
|
||||
MPT consists of 4 type of nodes:
|
||||
- Leaf node contains only value.
|
||||
- Extension node contains both key and value.
|
||||
- Branch node contains 2 or more children.
|
||||
- Hash node is a compressed node and contains only actual node's hash.
|
||||
The actual node must be retrieved from storage or over the network.
|
||||
|
||||
As an example here is a trie containing 3 pairs:
|
||||
- 0x1201 -> val1
|
||||
- 0x1203 -> val2
|
||||
- 0x1224 -> val3
|
||||
- 0x12 -> val4
|
||||
|
||||
ExtensionNode(0x0102), Next
|
||||
_______________________|
|
||||
|
|
||||
BranchNode [0, 1, 2, ...], Last -> Leaf(val4)
|
||||
| |
|
||||
| ExtensionNode [0x04], Next -> Leaf(val3)
|
||||
|
|
||||
BranchNode [0, 1, 2, 3, ...], Last -> HashNode(nil)
|
||||
| |
|
||||
| Leaf(val2)
|
||||
|
|
||||
Leaf(val1)
|
||||
|
||||
There are 3 invariants that this implementation has:
|
||||
- Branch node cannot have <= 1 children
|
||||
- Extension node cannot have zero-length key
|
||||
- Extension node cannot have another Extension node in it's next field
|
||||
|
||||
Thank to these restrictions, there is a single root hash for every set of key-value pairs
|
||||
irregardless of the order they were added/removed with.
|
||||
The actual trie structure can vary because of node -> HashNode compressing.
|
||||
|
||||
There is also one optimization which cost us almost nothing in terms of complexity but is very beneficial:
|
||||
When we perform get/put/delete on a speficic path, every Hash node which was retreived from storage is
|
||||
replaced by its uncompressed form, so that subsequent hits of this not don't use storage.
|
||||
*/
|
||||
package mpt
|
70
pkg/core/mpt/extension.go
Normal file
70
pkg/core/mpt/extension.go
Normal file
|
@ -0,0 +1,70 @@
|
|||
package mpt
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/nspcc-dev/neo-go/pkg/crypto/hash"
|
||||
"github.com/nspcc-dev/neo-go/pkg/io"
|
||||
"github.com/nspcc-dev/neo-go/pkg/util"
|
||||
)
|
||||
|
||||
// MaxKeyLength is the max length of the extension node key.
|
||||
const MaxKeyLength = 1125
|
||||
|
||||
// ExtensionNode represents MPT's extension node.
|
||||
type ExtensionNode struct {
|
||||
hash util.Uint256
|
||||
valid bool
|
||||
|
||||
key []byte
|
||||
next Node
|
||||
}
|
||||
|
||||
var _ Node = (*ExtensionNode)(nil)
|
||||
|
||||
// NewExtensionNode returns hash node with the specified key and next node.
|
||||
// Note: because it is a part of Trie, key must be mangled, i.e. must contain only bytes with high half = 0.
|
||||
func NewExtensionNode(key []byte, next Node) *ExtensionNode {
|
||||
return &ExtensionNode{
|
||||
key: key,
|
||||
next: next,
|
||||
}
|
||||
}
|
||||
|
||||
// Type implements Node interface.
|
||||
func (e ExtensionNode) Type() NodeType { return ExtensionT }
|
||||
|
||||
// Hash implements Node interface.
|
||||
func (e *ExtensionNode) Hash() util.Uint256 {
|
||||
if !e.valid {
|
||||
e.hash = hash.DoubleSha256(toBytes(e))
|
||||
e.valid = true
|
||||
}
|
||||
return e.hash
|
||||
}
|
||||
|
||||
// invalidateHash invalidates node hash.
|
||||
func (e *ExtensionNode) invalidateHash() {
|
||||
e.valid = false
|
||||
}
|
||||
|
||||
// DecodeBinary implements io.Serializable.
|
||||
func (e *ExtensionNode) DecodeBinary(r *io.BinReader) {
|
||||
sz := r.ReadVarUint()
|
||||
if sz > MaxKeyLength {
|
||||
r.Err = fmt.Errorf("extension node key is too big: %d", sz)
|
||||
return
|
||||
}
|
||||
e.valid = false
|
||||
e.key = make([]byte, sz)
|
||||
r.ReadBytes(e.key)
|
||||
e.next = new(HashNode)
|
||||
e.next.DecodeBinary(r)
|
||||
}
|
||||
|
||||
// EncodeBinary implements io.Serializable.
|
||||
func (e ExtensionNode) EncodeBinary(w *io.BinWriter) {
|
||||
w.WriteVarBytes(e.key)
|
||||
n := NewHashNode(e.next.Hash())
|
||||
n.EncodeBinary(w)
|
||||
}
|
61
pkg/core/mpt/hash.go
Normal file
61
pkg/core/mpt/hash.go
Normal file
|
@ -0,0 +1,61 @@
|
|||
package mpt
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/nspcc-dev/neo-go/pkg/io"
|
||||
"github.com/nspcc-dev/neo-go/pkg/util"
|
||||
)
|
||||
|
||||
// HashNode represents MPT's hash node.
|
||||
type HashNode struct {
|
||||
hash util.Uint256
|
||||
valid bool
|
||||
}
|
||||
|
||||
var _ Node = (*HashNode)(nil)
|
||||
|
||||
// NewHashNode returns hash node with the specified hash.
|
||||
func NewHashNode(h util.Uint256) *HashNode {
|
||||
return &HashNode{
|
||||
hash: h,
|
||||
valid: true,
|
||||
}
|
||||
}
|
||||
|
||||
// Type implements Node interface.
|
||||
func (h *HashNode) Type() NodeType { return HashT }
|
||||
|
||||
// Hash implements Node interface.
|
||||
func (h *HashNode) Hash() util.Uint256 {
|
||||
if !h.valid {
|
||||
panic("can't get hash of an empty HashNode")
|
||||
}
|
||||
return h.hash
|
||||
}
|
||||
|
||||
// IsEmpty returns true iff h is an empty node i.e. contains no hash.
|
||||
func (h *HashNode) IsEmpty() bool { return !h.valid }
|
||||
|
||||
// DecodeBinary implements io.Serializable.
|
||||
func (h *HashNode) DecodeBinary(r *io.BinReader) {
|
||||
sz := r.ReadVarUint()
|
||||
switch sz {
|
||||
case 0:
|
||||
h.valid = false
|
||||
case util.Uint256Size:
|
||||
h.valid = true
|
||||
r.ReadBytes(h.hash[:])
|
||||
default:
|
||||
r.Err = fmt.Errorf("invalid hash node size: %d", sz)
|
||||
}
|
||||
}
|
||||
|
||||
// EncodeBinary implements io.Serializable.
|
||||
func (h HashNode) EncodeBinary(w *io.BinWriter) {
|
||||
if !h.valid {
|
||||
w.WriteVarUint(0)
|
||||
return
|
||||
}
|
||||
w.WriteVarBytes(h.hash[:])
|
||||
}
|
35
pkg/core/mpt/helpers.go
Normal file
35
pkg/core/mpt/helpers.go
Normal file
|
@ -0,0 +1,35 @@
|
|||
package mpt
|
||||
|
||||
// lcp returns longest common prefix of a and b.
|
||||
// Note: it does no allocations.
|
||||
func lcp(a, b []byte) []byte {
|
||||
if len(a) < len(b) {
|
||||
return lcp(b, a)
|
||||
}
|
||||
|
||||
var i int
|
||||
for i = 0; i < len(b); i++ {
|
||||
if a[i] != b[i] {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return a[:i]
|
||||
}
|
||||
|
||||
// copySlice is a helper for copying slice if needed.
|
||||
func copySlice(a []byte) []byte {
|
||||
b := make([]byte, len(a))
|
||||
copy(b, a)
|
||||
return b
|
||||
}
|
||||
|
||||
// toNibbles mangles path by splitting every byte into 2 containing low- and high- 4-byte part.
|
||||
func toNibbles(path []byte) []byte {
|
||||
result := make([]byte, len(path)*2)
|
||||
for i := range path {
|
||||
result[i*2] = path[i] >> 4
|
||||
result[i*2+1] = path[i] & 0x0F
|
||||
}
|
||||
return result
|
||||
}
|
56
pkg/core/mpt/leaf.go
Normal file
56
pkg/core/mpt/leaf.go
Normal file
|
@ -0,0 +1,56 @@
|
|||
package mpt
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/nspcc-dev/neo-go/pkg/crypto/hash"
|
||||
"github.com/nspcc-dev/neo-go/pkg/io"
|
||||
"github.com/nspcc-dev/neo-go/pkg/util"
|
||||
)
|
||||
|
||||
// MaxValueLength is a max length of a leaf node value.
|
||||
const MaxValueLength = 1024 * 1024
|
||||
|
||||
// LeafNode represents MPT's leaf node.
|
||||
type LeafNode struct {
|
||||
hash util.Uint256
|
||||
valid bool
|
||||
|
||||
value []byte
|
||||
}
|
||||
|
||||
var _ Node = (*LeafNode)(nil)
|
||||
|
||||
// NewLeafNode returns hash node with the specified value.
|
||||
func NewLeafNode(value []byte) *LeafNode {
|
||||
return &LeafNode{value: value}
|
||||
}
|
||||
|
||||
// Type implements Node interface.
|
||||
func (n LeafNode) Type() NodeType { return LeafT }
|
||||
|
||||
// Hash implements Node interface.
|
||||
func (n *LeafNode) Hash() util.Uint256 {
|
||||
if !n.valid {
|
||||
n.hash = hash.DoubleSha256(toBytes(n))
|
||||
n.valid = true
|
||||
}
|
||||
return n.hash
|
||||
}
|
||||
|
||||
// DecodeBinary implements io.Serializable.
|
||||
func (n *LeafNode) DecodeBinary(r *io.BinReader) {
|
||||
sz := r.ReadVarUint()
|
||||
if sz > MaxValueLength {
|
||||
r.Err = fmt.Errorf("leaf node value is too big: %d", sz)
|
||||
return
|
||||
}
|
||||
n.valid = false
|
||||
n.value = make([]byte, sz)
|
||||
r.ReadBytes(n.value)
|
||||
}
|
||||
|
||||
// EncodeBinary implements io.Serializable.
|
||||
func (n LeafNode) EncodeBinary(w *io.BinWriter) {
|
||||
w.WriteVarBytes(n.value)
|
||||
}
|
70
pkg/core/mpt/node.go
Normal file
70
pkg/core/mpt/node.go
Normal file
|
@ -0,0 +1,70 @@
|
|||
package mpt
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/nspcc-dev/neo-go/pkg/io"
|
||||
"github.com/nspcc-dev/neo-go/pkg/util"
|
||||
)
|
||||
|
||||
// NodeType represents node type..
|
||||
type NodeType byte
|
||||
|
||||
// Node types definitions.
|
||||
const (
|
||||
BranchT NodeType = 0x00
|
||||
ExtensionT NodeType = 0x01
|
||||
HashT NodeType = 0x02
|
||||
LeafT NodeType = 0x03
|
||||
)
|
||||
|
||||
// NodeObject represents Node together with it's type.
|
||||
// It is used for serialization/deserialization where type info
|
||||
// is also expected.
|
||||
type NodeObject struct {
|
||||
Node
|
||||
}
|
||||
|
||||
// Node represents common interface of all MPT nodes.
|
||||
type Node interface {
|
||||
io.Serializable
|
||||
Hash() util.Uint256
|
||||
Type() NodeType
|
||||
}
|
||||
|
||||
// EncodeBinary implements io.Serializable.
|
||||
func (n NodeObject) EncodeBinary(w *io.BinWriter) {
|
||||
encodeNodeWithType(n.Node, w)
|
||||
}
|
||||
|
||||
// DecodeBinary implements io.Serializable.
|
||||
func (n *NodeObject) DecodeBinary(r *io.BinReader) {
|
||||
typ := NodeType(r.ReadB())
|
||||
switch typ {
|
||||
case BranchT:
|
||||
n.Node = new(BranchNode)
|
||||
case ExtensionT:
|
||||
n.Node = new(ExtensionNode)
|
||||
case HashT:
|
||||
n.Node = new(HashNode)
|
||||
case LeafT:
|
||||
n.Node = new(LeafNode)
|
||||
default:
|
||||
r.Err = fmt.Errorf("invalid node type: %x", typ)
|
||||
return
|
||||
}
|
||||
n.Node.DecodeBinary(r)
|
||||
}
|
||||
|
||||
// encodeNodeWithType encodes node together with it's type.
|
||||
func encodeNodeWithType(n Node, w *io.BinWriter) {
|
||||
w.WriteB(byte(n.Type()))
|
||||
n.EncodeBinary(w)
|
||||
}
|
||||
|
||||
// toBytes is a helper for serializing node.
|
||||
func toBytes(n Node) []byte {
|
||||
buf := io.NewBufBinWriter()
|
||||
encodeNodeWithType(n, buf.BinWriter)
|
||||
return buf.Bytes()
|
||||
}
|
94
pkg/core/mpt/node_test.go
Normal file
94
pkg/core/mpt/node_test.go
Normal file
|
@ -0,0 +1,94 @@
|
|||
package mpt
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/nspcc-dev/neo-go/pkg/internal/random"
|
||||
"github.com/nspcc-dev/neo-go/pkg/internal/testserdes"
|
||||
"github.com/nspcc-dev/neo-go/pkg/io"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func getTestFuncEncode(ok bool, expected, actual Node) func(t *testing.T) {
|
||||
return func(t *testing.T) {
|
||||
bs, err := testserdes.EncodeBinary(expected)
|
||||
require.NoError(t, err)
|
||||
err = testserdes.DecodeBinary(bs, actual)
|
||||
if !ok {
|
||||
require.Error(t, err)
|
||||
return
|
||||
}
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, expected.Type(), actual.Type())
|
||||
require.Equal(t, expected.Hash(), actual.Hash())
|
||||
}
|
||||
}
|
||||
|
||||
func TestNode_Serializable(t *testing.T) {
|
||||
t.Run("Leaf", func(t *testing.T) {
|
||||
t.Run("Good", func(t *testing.T) {
|
||||
l := NewLeafNode(random.Bytes(123))
|
||||
t.Run("Raw", getTestFuncEncode(true, l, new(LeafNode)))
|
||||
t.Run("WithType", getTestFuncEncode(true, &NodeObject{l}, new(NodeObject)))
|
||||
})
|
||||
t.Run("BigValue", getTestFuncEncode(false,
|
||||
NewLeafNode(random.Bytes(MaxValueLength+1)), new(LeafNode)))
|
||||
})
|
||||
|
||||
t.Run("Extension", func(t *testing.T) {
|
||||
t.Run("Good", func(t *testing.T) {
|
||||
e := NewExtensionNode(random.Bytes(42), NewLeafNode(random.Bytes(10)))
|
||||
t.Run("Raw", getTestFuncEncode(true, e, new(ExtensionNode)))
|
||||
t.Run("WithType", getTestFuncEncode(true, &NodeObject{e}, new(NodeObject)))
|
||||
})
|
||||
t.Run("BigKey", getTestFuncEncode(false,
|
||||
NewExtensionNode(random.Bytes(MaxKeyLength+1), NewLeafNode(random.Bytes(10))), new(ExtensionNode)))
|
||||
})
|
||||
|
||||
t.Run("Branch", func(t *testing.T) {
|
||||
b := NewBranchNode()
|
||||
b.Children[0] = NewLeafNode(random.Bytes(10))
|
||||
b.Children[lastChild] = NewHashNode(random.Uint256())
|
||||
t.Run("Raw", getTestFuncEncode(true, b, new(BranchNode)))
|
||||
t.Run("WithType", getTestFuncEncode(true, &NodeObject{b}, new(NodeObject)))
|
||||
})
|
||||
|
||||
t.Run("Hash", func(t *testing.T) {
|
||||
t.Run("Good", func(t *testing.T) {
|
||||
h := NewHashNode(random.Uint256())
|
||||
t.Run("Raw", getTestFuncEncode(true, h, new(HashNode)))
|
||||
t.Run("WithType", getTestFuncEncode(true, &NodeObject{h}, new(NodeObject)))
|
||||
})
|
||||
t.Run("Empty", func(t *testing.T) { // compare nodes, not hashes
|
||||
testserdes.EncodeDecodeBinary(t, new(HashNode), new(HashNode))
|
||||
})
|
||||
t.Run("InvalidSize", func(t *testing.T) {
|
||||
buf := io.NewBufBinWriter()
|
||||
buf.BinWriter.WriteVarBytes(make([]byte, 13))
|
||||
require.Error(t, testserdes.DecodeBinary(buf.Bytes(), new(HashNode)))
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("Invalid", func(t *testing.T) {
|
||||
require.Error(t, testserdes.DecodeBinary([]byte{0xFF}, new(NodeObject)))
|
||||
})
|
||||
}
|
||||
|
||||
// C# interoperability test
|
||||
// https://github.com/neo-project/neo/blob/neox-2.x/neo.UnitTests/UT_MPTTrie.cs#L135
|
||||
func TestRootHash(t *testing.T) {
|
||||
b := NewBranchNode()
|
||||
r := NewExtensionNode([]byte{0x0A, 0x0C}, b)
|
||||
|
||||
v1 := NewLeafNode([]byte{0xAB, 0xCD})
|
||||
l1 := NewExtensionNode([]byte{0x01}, v1)
|
||||
b.Children[0] = l1
|
||||
|
||||
v2 := NewLeafNode([]byte{0x22, 0x22})
|
||||
l2 := NewExtensionNode([]byte{0x09}, v2)
|
||||
b.Children[9] = l2
|
||||
|
||||
r1 := NewExtensionNode([]byte{0x0A, 0x0C, 0x00, 0x01}, v1)
|
||||
require.Equal(t, "dea3ab46e9461e885ed7091c1e533e0a8030b248d39cbc638962394eaca0fbb3", r1.Hash().StringLE())
|
||||
require.Equal(t, "93e8e1ffe2f83dd92fca67330e273bcc811bf64b8f8d9d1b25d5e7366b47d60d", r.Hash().StringLE())
|
||||
}
|
357
pkg/core/mpt/trie.go
Normal file
357
pkg/core/mpt/trie.go
Normal file
|
@ -0,0 +1,357 @@
|
|||
package mpt
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
|
||||
"github.com/nspcc-dev/neo-go/pkg/core/storage"
|
||||
"github.com/nspcc-dev/neo-go/pkg/crypto/hash"
|
||||
"github.com/nspcc-dev/neo-go/pkg/io"
|
||||
"github.com/nspcc-dev/neo-go/pkg/util"
|
||||
)
|
||||
|
||||
// Trie is an MPT trie storing all key-value pairs.
|
||||
type Trie struct {
|
||||
Store *storage.MemCachedStore
|
||||
|
||||
root Node
|
||||
}
|
||||
|
||||
// ErrNotFound is returned when requested trie item is missing.
|
||||
var ErrNotFound = errors.New("item not found")
|
||||
|
||||
// NewTrie returns new MPT trie. It accepts a MemCachedStore to decouple storage errors from logic errors
|
||||
// so that all storage errors are processed during `store.Persist()` at the caller.
|
||||
// This also has the benefit, that every `Put` can be considered an atomic operation.
|
||||
func NewTrie(root Node, store *storage.MemCachedStore) *Trie {
|
||||
if root == nil {
|
||||
root = new(HashNode)
|
||||
}
|
||||
|
||||
return &Trie{
|
||||
Store: store,
|
||||
root: root,
|
||||
}
|
||||
}
|
||||
|
||||
// Get returns value for the provided key in t.
|
||||
func (t *Trie) Get(key []byte) ([]byte, error) {
|
||||
path := toNibbles(key)
|
||||
r, bs, err := t.getWithPath(t.root, path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
t.root = r
|
||||
return bs, nil
|
||||
}
|
||||
|
||||
// getWithPath returns value the provided path in a subtrie rooting in curr.
|
||||
// It also returns a current node with all hash nodes along the path
|
||||
// replaced to their "unhashed" counterparts.
|
||||
func (t *Trie) getWithPath(curr Node, path []byte) (Node, []byte, error) {
|
||||
switch n := curr.(type) {
|
||||
case *LeafNode:
|
||||
if len(path) == 0 {
|
||||
return curr, copySlice(n.value), nil
|
||||
}
|
||||
case *BranchNode:
|
||||
i, path := splitPath(path)
|
||||
r, bs, err := t.getWithPath(n.Children[i], path)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
n.Children[i] = r
|
||||
return n, bs, nil
|
||||
case *HashNode:
|
||||
if !n.IsEmpty() {
|
||||
if r, err := t.getFromStore(n.hash); err == nil {
|
||||
return t.getWithPath(r, path)
|
||||
}
|
||||
}
|
||||
case *ExtensionNode:
|
||||
if bytes.HasPrefix(path, n.key) {
|
||||
r, bs, err := t.getWithPath(n.next, path[len(n.key):])
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
n.next = r
|
||||
return curr, bs, err
|
||||
}
|
||||
default:
|
||||
panic("invalid MPT node type")
|
||||
}
|
||||
return curr, nil, ErrNotFound
|
||||
}
|
||||
|
||||
// Put puts key-value pair in t.
|
||||
func (t *Trie) Put(key, value []byte) error {
|
||||
if len(key) > MaxKeyLength {
|
||||
return errors.New("key is too big")
|
||||
} else if len(value) > MaxValueLength {
|
||||
return errors.New("value is too big")
|
||||
}
|
||||
if len(value) == 0 {
|
||||
return t.Delete(key)
|
||||
}
|
||||
path := toNibbles(key)
|
||||
n := NewLeafNode(value)
|
||||
r, err := t.putIntoNode(t.root, path, n)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
t.root = r
|
||||
return nil
|
||||
}
|
||||
|
||||
// putIntoLeaf puts val to trie if current node is a Leaf.
|
||||
// It returns Node if curr needs to be replaced and error if any.
|
||||
func (t *Trie) putIntoLeaf(curr *LeafNode, path []byte, val Node) (Node, error) {
|
||||
v := val.(*LeafNode)
|
||||
if len(path) == 0 {
|
||||
return v, nil
|
||||
}
|
||||
|
||||
b := NewBranchNode()
|
||||
b.Children[path[0]] = newSubTrie(path[1:], v)
|
||||
b.Children[lastChild] = curr
|
||||
return b, nil
|
||||
}
|
||||
|
||||
// putIntoBranch puts val to trie if current node is a Branch.
|
||||
// It returns Node if curr needs to be replaced and error if any.
|
||||
func (t *Trie) putIntoBranch(curr *BranchNode, path []byte, val Node) (Node, error) {
|
||||
i, path := splitPath(path)
|
||||
r, err := t.putIntoNode(curr.Children[i], path, val)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
curr.Children[i] = r
|
||||
curr.invalidateHash()
|
||||
return curr, nil
|
||||
}
|
||||
|
||||
// putIntoExtension puts val to trie if current node is an Extension.
|
||||
// It returns Node if curr needs to be replaced and error if any.
|
||||
func (t *Trie) putIntoExtension(curr *ExtensionNode, path []byte, val Node) (Node, error) {
|
||||
if bytes.HasPrefix(path, curr.key) {
|
||||
r, err := t.putIntoNode(curr.next, path[len(curr.key):], val)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
curr.next = r
|
||||
curr.invalidateHash()
|
||||
return curr, nil
|
||||
}
|
||||
|
||||
pref := lcp(curr.key, path)
|
||||
lp := len(pref)
|
||||
keyTail := curr.key[lp:]
|
||||
pathTail := path[lp:]
|
||||
|
||||
s1 := newSubTrie(keyTail[1:], curr.next)
|
||||
b := NewBranchNode()
|
||||
b.Children[keyTail[0]] = s1
|
||||
|
||||
i, pathTail := splitPath(pathTail)
|
||||
s2 := newSubTrie(pathTail, val)
|
||||
b.Children[i] = s2
|
||||
|
||||
if lp > 0 {
|
||||
return NewExtensionNode(copySlice(pref), b), nil
|
||||
}
|
||||
return b, nil
|
||||
}
|
||||
|
||||
// putIntoHash puts val to trie if current node is a HashNode.
|
||||
// It returns Node if curr needs to be replaced and error if any.
|
||||
func (t *Trie) putIntoHash(curr *HashNode, path []byte, val Node) (Node, error) {
|
||||
if curr.IsEmpty() {
|
||||
return newSubTrie(path, val), nil
|
||||
}
|
||||
|
||||
result, err := t.getFromStore(curr.hash)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return t.putIntoNode(result, path, val)
|
||||
}
|
||||
|
||||
// newSubTrie create new trie containing node at provided path.
|
||||
func newSubTrie(path []byte, val Node) Node {
|
||||
if len(path) == 0 {
|
||||
return val
|
||||
}
|
||||
return NewExtensionNode(path, val)
|
||||
}
|
||||
|
||||
func (t *Trie) putIntoNode(curr Node, path []byte, val Node) (Node, error) {
|
||||
switch n := curr.(type) {
|
||||
case *LeafNode:
|
||||
return t.putIntoLeaf(n, path, val)
|
||||
case *BranchNode:
|
||||
return t.putIntoBranch(n, path, val)
|
||||
case *ExtensionNode:
|
||||
return t.putIntoExtension(n, path, val)
|
||||
case *HashNode:
|
||||
return t.putIntoHash(n, path, val)
|
||||
default:
|
||||
panic("invalid MPT node type")
|
||||
}
|
||||
}
|
||||
|
||||
// Delete removes key from trie.
|
||||
// It returns no error on missing key.
|
||||
func (t *Trie) Delete(key []byte) error {
|
||||
path := toNibbles(key)
|
||||
r, err := t.deleteFromNode(t.root, path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
t.root = r
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *Trie) deleteFromBranch(b *BranchNode, path []byte) (Node, error) {
|
||||
i, path := splitPath(path)
|
||||
r, err := t.deleteFromNode(b.Children[i], path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
b.Children[i] = r
|
||||
b.invalidateHash()
|
||||
var count, index int
|
||||
for i := range b.Children {
|
||||
h, ok := b.Children[i].(*HashNode)
|
||||
if !ok || !h.IsEmpty() {
|
||||
index = i
|
||||
count++
|
||||
}
|
||||
}
|
||||
// count is >= 1 because branch node had at least 2 children before deletion.
|
||||
if count > 1 {
|
||||
return b, nil
|
||||
}
|
||||
c := b.Children[index]
|
||||
if index == lastChild {
|
||||
return c, nil
|
||||
}
|
||||
if h, ok := c.(*HashNode); ok {
|
||||
c, err = t.getFromStore(h.Hash())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if e, ok := c.(*ExtensionNode); ok {
|
||||
e.key = append([]byte{byte(index)}, e.key...)
|
||||
e.invalidateHash()
|
||||
return e, nil
|
||||
}
|
||||
|
||||
return NewExtensionNode([]byte{byte(index)}, c), nil
|
||||
}
|
||||
|
||||
func (t *Trie) deleteFromExtension(n *ExtensionNode, path []byte) (Node, error) {
|
||||
if !bytes.HasPrefix(path, n.key) {
|
||||
return nil, ErrNotFound
|
||||
}
|
||||
r, err := t.deleteFromNode(n.next, path[len(n.key):])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
switch nxt := r.(type) {
|
||||
case *ExtensionNode:
|
||||
n.key = append(n.key, nxt.key...)
|
||||
n.next = nxt.next
|
||||
n.invalidateHash()
|
||||
case *HashNode:
|
||||
if nxt.IsEmpty() {
|
||||
return nxt, nil
|
||||
}
|
||||
default:
|
||||
n.next = r
|
||||
}
|
||||
return n, nil
|
||||
}
|
||||
|
||||
func (t *Trie) deleteFromNode(curr Node, path []byte) (Node, error) {
|
||||
switch n := curr.(type) {
|
||||
case *LeafNode:
|
||||
if len(path) == 0 {
|
||||
return new(HashNode), nil
|
||||
}
|
||||
return nil, ErrNotFound
|
||||
case *BranchNode:
|
||||
return t.deleteFromBranch(n, path)
|
||||
case *ExtensionNode:
|
||||
return t.deleteFromExtension(n, path)
|
||||
case *HashNode:
|
||||
if n.IsEmpty() {
|
||||
return nil, ErrNotFound
|
||||
}
|
||||
newNode, err := t.getFromStore(n.Hash())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return t.deleteFromNode(newNode, path)
|
||||
default:
|
||||
panic("invalid MPT node type")
|
||||
}
|
||||
}
|
||||
|
||||
// StateRoot returns root hash of t.
|
||||
func (t *Trie) StateRoot() util.Uint256 {
|
||||
if hn, ok := t.root.(*HashNode); ok && hn.IsEmpty() {
|
||||
return util.Uint256{}
|
||||
}
|
||||
return t.root.Hash()
|
||||
}
|
||||
|
||||
func makeStorageKey(mptKey []byte) []byte {
|
||||
return append([]byte{byte(storage.DataMPT)}, mptKey...)
|
||||
}
|
||||
|
||||
// Flush puts every node in the trie except Hash ones to the storage.
|
||||
// Because we care only about block-level changes, there is no need to put every
|
||||
// new node to storage. Normally, flush should be called with every StateRoot persist, i.e.
|
||||
// after every block.
|
||||
func (t *Trie) Flush() {
|
||||
t.flush(t.root)
|
||||
}
|
||||
|
||||
func (t *Trie) flush(node Node) {
|
||||
switch n := node.(type) {
|
||||
case *BranchNode:
|
||||
for i := range n.Children {
|
||||
t.flush(n.Children[i])
|
||||
}
|
||||
case *ExtensionNode:
|
||||
t.flush(n.next)
|
||||
case *HashNode:
|
||||
return
|
||||
}
|
||||
t.putToStore(node)
|
||||
}
|
||||
|
||||
func (t *Trie) putToStore(n Node) {
|
||||
if n.Type() == HashT {
|
||||
panic("can't put hash node in trie")
|
||||
}
|
||||
bs := toBytes(n)
|
||||
h := hash.DoubleSha256(bs)
|
||||
_ = t.Store.Put(makeStorageKey(h.BytesBE()), bs) // put in MemCached returns no errors
|
||||
}
|
||||
|
||||
func (t *Trie) getFromStore(h util.Uint256) (Node, error) {
|
||||
data, err := t.Store.Get(makeStorageKey(h.BytesBE()))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var n NodeObject
|
||||
r := io.NewBinReaderFromBuf(data)
|
||||
n.DecodeBinary(r)
|
||||
if r.Err != nil {
|
||||
return nil, r.Err
|
||||
}
|
||||
return n.Node, nil
|
||||
}
|
373
pkg/core/mpt/trie_test.go
Normal file
373
pkg/core/mpt/trie_test.go
Normal file
|
@ -0,0 +1,373 @@
|
|||
package mpt
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/nspcc-dev/neo-go/pkg/core/storage"
|
||||
"github.com/nspcc-dev/neo-go/pkg/internal/random"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func newTestStore() *storage.MemCachedStore {
|
||||
return storage.NewMemCachedStore(storage.NewMemoryStore())
|
||||
}
|
||||
|
||||
func newTestTrie(t *testing.T) *Trie {
|
||||
b := NewBranchNode()
|
||||
|
||||
l1 := NewLeafNode([]byte{0xAB, 0xCD})
|
||||
b.Children[0] = NewExtensionNode([]byte{0x01}, l1)
|
||||
|
||||
l2 := NewLeafNode([]byte{0x22, 0x22})
|
||||
b.Children[9] = NewExtensionNode([]byte{0x09}, l2)
|
||||
|
||||
v := NewLeafNode([]byte("hello"))
|
||||
h := NewHashNode(v.Hash())
|
||||
b.Children[10] = NewExtensionNode([]byte{0x0e}, h)
|
||||
|
||||
e := NewExtensionNode(toNibbles([]byte{0xAC}), b)
|
||||
tr := NewTrie(e, newTestStore())
|
||||
|
||||
tr.putToStore(e)
|
||||
tr.putToStore(b)
|
||||
tr.putToStore(l1)
|
||||
tr.putToStore(l2)
|
||||
tr.putToStore(v)
|
||||
tr.putToStore(b.Children[0])
|
||||
tr.putToStore(b.Children[9])
|
||||
tr.putToStore(b.Children[10])
|
||||
|
||||
return tr
|
||||
}
|
||||
|
||||
func TestTrie_PutIntoBranchNode(t *testing.T) {
|
||||
b := NewBranchNode()
|
||||
l := NewLeafNode([]byte{0x8})
|
||||
b.Children[0x7] = NewHashNode(l.Hash())
|
||||
b.Children[0x8] = NewHashNode(random.Uint256())
|
||||
tr := NewTrie(b, newTestStore())
|
||||
|
||||
// next
|
||||
require.NoError(t, tr.Put([]byte{}, []byte{0x12, 0x34}))
|
||||
tr.testHas(t, []byte{}, []byte{0x12, 0x34})
|
||||
|
||||
// empty hash node child
|
||||
require.NoError(t, tr.Put([]byte{0x66}, []byte{0x56}))
|
||||
tr.testHas(t, []byte{0x66}, []byte{0x56})
|
||||
require.True(t, isValid(tr.root))
|
||||
|
||||
// missing hash
|
||||
require.Error(t, tr.Put([]byte{0x70}, []byte{0x42}))
|
||||
require.True(t, isValid(tr.root))
|
||||
|
||||
// hash is in store
|
||||
tr.putToStore(l)
|
||||
require.NoError(t, tr.Put([]byte{0x70}, []byte{0x42}))
|
||||
require.True(t, isValid(tr.root))
|
||||
}
|
||||
|
||||
func TestTrie_PutIntoExtensionNode(t *testing.T) {
|
||||
l := NewLeafNode([]byte{0x11})
|
||||
key := []byte{0x12}
|
||||
e := NewExtensionNode(toNibbles(key), NewHashNode(l.Hash()))
|
||||
tr := NewTrie(e, newTestStore())
|
||||
|
||||
// missing hash
|
||||
require.Error(t, tr.Put(key, []byte{0x42}))
|
||||
|
||||
tr.putToStore(l)
|
||||
require.NoError(t, tr.Put(key, []byte{0x42}))
|
||||
tr.testHas(t, key, []byte{0x42})
|
||||
require.True(t, isValid(tr.root))
|
||||
}
|
||||
|
||||
func TestTrie_PutIntoHashNode(t *testing.T) {
|
||||
b := NewBranchNode()
|
||||
l := NewLeafNode(random.Bytes(5))
|
||||
e := NewExtensionNode([]byte{0x02}, l)
|
||||
b.Children[1] = NewHashNode(e.Hash())
|
||||
b.Children[9] = NewHashNode(random.Uint256())
|
||||
tr := NewTrie(b, newTestStore())
|
||||
|
||||
tr.putToStore(e)
|
||||
|
||||
t.Run("MissingLeafHash", func(t *testing.T) {
|
||||
_, err := tr.Get([]byte{0x12})
|
||||
require.Error(t, err)
|
||||
})
|
||||
|
||||
tr.putToStore(l)
|
||||
|
||||
val := random.Bytes(3)
|
||||
require.NoError(t, tr.Put([]byte{0x12, 0x34}, val))
|
||||
tr.testHas(t, []byte{0x12, 0x34}, val)
|
||||
tr.testHas(t, []byte{0x12}, l.value)
|
||||
require.True(t, isValid(tr.root))
|
||||
}
|
||||
|
||||
func TestTrie_Put(t *testing.T) {
|
||||
trExp := newTestTrie(t)
|
||||
|
||||
trAct := NewTrie(nil, newTestStore())
|
||||
require.NoError(t, trAct.Put([]byte{0xAC, 0x01}, []byte{0xAB, 0xCD}))
|
||||
require.NoError(t, trAct.Put([]byte{0xAC, 0x99}, []byte{0x22, 0x22}))
|
||||
require.NoError(t, trAct.Put([]byte{0xAC, 0xAE}, []byte("hello")))
|
||||
|
||||
// Note: the exact tries differ because of ("acae":"hello") node is stored as Hash node in test trie.
|
||||
require.Equal(t, trExp.root.Hash(), trAct.root.Hash())
|
||||
require.True(t, isValid(trAct.root))
|
||||
}
|
||||
|
||||
func TestTrie_PutInvalid(t *testing.T) {
|
||||
tr := NewTrie(nil, newTestStore())
|
||||
key, value := []byte("key"), []byte("value")
|
||||
|
||||
// big key
|
||||
require.Error(t, tr.Put(make([]byte, MaxKeyLength+1), value))
|
||||
|
||||
// big value
|
||||
require.Error(t, tr.Put(key, make([]byte, MaxValueLength+1)))
|
||||
|
||||
// this is ok though
|
||||
require.NoError(t, tr.Put(key, value))
|
||||
tr.testHas(t, key, value)
|
||||
}
|
||||
|
||||
func TestTrie_BigPut(t *testing.T) {
|
||||
tr := NewTrie(nil, newTestStore())
|
||||
items := []struct{ k, v string }{
|
||||
{"item with long key", "value1"},
|
||||
{"item with matching prefix", "value2"},
|
||||
{"another prefix", "value3"},
|
||||
{"another prefix 2", "value4"},
|
||||
{"another ", "value5"},
|
||||
}
|
||||
|
||||
for i := range items {
|
||||
require.NoError(t, tr.Put([]byte(items[i].k), []byte(items[i].v)))
|
||||
}
|
||||
|
||||
for i := range items {
|
||||
tr.testHas(t, []byte(items[i].k), []byte(items[i].v))
|
||||
}
|
||||
|
||||
t.Run("Rewrite", func(t *testing.T) {
|
||||
k, v := []byte(items[0].k), []byte{0x01, 0x23}
|
||||
require.NoError(t, tr.Put(k, v))
|
||||
tr.testHas(t, k, v)
|
||||
})
|
||||
|
||||
t.Run("Remove", func(t *testing.T) {
|
||||
k := []byte(items[1].k)
|
||||
require.NoError(t, tr.Put(k, []byte{}))
|
||||
tr.testHas(t, k, nil)
|
||||
})
|
||||
}
|
||||
|
||||
func (tr *Trie) testHas(t *testing.T, key, value []byte) {
|
||||
v, err := tr.Get(key)
|
||||
if value == nil {
|
||||
require.Error(t, err)
|
||||
return
|
||||
}
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, value, v)
|
||||
}
|
||||
|
||||
// isValid checks for 3 invariants:
|
||||
// - BranchNode contains > 1 children
|
||||
// - ExtensionNode do not contain another extension node
|
||||
// - ExtensionNode do not have nil key
|
||||
// It is used only during testing to catch possible bugs.
|
||||
func isValid(curr Node) bool {
|
||||
switch n := curr.(type) {
|
||||
case *BranchNode:
|
||||
var count int
|
||||
for i := range n.Children {
|
||||
if !isValid(n.Children[i]) {
|
||||
return false
|
||||
}
|
||||
hn, ok := n.Children[i].(*HashNode)
|
||||
if !ok || !hn.IsEmpty() {
|
||||
count++
|
||||
}
|
||||
}
|
||||
return count > 1
|
||||
case *ExtensionNode:
|
||||
_, ok := n.next.(*ExtensionNode)
|
||||
return len(n.key) != 0 && !ok
|
||||
default:
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
func TestTrie_Get(t *testing.T) {
|
||||
t.Run("HashNode", func(t *testing.T) {
|
||||
tr := newTestTrie(t)
|
||||
tr.testHas(t, []byte{0xAC, 0xAE}, []byte("hello"))
|
||||
})
|
||||
t.Run("UnfoldRoot", func(t *testing.T) {
|
||||
tr := newTestTrie(t)
|
||||
single := NewTrie(NewHashNode(tr.root.Hash()), tr.Store)
|
||||
single.testHas(t, []byte{0xAC}, nil)
|
||||
single.testHas(t, []byte{0xAC, 0x01}, []byte{0xAB, 0xCD})
|
||||
single.testHas(t, []byte{0xAC, 0x99}, []byte{0x22, 0x22})
|
||||
single.testHas(t, []byte{0xAC, 0xAE}, []byte("hello"))
|
||||
})
|
||||
}
|
||||
|
||||
func TestTrie_Flush(t *testing.T) {
|
||||
pairs := map[string][]byte{
|
||||
"": []byte("value0"),
|
||||
"key1": []byte("value1"),
|
||||
"key2": []byte("value2"),
|
||||
}
|
||||
|
||||
tr := NewTrie(nil, newTestStore())
|
||||
for k, v := range pairs {
|
||||
require.NoError(t, tr.Put([]byte(k), v))
|
||||
}
|
||||
|
||||
tr.Flush()
|
||||
tr = NewTrie(NewHashNode(tr.StateRoot()), tr.Store)
|
||||
for k, v := range pairs {
|
||||
actual, err := tr.Get([]byte(k))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, v, actual)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTrie_Delete(t *testing.T) {
|
||||
t.Run("Hash", func(t *testing.T) {
|
||||
t.Run("FromStore", func(t *testing.T) {
|
||||
l := NewLeafNode([]byte{0x12})
|
||||
tr := NewTrie(NewHashNode(l.Hash()), newTestStore())
|
||||
t.Run("NotInStore", func(t *testing.T) {
|
||||
require.Error(t, tr.Delete([]byte{}))
|
||||
})
|
||||
|
||||
tr.putToStore(l)
|
||||
tr.testHas(t, []byte{}, []byte{0x12})
|
||||
require.NoError(t, tr.Delete([]byte{}))
|
||||
tr.testHas(t, []byte{}, nil)
|
||||
})
|
||||
|
||||
t.Run("Empty", func(t *testing.T) {
|
||||
tr := NewTrie(nil, newTestStore())
|
||||
require.Error(t, tr.Delete([]byte{}))
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("Leaf", func(t *testing.T) {
|
||||
l := NewLeafNode([]byte{0x12, 0x34})
|
||||
tr := NewTrie(l, newTestStore())
|
||||
t.Run("NonExistentKey", func(t *testing.T) {
|
||||
require.Error(t, tr.Delete([]byte{0x12}))
|
||||
tr.testHas(t, []byte{}, []byte{0x12, 0x34})
|
||||
})
|
||||
require.NoError(t, tr.Delete([]byte{}))
|
||||
tr.testHas(t, []byte{}, nil)
|
||||
})
|
||||
|
||||
t.Run("Extension", func(t *testing.T) {
|
||||
t.Run("SingleKey", func(t *testing.T) {
|
||||
l := NewLeafNode([]byte{0x12, 0x34})
|
||||
e := NewExtensionNode([]byte{0x0A, 0x0B}, l)
|
||||
tr := NewTrie(e, newTestStore())
|
||||
|
||||
t.Run("NonExistentKey", func(t *testing.T) {
|
||||
require.Error(t, tr.Delete([]byte{}))
|
||||
tr.testHas(t, []byte{0xAB}, []byte{0x12, 0x34})
|
||||
})
|
||||
|
||||
require.NoError(t, tr.Delete([]byte{0xAB}))
|
||||
require.True(t, tr.root.(*HashNode).IsEmpty())
|
||||
})
|
||||
|
||||
t.Run("MultipleKeys", func(t *testing.T) {
|
||||
b := NewBranchNode()
|
||||
b.Children[0] = NewExtensionNode([]byte{0x01}, NewLeafNode([]byte{0x12, 0x34}))
|
||||
b.Children[6] = NewExtensionNode([]byte{0x07}, NewLeafNode([]byte{0x56, 0x78}))
|
||||
e := NewExtensionNode([]byte{0x01, 0x02}, b)
|
||||
tr := NewTrie(e, newTestStore())
|
||||
|
||||
h := e.Hash()
|
||||
require.NoError(t, tr.Delete([]byte{0x12, 0x01}))
|
||||
tr.testHas(t, []byte{0x12, 0x01}, nil)
|
||||
tr.testHas(t, []byte{0x12, 0x67}, []byte{0x56, 0x78})
|
||||
|
||||
require.NotEqual(t, h, tr.root.Hash())
|
||||
require.Equal(t, toNibbles([]byte{0x12, 0x67}), e.key)
|
||||
require.IsType(t, (*LeafNode)(nil), e.next)
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("Branch", func(t *testing.T) {
|
||||
t.Run("3 Children", func(t *testing.T) {
|
||||
b := NewBranchNode()
|
||||
b.Children[lastChild] = NewLeafNode([]byte{0x12})
|
||||
b.Children[0] = NewExtensionNode([]byte{0x01}, NewLeafNode([]byte{0x34}))
|
||||
b.Children[1] = NewExtensionNode([]byte{0x06}, NewLeafNode([]byte{0x56}))
|
||||
tr := NewTrie(b, newTestStore())
|
||||
require.NoError(t, tr.Delete([]byte{0x16}))
|
||||
tr.testHas(t, []byte{}, []byte{0x12})
|
||||
tr.testHas(t, []byte{0x01}, []byte{0x34})
|
||||
tr.testHas(t, []byte{0x16}, nil)
|
||||
})
|
||||
t.Run("2 Children", func(t *testing.T) {
|
||||
newt := func(t *testing.T) *Trie {
|
||||
b := NewBranchNode()
|
||||
b.Children[lastChild] = NewLeafNode([]byte{0x12})
|
||||
l := NewLeafNode([]byte{0x34})
|
||||
e := NewExtensionNode([]byte{0x06}, l)
|
||||
b.Children[5] = NewHashNode(e.Hash())
|
||||
tr := NewTrie(b, newTestStore())
|
||||
tr.putToStore(l)
|
||||
tr.putToStore(e)
|
||||
return tr
|
||||
}
|
||||
|
||||
t.Run("DeleteLast", func(t *testing.T) {
|
||||
t.Run("MergeExtension", func(t *testing.T) {
|
||||
tr := newt(t)
|
||||
require.NoError(t, tr.Delete([]byte{}))
|
||||
tr.testHas(t, []byte{}, nil)
|
||||
tr.testHas(t, []byte{0x56}, []byte{0x34})
|
||||
require.IsType(t, (*ExtensionNode)(nil), tr.root)
|
||||
})
|
||||
|
||||
t.Run("LeaveLeaf", func(t *testing.T) {
|
||||
c := NewBranchNode()
|
||||
c.Children[5] = NewLeafNode([]byte{0x05})
|
||||
c.Children[6] = NewLeafNode([]byte{0x06})
|
||||
|
||||
b := NewBranchNode()
|
||||
b.Children[lastChild] = NewLeafNode([]byte{0x12})
|
||||
b.Children[5] = c
|
||||
tr := NewTrie(b, newTestStore())
|
||||
|
||||
require.NoError(t, tr.Delete([]byte{}))
|
||||
tr.testHas(t, []byte{}, nil)
|
||||
tr.testHas(t, []byte{0x55}, []byte{0x05})
|
||||
tr.testHas(t, []byte{0x56}, []byte{0x06})
|
||||
require.IsType(t, (*ExtensionNode)(nil), tr.root)
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("DeleteMiddle", func(t *testing.T) {
|
||||
tr := newt(t)
|
||||
require.NoError(t, tr.Delete([]byte{0x56}))
|
||||
tr.testHas(t, []byte{}, []byte{0x12})
|
||||
tr.testHas(t, []byte{0x56}, nil)
|
||||
require.IsType(t, (*LeafNode)(nil), tr.root)
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestTrie_PanicInvalidRoot(t *testing.T) {
|
||||
tr := &Trie{Store: newTestStore()}
|
||||
require.Panics(t, func() { _ = tr.Put([]byte{1}, []byte{2}) })
|
||||
require.Panics(t, func() { _, _ = tr.Get([]byte{1}) })
|
||||
require.Panics(t, func() { _ = tr.Delete([]byte{1}) })
|
||||
}
|
|
@ -9,6 +9,7 @@ import (
|
|||
const (
|
||||
DataBlock KeyPrefix = 0x01
|
||||
DataTransaction KeyPrefix = 0x02
|
||||
DataMPT KeyPrefix = 0x03
|
||||
STAccount KeyPrefix = 0x40
|
||||
STNotification KeyPrefix = 0x4d
|
||||
STContract KeyPrefix = 0x50
|
||||
|
|
Loading…
Reference in a new issue