mirror of
https://github.com/luxfi/node.git
synced 2026-10-30 15:24:05 +00:00
1355 lines
32 KiB
Go
1355 lines
32 KiB
Go
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
|
|
// See the file LICENSE for licensing terms.
|
|
|
|
//go:build test
|
|
|
|
package merkledb
|
|
|
|
import (
|
|
"context"
|
|
"math/rand"
|
|
"strconv"
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/require"
|
|
"golang.org/x/sync/errgroup"
|
|
|
|
hash "github.com/luxfi/crypto/hash"
|
|
"github.com/luxfi/database"
|
|
"github.com/luxfi/database/memdb"
|
|
"github.com/luxfi/ids"
|
|
)
|
|
|
|
func getNodeValue(t Trie, key string) ([]byte, error) {
|
|
path := ToKey([]byte(key))
|
|
if asView, ok := t.(*view); ok {
|
|
if err := asView.applyValueChanges(context.Background()); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
var result *node
|
|
|
|
err := visitPathToKey(t, path, func(n *node) error {
|
|
result = n
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if result == nil || result.key != path {
|
|
return nil, database.ErrNotFound
|
|
}
|
|
|
|
return result.value.Value(), nil
|
|
}
|
|
|
|
func Test_GetValue_Safety(t *testing.T) {
|
|
require := require.New(t)
|
|
|
|
db, err := getBasicDB()
|
|
require.NoError(err)
|
|
|
|
view, err := db.NewView(
|
|
context.Background(),
|
|
ViewChanges{
|
|
BatchOps: []database.BatchOp{
|
|
{Key: []byte{0}, Value: []byte{0}},
|
|
},
|
|
},
|
|
)
|
|
require.NoError(err)
|
|
|
|
trieVal, err := view.GetValue(context.Background(), []byte{0})
|
|
require.NoError(err)
|
|
require.Equal([]byte{0}, trieVal)
|
|
trieVal[0] = 1
|
|
|
|
// should still be []byte{0} after edit
|
|
trieVal, err = view.GetValue(context.Background(), []byte{0})
|
|
require.NoError(err)
|
|
require.Equal([]byte{0}, trieVal)
|
|
}
|
|
|
|
func Test_GetValues_Safety(t *testing.T) {
|
|
require := require.New(t)
|
|
|
|
db, err := getBasicDB()
|
|
require.NoError(err)
|
|
|
|
view, err := db.NewView(
|
|
context.Background(),
|
|
ViewChanges{
|
|
BatchOps: []database.BatchOp{
|
|
{Key: []byte{0}, Value: []byte{0}},
|
|
},
|
|
},
|
|
)
|
|
require.NoError(err)
|
|
|
|
trieVals, errs := view.GetValues(context.Background(), [][]byte{{0}})
|
|
require.Len(errs, 1)
|
|
require.NoError(errs[0])
|
|
require.Equal([]byte{0}, trieVals[0])
|
|
trieVals[0][0] = 1
|
|
require.Equal([]byte{1}, trieVals[0])
|
|
|
|
// should still be []byte{0} after edit
|
|
trieVals, errs = view.GetValues(context.Background(), [][]byte{{0}})
|
|
require.Len(errs, 1)
|
|
require.NoError(errs[0])
|
|
require.Equal([]byte{0}, trieVals[0])
|
|
}
|
|
|
|
func TestVisitPathToKey(t *testing.T) {
|
|
require := require.New(t)
|
|
|
|
db, err := getBasicDB()
|
|
require.NoError(err)
|
|
|
|
trieIntf, err := db.NewView(context.Background(), ViewChanges{})
|
|
require.NoError(err)
|
|
require.IsType(&view{}, trieIntf)
|
|
trie := trieIntf.(*view)
|
|
|
|
var nodePath []*node
|
|
require.NoError(visitPathToKey(trie, ToKey(nil), func(n *node) error {
|
|
nodePath = append(nodePath, n)
|
|
return nil
|
|
}))
|
|
|
|
require.Empty(nodePath)
|
|
|
|
// Insert a key
|
|
key1 := []byte{0}
|
|
trieIntf, err = trie.NewView(
|
|
context.Background(),
|
|
ViewChanges{
|
|
BatchOps: []database.BatchOp{
|
|
{Key: key1, Value: []byte("value")},
|
|
},
|
|
},
|
|
)
|
|
require.NoError(err)
|
|
require.IsType(&view{}, trieIntf)
|
|
trie = trieIntf.(*view)
|
|
require.NoError(trie.applyValueChanges(context.Background()))
|
|
|
|
nodePath = make([]*node, 0, 1)
|
|
require.NoError(visitPathToKey(trie, ToKey(key1), func(n *node) error {
|
|
nodePath = append(nodePath, n)
|
|
return nil
|
|
}))
|
|
|
|
// 1 value
|
|
require.Len(nodePath, 1)
|
|
require.Equal(ToKey(key1), nodePath[0].key)
|
|
|
|
// Insert another key which is a child of the first
|
|
key2 := []byte{0, 1}
|
|
trieIntf, err = trie.NewView(
|
|
context.Background(),
|
|
ViewChanges{
|
|
BatchOps: []database.BatchOp{
|
|
{Key: key2, Value: []byte("value")},
|
|
},
|
|
},
|
|
)
|
|
require.NoError(err)
|
|
require.IsType(&view{}, trieIntf)
|
|
trie = trieIntf.(*view)
|
|
require.NoError(trie.applyValueChanges(context.Background()))
|
|
|
|
nodePath = make([]*node, 0, 2)
|
|
require.NoError(visitPathToKey(trie, ToKey(key2), func(n *node) error {
|
|
nodePath = append(nodePath, n)
|
|
return nil
|
|
}))
|
|
require.Len(nodePath, 2)
|
|
require.Equal(trie.root.Value(), nodePath[0])
|
|
require.Equal(ToKey(key1), nodePath[0].key)
|
|
require.Equal(ToKey(key2), nodePath[1].key)
|
|
|
|
// Trie is:
|
|
// [0]
|
|
// |
|
|
// [0,1]
|
|
// Insert a key which shares no prefix with the others
|
|
key3 := []byte{255}
|
|
trieIntf, err = trie.NewView(
|
|
context.Background(),
|
|
ViewChanges{
|
|
BatchOps: []database.BatchOp{
|
|
{Key: key3, Value: []byte("value")},
|
|
},
|
|
},
|
|
)
|
|
require.NoError(err)
|
|
require.IsType(&view{}, trieIntf)
|
|
trie = trieIntf.(*view)
|
|
require.NoError(trie.applyValueChanges(context.Background()))
|
|
|
|
// Trie is:
|
|
// []
|
|
// / \
|
|
// [0] [255]
|
|
// |
|
|
// [0,1]
|
|
nodePath = make([]*node, 0, 2)
|
|
require.NoError(visitPathToKey(trie, ToKey(key3), func(n *node) error {
|
|
nodePath = append(nodePath, n)
|
|
return nil
|
|
}))
|
|
|
|
require.Len(nodePath, 2)
|
|
require.Equal(trie.root.Value(), nodePath[0])
|
|
require.Zero(trie.root.Value().key.length)
|
|
require.Equal(ToKey(key3), nodePath[1].key)
|
|
|
|
// Other key path not affected
|
|
nodePath = make([]*node, 0, 3)
|
|
require.NoError(visitPathToKey(trie, ToKey(key2), func(n *node) error {
|
|
nodePath = append(nodePath, n)
|
|
return nil
|
|
}))
|
|
require.Len(nodePath, 3)
|
|
require.Equal(trie.root.Value(), nodePath[0])
|
|
require.Equal(ToKey(key1), nodePath[1].key)
|
|
require.Equal(ToKey(key2), nodePath[2].key)
|
|
|
|
// Gets closest node when key doesn't exist
|
|
key4 := []byte{0, 1, 2}
|
|
nodePath = make([]*node, 0, 3)
|
|
require.NoError(visitPathToKey(trie, ToKey(key4), func(n *node) error {
|
|
nodePath = append(nodePath, n)
|
|
return nil
|
|
}))
|
|
|
|
require.Len(nodePath, 3)
|
|
require.Equal(trie.root.Value(), nodePath[0])
|
|
require.Equal(ToKey(key1), nodePath[1].key)
|
|
require.Equal(ToKey(key2), nodePath[2].key)
|
|
|
|
// Gets just root when key doesn't exist and no key shares a prefix
|
|
key5 := []byte{128}
|
|
nodePath = make([]*node, 0, 1)
|
|
require.NoError(visitPathToKey(trie, ToKey(key5), func(n *node) error {
|
|
nodePath = append(nodePath, n)
|
|
return nil
|
|
}))
|
|
require.Len(nodePath, 1)
|
|
require.Equal(trie.root.Value(), nodePath[0])
|
|
}
|
|
|
|
func Test_Trie_ViewOnCommittedView(t *testing.T) {
|
|
require := require.New(t)
|
|
|
|
dbTrie, err := getBasicDB()
|
|
require.NoError(err)
|
|
require.NotNil(dbTrie)
|
|
|
|
committedTrie, err := dbTrie.NewView(
|
|
context.Background(),
|
|
ViewChanges{
|
|
BatchOps: []database.BatchOp{
|
|
{Key: []byte{0}, Value: []byte{0}},
|
|
},
|
|
},
|
|
)
|
|
require.NoError(err)
|
|
|
|
require.NoError(committedTrie.CommitToDB(context.Background()))
|
|
|
|
view, err := committedTrie.NewView(
|
|
context.Background(),
|
|
ViewChanges{
|
|
BatchOps: []database.BatchOp{
|
|
{Key: []byte{1}, Value: []byte{1}},
|
|
},
|
|
},
|
|
)
|
|
require.NoError(err)
|
|
require.NoError(view.CommitToDB(context.Background()))
|
|
|
|
val0, err := dbTrie.GetValue(context.Background(), []byte{0})
|
|
require.NoError(err)
|
|
require.Equal([]byte{0}, val0)
|
|
val1, err := dbTrie.GetValue(context.Background(), []byte{1})
|
|
require.NoError(err)
|
|
require.Equal([]byte{1}, val1)
|
|
}
|
|
|
|
func Test_Trie_WriteToDB(t *testing.T) {
|
|
require := require.New(t)
|
|
|
|
dbTrie, err := getBasicDB()
|
|
require.NoError(err)
|
|
require.NotNil(dbTrie)
|
|
|
|
trieIntf1, err := dbTrie.NewView(context.Background(), ViewChanges{})
|
|
require.NoError(err)
|
|
trie1 := trieIntf1.(*view)
|
|
|
|
// value hasn't been inserted so shouldn't exist
|
|
value, err := trie1.GetValue(context.Background(), []byte("key"))
|
|
require.ErrorIs(err, database.ErrNotFound)
|
|
require.Nil(value)
|
|
|
|
trieIntf2, err := trie1.NewView(
|
|
context.Background(),
|
|
ViewChanges{
|
|
BatchOps: []database.BatchOp{
|
|
{Key: []byte("key"), Value: []byte("value")},
|
|
},
|
|
},
|
|
)
|
|
require.NoError(err)
|
|
trie2 := trieIntf2.(*view)
|
|
|
|
value, err = getNodeValue(trie2, "key")
|
|
require.NoError(err)
|
|
require.Equal([]byte("value"), value)
|
|
|
|
require.NoError(trie1.CommitToDB(context.Background()))
|
|
require.NoError(trie2.CommitToDB(context.Background()))
|
|
|
|
key := []byte("key")
|
|
prefixedKey := make([]byte, len(key)+valueNodePrefixLen)
|
|
copy(prefixedKey, valueNodePrefix)
|
|
copy(prefixedKey[valueNodePrefixLen:], key)
|
|
rawBytes, err := dbTrie.baseDB.Get(prefixedKey)
|
|
require.NoError(err)
|
|
|
|
node, err := parseNode(dbTrie.hasher, ToKey(key), rawBytes)
|
|
require.NoError(err)
|
|
require.Equal([]byte("value"), node.value.Value())
|
|
}
|
|
|
|
func Test_Trie_InsertAndRetrieve(t *testing.T) {
|
|
require := require.New(t)
|
|
|
|
dbTrie, err := getBasicDB()
|
|
require.NoError(err)
|
|
require.NotNil(dbTrie)
|
|
|
|
// value hasn't been inserted so shouldn't exist
|
|
value, err := dbTrie.Get([]byte("key"))
|
|
require.ErrorIs(err, database.ErrNotFound)
|
|
require.Nil(value)
|
|
|
|
require.NoError(dbTrie.Put([]byte("key"), []byte("value")))
|
|
|
|
value, err = getNodeValue(dbTrie, "key")
|
|
require.NoError(err)
|
|
require.Equal([]byte("value"), value)
|
|
}
|
|
|
|
func Test_Trie_Overwrite(t *testing.T) {
|
|
require := require.New(t)
|
|
|
|
dbTrie, err := getBasicDB()
|
|
require.NoError(err)
|
|
require.NotNil(dbTrie)
|
|
trie, err := dbTrie.NewView(
|
|
context.Background(),
|
|
ViewChanges{
|
|
BatchOps: []database.BatchOp{
|
|
{Key: []byte("key"), Value: []byte("value0")},
|
|
{Key: []byte("key"), Value: []byte("value1")},
|
|
},
|
|
},
|
|
)
|
|
require.NoError(err)
|
|
value, err := getNodeValue(trie, "key")
|
|
require.NoError(err)
|
|
require.Equal([]byte("value1"), value)
|
|
|
|
trie, err = dbTrie.NewView(
|
|
context.Background(),
|
|
ViewChanges{
|
|
BatchOps: []database.BatchOp{
|
|
{Key: []byte("key"), Value: []byte("value2")},
|
|
},
|
|
},
|
|
)
|
|
require.NoError(err)
|
|
value, err = getNodeValue(trie, "key")
|
|
require.NoError(err)
|
|
require.Equal([]byte("value2"), value)
|
|
}
|
|
|
|
func Test_Trie_Delete(t *testing.T) {
|
|
require := require.New(t)
|
|
|
|
dbTrie, err := getBasicDB()
|
|
require.NoError(err)
|
|
require.NotNil(dbTrie)
|
|
|
|
trie, err := dbTrie.NewView(
|
|
context.Background(),
|
|
ViewChanges{
|
|
BatchOps: []database.BatchOp{
|
|
{Key: []byte("key"), Value: []byte("value0")},
|
|
},
|
|
},
|
|
)
|
|
require.NoError(err)
|
|
|
|
value, err := getNodeValue(trie, "key")
|
|
require.NoError(err)
|
|
require.Equal([]byte("value0"), value)
|
|
|
|
trie, err = dbTrie.NewView(
|
|
context.Background(),
|
|
ViewChanges{
|
|
BatchOps: []database.BatchOp{
|
|
{Key: []byte("key"), Delete: true},
|
|
},
|
|
},
|
|
)
|
|
require.NoError(err)
|
|
|
|
value, err = getNodeValue(trie, "key")
|
|
require.ErrorIs(err, database.ErrNotFound)
|
|
require.Nil(value)
|
|
}
|
|
|
|
func Test_Trie_DeleteMissingKey(t *testing.T) {
|
|
require := require.New(t)
|
|
|
|
trie, err := getBasicDB()
|
|
require.NoError(err)
|
|
require.NotNil(trie)
|
|
|
|
require.NoError(trie.DeleteContext(context.Background(), []byte("key")))
|
|
}
|
|
|
|
func Test_Trie_ExpandOnKeyPath(t *testing.T) {
|
|
require := require.New(t)
|
|
|
|
dbTrie, err := getBasicDB()
|
|
require.NoError(err)
|
|
require.NotNil(dbTrie)
|
|
trieIntf, err := dbTrie.NewView(
|
|
context.Background(),
|
|
ViewChanges{
|
|
BatchOps: []database.BatchOp{
|
|
{Key: []byte("key"), Value: []byte("value0")},
|
|
},
|
|
},
|
|
)
|
|
require.NoError(err)
|
|
trie := trieIntf.(*view)
|
|
|
|
value, err := getNodeValue(trie, "key")
|
|
require.NoError(err)
|
|
require.Equal([]byte("value0"), value)
|
|
|
|
trieIntf, err = trie.NewView(
|
|
context.Background(),
|
|
ViewChanges{
|
|
BatchOps: []database.BatchOp{
|
|
{Key: []byte("key1"), Value: []byte("value1")},
|
|
},
|
|
},
|
|
)
|
|
require.NoError(err)
|
|
trie = trieIntf.(*view)
|
|
|
|
value, err = getNodeValue(trie, "key")
|
|
require.NoError(err)
|
|
require.Equal([]byte("value0"), value)
|
|
|
|
value, err = getNodeValue(trie, "key1")
|
|
require.NoError(err)
|
|
require.Equal([]byte("value1"), value)
|
|
|
|
trieIntf, err = trie.NewView(
|
|
context.Background(),
|
|
ViewChanges{
|
|
BatchOps: []database.BatchOp{
|
|
{Key: []byte("key12"), Value: []byte("value12")},
|
|
},
|
|
},
|
|
)
|
|
require.NoError(err)
|
|
trie = trieIntf.(*view)
|
|
|
|
value, err = getNodeValue(trie, "key")
|
|
require.NoError(err)
|
|
require.Equal([]byte("value0"), value)
|
|
|
|
value, err = getNodeValue(trie, "key1")
|
|
require.NoError(err)
|
|
require.Equal([]byte("value1"), value)
|
|
|
|
value, err = getNodeValue(trie, "key12")
|
|
require.NoError(err)
|
|
require.Equal([]byte("value12"), value)
|
|
}
|
|
|
|
func Test_Trie_CompressedKeys(t *testing.T) {
|
|
require := require.New(t)
|
|
|
|
dbTrie, err := getBasicDB()
|
|
require.NoError(err)
|
|
require.NotNil(dbTrie)
|
|
trieIntf, err := dbTrie.NewView(
|
|
context.Background(),
|
|
ViewChanges{
|
|
BatchOps: []database.BatchOp{
|
|
{Key: []byte("key12"), Value: []byte("value12")},
|
|
},
|
|
},
|
|
)
|
|
require.NoError(err)
|
|
trie := trieIntf.(*view)
|
|
|
|
value, err := getNodeValue(trie, "key12")
|
|
require.NoError(err)
|
|
require.Equal([]byte("value12"), value)
|
|
|
|
trieIntf, err = trie.NewView(
|
|
context.Background(),
|
|
ViewChanges{
|
|
BatchOps: []database.BatchOp{
|
|
{Key: []byte("key1"), Value: []byte("value1")},
|
|
},
|
|
},
|
|
)
|
|
require.NoError(err)
|
|
trie = trieIntf.(*view)
|
|
|
|
value, err = getNodeValue(trie, "key12")
|
|
require.NoError(err)
|
|
require.Equal([]byte("value12"), value)
|
|
|
|
value, err = getNodeValue(trie, "key1")
|
|
require.NoError(err)
|
|
require.Equal([]byte("value1"), value)
|
|
|
|
trieIntf, err = trie.NewView(
|
|
context.Background(),
|
|
ViewChanges{
|
|
BatchOps: []database.BatchOp{
|
|
{Key: []byte("key"), Value: []byte("value")},
|
|
},
|
|
},
|
|
)
|
|
require.NoError(err)
|
|
trie = trieIntf.(*view)
|
|
|
|
value, err = getNodeValue(trie, "key12")
|
|
require.NoError(err)
|
|
require.Equal([]byte("value12"), value)
|
|
|
|
value, err = getNodeValue(trie, "key1")
|
|
require.NoError(err)
|
|
require.Equal([]byte("value1"), value)
|
|
|
|
value, err = getNodeValue(trie, "key")
|
|
require.NoError(err)
|
|
require.Equal([]byte("value"), value)
|
|
}
|
|
|
|
func Test_Trie_SplitBranch(t *testing.T) {
|
|
require := require.New(t)
|
|
|
|
dbTrie, err := getBasicDB()
|
|
require.NoError(err)
|
|
require.NotNil(dbTrie)
|
|
|
|
// force a new node to generate with common prefix "key1" and have these two nodes as children
|
|
trie, err := dbTrie.NewView(
|
|
context.Background(),
|
|
ViewChanges{
|
|
BatchOps: []database.BatchOp{
|
|
{Key: []byte("key12"), Value: []byte("value12")},
|
|
{Key: []byte("key134"), Value: []byte("value134")},
|
|
},
|
|
},
|
|
)
|
|
require.NoError(err)
|
|
|
|
value, err := getNodeValue(trie, "key12")
|
|
require.NoError(err)
|
|
require.Equal([]byte("value12"), value)
|
|
|
|
value, err = getNodeValue(trie, "key134")
|
|
require.NoError(err)
|
|
require.Equal([]byte("value134"), value)
|
|
}
|
|
|
|
func Test_Trie_HashCountOnBranch(t *testing.T) {
|
|
require := require.New(t)
|
|
|
|
dbTrie, err := getBasicDB()
|
|
require.NoError(err)
|
|
require.NotNil(dbTrie)
|
|
|
|
key1, key2, keyPrefix := []byte("12"), []byte("1F"), []byte("1")
|
|
|
|
view1, err := dbTrie.NewView(
|
|
context.Background(),
|
|
ViewChanges{
|
|
BatchOps: []database.BatchOp{
|
|
{Key: key1, Value: []byte("")},
|
|
},
|
|
})
|
|
require.NoError(err)
|
|
|
|
// trie is:
|
|
// [1]
|
|
|
|
// create new node with common prefix whose children
|
|
// are key1, key2
|
|
view2, err := view1.NewView(
|
|
context.Background(),
|
|
ViewChanges{
|
|
BatchOps: []database.BatchOp{
|
|
{Key: key2, Value: []byte("")},
|
|
},
|
|
})
|
|
require.NoError(err)
|
|
|
|
// trie is:
|
|
// [1]
|
|
// / \
|
|
// [12] [1F]
|
|
|
|
// clear the hash count to ignore setup
|
|
dbTrie.metrics.(*mockMetrics).hashCount = 0
|
|
|
|
// calculate the root
|
|
_, err = view2.GetMerkleRoot(context.Background())
|
|
require.NoError(err)
|
|
|
|
// Make sure the root is an intermediate node with the expected common prefix.
|
|
// Note it's only created on call to GetMerkleRoot, not in NewView.
|
|
prefixNode, err := view2.getEditableNode(ToKey(keyPrefix), false)
|
|
require.NoError(err)
|
|
root := view2.getRoot().Value()
|
|
require.Equal(root, prefixNode)
|
|
require.Len(root.children, 2)
|
|
|
|
// Had to hash each of the new nodes ("12" and "1F") and the new root
|
|
require.Equal(int64(3), dbTrie.metrics.(*mockMetrics).hashCount)
|
|
}
|
|
|
|
func Test_Trie_HashCountOnDelete(t *testing.T) {
|
|
require := require.New(t)
|
|
|
|
dbTrie, err := getBasicDB()
|
|
require.NoError(err)
|
|
|
|
trie, err := dbTrie.NewView(
|
|
context.Background(),
|
|
ViewChanges{
|
|
BatchOps: []database.BatchOp{
|
|
{Key: []byte("k"), Value: []byte("value0")},
|
|
{Key: []byte("ke"), Value: []byte("value1")},
|
|
{Key: []byte("key"), Value: []byte("value2")},
|
|
{Key: []byte("key1"), Value: []byte("value3")},
|
|
{Key: []byte("key2"), Value: []byte("value4")},
|
|
},
|
|
},
|
|
)
|
|
require.NoError(err)
|
|
require.NotNil(trie)
|
|
|
|
require.NoError(trie.CommitToDB(context.Background()))
|
|
oldCount := dbTrie.metrics.(*mockMetrics).hashCount
|
|
|
|
// delete the middle values
|
|
view, err := trie.NewView(
|
|
context.Background(),
|
|
ViewChanges{
|
|
BatchOps: []database.BatchOp{
|
|
{Key: []byte("k"), Delete: true},
|
|
{Key: []byte("ke"), Delete: true},
|
|
{Key: []byte("key"), Delete: true},
|
|
},
|
|
},
|
|
)
|
|
require.NoError(err)
|
|
require.NoError(view.CommitToDB(context.Background()))
|
|
|
|
// trie is:
|
|
// [key0] (first 28 bits)
|
|
// / \
|
|
// [key1] [key2]
|
|
root := view.getRoot().Value()
|
|
expectedRootKey := ToKey([]byte("key0")).Take(28)
|
|
require.Equal(expectedRootKey, root.key)
|
|
require.Len(root.children, 2)
|
|
|
|
// Had to hash the new root but not [key1] or [key2] nodes
|
|
require.Equal(oldCount+1, dbTrie.metrics.(*mockMetrics).hashCount)
|
|
}
|
|
|
|
func Test_Trie_NoExistingResidual(t *testing.T) {
|
|
require := require.New(t)
|
|
|
|
dbTrie, err := getBasicDB()
|
|
require.NoError(err)
|
|
require.NotNil(dbTrie)
|
|
|
|
trie, err := dbTrie.NewView(
|
|
context.Background(),
|
|
ViewChanges{
|
|
BatchOps: []database.BatchOp{
|
|
{Key: []byte("k"), Value: []byte("1")},
|
|
{Key: []byte("ke"), Value: []byte("2")},
|
|
{Key: []byte("key1"), Value: []byte("3")},
|
|
{Key: []byte("key123"), Value: []byte("4")},
|
|
},
|
|
},
|
|
)
|
|
require.NoError(err)
|
|
require.NotNil(trie)
|
|
|
|
value, err := getNodeValue(trie, "k")
|
|
require.NoError(err)
|
|
require.Equal([]byte("1"), value)
|
|
|
|
value, err = getNodeValue(trie, "ke")
|
|
require.NoError(err)
|
|
require.Equal([]byte("2"), value)
|
|
|
|
value, err = getNodeValue(trie, "key1")
|
|
require.NoError(err)
|
|
require.Equal([]byte("3"), value)
|
|
|
|
value, err = getNodeValue(trie, "key123")
|
|
require.NoError(err)
|
|
require.Equal([]byte("4"), value)
|
|
}
|
|
|
|
func Test_Trie_BatchApply(t *testing.T) {
|
|
require := require.New(t)
|
|
|
|
dbTrie, err := getBasicDB()
|
|
require.NoError(err)
|
|
require.NotNil(dbTrie)
|
|
|
|
trie, err := dbTrie.NewView(
|
|
context.Background(),
|
|
ViewChanges{
|
|
BatchOps: []database.BatchOp{
|
|
{Key: []byte("key1"), Value: []byte("value1")},
|
|
{Key: []byte("key12"), Value: []byte("value12")},
|
|
{Key: []byte("key134"), Value: []byte("value134")},
|
|
{Key: []byte("key1"), Delete: true},
|
|
},
|
|
},
|
|
)
|
|
require.NoError(err)
|
|
require.NotNil(trie)
|
|
|
|
value, err := getNodeValue(trie, "key12")
|
|
require.NoError(err)
|
|
require.Equal([]byte("value12"), value)
|
|
|
|
value, err = getNodeValue(trie, "key134")
|
|
require.NoError(err)
|
|
require.Equal([]byte("value134"), value)
|
|
|
|
_, err = getNodeValue(trie, "key1")
|
|
require.ErrorIs(err, database.ErrNotFound)
|
|
}
|
|
|
|
func Test_Trie_ChainDeletion(t *testing.T) {
|
|
require := require.New(t)
|
|
|
|
trie, err := getBasicDB()
|
|
require.NoError(err)
|
|
require.NotNil(trie)
|
|
newTrie, err := trie.NewView(
|
|
context.Background(),
|
|
ViewChanges{
|
|
BatchOps: []database.BatchOp{
|
|
{Key: []byte("k"), Value: []byte("value0")},
|
|
{Key: []byte("ke"), Value: []byte("value1")},
|
|
{Key: []byte("key"), Value: []byte("value2")},
|
|
{Key: []byte("key1"), Value: []byte("value3")},
|
|
},
|
|
},
|
|
)
|
|
require.NoError(err)
|
|
|
|
require.NoError(newTrie.(*view).applyValueChanges(context.Background()))
|
|
maybeRoot := newTrie.getRoot()
|
|
require.NoError(err)
|
|
require.True(maybeRoot.HasValue())
|
|
require.Equal([]byte("value0"), maybeRoot.Value().value.Value())
|
|
require.Len(maybeRoot.Value().children, 1)
|
|
|
|
newTrie, err = newTrie.NewView(
|
|
context.Background(),
|
|
ViewChanges{
|
|
BatchOps: []database.BatchOp{
|
|
{Key: []byte("k"), Delete: true},
|
|
{Key: []byte("ke"), Delete: true},
|
|
{Key: []byte("key"), Delete: true},
|
|
{Key: []byte("key1"), Delete: true},
|
|
},
|
|
},
|
|
)
|
|
require.NoError(err)
|
|
require.NoError(newTrie.(*view).applyValueChanges(context.Background()))
|
|
|
|
// trie should be empty
|
|
root := newTrie.getRoot()
|
|
require.False(root.HasValue())
|
|
}
|
|
|
|
func Test_Trie_Invalidate_Siblings_On_Commit(t *testing.T) {
|
|
require := require.New(t)
|
|
|
|
dbTrie, err := getBasicDB()
|
|
require.NoError(err)
|
|
require.NotNil(dbTrie)
|
|
|
|
view1, err := dbTrie.NewView(context.Background(), ViewChanges{})
|
|
require.NoError(err)
|
|
|
|
view2, err := view1.NewView(
|
|
context.Background(),
|
|
ViewChanges{
|
|
BatchOps: []database.BatchOp{
|
|
{Key: []byte{0}, Value: []byte{0}},
|
|
},
|
|
},
|
|
)
|
|
require.NoError(err)
|
|
|
|
// Siblings of view2
|
|
sibling1, err := view1.NewView(context.Background(), ViewChanges{})
|
|
require.NoError(err)
|
|
sibling2, err := view1.NewView(context.Background(), ViewChanges{})
|
|
require.NoError(err)
|
|
|
|
require.False(sibling1.(*view).isInvalid())
|
|
require.False(sibling2.(*view).isInvalid())
|
|
|
|
require.NoError(view1.CommitToDB(context.Background()))
|
|
require.NoError(view2.CommitToDB(context.Background()))
|
|
|
|
require.True(sibling1.(*view).isInvalid())
|
|
require.True(sibling2.(*view).isInvalid())
|
|
require.False(view2.(*view).isInvalid())
|
|
}
|
|
|
|
func Test_Trie_NodeCollapse(t *testing.T) {
|
|
require := require.New(t)
|
|
|
|
dbTrie, err := getBasicDB()
|
|
require.NoError(err)
|
|
require.NotNil(dbTrie)
|
|
|
|
kvs := []database.BatchOp{
|
|
{Key: []byte("k"), Value: []byte("value0")},
|
|
{Key: []byte("ke"), Value: []byte("value1")},
|
|
{Key: []byte("key"), Value: []byte("value2")},
|
|
{Key: []byte("key1"), Value: []byte("value3")},
|
|
{Key: []byte("key2"), Value: []byte("value4")},
|
|
}
|
|
|
|
trie, err := dbTrie.NewView(
|
|
context.Background(),
|
|
ViewChanges{
|
|
BatchOps: kvs,
|
|
},
|
|
)
|
|
require.NoError(err)
|
|
|
|
require.NoError(trie.(*view).applyValueChanges(context.Background()))
|
|
|
|
for _, kv := range kvs {
|
|
node, err := trie.getEditableNode(ToKey(kv.Key), true)
|
|
require.NoError(err)
|
|
|
|
require.Equal(kv.Value, node.value.Value())
|
|
}
|
|
|
|
// delete some values
|
|
deletedKVs, remainingKVs := kvs[:3], kvs[3:]
|
|
deleteOps := make([]database.BatchOp, len(deletedKVs))
|
|
for i, kv := range deletedKVs {
|
|
deleteOps[i] = database.BatchOp{
|
|
Key: kv.Key,
|
|
Delete: true,
|
|
}
|
|
}
|
|
|
|
trie, err = trie.NewView(
|
|
context.Background(),
|
|
ViewChanges{
|
|
BatchOps: deleteOps,
|
|
},
|
|
)
|
|
require.NoError(err)
|
|
|
|
require.NoError(trie.(*view).applyValueChanges(context.Background()))
|
|
|
|
for _, kv := range deletedKVs {
|
|
_, err := trie.getEditableNode(ToKey(kv.Key), true)
|
|
require.ErrorIs(err, database.ErrNotFound)
|
|
}
|
|
|
|
// make sure the other values are still there
|
|
for _, kv := range remainingKVs {
|
|
node, err := trie.getEditableNode(ToKey(kv.Key), true)
|
|
require.NoError(err)
|
|
|
|
require.Equal(kv.Value, node.value.Value())
|
|
}
|
|
}
|
|
|
|
func Test_Trie_MultipleStates(t *testing.T) {
|
|
randCount := int64(0)
|
|
for _, commitApproach := range []string{"never", "before", "after"} {
|
|
t.Run(commitApproach, func(t *testing.T) {
|
|
require := require.New(t)
|
|
|
|
r := rand.New(rand.NewSource(randCount)) // #nosec G404
|
|
randCount++
|
|
rdb := memdb.New()
|
|
defer rdb.Close()
|
|
db, err := New(
|
|
context.Background(),
|
|
rdb,
|
|
NewConfig(),
|
|
)
|
|
require.NoError(err)
|
|
defer db.Close()
|
|
|
|
initialSet := 1000
|
|
// Populate initial set of keys
|
|
ops := make([]database.BatchOp, 0, initialSet)
|
|
require.NoError(err)
|
|
kv := [][]byte{}
|
|
for i := 0; i < initialSet; i++ {
|
|
k := []byte(strconv.Itoa(i))
|
|
kv = append(kv, k)
|
|
ops = append(ops, database.BatchOp{Key: k, Value: hash.ComputeHash256(k)})
|
|
}
|
|
root, err := db.NewView(
|
|
context.Background(),
|
|
ViewChanges{
|
|
BatchOps: ops,
|
|
},
|
|
)
|
|
require.NoError(err)
|
|
|
|
// Get initial root
|
|
_, err = root.GetMerkleRoot(context.Background())
|
|
require.NoError(err)
|
|
|
|
if commitApproach == "before" {
|
|
require.NoError(root.CommitToDB(context.Background()))
|
|
}
|
|
|
|
// Populate additional states
|
|
concurrentStates := []Trie{}
|
|
for i := 0; i < 5; i++ {
|
|
newState, err := root.NewView(context.Background(), ViewChanges{})
|
|
require.NoError(err)
|
|
concurrentStates = append(concurrentStates, newState)
|
|
}
|
|
|
|
if commitApproach == "after" {
|
|
require.NoError(root.CommitToDB(context.Background()))
|
|
}
|
|
|
|
// Process ops
|
|
newStart := initialSet
|
|
concurrentOps := make([][]database.BatchOp, len(concurrentStates))
|
|
for i := 0; i < 100; i++ {
|
|
if r.Intn(100) < 20 {
|
|
// New Key
|
|
for index := range concurrentStates {
|
|
k := []byte(strconv.Itoa(newStart))
|
|
concurrentOps[index] = append(concurrentOps[index], database.BatchOp{Key: k, Value: hash.ComputeHash256(k)})
|
|
}
|
|
newStart++
|
|
} else {
|
|
// Fetch and update old
|
|
selectedKey := kv[r.Intn(len(kv))]
|
|
var pastV []byte
|
|
for index, state := range concurrentStates {
|
|
v, err := state.GetValue(context.Background(), selectedKey)
|
|
require.NoError(err)
|
|
if pastV == nil {
|
|
pastV = v
|
|
} else {
|
|
require.Equal(pastV, v)
|
|
}
|
|
concurrentOps[index] = append(concurrentOps[index], database.BatchOp{Key: selectedKey, Value: hash.ComputeHash256(v)})
|
|
}
|
|
}
|
|
}
|
|
for index, state := range concurrentStates {
|
|
concurrentStates[index], err = state.NewView(
|
|
context.Background(),
|
|
ViewChanges{
|
|
BatchOps: concurrentOps[index],
|
|
},
|
|
)
|
|
require.NoError(err)
|
|
}
|
|
|
|
// Generate roots
|
|
var pastRoot ids.ID
|
|
for _, state := range concurrentStates {
|
|
mroot, err := state.GetMerkleRoot(context.Background())
|
|
require.NoError(err)
|
|
if pastRoot == ids.Empty {
|
|
pastRoot = mroot
|
|
} else {
|
|
require.Equal(pastRoot, mroot)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestNewViewOnCommittedView(t *testing.T) {
|
|
require := require.New(t)
|
|
|
|
db, err := getBasicDB()
|
|
require.NoError(err)
|
|
|
|
// Create a view
|
|
view1Intf, err := db.NewView(context.Background(), ViewChanges{BatchOps: []database.BatchOp{{Key: []byte{1}, Value: []byte{1}}}})
|
|
require.NoError(err)
|
|
require.IsType(&view{}, view1Intf)
|
|
view1 := view1Intf.(*view)
|
|
|
|
// view1
|
|
// |
|
|
// db
|
|
|
|
require.Len(db.childViews, 1)
|
|
require.Contains(db.childViews, view1)
|
|
require.Equal(db, view1.parentTrie)
|
|
|
|
// Commit the view
|
|
require.NoError(view1.CommitToDB(context.Background()))
|
|
|
|
// view1 (committed)
|
|
// |
|
|
// db
|
|
|
|
require.Len(db.childViews, 1)
|
|
require.Contains(db.childViews, view1)
|
|
require.Equal(db, view1.parentTrie)
|
|
|
|
// Create a new view on the committed view
|
|
view2Intf, err := view1.NewView(context.Background(), ViewChanges{})
|
|
require.NoError(err)
|
|
require.IsType(&view{}, view2Intf)
|
|
view2 := view2Intf.(*view)
|
|
|
|
// view2
|
|
// |
|
|
// view1 (committed)
|
|
// |
|
|
// db
|
|
|
|
require.Equal(db, view2.parentTrie)
|
|
require.Contains(db.childViews, view1)
|
|
require.Contains(db.childViews, view2)
|
|
require.Len(db.childViews, 2)
|
|
|
|
// Make sure the new view has the right value
|
|
got, err := view2.GetValue(context.Background(), []byte{1})
|
|
require.NoError(err)
|
|
require.Equal([]byte{1}, got)
|
|
|
|
// Make another view
|
|
view3Intf, err := view2.NewView(context.Background(), ViewChanges{})
|
|
require.NoError(err)
|
|
require.IsType(&view{}, view3Intf)
|
|
view3 := view3Intf.(*view)
|
|
|
|
// view3
|
|
// |
|
|
// view2
|
|
// |
|
|
// view1 (committed)
|
|
// |
|
|
// db
|
|
|
|
require.Equal(view2, view3.parentTrie)
|
|
require.Contains(view2.childViews, view3)
|
|
require.Len(view2.childViews, 1)
|
|
require.Contains(db.childViews, view1)
|
|
require.Contains(db.childViews, view2)
|
|
require.Len(db.childViews, 2)
|
|
|
|
// Commit view2
|
|
require.NoError(view2.CommitToDB(context.Background()))
|
|
|
|
// view3
|
|
// |
|
|
// view2 (committed)
|
|
// |
|
|
// view1 (committed)
|
|
// |
|
|
// db
|
|
|
|
// Note that view2 being committed invalidates view1
|
|
require.True(view1.invalidated)
|
|
require.Contains(db.childViews, view2)
|
|
require.Contains(db.childViews, view3)
|
|
require.Len(db.childViews, 2)
|
|
require.Equal(db, view3.parentTrie)
|
|
|
|
// Commit view3
|
|
require.NoError(view3.CommitToDB(context.Background()))
|
|
|
|
// view3 being committed invalidates view2
|
|
require.True(view2.invalidated)
|
|
require.Contains(db.childViews, view3)
|
|
require.Len(db.childViews, 1)
|
|
require.Equal(db, view3.parentTrie)
|
|
}
|
|
|
|
func Test_View_NewView(t *testing.T) {
|
|
require := require.New(t)
|
|
|
|
db, err := getBasicDB()
|
|
require.NoError(err)
|
|
|
|
// Create a view
|
|
view1Intf, err := db.NewView(context.Background(), ViewChanges{})
|
|
require.NoError(err)
|
|
require.IsType(&view{}, view1Intf)
|
|
view1 := view1Intf.(*view)
|
|
|
|
// Create a view atop view1
|
|
view2Intf, err := view1.NewView(context.Background(), ViewChanges{})
|
|
require.NoError(err)
|
|
require.IsType(&view{}, view2Intf)
|
|
view2 := view2Intf.(*view)
|
|
|
|
// view2
|
|
// |
|
|
// view1
|
|
// |
|
|
// db
|
|
|
|
// Assert view2's parent is view1
|
|
require.Equal(view1, view2.parentTrie)
|
|
require.Contains(view1.childViews, view2)
|
|
require.Len(view1.childViews, 1)
|
|
|
|
// Commit view1
|
|
require.NoError(view1.CommitToDB(context.Background()))
|
|
|
|
// Make another view atop view1
|
|
view3Intf, err := view1.NewView(context.Background(), ViewChanges{})
|
|
require.NoError(err)
|
|
require.IsType(&view{}, view3Intf)
|
|
view3 := view3Intf.(*view)
|
|
|
|
// view3
|
|
// |
|
|
// view2
|
|
// |
|
|
// view1
|
|
// |
|
|
// db
|
|
|
|
// Assert view3's parent is db
|
|
require.Equal(db, view3.parentTrie)
|
|
require.Contains(db.childViews, view3)
|
|
require.NotContains(view1.childViews, view3)
|
|
|
|
// Assert that NewPreallocatedView on an invalid view fails
|
|
invalidView := &view{invalidated: true}
|
|
_, err = invalidView.NewView(context.Background(), ViewChanges{})
|
|
require.ErrorIs(err, ErrInvalid)
|
|
}
|
|
|
|
func TestViewInvalidate(t *testing.T) {
|
|
require := require.New(t)
|
|
|
|
db, err := getBasicDB()
|
|
require.NoError(err)
|
|
|
|
// Create a view
|
|
view1Intf, err := db.NewView(context.Background(), ViewChanges{})
|
|
require.NoError(err)
|
|
require.IsType(&view{}, view1Intf)
|
|
view1 := view1Intf.(*view)
|
|
|
|
// Create 2 views atop view1
|
|
view2Intf, err := view1.NewView(context.Background(), ViewChanges{})
|
|
require.NoError(err)
|
|
require.IsType(&view{}, view2Intf)
|
|
view2 := view2Intf.(*view)
|
|
|
|
view3Intf, err := view1.NewView(context.Background(), ViewChanges{})
|
|
require.NoError(err)
|
|
require.IsType(&view{}, view3Intf)
|
|
view3 := view3Intf.(*view)
|
|
|
|
// view2 view3
|
|
// | /
|
|
// view1
|
|
// |
|
|
// db
|
|
|
|
// Invalidate view1
|
|
view1.invalidate()
|
|
|
|
require.Empty(view1.childViews)
|
|
require.True(view1.invalidated)
|
|
require.True(view2.invalidated)
|
|
require.True(view3.invalidated)
|
|
}
|
|
|
|
func Test_Trie_ConcurrentNewViewAndCommit(t *testing.T) {
|
|
require := require.New(t)
|
|
|
|
trie, err := getBasicDB()
|
|
require.NoError(err)
|
|
require.NotNil(trie)
|
|
|
|
newTrie, err := trie.NewView(
|
|
context.Background(),
|
|
ViewChanges{
|
|
BatchOps: []database.BatchOp{
|
|
{Key: []byte("key"), Value: []byte("value0")},
|
|
},
|
|
},
|
|
)
|
|
require.NoError(err)
|
|
|
|
eg := errgroup.Group{}
|
|
eg.Go(func() error {
|
|
return newTrie.CommitToDB(context.Background())
|
|
})
|
|
|
|
view, err := newTrie.NewView(context.Background(), ViewChanges{})
|
|
require.NoError(err)
|
|
require.NotNil(view)
|
|
|
|
require.NoError(eg.Wait())
|
|
}
|
|
|
|
// Returns the path of the only child of this node.
|
|
// Assumes this node has exactly one child.
|
|
func getSingleChildKey(n *node, tokenSize int) Key {
|
|
for index, entry := range n.children {
|
|
return n.key.Extend(ToToken(index, tokenSize), entry.compressedKey)
|
|
}
|
|
return Key{}
|
|
}
|
|
|
|
func TestTrieCommitToDBInvalid(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
trieFunc func(*require.Assertions, *merkleDB) View
|
|
expectedErr error
|
|
}{
|
|
{
|
|
name: "invalid",
|
|
trieFunc: func(require *require.Assertions, db *merkleDB) View {
|
|
nView, err := db.NewView(context.Background(), ViewChanges{})
|
|
require.NoError(err)
|
|
|
|
// Invalidate the view
|
|
nView.(*view).invalidate()
|
|
return nView
|
|
},
|
|
expectedErr: ErrInvalid,
|
|
},
|
|
{
|
|
name: "committed",
|
|
trieFunc: func(require *require.Assertions, db *merkleDB) View {
|
|
view, err := db.NewView(context.Background(), ViewChanges{})
|
|
require.NoError(err)
|
|
|
|
// Commit the view
|
|
require.NoError(view.CommitToDB(context.Background()))
|
|
return view
|
|
},
|
|
expectedErr: ErrCommitted,
|
|
},
|
|
{
|
|
name: "parent not database",
|
|
trieFunc: func(require *require.Assertions, db *merkleDB) View {
|
|
nView, err := db.NewView(context.Background(), ViewChanges{})
|
|
require.NoError(err)
|
|
|
|
// Change the parent
|
|
nView.(*view).parentTrie = &view{}
|
|
return nView
|
|
},
|
|
expectedErr: ErrParentNotDatabase,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
require := require.New(t)
|
|
|
|
// Make a database
|
|
db, err := getBasicDB()
|
|
require.NoError(err)
|
|
|
|
trie := tt.trieFunc(require, db)
|
|
err = trie.CommitToDB(context.Background())
|
|
require.ErrorIs(err, tt.expectedErr)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestTrieCommitToDBValid(t *testing.T) {
|
|
require := require.New(t)
|
|
|
|
// Make a database
|
|
db, err := getBasicDB()
|
|
require.NoError(err)
|
|
|
|
// Put 2 key-value pairs
|
|
key1, value1 := []byte("key1"), []byte("value1")
|
|
key2, value2 := []byte("key2"), []byte("value2")
|
|
require.NoError(db.Put(key1, value1))
|
|
require.NoError(db.Put(key2, value2))
|
|
|
|
// Make a view
|
|
key3, value3 := []byte("key3"), []byte("value3")
|
|
// Delete a key-value pair, modify a key-value pair,
|
|
// and insert a new key-value pair
|
|
view, err := db.NewView(
|
|
context.Background(),
|
|
ViewChanges{
|
|
BatchOps: []database.BatchOp{
|
|
{Key: key1, Delete: true},
|
|
{Key: key2, Value: value3},
|
|
{Key: key3, Value: value3},
|
|
},
|
|
},
|
|
)
|
|
require.NoError(err)
|
|
|
|
// Commit the view
|
|
require.NoError(view.CommitToDB(context.Background()))
|
|
|
|
// Make sure the database has the right values
|
|
_, err = db.Get(key1)
|
|
require.ErrorIs(err, database.ErrNotFound)
|
|
|
|
got, err := db.Get(key2)
|
|
require.NoError(err)
|
|
require.Equal(value3, got)
|
|
|
|
got, err = db.Get(key3)
|
|
require.NoError(err)
|
|
require.Equal(value3, got)
|
|
}
|