From f849daf059a33fdd633581384277c18043163dfc Mon Sep 17 00:00:00 2001 From: i-norden Date: Wed, 12 Jun 2024 13:13:17 -0400 Subject: [PATCH 01/18] Support a legacy db key format and further functionality required to operate in a pure-preimage query mode --- import.go | 60 ++++++++++++++++++----- mutable_tree.go | 120 +++++++++++++++++++++++++++++++++++++++------- node.go | 56 ++++++++++++++++++++-- nodedb.go | 71 ++++++++++++++++++++++----- testutils_test.go | 4 +- 5 files changed, 267 insertions(+), 44 deletions(-) diff --git a/import.go b/import.go index 56566feae..364e33b55 100644 --- a/import.go +++ b/import.go @@ -68,15 +68,28 @@ func (i *Importer) writeNode(node *Node) error { buf.Reset() defer bufPool.Put(buf) - if err := node.writeBytes(buf); err != nil { - return err + if i.tree.useLegacyFormat { + if err := node.writeLegacyBytes(buf); err != nil { + return err + } + } else { + if err := node.writeBytes(buf); err != nil { + return err + } } bytesCopy := make([]byte, buf.Len()) copy(bytesCopy, buf.Bytes()) - if err := i.batch.Set(i.tree.ndb.nodeKey(node.GetKey()), bytesCopy); err != nil { - return err + if i.tree.useLegacyFormat { + node.isLegacy = true + if err := i.batch.Set(i.tree.ndb.legacyNodeKey(node.GetKey()), bytesCopy); err != nil { + return err + } + } else { + if err := i.batch.Set(i.tree.ndb.nodeKey(node.GetKey()), bytesCopy); err != nil { + return err + } } i.batchSize++ @@ -193,10 +206,18 @@ func (i *Importer) Commit() error { return ErrNoImport } + var rootHash []byte switch len(i.stack) { case 0: - if err := i.batch.Set(i.tree.ndb.nodeKey(GetRootKey(i.version)), []byte{}); err != nil { - return err + if i.tree.useLegacyFormat { + rootHash = []byte{} + if err := i.batch.Set(i.tree.ndb.legacyNodeKey(GetRootKey(i.version)), []byte{}); err != nil { + return err + } + } else { + if err := i.batch.Set(i.tree.ndb.nodeKey(GetRootKey(i.version)), []byte{}); err != nil { + return err + } } case 1: i.stack[0].nodeKey.nonce = 1 @@ -204,8 +225,18 @@ func (i *Importer) Commit() error { return err } if i.stack[0].nodeKey.version < i.version { // it means there is no update in the given version - if err := i.batch.Set(i.tree.ndb.nodeKey(GetRootKey(i.version)), i.tree.ndb.nodeKey(i.stack[0].nodeKey.GetKey())); err != nil { - return err + if i.tree.useLegacyFormat { + if len(i.stack[0].hash) == 0 { + i.stack[0]._hash(i.version) + } + rootHash = i.stack[0].hash + if err := i.batch.Set(i.tree.ndb.legacyNodeKey(GetRootKey(i.version)), i.tree.ndb.legacyNodeKey(rootHash)); err != nil { + return err + } + } else { + if err := i.batch.Set(i.tree.ndb.nodeKey(GetRootKey(i.version)), i.tree.ndb.nodeKey(i.stack[0].nodeKey.GetKey())); err != nil { + return err + } } } default: @@ -219,9 +250,16 @@ func (i *Importer) Commit() error { } i.tree.ndb.resetLatestVersion(i.version) - _, err = i.tree.LoadVersion(i.version) - if err != nil { - return err + if i.tree.useLegacyFormat { + _, err = i.tree.LoadVersionByRootHash(i.version, rootHash) + if err != nil { + return err + } + } else { + _, err = i.tree.LoadVersion(i.version) + if err != nil { + return err + } } i.Close() diff --git a/mutable_tree.go b/mutable_tree.go index e71253852..12eba17a8 100644 --- a/mutable_tree.go +++ b/mutable_tree.go @@ -42,6 +42,8 @@ type MutableTree struct { unsavedFastNodeRemovals *sync.Map // map[string]interface{} FastNodes that have not yet been removed from disk ndb *nodeDB skipFastStorageUpgrade bool // If true, the tree will work like no fast storage and always not upgrade fast storage + useLegacyFormat bool // If true, save nodes to the DB with the legacy format + rootHash []byte mtx sync.Mutex } @@ -67,6 +69,30 @@ func NewMutableTree(db dbm.DB, cacheSize int, skipFastStorageUpgrade bool, lg lo } } +func NewLegacyMutableTree(db dbm.DB, cacheSize int, skipFastStorageUpgrade, noStoreVersion bool, + rootHash []byte, lg log.Logger, options ...Option, +) *MutableTree { + opts := DefaultOptions() + for _, opt := range options { + opt(&opts) + } + + ndb := newLegacyNodeDB(db, cacheSize, opts, noStoreVersion, lg) + head := &ImmutableTree{ndb: ndb, skipFastStorageUpgrade: skipFastStorageUpgrade} + + return &MutableTree{ + logger: lg, + ImmutableTree: head, + lastSaved: head.clone(), + unsavedFastNodeAdditions: &sync.Map{}, + unsavedFastNodeRemovals: &sync.Map{}, + ndb: ndb, + skipFastStorageUpgrade: skipFastStorageUpgrade, + useLegacyFormat: true, + rootHash: rootHash, + } +} + // IsEmpty returns whether or not the tree has any keys. Only trees that are // not empty can be saved. func (tree *MutableTree) IsEmpty() bool { @@ -170,7 +196,15 @@ func (tree *MutableTree) Set(key, value []byte) (updated bool, err error) { // The returned value must not be modified, since it may point to data stored within IAVL. func (tree *MutableTree) Get(key []byte) ([]byte, error) { if tree.root == nil { - return nil, nil + if tree.rootHash != nil { + root, err := tree.ndb.GetNode(tree.rootHash) + if err != nil { + return nil, err + } + tree.root = root + } else { + return nil, nil + } } if !tree.skipFastStorageUpgrade { @@ -253,7 +287,7 @@ func (tree *MutableTree) set(key []byte, value []byte) (updated bool, err error) if !tree.skipFastStorageUpgrade { tree.addUnsavedAddition(key, fastnode.NewNode(key, value, tree.version+1)) } - tree.ImmutableTree.root = NewNode(key, value) + tree.ImmutableTree.root = NewNode(key, value, tree.useLegacyFormat) return updated, nil } @@ -312,7 +346,7 @@ func (tree *MutableTree) recursiveSetLeaf(node *Node, key []byte, value []byte) subtreeHeight: 1, size: 2, nodeKey: nil, - leftNode: NewNode(key, value), + leftNode: NewNode(key, value, tree.useLegacyFormat), rightNode: node, }, false, nil case 1: // setKey > leafKey @@ -322,10 +356,10 @@ func (tree *MutableTree) recursiveSetLeaf(node *Node, key []byte, value []byte) size: 2, nodeKey: nil, leftNode: node, - rightNode: NewNode(key, value), + rightNode: NewNode(key, value, tree.useLegacyFormat), }, false, nil default: - return NewNode(key, value), true, nil + return NewNode(key, value, tree.useLegacyFormat), true, nil } } @@ -510,6 +544,41 @@ func (tree *MutableTree) LoadVersion(targetVersion int64) (int64, error) { return latestVersion, nil } +// LoadVersionByRootHash loads a tree using the provided version and roothash +func (tree *MutableTree) LoadVersionByRootHash(version int64, rootHash []byte) (int64, error) { + if len(rootHash) == 0 { + return 0, errors.New("LoadVersionByRootHash must be provided a non-empty rootHash argument") + } + + tree.mtx.Lock() + defer tree.mtx.Unlock() + + t := &ImmutableTree{ + ndb: tree.ndb, + version: version, + skipFastStorageUpgrade: tree.skipFastStorageUpgrade, + } + + var err error + t.root, err = tree.ndb.GetNode(rootHash) + if err != nil { + return 0, err + } + + tree.ImmutableTree = t + tree.lastSaved = t.clone() + tree.rootHash = rootHash + + if !tree.skipFastStorageUpgrade { + // Attempt to upgrade + if _, err := tree.enableFastStorageAndCommitIfNotEnabled(); err != nil { + return 0, err + } + } + + return version, nil +} + // loadVersionForOverwriting attempts to load a tree at a previously committed // version, or the latest version below it. Any versions greater than targetVersion will be deleted. func (tree *MutableTree) LoadVersionForOverwriting(targetVersion int64) error { @@ -738,17 +807,30 @@ func (tree *MutableTree) SaveVersion() ([]byte, int64, error) { } else { if tree.root.nodeKey != nil { // it means there are no updated nodes - if err := tree.ndb.SaveRoot(version, tree.root.nodeKey); err != nil { - return nil, 0, err - } - // it means the reference node is a legacy node - if tree.root.isLegacy { - // it will update the legacy node to the new format - // which ensures the reference node is not a legacy node - tree.root.isLegacy = false + if tree.useLegacyFormat { + if len(tree.root.hash) == 0 { + tree.root._hash(version) + } + if err := tree.ndb.SaveLegacyRoot(version, tree.root.hash); err != nil { + return nil, 0, err + } + tree.root.isLegacy = true if err := tree.ndb.SaveNode(tree.root); err != nil { return nil, 0, fmt.Errorf("failed to save the reference legacy node: %w", err) } + } else { + if err := tree.ndb.SaveRoot(version, tree.root.nodeKey); err != nil { + return nil, 0, err + } + // it means the reference node is a legacy node + if tree.root.isLegacy { + // it will update the legacy node to the new format + // which ensures the reference node is not a legacy node + tree.root.isLegacy = false + if err := tree.ndb.SaveNode(tree.root); err != nil { + return nil, 0, fmt.Errorf("failed to save the reference legacy node: %w", err) + } + } } } else { if err := tree.saveNewNodes(version); err != nil { @@ -772,7 +854,9 @@ func (tree *MutableTree) SaveVersion() ([]byte, int64, error) { tree.unsavedFastNodeRemovals = &sync.Map{} } - return tree.Hash(), version, nil + hash := tree.Hash() + tree.rootHash = hash + return hash, version, nil } func (tree *MutableTree) saveFastNodeVersion(latestVersion int64) error { @@ -1010,12 +1094,14 @@ func (tree *MutableTree) saveNewNodes(version int64) error { newNodes := make([]*Node, 0) var recursiveAssignKey func(*Node) ([]byte, error) recursiveAssignKey = func(node *Node) ([]byte, error) { - if node.nodeKey != nil { + node.isLegacy = tree.useLegacyFormat + if (!node.isLegacy && node.nodeKey != nil) || (node.isLegacy && node.hash != nil) { if node.nodeKey.nonce != 0 { - return node.nodeKey.GetKey(), nil + return node.GetKey(), nil } return node.hash, nil } + nonce++ node.nodeKey = &NodeKey{ version: version, @@ -1038,7 +1124,7 @@ func (tree *MutableTree) saveNewNodes(version int64) error { node._hash(version) newNodes = append(newNodes, node) - return node.nodeKey.GetKey(), nil + return node.GetKey(), nil } if _, err := recursiveAssignKey(tree.root); err != nil { diff --git a/node.go b/node.go index e71e415ba..905528ffd 100644 --- a/node.go +++ b/node.go @@ -77,12 +77,13 @@ type Node struct { var _ cache.Node = (*Node)(nil) // NewNode returns a new node from a key, value and version. -func NewNode(key []byte, value []byte) *Node { +func NewNode(key []byte, value []byte, useLegacy bool) *Node { return &Node{ key: key, value: value, subtreeHeight: 0, size: 1, + isLegacy: useLegacy, } } @@ -243,11 +244,9 @@ func MakeLegacyNode(hash, buf []byte) (*Node, error) { nodeKey: &NodeKey{version: ver}, key: key, hash: hash, - isLegacy: true, } // Read node body. - if node.isLeaf() { val, _, err := encoding.DecodeBytes(buf) if err != nil { @@ -612,6 +611,9 @@ func (node *Node) writeBytes(w io.Writer) error { if node.leftNodeKey == nil { return ErrLeftNodeKeyEmpty } + if node.rightNodeKey == nil { + return ErrRightNodeKeyEmpty + } // check if children NodeKeys are legacy mode if len(node.leftNodeKey) == hashSize { mode += ModeLegacyLeftNode @@ -662,6 +664,54 @@ func (node *Node) writeBytes(w io.Writer) error { return nil } +func (node *Node) writeLegacyBytes(w io.Writer) error { + if node == nil { + return errors.New("cannot write nil node") + } + err := encoding.EncodeVarint(w, int64(node.subtreeHeight)) + if err != nil { + return fmt.Errorf("writing height, %w", err) + } + err = encoding.EncodeVarint(w, node.size) + if err != nil { + return fmt.Errorf("writing size, %w", err) + } + err = encoding.EncodeVarint(w, node.nodeKey.version) + if err != nil { + return fmt.Errorf("writing version, %w", err) + } + + // Unlike writeHashBytes, key is written for inner nodes. + err = encoding.EncodeBytes(w, node.key) + if err != nil { + return fmt.Errorf("writing key, %w", err) + } + + if node.isLeaf() { + err = encoding.EncodeBytes(w, node.value) + if err != nil { + return fmt.Errorf("writing value, %w", err) + } + } else { + if len(node.leftNodeKey) != hashSize { + return errors.New("node provided to writeLegacyBytes does not have a hash for leftNodeKey") + } + err = encoding.EncodeBytes(w, node.leftNodeKey) + if err != nil { + return fmt.Errorf("writing left hash, %w", err) + } + + if len(node.leftNodeKey) != 32 { + return errors.New("node provided to writeLegacyBytes does not have a hash for rightNodeKey") + } + err = encoding.EncodeBytes(w, node.rightNodeKey) + if err != nil { + return fmt.Errorf("writing right hash, %w", err) + } + } + return nil +} + func (node *Node) getLeftNode(t *ImmutableTree) (*Node, error) { if node.leftNode != nil { return node.leftNode, nil diff --git a/nodedb.go b/nodedb.go index f3c1f06c3..58e865b9a 100644 --- a/nodedb.go +++ b/nodedb.go @@ -90,10 +90,11 @@ type nodeDB struct { legacyLatestVersion int64 // Latest version of nodeDB in legacy format. nodeCache cache.Cache // Cache for nodes in the regular tree that consists of key-value pairs at any version. fastNodeCache cache.Cache // Cache for nodes in the fast index that represents only key-value pairs at the latest version. + useLegacyFormat bool } func newNodeDB(db dbm.DB, cacheSize int, opts Options, lg log.Logger) *nodeDB { - storeVersion, err := db.Get(metadataKeyFormat.Key([]byte(storageVersionKey))) + storeVersion, err := db.Get(metadataKeyFormat.Key(ibytes.UnsafeStrToBytes(storageVersionKey))) if err != nil || storeVersion == nil { storeVersion = []byte(defaultStorageVersionValue) @@ -114,6 +115,34 @@ func newNodeDB(db dbm.DB, cacheSize int, opts Options, lg log.Logger) *nodeDB { } } +func newLegacyNodeDB(db dbm.DB, cacheSize int, opts Options, noStoreVersion bool, lg log.Logger) *nodeDB { + var storeVersion []byte + if noStoreVersion { + storeVersion = []byte(defaultStorageVersionValue) + } else { + var err error + storeVersion, err = db.Get(metadataKeyFormat.Key(ibytes.UnsafeStrToBytes(storageVersionKey))) + if err != nil || storeVersion == nil { + storeVersion = []byte(defaultStorageVersionValue) + } + } + + return &nodeDB{ + logger: lg, + db: db, + batch: NewBatchWithFlusher(db, opts.FlushThreshold), + opts: opts, + firstVersion: 0, + latestVersion: 0, // initially invalid + legacyLatestVersion: 0, + nodeCache: cache.New(cacheSize), + fastNodeCache: cache.New(fastNodeCacheSize), + versionReaders: make(map[int64]uint32, 8), + storageVersion: string(storeVersion), + useLegacyFormat: true, + } +} + // GetNode gets a node from memory or disk. If it is an inner node, it does not // load its children. // It is used for both formats of nodes: legacy and new. @@ -135,9 +164,9 @@ func (ndb *nodeDB) GetNode(nk []byte) (*Node, error) { ndb.opts.Stat.IncCacheMissCnt() // Doesn't exist, load. - isLegcyNode := len(nk) == hashSize + isLegacyNode := len(nk) == hashSize var nodeKey []byte - if isLegcyNode { + if isLegacyNode { nodeKey = ndb.legacyNodeKey(nk) } else { nodeKey = ndb.nodeKey(nk) @@ -151,11 +180,12 @@ func (ndb *nodeDB) GetNode(nk []byte) (*Node, error) { } var node *Node - if isLegcyNode { + if isLegacyNode { node, err = MakeLegacyNode(nk, buf) if err != nil { return nil, fmt.Errorf("error reading Legacy Node. bytes: %x, error: %v", buf, err) } + node.isLegacy = ndb.useLegacyFormat } else { node, err = MakeNode(nk, buf) if err != nil { @@ -209,7 +239,7 @@ func (ndb *nodeDB) SaveNode(node *Node) error { ndb.mtx.Lock() defer ndb.mtx.Unlock() - if node.nodeKey == nil { + if node.nodeKey == nil || (ndb.useLegacyFormat && node.hash == nil) { return ErrNodeMissingNodeKey } @@ -217,12 +247,21 @@ func (ndb *nodeDB) SaveNode(node *Node) error { var buf bytes.Buffer buf.Grow(node.encodedSize()) - if err := node.writeBytes(&buf); err != nil { - return err - } - - if err := ndb.batch.Set(ndb.nodeKey(node.GetKey()), buf.Bytes()); err != nil { - return err + nk := node.GetKey() + if len(nk) == hashSize { + if err := node.writeLegacyBytes(&buf); err != nil { + return err + } + if err := ndb.batch.Set(ndb.legacyNodeKey(nk), buf.Bytes()); err != nil { + return err + } + } else { + if err := node.writeBytes(&buf); err != nil { + return err + } + if err := ndb.batch.Set(ndb.nodeKey(nk), buf.Bytes()); err != nil { + return err + } } ndb.logger.Debug("BATCH SAVE", "node", node) @@ -328,6 +367,9 @@ func (ndb *nodeDB) saveFastNodeUnlocked(node *fastnode.Node, shouldAddToCache bo // Has checks if a node key exists in the database. func (ndb *nodeDB) Has(nk []byte) (bool, error) { + if len(nk) == hashSize { + return ndb.db.Has(ndb.legacyNodeKey(nk)) + } return ndb.db.Has(ndb.nodeKey(nk)) } @@ -828,6 +870,13 @@ func (ndb *nodeDB) SaveRoot(version int64, nk *NodeKey) error { return ndb.batch.Set(nodeKeyFormat.Key(GetRootKey(version)), nodeKeyFormat.Key(nk.GetKey())) } +// SaveLegacyRoot saves the root when no updates. +func (ndb *nodeDB) SaveLegacyRoot(version int64, key []byte) error { + ndb.mtx.Lock() + defer ndb.mtx.Unlock() + return ndb.batch.Set(legacyNodeKeyFormat.Key(GetRootKey(version)), legacyNodeKeyFormat.Key(key)) +} + // Traverse fast nodes and return error if any, nil otherwise func (ndb *nodeDB) traverseFastNodes(fn func(k, v []byte) error) error { return ndb.traversePrefix(fastKeyFormat.Key(), fn) diff --git a/testutils_test.go b/testutils_test.go index f150873f3..638126c6f 100644 --- a/testutils_test.go +++ b/testutils_test.go @@ -53,12 +53,12 @@ func N(l, r interface{}) *Node { if _, ok := l.(*Node); ok { left = l.(*Node) } else { - left = NewNode(i2b(l.(int)), nil) + left = NewNode(i2b(l.(int)), nil, false) } if _, ok := r.(*Node); ok { right = r.(*Node) } else { - right = NewNode(i2b(r.(int)), nil) + right = NewNode(i2b(r.(int)), nil, false) } n := &Node{ From b084dbeb90b51287609c1b281fac2d19a2cfa525 Mon Sep 17 00:00:00 2001 From: i-norden Date: Thu, 20 Jun 2024 17:54:53 -0400 Subject: [PATCH 02/18] mutable_tree tests for legacy mode + fix in nodedb root hash saving --- mutable_tree_test.go | 1560 ++++++++++++++++++++++++++++++++++++++++-- nodedb.go | 17 +- testutils_test.go | 15 + 3 files changed, 1514 insertions(+), 78 deletions(-) diff --git a/mutable_tree_test.go b/mutable_tree_test.go index 6e58843ff..f6c065525 100644 --- a/mutable_tree_test.go +++ b/mutable_tree_test.go @@ -39,6 +39,12 @@ func setupMutableTree(skipFastStorageUpgrade bool) *MutableTree { return tree } +func setupLegacyMutableTree(skipFastStorageUpgrade bool) *MutableTree { + memDB := dbm.NewMemDB() + tree := NewLegacyMutableTree(memDB, 0, skipFastStorageUpgrade, true, nil, log.NewNopLogger()) + return tree +} + // TestIterateConcurrency throws "fatal error: concurrent map writes" when fast node is enabled func TestIterateConcurrency(t *testing.T) { if testing.Short() { @@ -62,6 +68,29 @@ func TestIterateConcurrency(t *testing.T) { wg.Wait() } +// TestLegacyIterateConcurrency throws "fatal error: concurrent map writes" when fast node is enabled +func TestLegacyIterateConcurrency(t *testing.T) { + if testing.Short() { + t.Skip("skipping test in short mode.") + } + tree := setupLegacyMutableTree(true) + wg := new(sync.WaitGroup) + for i := 0; i < 100; i++ { + for j := 0; j < maxIterator; j++ { + wg.Add(1) + go func(i, j int) { + defer wg.Done() + _, err := tree.Set([]byte(fmt.Sprintf("%d%d", i, j)), iavlrand.RandBytes(1)) + require.NoError(t, err) + }(i, j) + } + tree.Iterate(func(_, _ []byte) bool { //nolint:errcheck + return false + }) + } + wg.Wait() +} + // TestConcurrency throws "fatal error: concurrent map iteration and map write" and // also sometimes "fatal error: concurrent map writes" when fast node is enabled func TestIteratorConcurrency(t *testing.T) { @@ -89,6 +118,33 @@ func TestIteratorConcurrency(t *testing.T) { wg.Wait() } +// TestLegacyIteratorConcurrency throws "fatal error: concurrent map iteration and map write" and +// also sometimes "fatal error: concurrent map writes" when fast node is enabled +func TestLegacyIteratorConcurrency(t *testing.T) { + if testing.Short() { + t.Skip("skipping test in short mode.") + } + tree := setupLegacyMutableTree(true) + _, err := tree.LoadVersion(0) + require.NoError(t, err) + // So much slower + wg := new(sync.WaitGroup) + for i := 0; i < 100; i++ { + for j := 0; j < maxIterator; j++ { + wg.Add(1) + go func(i, j int) { + defer wg.Done() + _, err := tree.Set([]byte(fmt.Sprintf("%d%d", i, j)), iavlrand.RandBytes(1)) + require.NoError(t, err) + }(i, j) + } + itr, _ := tree.Iterator(nil, nil, true) + for ; itr.Valid(); itr.Next() { //nolint:revive + } // do nothing + } + wg.Wait() +} + // TestNewIteratorConcurrency throws "fatal error: concurrent map writes" when fast node is enabled func TestNewIteratorConcurrency(t *testing.T) { if testing.Short() { @@ -112,6 +168,29 @@ func TestNewIteratorConcurrency(t *testing.T) { } } +// TestNewLegacyIteratorConcurrency throws "fatal error: concurrent map writes" when fast node is enabled +func TestNewLegacyIteratorConcurrency(t *testing.T) { + if testing.Short() { + t.Skip("skipping test in short mode.") + } + tree := setupLegacyMutableTree(true) + for i := 0; i < 100; i++ { + wg := new(sync.WaitGroup) + it := NewIterator(nil, nil, true, tree.ImmutableTree) + for j := 0; j < maxIterator; j++ { + wg.Add(1) + go func(i, j int) { + defer wg.Done() + _, err := tree.Set([]byte(fmt.Sprintf("%d%d", i, j)), iavlrand.RandBytes(1)) + require.NoError(t, err) + }(i, j) + } + for ; it.Valid(); it.Next() { //nolint:revive + } // do nothing + wg.Wait() + } +} + func TestDelete(t *testing.T) { tree := setupMutableTree(false) @@ -133,6 +212,27 @@ func TestDelete(t *testing.T) { require.Equal(t, 0, bytes.Compare([]byte("Fred"), proof.GetExist().Value)) } +func TestLegacyDelete(t *testing.T) { + tree := setupLegacyMutableTree(false) + + _, err := tree.set([]byte("k1"), []byte("Fred")) + require.NoError(t, err) + _, version, err := tree.SaveVersion() + require.NoError(t, err) + _, _, err = tree.SaveVersion() + require.NoError(t, err) + + require.NoError(t, tree.DeleteVersionsTo(version)) + + proof, err := tree.GetVersionedProof([]byte("k1"), version) + require.EqualError(t, err, ErrVersionDoesNotExist.Error()) + require.Nil(t, proof) + + proof, err = tree.GetVersionedProof([]byte("k1"), version+1) + require.Nil(t, err) + require.Equal(t, 0, bytes.Compare([]byte("Fred"), proof.GetExist().Value)) +} + func TestGetRemove(t *testing.T) { require := require.New(t) tree := setupMutableTree(false) @@ -174,6 +274,47 @@ func TestGetRemove(t *testing.T) { testGet(false) } +func TestLegacyGetRemove(t *testing.T) { + require := require.New(t) + tree := setupLegacyMutableTree(false) + testGet := func(exists bool) { + v, err := tree.Get(tKey1) + require.NoError(err) + if exists { + require.Equal(tVal1, v, "key should exist") + } else { + require.Nil(v, "key should not exist") + } + } + + testGet(false) + + ok, err := tree.Set(tKey1, tVal1) + require.NoError(err) + require.False(ok, "new key set: nothing to update") + + // add second key to avoid tree.root removal + ok, err = tree.Set(tKey2, tVal2) + require.NoError(err) + require.False(ok, "new key set: nothing to update") + + testGet(true) + + // Save to tree.ImmutableTree + _, version, err := tree.SaveVersion() + require.NoError(err) + require.Equal(int64(1), version) + + testGet(true) + + v, ok, err := tree.Remove(tKey1) + require.NoError(err) + require.True(ok, "key should be removed") + require.Equal(tVal1, v, "key should exist") + + testGet(false) +} + func TestTraverse(t *testing.T) { tree := setupMutableTree(false) @@ -185,6 +326,17 @@ func TestTraverse(t *testing.T) { require.Equal(t, 11, tree.nodeSize(), "Size of tree unexpected") } +func TestLegacyTraverse(t *testing.T) { + tree := setupLegacyMutableTree(false) + + for i := 0; i < 6; i++ { + _, err := tree.set([]byte(fmt.Sprintf("k%d", i)), []byte(fmt.Sprintf("v%d", i))) + require.NoError(t, err) + } + + require.Equal(t, 11, tree.nodeSize(), "Size of tree unexpected") +} + func TestMutableTree_DeleteVersionsTo(t *testing.T) { tree := setupMutableTree(false) @@ -240,6 +392,65 @@ func TestMutableTree_DeleteVersionsTo(t *testing.T) { } } +func TestLegacyMutableTree_DeleteVersionsTo(t *testing.T) { + tree := setupLegacyMutableTree(false) + + type entry struct { + key []byte + value []byte + } + + versionEntries := make(map[int64][]entry) + + // create 10 tree versions, each with 1000 random key/value entries + for i := 0; i < 10; i++ { + entries := make([]entry, 1000) + + for j := 0; j < 1000; j++ { + k := iavlrand.RandBytes(10) + v := iavlrand.RandBytes(10) + + entries[j] = entry{k, v} + _, err := tree.Set(k, v) + require.NoError(t, err) + } + + _, v, err := tree.SaveVersion() + require.NoError(t, err) + + versionEntries[v] = entries + } + + // delete even versions + versionToDelete := int64(8) + println("tree version:") + println(tree.version) + println("using legacy version:") + println(tree.useLegacyFormat) + require.NoError(t, tree.DeleteVersionsTo(versionToDelete)) + + // ensure even versions have been deleted + for v := int64(1); v <= versionToDelete; v++ { + _, err := tree.LoadVersion(v) + require.Error(t, err) + } + + // ensure odd number versions exist and we can query for all set entries + for _, v := range []int64{9, 10} { + _, err := tree.LoadVersion(v) + require.NoError(t, err) + + for _, e := range versionEntries[v] { + val, err := tree.Get(e.key) + require.NoError(t, err) + if !bytes.Equal(e.value, val) { + t.Log(val) + } + // require.Equal(t, e.value, val) + } + } +} + func TestMutableTree_LoadVersion_Empty(t *testing.T) { tree := setupMutableTree(false) @@ -255,6 +466,21 @@ func TestMutableTree_LoadVersion_Empty(t *testing.T) { require.Error(t, err) } +func TestLegacyMutableTree_LoadVersion_Empty(t *testing.T) { + tree := setupLegacyMutableTree(false) + + version, err := tree.LoadVersion(0) + require.NoError(t, err) + assert.EqualValues(t, 0, version) + + version, err = tree.LoadVersion(-1) + require.NoError(t, err) + assert.EqualValues(t, 0, version) + + _, err = tree.LoadVersion(3) + require.Error(t, err) +} + func TestMutableTree_InitialVersion(t *testing.T) { memDB := dbm.NewMemDB() tree := NewMutableTree(memDB, 0, false, log.NewNopLogger(), InitialVersionOption(9)) @@ -295,6 +521,46 @@ func TestMutableTree_InitialVersion(t *testing.T) { assert.EqualValues(t, 11, version) } +func TestLegacyMutableTree_InitialVersion(t *testing.T) { + memDB := dbm.NewMemDB() + tree := NewLegacyMutableTree(memDB, 0, false, false, nil, log.NewNopLogger(), InitialVersionOption(9)) + + _, err := tree.Set([]byte("a"), []byte{0x01}) + require.NoError(t, err) + _, version, err := tree.SaveVersion() + require.NoError(t, err) + assert.EqualValues(t, 9, version) + + _, err = tree.Set([]byte("b"), []byte{0x02}) + require.NoError(t, err) + _, version, err = tree.SaveVersion() + require.NoError(t, err) + assert.EqualValues(t, 10, version) + + // Reloading the tree with the same initial version is fine + tree = NewMutableTree(memDB, 0, false, log.NewNopLogger(), InitialVersionOption(9)) + version, err = tree.Load() + require.NoError(t, err) + assert.EqualValues(t, 10, version) + + // Reloading the tree with an initial version beyond the lowest should error + tree = NewMutableTree(memDB, 0, false, log.NewNopLogger(), InitialVersionOption(10)) + _, err = tree.Load() + require.Error(t, err) + + // Reloading the tree with a lower initial version is fine, and new versions can be produced + tree = NewMutableTree(memDB, 0, false, log.NewNopLogger(), InitialVersionOption(3)) + version, err = tree.Load() + require.NoError(t, err) + assert.EqualValues(t, 10, version) + + _, err = tree.Set([]byte("c"), []byte{0x03}) + require.NoError(t, err) + _, version, err = tree.SaveVersion() + require.NoError(t, err) + assert.EqualValues(t, 11, version) +} + func TestMutableTree_SetInitialVersion(t *testing.T) { tree := setupMutableTree(false) tree.SetInitialVersion(9) @@ -306,6 +572,17 @@ func TestMutableTree_SetInitialVersion(t *testing.T) { assert.EqualValues(t, 9, version) } +func TestLegacyMutableTree_SetInitialVersion(t *testing.T) { + tree := setupLegacyMutableTree(false) + tree.SetInitialVersion(9) + + _, err := tree.Set([]byte("a"), []byte{0x01}) + require.NoError(t, err) + _, version, err := tree.SaveVersion() + require.NoError(t, err) + assert.EqualValues(t, 9, version) +} + func BenchmarkMutableTree_Set(b *testing.B) { db := dbm.NewMemDB() t := NewMutableTree(db, 100000, false, log.NewNopLogger()) @@ -324,14 +601,55 @@ func BenchmarkMutableTree_Set(b *testing.B) { } } -func prepareTree(t *testing.T) *MutableTree { - mdb := dbm.NewMemDB() - tree := NewMutableTree(mdb, 1000, false, log.NewNopLogger()) - for i := 0; i < 100; i++ { - _, err := tree.Set([]byte{byte(i)}, []byte("a")) - require.NoError(t, err) - } - _, ver, err := tree.SaveVersion() +func BenchmarkLegacyMutableTree_Set(b *testing.B) { + db := dbm.NewMemDB() + t := NewLegacyMutableTree(db, 100000, false, false, nil, log.NewNopLogger()) + for i := 0; i < 1000000; i++ { + _, err := t.Set(iavlrand.RandBytes(10), []byte{}) + require.NoError(b, err) + } + b.ReportAllocs() + runtime.GC() + + b.ResetTimer() + + for i := 0; i < b.N; i++ { + _, err := t.Set(iavlrand.RandBytes(10), []byte{}) + require.NoError(b, err) + } +} + +func prepareTree(t *testing.T) *MutableTree { + mdb := dbm.NewMemDB() + tree := NewMutableTree(mdb, 1000, false, log.NewNopLogger()) + for i := 0; i < 100; i++ { + _, err := tree.Set([]byte{byte(i)}, []byte("a")) + require.NoError(t, err) + } + _, ver, err := tree.SaveVersion() + require.True(t, ver == 1) + require.NoError(t, err) + for i := 0; i < 100; i++ { + _, err = tree.Set([]byte{byte(i)}, []byte("b")) + require.NoError(t, err) + } + _, ver, err = tree.SaveVersion() + require.True(t, ver == 2) + require.NoError(t, err) + + newTree := NewMutableTree(mdb, 1000, false, log.NewNopLogger()) + + return newTree +} + +func prepareLegacyTree(t *testing.T) *MutableTree { + mdb := dbm.NewMemDB() + tree := NewLegacyMutableTree(mdb, 1000, false, false, nil, log.NewNopLogger()) + for i := 0; i < 100; i++ { + _, err := tree.Set([]byte{byte(i)}, []byte("a")) + require.NoError(t, err) + } + _, ver, err := tree.SaveVersion() require.True(t, ver == 1) require.NoError(t, err) for i := 0; i < 100; i++ { @@ -354,6 +672,13 @@ func TestMutableTree_VersionExists(t *testing.T) { require.False(t, tree.VersionExists(3)) } +func TestLegacyMutableTree_VersionExists(t *testing.T) { + tree := prepareLegacyTree(t) + require.True(t, tree.VersionExists(1)) + require.True(t, tree.VersionExists(2)) + require.False(t, tree.VersionExists(3)) +} + func checkGetVersioned(t *testing.T, tree *MutableTree, version int64, key, value []byte) { val, err := tree.GetVersioned(key, version) require.NoError(t, err) @@ -379,6 +704,25 @@ func TestMutableTree_GetVersioned(t *testing.T) { checkGetVersioned(t, tree, 3, []byte{1}, nil) } +func TestLegacyMutableTree_GetVersioned(t *testing.T) { + tree := prepareLegacyTree(t) + ver, err := tree.LoadVersion(1) + require.True(t, ver == 2) + require.NoError(t, err) + // check key of unloaded version + checkGetVersioned(t, tree, 1, []byte{1}, []byte("a")) + checkGetVersioned(t, tree, 2, []byte{1}, []byte("b")) + checkGetVersioned(t, tree, 3, []byte{1}, nil) + + tree = prepareLegacyTree(t) + ver, err = tree.LoadVersion(2) + require.True(t, ver == 2) + require.NoError(t, err) + checkGetVersioned(t, tree, 1, []byte{1}, []byte("a")) + checkGetVersioned(t, tree, 2, []byte{1}, []byte("b")) + checkGetVersioned(t, tree, 3, []byte{1}, nil) +} + func TestMutableTree_DeleteVersion(t *testing.T) { tree := prepareTree(t) ver, err := tree.LoadVersion(2) @@ -395,6 +739,22 @@ func TestMutableTree_DeleteVersion(t *testing.T) { require.Error(t, tree.DeleteVersionsTo(2)) } +func TestLegacyMutableTree_DeleteVersion(t *testing.T) { + tree := prepareLegacyTree(t) + ver, err := tree.LoadVersion(2) + require.True(t, ver == 2) + require.NoError(t, err) + + require.NoError(t, tree.DeleteVersionsTo(1)) + + require.False(t, tree.VersionExists(1)) + require.True(t, tree.VersionExists(2)) + require.False(t, tree.VersionExists(3)) + + // cannot delete latest version + require.Error(t, tree.DeleteVersionsTo(2)) +} + func TestMutableTree_LazyLoadVersionWithEmptyTree(t *testing.T) { mdb := dbm.NewMemDB() tree := NewMutableTree(mdb, 1000, false, log.NewNopLogger()) @@ -414,6 +774,25 @@ func TestMutableTree_LazyLoadVersionWithEmptyTree(t *testing.T) { require.True(t, newTree1.root == newTree2.root) } +func TestLegacyMutableTree_LazyLoadVersionWithEmptyTree(t *testing.T) { + mdb := dbm.NewMemDB() + tree := NewLegacyMutableTree(mdb, 1000, false, false, nil, log.NewNopLogger()) + _, v1, err := tree.SaveVersion() + require.NoError(t, err) + + newTree1 := NewLegacyMutableTree(mdb, 1000, false, false, nil, log.NewNopLogger()) + v2, err := newTree1.LoadVersion(1) + require.NoError(t, err) + require.True(t, v1 == v2) + + newTree2 := NewLegacyMutableTree(mdb, 1000, false, false, nil, log.NewNopLogger()) + v2, err = newTree1.LoadVersion(1) + require.NoError(t, err) + require.True(t, v1 == v2) + + require.True(t, newTree1.root == newTree2.root) +} + func TestMutableTree_SetSimple(t *testing.T) { mdb := dbm.NewMemDB() tree := NewMutableTree(mdb, 0, false, log.NewNopLogger()) @@ -442,6 +821,34 @@ func TestMutableTree_SetSimple(t *testing.T) { require.Equal(t, int64(1), fastNodeAddition.GetVersionLastUpdatedAt()) } +func TestLegacyMutableTree_SetSimple(t *testing.T) { + mdb := dbm.NewMemDB() + tree := NewLegacyMutableTree(mdb, 0, false, false, nil, log.NewNopLogger()) + + const testKey1 = "a" + const testVal1 = "test" + + isUpdated, err := tree.Set([]byte(testKey1), []byte(testVal1)) + require.NoError(t, err) + require.False(t, isUpdated) + + fastValue, err := tree.Get([]byte(testKey1)) + require.NoError(t, err) + _, regularValue, err := tree.GetWithIndex([]byte(testKey1)) + require.NoError(t, err) + + require.Equal(t, []byte(testVal1), fastValue) + require.Equal(t, []byte(testVal1), regularValue) + + fastNodeAdditions := tree.getUnsavedFastNodeAdditions() + require.Equal(t, 1, len(fastNodeAdditions)) + + fastNodeAddition := fastNodeAdditions[testKey1] + require.Equal(t, []byte(testKey1), fastNodeAddition.GetKey()) + require.Equal(t, []byte(testVal1), fastNodeAddition.GetValue()) + require.Equal(t, int64(1), fastNodeAddition.GetVersionLastUpdatedAt()) +} + func TestMutableTree_SetTwoKeys(t *testing.T) { tree := setupMutableTree(false) @@ -487,6 +894,51 @@ func TestMutableTree_SetTwoKeys(t *testing.T) { require.Equal(t, int64(1), fastNodeAddition.GetVersionLastUpdatedAt()) } +func TestLegacyMutableTree_SetTwoKeys(t *testing.T) { + tree := setupLegacyMutableTree(false) + + const testKey1 = "a" + const testVal1 = "test" + + const testKey2 = "b" + const testVal2 = "test2" + + isUpdated, err := tree.Set([]byte(testKey1), []byte(testVal1)) + require.NoError(t, err) + require.False(t, isUpdated) + + isUpdated, err = tree.Set([]byte(testKey2), []byte(testVal2)) + require.NoError(t, err) + require.False(t, isUpdated) + + fastValue, err := tree.Get([]byte(testKey1)) + require.NoError(t, err) + _, regularValue, err := tree.GetWithIndex([]byte(testKey1)) + require.NoError(t, err) + require.Equal(t, []byte(testVal1), fastValue) + require.Equal(t, []byte(testVal1), regularValue) + + fastValue2, err := tree.Get([]byte(testKey2)) + require.NoError(t, err) + _, regularValue2, err := tree.GetWithIndex([]byte(testKey2)) + require.NoError(t, err) + require.Equal(t, []byte(testVal2), fastValue2) + require.Equal(t, []byte(testVal2), regularValue2) + + fastNodeAdditions := tree.getUnsavedFastNodeAdditions() + require.Equal(t, 2, len(fastNodeAdditions)) + + fastNodeAddition := fastNodeAdditions[testKey1] + require.Equal(t, []byte(testKey1), fastNodeAddition.GetKey()) + require.Equal(t, []byte(testVal1), fastNodeAddition.GetValue()) + require.Equal(t, int64(1), fastNodeAddition.GetVersionLastUpdatedAt()) + + fastNodeAddition = fastNodeAdditions[testKey2] + require.Equal(t, []byte(testKey2), fastNodeAddition.GetKey()) + require.Equal(t, []byte(testVal2), fastNodeAddition.GetValue()) + require.Equal(t, int64(1), fastNodeAddition.GetVersionLastUpdatedAt()) +} + func TestMutableTree_SetOverwrite(t *testing.T) { tree := setupMutableTree(false) const testKey1 = "a" @@ -517,6 +969,36 @@ func TestMutableTree_SetOverwrite(t *testing.T) { require.Equal(t, int64(1), fastNodeAddition.GetVersionLastUpdatedAt()) } +func TestLegacyMutableTree_SetOverwrite(t *testing.T) { + tree := setupLegacyMutableTree(false) + const testKey1 = "a" + const testVal1 = "test" + const testVal2 = "test2" + + isUpdated, err := tree.Set([]byte(testKey1), []byte(testVal1)) + require.NoError(t, err) + require.False(t, isUpdated) + + isUpdated, err = tree.Set([]byte(testKey1), []byte(testVal2)) + require.NoError(t, err) + require.True(t, isUpdated) + + fastValue, err := tree.Get([]byte(testKey1)) + require.NoError(t, err) + _, regularValue, err := tree.GetWithIndex([]byte(testKey1)) + require.NoError(t, err) + require.Equal(t, []byte(testVal2), fastValue) + require.Equal(t, []byte(testVal2), regularValue) + + fastNodeAdditions := tree.getUnsavedFastNodeAdditions() + require.Equal(t, 1, len(fastNodeAdditions)) + + fastNodeAddition := fastNodeAdditions[testKey1] + require.Equal(t, []byte(testKey1), fastNodeAddition.GetKey()) + require.Equal(t, []byte(testVal2), fastNodeAddition.GetValue()) + require.Equal(t, int64(1), fastNodeAddition.GetVersionLastUpdatedAt()) +} + func TestMutableTree_SetRemoveSet(t *testing.T) { tree := setupMutableTree(false) const testKey1 = "a" @@ -585,63 +1067,131 @@ func TestMutableTree_SetRemoveSet(t *testing.T) { require.Equal(t, 0, len(fastNodeRemovals)) } -func TestMutableTree_FastNodeIntegration(t *testing.T) { - mdb := dbm.NewMemDB() - tree := NewMutableTree(mdb, 1000, false, log.NewNopLogger()) - - const key1 = "a" - const key2 = "b" - const key3 = "c" - +func TestLegacyMutableTree_SetRemoveSet(t *testing.T) { + tree := setupLegacyMutableTree(false) + const testKey1 = "a" const testVal1 = "test" - const testVal2 = "test2" - // Set key1 - res, err := tree.Set([]byte(key1), []byte(testVal1)) + // Set 1 + isUpdated, err := tree.Set([]byte(testKey1), []byte(testVal1)) require.NoError(t, err) - require.False(t, res) - - unsavedNodeAdditions := tree.getUnsavedFastNodeAdditions() - require.Equal(t, len(unsavedNodeAdditions), 1) + require.False(t, isUpdated) - // Set key2 - res, err = tree.Set([]byte(key2), []byte(testVal1)) + fastValue, err := tree.Get([]byte(testKey1)) require.NoError(t, err) - require.False(t, res) - - unsavedNodeAdditions = tree.getUnsavedFastNodeAdditions() - require.Equal(t, len(unsavedNodeAdditions), 2) - - // Set key3 - res, err = tree.Set([]byte(key3), []byte(testVal1)) + _, regularValue, err := tree.GetWithIndex([]byte(testKey1)) require.NoError(t, err) - require.False(t, res) - - unsavedNodeAdditions = tree.getUnsavedFastNodeAdditions() - require.Equal(t, len(unsavedNodeAdditions), 3) + require.Equal(t, []byte(testVal1), fastValue) + require.Equal(t, []byte(testVal1), regularValue) - // Set key3 with new value - res, err = tree.Set([]byte(key3), []byte(testVal2)) - require.NoError(t, err) - require.True(t, res) + fastNodeAdditions := tree.getUnsavedFastNodeAdditions() + require.Equal(t, 1, len(fastNodeAdditions)) - unsavedNodeAdditions = tree.getUnsavedFastNodeAdditions() - require.Equal(t, len(unsavedNodeAdditions), 3) + fastNodeAddition := fastNodeAdditions[testKey1] + require.Equal(t, []byte(testKey1), fastNodeAddition.GetKey()) + require.Equal(t, []byte(testVal1), fastNodeAddition.GetValue()) + require.Equal(t, int64(1), fastNodeAddition.GetVersionLastUpdatedAt()) - // Remove key2 - removedVal, isRemoved, err := tree.Remove([]byte(key2)) + // Remove + removedVal, isRemoved, err := tree.Remove([]byte(testKey1)) require.NoError(t, err) + require.NotNil(t, removedVal) require.True(t, isRemoved) - require.Equal(t, []byte(testVal1), removedVal) - unsavedNodeAdditions = tree.getUnsavedFastNodeAdditions() - require.Equal(t, len(unsavedNodeAdditions), 2) + fastNodeAdditions = tree.getUnsavedFastNodeAdditions() + require.Equal(t, 0, len(fastNodeAdditions)) - unsavedNodeRemovals := tree.getUnsavedFastNodeRemovals() - require.Equal(t, len(unsavedNodeRemovals), 1) + fastNodeRemovals := tree.getUnsavedFastNodeRemovals() + require.Equal(t, 1, len(fastNodeRemovals)) - // Save - _, _, err = tree.SaveVersion() + fastValue, err = tree.Get([]byte(testKey1)) + require.NoError(t, err) + _, regularValue, err = tree.GetWithIndex([]byte(testKey1)) + require.NoError(t, err) + require.Nil(t, fastValue) + require.Nil(t, regularValue) + + // Set 2 + isUpdated, err = tree.Set([]byte(testKey1), []byte(testVal1)) + require.NoError(t, err) + require.False(t, isUpdated) + + fastValue, err = tree.Get([]byte(testKey1)) + require.NoError(t, err) + _, regularValue, err = tree.GetWithIndex([]byte(testKey1)) + require.NoError(t, err) + require.Equal(t, []byte(testVal1), fastValue) + require.Equal(t, []byte(testVal1), regularValue) + + fastNodeAdditions = tree.getUnsavedFastNodeAdditions() + require.Equal(t, 1, len(fastNodeAdditions)) + + fastNodeAddition = fastNodeAdditions[testKey1] + require.Equal(t, []byte(testKey1), fastNodeAddition.GetKey()) + require.Equal(t, []byte(testVal1), fastNodeAddition.GetValue()) + require.Equal(t, int64(1), fastNodeAddition.GetVersionLastUpdatedAt()) + + fastNodeRemovals = tree.getUnsavedFastNodeRemovals() + require.Equal(t, 0, len(fastNodeRemovals)) +} + +func TestMutableTree_FastNodeIntegration(t *testing.T) { + mdb := dbm.NewMemDB() + tree := NewMutableTree(mdb, 1000, false, log.NewNopLogger()) + + const key1 = "a" + const key2 = "b" + const key3 = "c" + + const testVal1 = "test" + const testVal2 = "test2" + + // Set key1 + res, err := tree.Set([]byte(key1), []byte(testVal1)) + require.NoError(t, err) + require.False(t, res) + + unsavedNodeAdditions := tree.getUnsavedFastNodeAdditions() + require.Equal(t, len(unsavedNodeAdditions), 1) + + // Set key2 + res, err = tree.Set([]byte(key2), []byte(testVal1)) + require.NoError(t, err) + require.False(t, res) + + unsavedNodeAdditions = tree.getUnsavedFastNodeAdditions() + require.Equal(t, len(unsavedNodeAdditions), 2) + + // Set key3 + res, err = tree.Set([]byte(key3), []byte(testVal1)) + require.NoError(t, err) + require.False(t, res) + + unsavedNodeAdditions = tree.getUnsavedFastNodeAdditions() + require.Equal(t, len(unsavedNodeAdditions), 3) + + // Set key3 with new value + res, err = tree.Set([]byte(key3), []byte(testVal2)) + require.NoError(t, err) + require.True(t, res) + + unsavedNodeAdditions = tree.getUnsavedFastNodeAdditions() + require.Equal(t, len(unsavedNodeAdditions), 3) + + // Remove key2 + removedVal, isRemoved, err := tree.Remove([]byte(key2)) + require.NoError(t, err) + require.True(t, isRemoved) + require.Equal(t, []byte(testVal1), removedVal) + + unsavedNodeAdditions = tree.getUnsavedFastNodeAdditions() + require.Equal(t, len(unsavedNodeAdditions), 2) + + unsavedNodeRemovals := tree.getUnsavedFastNodeRemovals() + require.Equal(t, len(unsavedNodeRemovals), 1) + + // Save + _, _, err = tree.SaveVersion() require.NoError(t, err) unsavedNodeAdditions = tree.getUnsavedFastNodeAdditions() @@ -679,11 +1229,110 @@ func TestMutableTree_FastNodeIntegration(t *testing.T) { require.Equal(t, []byte(testVal2), regularValue) } +func TestLegacyMutableTree_FastNodeIntegration(t *testing.T) { + mdb := dbm.NewMemDB() + tree := NewLegacyMutableTree(mdb, 1000, false, false, nil, log.NewNopLogger()) + + const key1 = "a" + const key2 = "b" + const key3 = "c" + + const testVal1 = "test" + const testVal2 = "test2" + + // Set key1 + res, err := tree.Set([]byte(key1), []byte(testVal1)) + require.NoError(t, err) + require.False(t, res) + + unsavedNodeAdditions := tree.getUnsavedFastNodeAdditions() + require.Equal(t, len(unsavedNodeAdditions), 1) + + // Set key2 + res, err = tree.Set([]byte(key2), []byte(testVal1)) + require.NoError(t, err) + require.False(t, res) + + unsavedNodeAdditions = tree.getUnsavedFastNodeAdditions() + require.Equal(t, len(unsavedNodeAdditions), 2) + + // Set key3 + res, err = tree.Set([]byte(key3), []byte(testVal1)) + require.NoError(t, err) + require.False(t, res) + + unsavedNodeAdditions = tree.getUnsavedFastNodeAdditions() + require.Equal(t, len(unsavedNodeAdditions), 3) + + // Set key3 with new value + res, err = tree.Set([]byte(key3), []byte(testVal2)) + require.NoError(t, err) + require.True(t, res) + + unsavedNodeAdditions = tree.getUnsavedFastNodeAdditions() + require.Equal(t, len(unsavedNodeAdditions), 3) + + // Remove key2 + removedVal, isRemoved, err := tree.Remove([]byte(key2)) + require.NoError(t, err) + require.True(t, isRemoved) + require.Equal(t, []byte(testVal1), removedVal) + + unsavedNodeAdditions = tree.getUnsavedFastNodeAdditions() + require.Equal(t, len(unsavedNodeAdditions), 2) + + unsavedNodeRemovals := tree.getUnsavedFastNodeRemovals() + require.Equal(t, len(unsavedNodeRemovals), 1) + + // Save + _, _, err = tree.SaveVersion() + require.NoError(t, err) + + unsavedNodeAdditions = tree.getUnsavedFastNodeAdditions() + require.Equal(t, len(unsavedNodeAdditions), 0) + + unsavedNodeRemovals = tree.getUnsavedFastNodeRemovals() + require.Equal(t, len(unsavedNodeRemovals), 0) + + // Load + t2 := NewLegacyMutableTree(mdb, 0, false, false, nil, log.NewNopLogger()) + + _, err = t2.Load() + require.NoError(t, err) + + // Get and GetFast + fastValue, err := t2.Get([]byte(key1)) + require.NoError(t, err) + _, regularValue, err := tree.GetWithIndex([]byte(key1)) + require.NoError(t, err) + require.Equal(t, []byte(testVal1), fastValue) + require.Equal(t, []byte(testVal1), regularValue) + + fastValue, err = t2.Get([]byte(key2)) + require.NoError(t, err) + _, regularValue, err = t2.GetWithIndex([]byte(key2)) + require.NoError(t, err) + require.Nil(t, fastValue) + require.Nil(t, regularValue) + + fastValue, err = t2.Get([]byte(key3)) + require.NoError(t, err) + _, regularValue, err = tree.GetWithIndex([]byte(key3)) + require.NoError(t, err) + require.Equal(t, []byte(testVal2), fastValue) + require.Equal(t, []byte(testVal2), regularValue) +} + func TestIterate_MutableTree_Unsaved(t *testing.T) { tree, mirror := getRandomizedTreeAndMirror(t) assertMutableMirrorIterate(t, tree, mirror) } +func TestIterate_LegacyMutableTree_Unsaved(t *testing.T) { + tree, mirror := getRandomizedLegacyTreeAndMirror(t) + assertMutableMirrorIterate(t, tree, mirror) +} + func TestIterate_MutableTree_Saved(t *testing.T) { tree, mirror := getRandomizedTreeAndMirror(t) @@ -693,6 +1342,15 @@ func TestIterate_MutableTree_Saved(t *testing.T) { assertMutableMirrorIterate(t, tree, mirror) } +func TestIterate_LegacyMutableTree_Saved(t *testing.T) { + tree, mirror := getRandomizedLegacyTreeAndMirror(t) + + _, _, err := tree.SaveVersion() + require.NoError(t, err) + + assertMutableMirrorIterate(t, tree, mirror) +} + func TestIterate_MutableTree_Unsaved_NextVersion(t *testing.T) { tree, mirror := getRandomizedTreeAndMirror(t) @@ -706,6 +1364,19 @@ func TestIterate_MutableTree_Unsaved_NextVersion(t *testing.T) { assertMutableMirrorIterate(t, tree, mirror) } +func TestIterate_LegacyMutableTree_Unsaved_NextVersion(t *testing.T) { + tree, mirror := getRandomizedLegacyTreeAndMirror(t) + + _, _, err := tree.SaveVersion() + require.NoError(t, err) + + assertMutableMirrorIterate(t, tree, mirror) + + randomizeTreeAndMirror(t, tree, mirror) + + assertMutableMirrorIterate(t, tree, mirror) +} + func TestIterator_MutableTree_Invalid(t *testing.T) { tree := getTestTree(0) @@ -715,6 +1386,15 @@ func TestIterator_MutableTree_Invalid(t *testing.T) { require.False(t, itr.Valid()) } +func TestIterator_LegacyMutableTree_Invalid(t *testing.T) { + tree := getLegacyTestTree(0) + + itr, err := tree.Iterator([]byte("a"), []byte("b"), true) + require.NoError(t, err) + require.NotNil(t, itr) + require.False(t, itr.Valid()) +} + func TestUpgradeStorageToFast_LatestVersion_Success(t *testing.T) { // Setup db := dbm.NewMemDB() @@ -745,6 +1425,36 @@ func TestUpgradeStorageToFast_LatestVersion_Success(t *testing.T) { require.True(t, isFastCacheEnabled) } +func TestLegacyUpgradeStorageToFast_LatestVersion_Success(t *testing.T) { + // Setup + db := dbm.NewMemDB() + tree := NewLegacyMutableTree(db, 1000, false, false, nil, log.NewNopLogger()) + + // Default version when storage key does not exist in the db + isFastCacheEnabled, err := tree.IsFastCacheEnabled() + require.NoError(t, err) + require.False(t, isFastCacheEnabled) + + mirror := make(map[string]string) + // Fill with some data + randomizeTreeAndMirror(t, tree, mirror) + + // Enable fast storage + isUpgradeable, err := tree.IsUpgradeable() + require.True(t, isUpgradeable) + require.NoError(t, err) + enabled, err := tree.enableFastStorageAndCommitIfNotEnabled() + require.NoError(t, err) + require.True(t, enabled) + isUpgradeable, err = tree.IsUpgradeable() + require.False(t, isUpgradeable) + require.NoError(t, err) + + isFastCacheEnabled, err = tree.IsFastCacheEnabled() + require.NoError(t, err) + require.True(t, isFastCacheEnabled) +} + func TestUpgradeStorageToFast_AlreadyUpgraded_Success(t *testing.T) { // Setup db := dbm.NewMemDB() @@ -782,6 +1492,43 @@ func TestUpgradeStorageToFast_AlreadyUpgraded_Success(t *testing.T) { require.True(t, isFastCacheEnabled) } +func TestLegacyUpgradeStorageToFast_AlreadyUpgraded_Success(t *testing.T) { + // Setup + db := dbm.NewMemDB() + tree := NewLegacyMutableTree(db, 1000, false, false, nil, log.NewNopLogger()) + + // Default version when storage key does not exist in the db + isFastCacheEnabled, err := tree.IsFastCacheEnabled() + require.NoError(t, err) + require.False(t, isFastCacheEnabled) + + mirror := make(map[string]string) + // Fill with some data + randomizeTreeAndMirror(t, tree, mirror) + + // Enable fast storage + isUpgradeable, err := tree.IsUpgradeable() + require.True(t, isUpgradeable) + require.NoError(t, err) + enabled, err := tree.enableFastStorageAndCommitIfNotEnabled() + require.NoError(t, err) + require.True(t, enabled) + isFastCacheEnabled, err = tree.IsFastCacheEnabled() + require.NoError(t, err) + require.True(t, isFastCacheEnabled) + isUpgradeable, err = tree.IsUpgradeable() + require.False(t, isUpgradeable) + require.NoError(t, err) + + // Test enabling fast storage when already enabled + enabled, err = tree.enableFastStorageAndCommitIfNotEnabled() + require.NoError(t, err) + require.False(t, enabled) + isFastCacheEnabled, err = tree.IsFastCacheEnabled() + require.NoError(t, err) + require.True(t, isFastCacheEnabled) +} + func TestUpgradeStorageToFast_DbErrorConstructor_Failure(t *testing.T) { ctrl := gomock.NewController(t) dbMock := mock.NewMockDB(ctrl) @@ -806,6 +1553,30 @@ func TestUpgradeStorageToFast_DbErrorConstructor_Failure(t *testing.T) { require.False(t, isFastCacheEnabled) } +func TestLegacyUpgradeStorageToFast_DbErrorConstructor_Failure(t *testing.T) { + ctrl := gomock.NewController(t) + dbMock := mock.NewMockDB(ctrl) + rIterMock := mock.NewMockIterator(ctrl) + + // rIterMock is used to get the latest version from disk. We are mocking that rIterMock returns latestTreeVersion from disk + rIterMock.EXPECT().Valid().Return(true).Times(1) + rIterMock.EXPECT().Key().Return(nodeKeyFormat.Key(GetRootKey(1))) + rIterMock.EXPECT().Close().Return(nil).Times(1) + + expectedError := errors.New("some db error") + + dbMock.EXPECT().Get(gomock.Any()).Return(nil, expectedError).Times(1) + dbMock.EXPECT().NewBatchWithSize(gomock.Any()).Return(nil).Times(1) + dbMock.EXPECT().ReverseIterator(gomock.Any(), gomock.Any()).Return(rIterMock, nil).Times(1) + + tree := NewLegacyMutableTree(dbMock, 0, false, false, nil, log.NewNopLogger()) + require.NotNil(t, tree) + + isFastCacheEnabled, err := tree.IsFastCacheEnabled() + require.NoError(t, err) + require.False(t, isFastCacheEnabled) +} + func TestUpgradeStorageToFast_DbErrorEnableFastStorage_Failure(t *testing.T) { ctrl := gomock.NewController(t) dbMock := mock.NewMockDB(ctrl) @@ -849,6 +1620,49 @@ func TestUpgradeStorageToFast_DbErrorEnableFastStorage_Failure(t *testing.T) { require.False(t, isFastCacheEnabled) } +func TestLegacyUpgradeStorageToFast_DbErrorEnableFastStorage_Failure(t *testing.T) { + ctrl := gomock.NewController(t) + dbMock := mock.NewMockDB(ctrl) + rIterMock := mock.NewMockIterator(ctrl) + + // rIterMock is used to get the latest version from disk. We are mocking that rIterMock returns latestTreeVersion from disk + rIterMock.EXPECT().Valid().Return(true).Times(1) + rIterMock.EXPECT().Key().Return(nodeKeyFormat.Key(GetRootKey(1))) + rIterMock.EXPECT().Close().Return(nil).Times(1) + + expectedError := errors.New("some db error") + + batchMock := mock.NewMockBatch(ctrl) + + dbMock.EXPECT().Get(gomock.Any()).Return(nil, nil).Times(1) + dbMock.EXPECT().NewBatchWithSize(gomock.Any()).Return(batchMock).Times(1) + dbMock.EXPECT().ReverseIterator(gomock.Any(), gomock.Any()).Return(rIterMock, nil).Times(1) + + iterMock := mock.NewMockIterator(ctrl) + dbMock.EXPECT().Iterator(gomock.Any(), gomock.Any()).Return(iterMock, nil) + iterMock.EXPECT().Error() + iterMock.EXPECT().Valid().Times(2) + iterMock.EXPECT().Close() + + batchMock.EXPECT().Set(gomock.Any(), gomock.Any()).Return(expectedError).Times(1) + batchMock.EXPECT().GetByteSize().Return(100, nil).Times(1) + + tree := NewLegacyMutableTree(dbMock, 0, false, false, nil, log.NewNopLogger()) + require.NotNil(t, tree) + + isFastCacheEnabled, err := tree.IsFastCacheEnabled() + require.NoError(t, err) + require.False(t, isFastCacheEnabled) + + enabled, err := tree.enableFastStorageAndCommitIfNotEnabled() + require.ErrorIs(t, err, expectedError) + require.False(t, enabled) + + isFastCacheEnabled, err = tree.IsFastCacheEnabled() + require.NoError(t, err) + require.False(t, isFastCacheEnabled) +} + func TestFastStorageReUpgradeProtection_NoForceUpgrade_Success(t *testing.T) { ctrl := gomock.NewController(t) dbMock := mock.NewMockDB(ctrl) @@ -867,11 +1681,151 @@ func TestFastStorageReUpgradeProtection_NoForceUpgrade_Success(t *testing.T) { rIterMock.EXPECT().Key().Return(nodeKeyFormat.Key(GetRootKey(1))) rIterMock.EXPECT().Close().Return(nil).Times(1) - batchMock := mock.NewMockBatch(ctrl) + batchMock := mock.NewMockBatch(ctrl) + + dbMock.EXPECT().Get(gomock.Any()).Return(expectedStorageVersion, nil).Times(1) + dbMock.EXPECT().NewBatchWithSize(gomock.Any()).Return(batchMock).Times(1) + dbMock.EXPECT().ReverseIterator(gomock.Any(), gomock.Any()).Return(rIterMock, nil).Times(1) // called to get latest version + + tree := NewMutableTree(dbMock, 0, false, log.NewNopLogger()) + require.NotNil(t, tree) + + // Pretend that we called Load and have the latest state in the tree + tree.version = latestTreeVersion + latestVersion, err := tree.ndb.getLatestVersion() + require.NoError(t, err) + require.Equal(t, latestVersion, int64(latestTreeVersion)) + + // Ensure that the right branch of enableFastStorageAndCommitIfNotEnabled will be triggered + isFastCacheEnabled, err := tree.IsFastCacheEnabled() + require.NoError(t, err) + require.True(t, isFastCacheEnabled) + shouldForce, err := tree.ndb.shouldForceFastStorageUpgrade() + require.False(t, shouldForce) + require.NoError(t, err) + + enabled, err := tree.enableFastStorageAndCommitIfNotEnabled() + require.NoError(t, err) + require.False(t, enabled) +} + +func TestLegacyFastStorageReUpgradeProtection_NoForceUpgrade_Success(t *testing.T) { + ctrl := gomock.NewController(t) + dbMock := mock.NewMockDB(ctrl) + rIterMock := mock.NewMockIterator(ctrl) + + // We are trying to test downgrade and re-upgrade protection + // We need to set up a state where latest fast storage version is equal to latest tree version + const latestFastStorageVersionOnDisk = 1 + const latestTreeVersion = latestFastStorageVersionOnDisk + + // Setup fake reverse iterator db to traverse root versions, called by ndb's getLatestVersion + expectedStorageVersion := []byte(fastStorageVersionValue + fastStorageVersionDelimiter + strconv.Itoa(latestFastStorageVersionOnDisk)) + + // rIterMock is used to get the latest version from disk. We are mocking that rIterMock returns latestTreeVersion from disk + rIterMock.EXPECT().Valid().Return(true).Times(1) + rIterMock.EXPECT().Key().Return(nodeKeyFormat.Key(GetRootKey(1))) + rIterMock.EXPECT().Close().Return(nil).Times(1) + + batchMock := mock.NewMockBatch(ctrl) + + dbMock.EXPECT().Get(gomock.Any()).Return(expectedStorageVersion, nil).Times(1) + dbMock.EXPECT().NewBatchWithSize(gomock.Any()).Return(batchMock).Times(1) + dbMock.EXPECT().ReverseIterator(gomock.Any(), gomock.Any()).Return(rIterMock, nil).Times(1) // called to get latest version + + tree := NewLegacyMutableTree(dbMock, 0, false, false, nil, log.NewNopLogger()) + require.NotNil(t, tree) + + // Pretend that we called Load and have the latest state in the tree + tree.version = latestTreeVersion + latestVersion, err := tree.ndb.getLatestVersion() + require.NoError(t, err) + require.Equal(t, latestVersion, int64(latestTreeVersion)) + + // Ensure that the right branch of enableFastStorageAndCommitIfNotEnabled will be triggered + isFastCacheEnabled, err := tree.IsFastCacheEnabled() + require.NoError(t, err) + require.True(t, isFastCacheEnabled) + shouldForce, err := tree.ndb.shouldForceFastStorageUpgrade() + require.False(t, shouldForce) + require.NoError(t, err) + + enabled, err := tree.enableFastStorageAndCommitIfNotEnabled() + require.NoError(t, err) + require.False(t, enabled) +} + +func TestFastStorageReUpgradeProtection_ForceUpgradeFirstTime_NoForceSecondTime_Success(t *testing.T) { + ctrl := gomock.NewController(t) + dbMock := mock.NewMockDB(ctrl) + batchMock := mock.NewMockBatch(ctrl) + iterMock := mock.NewMockIterator(ctrl) + rIterMock := mock.NewMockIterator(ctrl) + + // We are trying to test downgrade and re-upgrade protection + // We need to set up a state where latest fast storage version is of a lower version + // than tree version + const latestFastStorageVersionOnDisk = 1 + const latestTreeVersion = latestFastStorageVersionOnDisk + 1 + + // Setup db for iterator and reverse iterator mocks + expectedStorageVersion := []byte(fastStorageVersionValue + fastStorageVersionDelimiter + strconv.Itoa(latestFastStorageVersionOnDisk)) + + // Setup fake reverse iterator db to traverse root versions, called by ndb's getLatestVersion + // rItr, err := db.ReverseIterator(rootKeyFormat.Key(1), rootKeyFormat.Key(latestTreeVersion + 1)) + // require.NoError(t, err) + + // dbMock represents the underlying database under the hood of nodeDB + dbMock.EXPECT().Get(gomock.Any()).Return(expectedStorageVersion, nil).Times(1) + + dbMock.EXPECT().NewBatchWithSize(gomock.Any()).Return(batchMock).Times(2) + dbMock.EXPECT().ReverseIterator(gomock.Any(), gomock.Any()).Return(rIterMock, nil).Times(1) // called to get latest version + startFormat := fastKeyFormat.Key() + endFormat := fastKeyFormat.Key() + endFormat[0]++ + dbMock.EXPECT().Iterator(startFormat, endFormat).Return(iterMock, nil).Times(1) + + // rIterMock is used to get the latest version from disk. We are mocking that rIterMock returns latestTreeVersion from disk + rIterMock.EXPECT().Valid().Return(true).Times(1) + rIterMock.EXPECT().Key().Return(nodeKeyFormat.Key(GetRootKey(latestTreeVersion))) + rIterMock.EXPECT().Close().Return(nil).Times(1) + + fastNodeKeyToDelete := []byte("some_key") - dbMock.EXPECT().Get(gomock.Any()).Return(expectedStorageVersion, nil).Times(1) - dbMock.EXPECT().NewBatchWithSize(gomock.Any()).Return(batchMock).Times(1) - dbMock.EXPECT().ReverseIterator(gomock.Any(), gomock.Any()).Return(rIterMock, nil).Times(1) // called to get latest version + // batchMock represents a structure that receives all the updates related to + // upgrade and then commits them all in the end. + updatedExpectedStorageVersion := make([]byte, len(expectedStorageVersion)) + copy(updatedExpectedStorageVersion, expectedStorageVersion) + updatedExpectedStorageVersion[len(updatedExpectedStorageVersion)-1]++ + batchMock.EXPECT().GetByteSize().Return(100, nil).Times(2) + batchMock.EXPECT().Delete(fastKeyFormat.Key(fastNodeKeyToDelete)).Return(nil).Times(1) + batchMock.EXPECT().Set(metadataKeyFormat.Key([]byte(storageVersionKey)), updatedExpectedStorageVersion).Return(nil).Times(1) + batchMock.EXPECT().Write().Return(nil).Times(1) + batchMock.EXPECT().Close().Return(nil).Times(1) + + // iterMock is used to mock the underlying db iterator behing fast iterator + // Here, we want to mock the behavior of deleting fast nodes from disk when + // force upgrade is detected. + iterMock.EXPECT().Valid().Return(true).Times(1) + iterMock.EXPECT().Error().Return(nil).Times(1) + iterMock.EXPECT().Key().Return(fastKeyFormat.Key(fastNodeKeyToDelete)).Times(1) + // encode value + var buf bytes.Buffer + testValue := "test_value" + buf.Grow(encoding.EncodeVarintSize(int64(latestFastStorageVersionOnDisk)) + encoding.EncodeBytesSize([]byte(testValue))) + err := encoding.EncodeVarint(&buf, int64(latestFastStorageVersionOnDisk)) + require.NoError(t, err) + err = encoding.EncodeBytes(&buf, []byte(testValue)) + require.NoError(t, err) + iterMock.EXPECT().Value().Return(buf.Bytes()).Times(1) // this is encoded as version 1 with value "2" + iterMock.EXPECT().Valid().Return(true).Times(1) + // Call Next at the end of loop iteration + iterMock.EXPECT().Next().Return().Times(1) + iterMock.EXPECT().Error().Return(nil).Times(1) + iterMock.EXPECT().Valid().Return(false).Times(1) + // Call Valid after first iteraton + iterMock.EXPECT().Valid().Return(false).Times(1) + iterMock.EXPECT().Close().Return(nil).Times(1) tree := NewMutableTree(dbMock, 0, false, log.NewNopLogger()) require.NotNil(t, tree) @@ -887,15 +1841,21 @@ func TestFastStorageReUpgradeProtection_NoForceUpgrade_Success(t *testing.T) { require.NoError(t, err) require.True(t, isFastCacheEnabled) shouldForce, err := tree.ndb.shouldForceFastStorageUpgrade() - require.False(t, shouldForce) + require.True(t, shouldForce) require.NoError(t, err) + // Actual method under test enabled, err := tree.enableFastStorageAndCommitIfNotEnabled() require.NoError(t, err) + require.True(t, enabled) + + // Test that second time we call this, force upgrade does not happen + enabled, err = tree.enableFastStorageAndCommitIfNotEnabled() + require.NoError(t, err) require.False(t, enabled) } -func TestFastStorageReUpgradeProtection_ForceUpgradeFirstTime_NoForceSecondTime_Success(t *testing.T) { +func TestLegacyFastStorageReUpgradeProtection_ForceUpgradeFirstTime_NoForceSecondTime_Success(t *testing.T) { ctrl := gomock.NewController(t) dbMock := mock.NewMockDB(ctrl) batchMock := mock.NewMockBatch(ctrl) @@ -967,7 +1927,7 @@ func TestFastStorageReUpgradeProtection_ForceUpgradeFirstTime_NoForceSecondTime_ iterMock.EXPECT().Valid().Return(false).Times(1) iterMock.EXPECT().Close().Return(nil).Times(1) - tree := NewMutableTree(dbMock, 0, false, log.NewNopLogger()) + tree := NewLegacyMutableTree(dbMock, 0, false, false, nil, log.NewNopLogger()) require.NotNil(t, tree) // Pretend that we called Load and have the latest state in the tree @@ -1062,6 +2022,73 @@ func TestUpgradeStorageToFast_Integration_Upgraded_FastIterator_Success(t *testi }) } +func TestLegacyUpgradeStorageToFast_Integration_Upgraded_FastIterator_Success(t *testing.T) { + // Setup + tree, mirror := setupLegacyTreeAndMirror(t, 100, false) + + isFastCacheEnabled, err := tree.IsFastCacheEnabled() + require.NoError(t, err) + require.False(t, isFastCacheEnabled) + isUpgradeable, err := tree.IsUpgradeable() + require.True(t, isUpgradeable) + require.NoError(t, err) + + // Should auto enable in save version + _, _, err = tree.SaveVersion() + require.NoError(t, err) + + isFastCacheEnabled, err = tree.IsFastCacheEnabled() + require.NoError(t, err) + require.True(t, isFastCacheEnabled) + isUpgradeable, err = tree.IsUpgradeable() + require.False(t, isUpgradeable) + require.NoError(t, err) + + sut := NewLegacyMutableTree(tree.ndb.db, 1000, false, false, nil, log.NewNopLogger()) + + isFastCacheEnabled, err = sut.IsFastCacheEnabled() + require.NoError(t, err) + require.False(t, isFastCacheEnabled) + isUpgradeable, err = sut.IsUpgradeable() + require.False(t, isUpgradeable) // upgraded in save version + require.NoError(t, err) + + // Load version - should auto enable fast storage + version, err := sut.Load() + require.NoError(t, err) + + isFastCacheEnabled, err = tree.IsFastCacheEnabled() + require.NoError(t, err) + require.True(t, isFastCacheEnabled) + + require.Equal(t, int64(1), version) + + // Test that upgraded mutable tree iterates as expected + t.Run("Mutable tree", func(t *testing.T) { + i := 0 + sut.Iterate(func(k, v []byte) bool { //nolint:errcheck + require.Equal(t, []byte(mirror[i][0]), k) + require.Equal(t, []byte(mirror[i][1]), v) + i++ + return false + }) + }) + + // Test that upgraded immutable tree iterates as expected + t.Run("Immutable tree", func(t *testing.T) { + immutableTree, err := sut.GetImmutable(sut.version) + require.NoError(t, err) + + i := 0 + immutableTree.Iterate(func(k, v []byte) bool { //nolint:errcheck + require.Equal(t, []byte(mirror[i][0]), k) + require.Equal(t, []byte(mirror[i][1]), v) + i++ + return false + }) + }) +} + func TestUpgradeStorageToFast_Integration_Upgraded_GetFast_Success(t *testing.T) { // Setup tree, mirror := setupTreeAndMirror(t, 100, false) @@ -1118,30 +2145,182 @@ func TestUpgradeStorageToFast_Integration_Upgraded_GetFast_Success(t *testing.T) for _, kv := range mirror { v, err := immutableTree.Get([]byte(kv[0])) require.NoError(t, err) - require.Equal(t, []byte(kv[1]), v) + require.Equal(t, []byte(kv[1]), v) + } + }) +} + +func TestLegacyUpgradeStorageToFast_Integration_Upgraded_GetFast_Success(t *testing.T) { + // Setup + tree, mirror := setupLegacyTreeAndMirror(t, 100, false) + + isFastCacheEnabled, err := tree.IsFastCacheEnabled() + require.NoError(t, err) + require.False(t, isFastCacheEnabled) + isUpgradeable, err := tree.IsUpgradeable() + require.True(t, isUpgradeable) + require.NoError(t, err) + + // Should auto enable in save version + _, _, err = tree.SaveVersion() + require.NoError(t, err) + + isFastCacheEnabled, err = tree.IsFastCacheEnabled() + require.NoError(t, err) + require.True(t, isFastCacheEnabled) + isUpgradeable, err = tree.IsUpgradeable() + require.False(t, isUpgradeable) + require.NoError(t, err) + + sut := NewLegacyMutableTree(tree.ndb.db, 1000, false, false, nil, log.NewNopLogger()) + + isFastCacheEnabled, err = sut.IsFastCacheEnabled() + require.NoError(t, err) + require.False(t, isFastCacheEnabled) + isUpgradeable, err = sut.IsUpgradeable() + require.False(t, isUpgradeable) // upgraded in save version + require.NoError(t, err) + + // LazyLoadVersion - should auto enable fast storage + version, err := sut.LoadVersion(1) + require.NoError(t, err) + + isFastCacheEnabled, err = tree.IsFastCacheEnabled() + require.NoError(t, err) + require.True(t, isFastCacheEnabled) + + require.Equal(t, int64(1), version) + + t.Run("Mutable tree", func(t *testing.T) { + for _, kv := range mirror { + v, err := sut.Get([]byte(kv[0])) + require.NoError(t, err) + require.Equal(t, []byte(kv[1]), v) + } + }) + + t.Run("Immutable tree", func(t *testing.T) { + immutableTree, err := sut.GetImmutable(sut.version) + require.NoError(t, err) + + for _, kv := range mirror { + v, err := immutableTree.Get([]byte(kv[0])) + require.NoError(t, err) + require.Equal(t, []byte(kv[1]), v) + } + }) +} + +func TestUpgradeStorageToFast_Success(t *testing.T) { + commitGap := 1000 + + type fields struct { + nodeCount int + } + tests := []struct { + name string + fields fields + }{ + {"less than commit gap", fields{nodeCount: 100}}, + {"equal to commit gap", fields{nodeCount: commitGap}}, + {"great than commit gap", fields{nodeCount: commitGap + 100}}, + {"two times commit gap", fields{nodeCount: commitGap * 2}}, + {"two times plus commit gap", fields{nodeCount: commitGap*2 + 1}}, + } + + for _, tt := range tests { + tree, mirror := setupTreeAndMirror(t, tt.fields.nodeCount, false) + enabled, err := tree.enableFastStorageAndCommitIfNotEnabled() + require.Nil(t, err) + require.True(t, enabled) + t.Run(tt.name, func(t *testing.T) { + i := 0 + iter := NewFastIterator(nil, nil, true, tree.ndb) + for ; iter.Valid(); iter.Next() { + require.Equal(t, []byte(mirror[i][0]), iter.Key()) + require.Equal(t, []byte(mirror[i][1]), iter.Value()) + i++ + } + require.Equal(t, len(mirror), i) + }) + } +} + +func TestLegacyUpgradeStorageToFast_Success(t *testing.T) { + commitGap := 1000 + + type fields struct { + nodeCount int + } + tests := []struct { + name string + fields fields + }{ + {"less than commit gap", fields{nodeCount: 100}}, + {"equal to commit gap", fields{nodeCount: commitGap}}, + {"great than commit gap", fields{nodeCount: commitGap + 100}}, + {"two times commit gap", fields{nodeCount: commitGap * 2}}, + {"two times plus commit gap", fields{nodeCount: commitGap*2 + 1}}, + } + + for _, tt := range tests { + tree, mirror := setupLegacyTreeAndMirror(t, tt.fields.nodeCount, false) + enabled, err := tree.enableFastStorageAndCommitIfNotEnabled() + require.Nil(t, err) + require.True(t, enabled) + t.Run(tt.name, func(t *testing.T) { + i := 0 + iter := NewFastIterator(nil, nil, true, tree.ndb) + for ; iter.Valid(); iter.Next() { + require.Equal(t, []byte(mirror[i][0]), iter.Key()) + require.Equal(t, []byte(mirror[i][1]), iter.Value()) + i++ + } + require.Equal(t, len(mirror), i) + }) + } +} + +func TestUpgradeStorageToFast_Delete_Stale_Success(t *testing.T) { + // we delete fast node, in case of deadlock. we should limit the stale count lower than chBufferSize(64) + commitGap := 5 + + valStale := "val_stale" + addStaleKey := func(ndb *nodeDB, staleCount int) { + keyPrefix := "key_prefix" + b := ndb.db.NewBatch() + for i := 0; i < staleCount; i++ { + key := fmt.Sprintf("%s_%d", keyPrefix, i) + + node := fastnode.NewNode([]byte(key), []byte(valStale), 100) + var buf bytes.Buffer + buf.Grow(node.EncodedSize()) + err := node.WriteBytes(&buf) + require.NoError(t, err) + err = b.Set(ndb.fastNodeKey([]byte(key)), buf.Bytes()) + require.NoError(t, err) } - }) -} - -func TestUpgradeStorageToFast_Success(t *testing.T) { - commitGap := 1000 - + require.NoError(t, b.Write()) + } type fields struct { - nodeCount int + nodeCount int + staleCount int } + tests := []struct { name string fields fields }{ - {"less than commit gap", fields{nodeCount: 100}}, - {"equal to commit gap", fields{nodeCount: commitGap}}, - {"great than commit gap", fields{nodeCount: commitGap + 100}}, - {"two times commit gap", fields{nodeCount: commitGap * 2}}, - {"two times plus commit gap", fields{nodeCount: commitGap*2 + 1}}, + {"stale less than commit gap", fields{nodeCount: 100, staleCount: 4}}, + {"stale equal to commit gap", fields{nodeCount: commitGap, staleCount: commitGap}}, + {"stale great than commit gap", fields{nodeCount: commitGap + 100, staleCount: commitGap*2 - 1}}, + {"stale twice commit gap", fields{nodeCount: commitGap + 100, staleCount: commitGap * 2}}, + {"stale great than twice commit gap", fields{nodeCount: commitGap, staleCount: commitGap*2 + 1}}, } for _, tt := range tests { tree, mirror := setupTreeAndMirror(t, tt.fields.nodeCount, false) + addStaleKey(tree.ndb, tt.fields.staleCount) enabled, err := tree.enableFastStorageAndCommitIfNotEnabled() require.Nil(t, err) require.True(t, enabled) @@ -1158,7 +2337,7 @@ func TestUpgradeStorageToFast_Success(t *testing.T) { } } -func TestUpgradeStorageToFast_Delete_Stale_Success(t *testing.T) { +func TestLegacyUpgradeStorageToFast_Delete_Stale_Success(t *testing.T) { // we delete fast node, in case of deadlock. we should limit the stale count lower than chBufferSize(64) commitGap := 5 @@ -1196,7 +2375,7 @@ func TestUpgradeStorageToFast_Delete_Stale_Success(t *testing.T) { } for _, tt := range tests { - tree, mirror := setupTreeAndMirror(t, tt.fields.nodeCount, false) + tree, mirror := setupLegacyTreeAndMirror(t, tt.fields.nodeCount, false) addStaleKey(tree.ndb, tt.fields.staleCount) enabled, err := tree.enableFastStorageAndCommitIfNotEnabled() require.Nil(t, err) @@ -1243,6 +2422,35 @@ func setupTreeAndMirror(t *testing.T, numEntries int, skipFastStorageUpgrade boo return tree, mirror } +func setupLegacyTreeAndMirror(t *testing.T, numEntries int, skipFastStorageUpgrade bool) (*MutableTree, [][]string) { + db := dbm.NewMemDB() + + tree := NewLegacyMutableTree(db, 0, skipFastStorageUpgrade, false, nil, log.NewNopLogger()) + + keyPrefix, valPrefix := "key", "val" + + mirror := make([][]string, 0, numEntries) + for i := 0; i < numEntries; i++ { + key := fmt.Sprintf("%s_%d", keyPrefix, i) + val := fmt.Sprintf("%s_%d", valPrefix, i) + mirror = append(mirror, []string{key, val}) + updated, err := tree.Set([]byte(key), []byte(val)) + require.False(t, updated) + require.NoError(t, err) + } + + // Delete fast nodes from database to mimic a version with no upgrade + for i := 0; i < numEntries; i++ { + key := fmt.Sprintf("%s_%d", keyPrefix, i) + require.NoError(t, db.Delete(fastKeyFormat.Key([]byte(key)))) + } + + sort.Slice(mirror, func(i, j int) bool { + return mirror[i][0] < mirror[j][0] + }) + return tree, mirror +} + func TestNoFastStorageUpgrade_Integration_SaveVersion_Load_Get_Success(t *testing.T) { // Setup tree, mirror := setupTreeAndMirror(t, 100, true) @@ -1330,6 +2538,93 @@ func TestNoFastStorageUpgrade_Integration_SaveVersion_Load_Get_Success(t *testin }) } +func TestLegacyNoFastStorageUpgrade_Integration_SaveVersion_Load_Get_Success(t *testing.T) { + // Setup + tree, mirror := setupLegacyTreeAndMirror(t, 100, true) + + isFastCacheEnabled, err := tree.IsFastCacheEnabled() + require.NoError(t, err) + require.False(t, isFastCacheEnabled) + isUpgradeable, err := tree.IsUpgradeable() + require.False(t, isUpgradeable) + require.NoError(t, err) + + // Should Not auto enable in save version + _, _, err = tree.SaveVersion() + require.NoError(t, err) + + isFastCacheEnabled, err = tree.IsFastCacheEnabled() + require.NoError(t, err) + require.False(t, isFastCacheEnabled) + isUpgradeable, err = tree.IsUpgradeable() + require.False(t, isUpgradeable) + require.NoError(t, err) + + sut := NewLegacyMutableTree(tree.ndb.db, 1000, true, false, nil, log.NewNopLogger()) + + isFastCacheEnabled, err = sut.IsFastCacheEnabled() + require.NoError(t, err) + require.False(t, isFastCacheEnabled) + isUpgradeable, err = sut.IsUpgradeable() + require.False(t, isUpgradeable) + require.NoError(t, err) + + // LazyLoadVersion - should not auto enable fast storage + version, err := sut.LoadVersion(1) + require.NoError(t, err) + require.Equal(t, int64(1), version) + + isFastCacheEnabled, err = sut.IsFastCacheEnabled() + require.NoError(t, err) + require.False(t, isFastCacheEnabled) + + // Load - should not auto enable fast storage + version, err = sut.Load() + require.NoError(t, err) + require.Equal(t, int64(1), version) + + isFastCacheEnabled, err = sut.IsFastCacheEnabled() + require.NoError(t, err) + require.False(t, isFastCacheEnabled) + + // LoadVersion - should not auto enable fast storage + version, err = sut.LoadVersion(1) + require.NoError(t, err) + require.Equal(t, int64(1), version) + + isFastCacheEnabled, err = sut.IsFastCacheEnabled() + require.NoError(t, err) + require.False(t, isFastCacheEnabled) + + // LoadVersionForOverwriting - should not auto enable fast storage + err = sut.LoadVersionForOverwriting(1) + require.NoError(t, err) + require.Equal(t, int64(1), version) + + isFastCacheEnabled, err = sut.IsFastCacheEnabled() + require.NoError(t, err) + require.False(t, isFastCacheEnabled) + + t.Run("Mutable tree", func(t *testing.T) { + for _, kv := range mirror { + v, err := sut.Get([]byte(kv[0])) + require.NoError(t, err) + require.Equal(t, []byte(kv[1]), v) + } + }) + + t.Run("Immutable tree", func(t *testing.T) { + immutableTree, err := sut.GetImmutable(sut.version) + require.NoError(t, err) + + for _, kv := range mirror { + v, err := immutableTree.Get([]byte(kv[0])) + require.NoError(t, err) + require.Equal(t, []byte(kv[1]), v) + } + }) +} + func TestNoFastStorageUpgrade_Integration_SaveVersion_Load_Iterate_Success(t *testing.T) { // Setup tree, mirror := setupTreeAndMirror(t, 100, true) @@ -1405,6 +2700,81 @@ func TestNoFastStorageUpgrade_Integration_SaveVersion_Load_Iterate_Success(t *te }) } +func TestLegacyNoFastStorageUpgrade_Integration_SaveVersion_Load_Iterate_Success(t *testing.T) { + // Setup + tree, mirror := setupLegacyTreeAndMirror(t, 100, true) + + isFastCacheEnabled, err := tree.IsFastCacheEnabled() + require.NoError(t, err) + require.False(t, isFastCacheEnabled) + isUpgradeable, err := tree.IsUpgradeable() + require.False(t, isUpgradeable) + require.NoError(t, err) + + // Should Not auto enable in save version + _, _, err = tree.SaveVersion() + require.NoError(t, err) + + isFastCacheEnabled, err = tree.IsFastCacheEnabled() + require.NoError(t, err) + require.False(t, isFastCacheEnabled) + isUpgradeable, err = tree.IsUpgradeable() + require.False(t, isUpgradeable) + require.NoError(t, err) + + sut := NewLegacyMutableTree(tree.ndb.db, 1000, true, false, nil, log.NewNopLogger()) + + isFastCacheEnabled, err = sut.IsFastCacheEnabled() + require.NoError(t, err) + require.False(t, isFastCacheEnabled) + isUpgradeable, err = sut.IsUpgradeable() + require.False(t, isUpgradeable) + require.NoError(t, err) + + // Load - should not auto enable fast storage + version, err := sut.Load() + require.NoError(t, err) + require.Equal(t, int64(1), version) + + isFastCacheEnabled, err = sut.IsFastCacheEnabled() + require.NoError(t, err) + require.False(t, isFastCacheEnabled) + + // Load - should not auto enable fast storage + version, err = sut.Load() + require.NoError(t, err) + require.Equal(t, int64(1), version) + + isFastCacheEnabled, err = tree.IsFastCacheEnabled() + require.NoError(t, err) + require.False(t, isFastCacheEnabled) + + // Test that the mutable tree iterates as expected + t.Run("Mutable tree", func(t *testing.T) { + i := 0 + sut.Iterate(func(k, v []byte) bool { //nolint: errcheck + require.Equal(t, []byte(mirror[i][0]), k) + require.Equal(t, []byte(mirror[i][1]), v) + i++ + return false + }) + }) + + // Test that the immutable tree iterates as expected + t.Run("Immutable tree", func(t *testing.T) { + immutableTree, err := sut.GetImmutable(sut.version) + require.NoError(t, err) + + i := 0 + immutableTree.Iterate(func(k, v []byte) bool { //nolint: errcheck + require.Equal(t, []byte(mirror[i][0]), k) + require.Equal(t, []byte(mirror[i][1]), v) + i++ + return false + }) + }) +} + // TestMutableTree_InitialVersion_FirstVersion demonstrate the un-intuitive behavior, // when InitialVersion is set the nodes created in the first version are not assigned with expected version number. func TestMutableTree_InitialVersion_FirstVersion(t *testing.T) { @@ -1438,6 +2808,37 @@ func TestMutableTree_InitialVersion_FirstVersion(t *testing.T) { require.Equal(t, initialVersion+1, node.nodeKey.version) } +func TestLegacyMutableTree_InitialVersion_FirstVersion(t *testing.T) { + db := dbm.NewMemDB() + + initialVersion := int64(1000) + tree := NewLegacyMutableTree(db, 0, true, false, nil, log.NewNopLogger(), InitialVersionOption(uint64(initialVersion))) + + _, err := tree.Set([]byte("hello"), []byte("world")) + require.NoError(t, err) + + _, version, err := tree.SaveVersion() + require.NoError(t, err) + require.Equal(t, initialVersion, version) + rootKey := GetRootKey(version) + // the nodes created at the first version are not assigned with the `InitialVersion` + node, err := tree.ndb.GetNode(rootKey) + require.NoError(t, err) + require.Equal(t, initialVersion, node.nodeKey.version) + + _, err = tree.Set([]byte("hello"), []byte("world1")) + require.NoError(t, err) + + _, version, err = tree.SaveVersion() + require.NoError(t, err) + require.Equal(t, initialVersion+1, version) + rootKey = GetRootKey(version) + // the following versions behaves normally + node, err = tree.ndb.GetNode(rootKey) + require.NoError(t, err) + require.Equal(t, initialVersion+1, node.nodeKey.version) +} + func TestMutableTreeClose(t *testing.T) { db := dbm.NewMemDB() tree := NewMutableTree(db, 0, true, log.NewNopLogger()) @@ -1450,3 +2851,16 @@ func TestMutableTreeClose(t *testing.T) { require.NoError(t, tree.Close()) } + +func TestLegacyMutableTreeClose(t *testing.T) { + db := dbm.NewMemDB() + tree := NewLegacyMutableTree(db, 0, true, false, nil, log.NewNopLogger()) + + _, err := tree.Set([]byte("hello"), []byte("world")) + require.NoError(t, err) + + _, _, err = tree.SaveVersion() + require.NoError(t, err) + + require.NoError(t, tree.Close()) +} diff --git a/nodedb.go b/nodedb.go index 58e865b9a..d66acd631 100644 --- a/nodedb.go +++ b/nodedb.go @@ -165,8 +165,11 @@ func (ndb *nodeDB) GetNode(nk []byte) (*Node, error) { // Doesn't exist, load. isLegacyNode := len(nk) == hashSize + //println("key length:") + //println(len(nk)) var nodeKey []byte if isLegacyNode { + println("IS LEGACY NODE") nodeKey = ndb.legacyNodeKey(nk) } else { nodeKey = ndb.nodeKey(nk) @@ -239,7 +242,7 @@ func (ndb *nodeDB) SaveNode(node *Node) error { ndb.mtx.Lock() defer ndb.mtx.Unlock() - if node.nodeKey == nil || (ndb.useLegacyFormat && node.hash == nil) { + if (!ndb.useLegacyFormat && node.nodeKey == nil) || (ndb.useLegacyFormat && node.hash == nil) { return ErrNodeMissingNodeKey } @@ -699,6 +702,7 @@ func (ndb *nodeDB) resetFirstVersion(version int64) { func (ndb *nodeDB) getLegacyLatestVersion() (int64, error) { ndb.mtx.Lock() + // consider applying latestVersion = ndb.latestVersion here for legacy mode latestVersion := ndb.legacyLatestVersion ndb.mtx.Unlock() @@ -727,10 +731,13 @@ func (ndb *nodeDB) getLegacyLatestVersion() (int64, error) { return 0, err } - // If there are no legacy versions, set -1 - ndb.resetLegacyLatestVersion(-1) + // If not operating in legacyMode and there are no legacy versions, set -1 + if !ndb.useLegacyFormat { + ndb.resetLegacyLatestVersion(-1) + return -1, nil + } - return -1, nil + return latestVersion, nil } func (ndb *nodeDB) resetLegacyLatestVersion(version int64) { @@ -874,7 +881,7 @@ func (ndb *nodeDB) SaveRoot(version int64, nk *NodeKey) error { func (ndb *nodeDB) SaveLegacyRoot(version int64, key []byte) error { ndb.mtx.Lock() defer ndb.mtx.Unlock() - return ndb.batch.Set(legacyNodeKeyFormat.Key(GetRootKey(version)), legacyNodeKeyFormat.Key(key)) + return ndb.batch.Set(ndb.legacyRootKey(version), key) } // Traverse fast nodes and return error if any, nil otherwise diff --git a/testutils_test.go b/testutils_test.go index 638126c6f..565fc0b75 100644 --- a/testutils_test.go +++ b/testutils_test.go @@ -47,6 +47,10 @@ func getTestTree(cacheSize int) *MutableTree { return NewMutableTree(dbm.NewMemDB(), cacheSize, false, log.NewNopLogger()) } +func getLegacyTestTree(cacheSize int) *MutableTree { + return NewLegacyMutableTree(dbm.NewMemDB(), cacheSize, false, false, nil, log.NewNopLogger()) +} + // Convenience for a new node func N(l, r interface{}) *Node { var left, right *Node @@ -173,6 +177,17 @@ func getRandomizedTreeAndMirror(t *testing.T) (*MutableTree, map[string]string) return tree, mirror } +func getRandomizedLegacyTreeAndMirror(t *testing.T) (*MutableTree, map[string]string) { + const cacheSize = 100 + + tree := getLegacyTestTree(cacheSize) + + mirror := make(map[string]string) + + randomizeTreeAndMirror(t, tree, mirror) + return tree, mirror +} + func randomizeTreeAndMirror(t *testing.T, tree *MutableTree, mirror map[string]string) { if mirror == nil { mirror = make(map[string]string) From 16bd5ee8b703181cb468e657faa6ff885108812f Mon Sep 17 00:00:00 2001 From: i-norden Date: Mon, 24 Jun 2024 04:33:35 -0400 Subject: [PATCH 03/18] fixes part 2 --- mutable_tree.go | 39 ++++++++++++++++----------------------- mutable_tree_test.go | 4 ---- nodedb.go | 20 +++++++++++++------- 3 files changed, 29 insertions(+), 34 deletions(-) diff --git a/mutable_tree.go b/mutable_tree.go index 12eba17a8..12a5a73f8 100644 --- a/mutable_tree.go +++ b/mutable_tree.go @@ -511,6 +511,7 @@ func (tree *MutableTree) LoadVersion(targetVersion int64) (int64, error) { targetVersion = latestVersion } if !tree.VersionExists(targetVersion) { + println("here 1") return 0, ErrVersionDoesNotExist } rootNodeKey, err := tree.ndb.GetRoot(targetVersion) @@ -805,37 +806,29 @@ func (tree *MutableTree) SaveVersion() ([]byte, int64, error) { return nil, 0, err } } else { - if tree.root.nodeKey != nil { + if tree.root.nodeKey != nil && !tree.useLegacyFormat { // it means there are no updated nodes - if tree.useLegacyFormat { - if len(tree.root.hash) == 0 { - tree.root._hash(version) - } - if err := tree.ndb.SaveLegacyRoot(version, tree.root.hash); err != nil { - return nil, 0, err - } - tree.root.isLegacy = true + if err := tree.ndb.SaveRoot(version, tree.root.nodeKey); err != nil { + return nil, 0, err + } + // it means the reference node is a legacy node + if tree.root.isLegacy { + // it will update the legacy node to the new format + // which ensures the reference node is not a legacy node + tree.root.isLegacy = false if err := tree.ndb.SaveNode(tree.root); err != nil { return nil, 0, fmt.Errorf("failed to save the reference legacy node: %w", err) } - } else { - if err := tree.ndb.SaveRoot(version, tree.root.nodeKey); err != nil { - return nil, 0, err - } - // it means the reference node is a legacy node - if tree.root.isLegacy { - // it will update the legacy node to the new format - // which ensures the reference node is not a legacy node - tree.root.isLegacy = false - if err := tree.ndb.SaveNode(tree.root); err != nil { - return nil, 0, fmt.Errorf("failed to save the reference legacy node: %w", err) - } - } } } else { if err := tree.saveNewNodes(version); err != nil { return nil, 0, err } + if tree.useLegacyFormat { + if err := tree.ndb.SaveLegacyRoot(version, tree.root.hash); err != nil { + return nil, 0, err + } + } } } @@ -1096,7 +1089,7 @@ func (tree *MutableTree) saveNewNodes(version int64) error { recursiveAssignKey = func(node *Node) ([]byte, error) { node.isLegacy = tree.useLegacyFormat if (!node.isLegacy && node.nodeKey != nil) || (node.isLegacy && node.hash != nil) { - if node.nodeKey.nonce != 0 { + if node.nodeKey != nil && node.nodeKey.nonce != 0 { return node.GetKey(), nil } return node.hash, nil diff --git a/mutable_tree_test.go b/mutable_tree_test.go index f6c065525..bd4f821e0 100644 --- a/mutable_tree_test.go +++ b/mutable_tree_test.go @@ -423,10 +423,6 @@ func TestLegacyMutableTree_DeleteVersionsTo(t *testing.T) { // delete even versions versionToDelete := int64(8) - println("tree version:") - println(tree.version) - println("using legacy version:") - println(tree.useLegacyFormat) require.NoError(t, tree.DeleteVersionsTo(versionToDelete)) // ensure even versions have been deleted diff --git a/nodedb.go b/nodedb.go index d66acd631..030bcb6cf 100644 --- a/nodedb.go +++ b/nodedb.go @@ -165,11 +165,8 @@ func (ndb *nodeDB) GetNode(nk []byte) (*Node, error) { // Doesn't exist, load. isLegacyNode := len(nk) == hashSize - //println("key length:") - //println(len(nk)) var nodeKey []byte if isLegacyNode { - println("IS LEGACY NODE") nodeKey = ndb.legacyNodeKey(nk) } else { nodeKey = ndb.nodeKey(nk) @@ -570,7 +567,7 @@ func (ndb *nodeDB) DeleteVersionsTo(toVersion int64) error { // If the legacy version is greater than the toVersion, we don't need to delete anything. // It will delete the legacy versions at once. - if legacyLatestVersion > toVersion { + if !ndb.useLegacyFormat && legacyLatestVersion > toVersion { return nil } @@ -597,8 +594,8 @@ func (ndb *nodeDB) DeleteVersionsTo(toVersion int64) error { } ndb.mtx.Unlock() - // Delete the legacy versions - if legacyLatestVersion >= first { + // Delete the legacy versions unless we are using the legacy format + if !ndb.useLegacyFormat && legacyLatestVersion >= first { // Delete the last version for the legacyLastVersion if err := ndb.traverseOrphans(legacyLatestVersion, legacyLatestVersion+1, func(orphan *Node) error { return ndb.batch.Delete(ndb.legacyNodeKey(orphan.hash)) @@ -608,6 +605,7 @@ func (ndb *nodeDB) DeleteVersionsTo(toVersion int64) error { // reset the legacy latest version forcibly to avoid multiple calls ndb.resetLegacyLatestVersion(-1) go func() { + println(legacyLatestVersion) if err := ndb.deleteLegacyVersions(legacyLatestVersion); err != nil { ndb.logger.Error("Error deleting legacy versions", "err", err) } @@ -703,7 +701,12 @@ func (ndb *nodeDB) resetFirstVersion(version int64) { func (ndb *nodeDB) getLegacyLatestVersion() (int64, error) { ndb.mtx.Lock() // consider applying latestVersion = ndb.latestVersion here for legacy mode - latestVersion := ndb.legacyLatestVersion + var latestVersion int64 + if ndb.useLegacyFormat { + latestVersion = ndb.latestVersion + } else { + latestVersion = ndb.legacyLatestVersion + } ndb.mtx.Unlock() if latestVersion != 0 { @@ -820,6 +823,8 @@ func (ndb *nodeDB) GetRoot(version int64) ([]byte, error) { return nil, err } if val == nil { + println("here 2") + fmt.Printf("version %d missing\r\n", version) return nil, ErrVersionDoesNotExist } if len(val) == 0 { // empty root @@ -847,6 +852,7 @@ func (ndb *nodeDB) GetRoot(version int64) ([]byte, error) { return nil, err } if val == nil { + println("here 3") return nil, ErrVersionDoesNotExist } return rnk.GetKey(), nil From 8fadf17f101dbc02b259d869546f6f359ff99573 Mon Sep 17 00:00:00 2001 From: i-norden Date: Wed, 26 Jun 2024 11:13:27 -0400 Subject: [PATCH 04/18] fix deletion of a specific legacy version --- mutable_tree.go | 1 - nodedb.go | 152 +++++++++++++++++++++++++++++++++++++++++++----- 2 files changed, 139 insertions(+), 14 deletions(-) diff --git a/mutable_tree.go b/mutable_tree.go index 12a5a73f8..71df0bc79 100644 --- a/mutable_tree.go +++ b/mutable_tree.go @@ -511,7 +511,6 @@ func (tree *MutableTree) LoadVersion(targetVersion int64) (int64, error) { targetVersion = latestVersion } if !tree.VersionExists(targetVersion) { - println("here 1") return 0, ErrVersionDoesNotExist } rootNodeKey, err := tree.ndb.GetRoot(targetVersion) diff --git a/nodedb.go b/nodedb.go index 030bcb6cf..500dd6866 100644 --- a/nodedb.go +++ b/nodedb.go @@ -437,6 +437,74 @@ func (ndb *nodeDB) deleteVersion(version int64) error { return nil } +func (ndb *nodeDB) getPreviousVersion(version int64) (int64, error) { + itr, err := ndb.db.ReverseIterator( + legacyRootKeyFormat.Key(1), + legacyRootKeyFormat.Key(version), + ) + if err != nil { + return 0, err + } + defer itr.Close() + + pversion := int64(-1) + for ; itr.Valid(); itr.Next() { + + k := itr.Key() + legacyRootKeyFormat.Scan(k, &pversion) + return pversion, nil + } + + if err := itr.Error(); err != nil { + return 0, err + } + + return 0, nil +} + +func (ndb *nodeDB) deleteLegacyOrphans(version int64) error { + ndb.mtx.Lock() + defer ndb.mtx.Unlock() + // Will be zero if there is no previous version. + predecessor, err := ndb.getPreviousVersion(version) + if err != nil { + return err + } + + // Traverse orphans with a lifetime ending at the version specified. + // TODO optimize. + return ndb.traverseOrphansVersion(version, func(key, hash []byte) error { + var fromVersion, toVersion int64 + + // See comment on `orphanKeyFmt`. Note that here, `version` and + // `toVersion` are always equal. + legacyOrphanKeyFormat.Scan(key, &toVersion, &fromVersion) + + // Delete orphan key and reverse-lookup key. + if err := ndb.batch.Delete(key); err != nil { + return err + } + + // If there is no predecessor, or the predecessor is earlier than the + // beginning of the lifetime (ie: negative lifetime), or the lifetime + // spans a single version and that version is the one being deleted, we + // can delete the orphan. Otherwise, we shorten its lifetime, by + // moving its endpoint to the previous version. + if predecessor < fromVersion || fromVersion == toVersion { + if err := ndb.batch.Delete(ndb.nodeKey(hash)); err != nil { + return err + } + ndb.nodeCache.Remove(hash) + } else { + err := ndb.saveOrphan(hash, fromVersion, predecessor) + if err != nil { + return err + } + } + return nil + }) +} + // deleteLegacyNodes deletes all legacy nodes with the given version from disk. // NOTE: This is only used for DeleteVersionsFrom. func (ndb *nodeDB) deleteLegacyNodes(version int64, nk []byte) error { @@ -500,6 +568,25 @@ func (ndb *nodeDB) deleteLegacyVersions(legacyLatestVersion int64) error { return nil } +// deleteLegacyVersion deletes a legacy tree version from disk. +// calls deleteOrphans(version), deleteRoot(version, checkLatestVersion) +func (ndb *nodeDB) deleteLegacyVersion(version int64, checkLatestVersion bool) error { + if ndb.versionReaders[version] > 0 { + return fmt.Errorf("unable to delete version %v, it has %v active readers", version, ndb.versionReaders[version]) + } + + err := ndb.deleteLegacyOrphans(version) + if err != nil { + return err + } + + err = ndb.deleteLegacyRoot(version, checkLatestVersion) + if err != nil { + return err + } + return err +} + // DeleteVersionsFrom permanently deletes all tree versions from the given version upwards. func (ndb *nodeDB) DeleteVersionsFrom(fromVersion int64) error { latest, err := ndb.getLatestVersion() @@ -605,7 +692,6 @@ func (ndb *nodeDB) DeleteVersionsTo(toVersion int64) error { // reset the legacy latest version forcibly to avoid multiple calls ndb.resetLegacyLatestVersion(-1) go func() { - println(legacyLatestVersion) if err := ndb.deleteLegacyVersions(legacyLatestVersion); err != nil { ndb.logger.Error("Error deleting legacy versions", "err", err) } @@ -614,8 +700,14 @@ func (ndb *nodeDB) DeleteVersionsTo(toVersion int64) error { } for version := first; version <= toVersion; version++ { - if err := ndb.deleteVersion(version); err != nil { - return err + if ndb.useLegacyFormat { + if err := ndb.deleteLegacyVersion(version, true); err != nil { + return err + } + } else { + if err := ndb.deleteVersion(version); err != nil { + return err + } } ndb.resetFirstVersion(version + 1) } @@ -641,6 +733,10 @@ func (ndb *nodeDB) fastNodeKey(key []byte) []byte { return fastKeyFormat.KeyBytes(key) } +func (ndb *nodeDB) legacyOrphanKey(fromVersion, toVersion int64, hash []byte) []byte { + return legacyOrphanKeyFormat.Key(toVersion, fromVersion, hash) +} + func (ndb *nodeDB) legacyNodeKey(nk []byte) []byte { return legacyNodeKeyFormat.Key(nk) } @@ -692,6 +788,23 @@ func (ndb *nodeDB) getFirstVersion() (int64, error) { return latestVersion, nil } +// deleteRoot deletes the root entry from disk, but not the node it points to. +func (ndb *nodeDB) deleteLegacyRoot(version int64, checkLatestVersion bool) error { + latestVersion, err := ndb.getLatestVersion() + if err != nil { + return err + } + + if checkLatestVersion && version == latestVersion { + return errors.New("tried to delete latest version") + } + if err := ndb.batch.Delete(ndb.legacyRootKey(version)); err != nil { + return err + } + + return nil +} + func (ndb *nodeDB) resetFirstVersion(version int64) { ndb.mtx.Lock() defer ndb.mtx.Unlock() @@ -701,12 +814,12 @@ func (ndb *nodeDB) resetFirstVersion(version int64) { func (ndb *nodeDB) getLegacyLatestVersion() (int64, error) { ndb.mtx.Lock() // consider applying latestVersion = ndb.latestVersion here for legacy mode - var latestVersion int64 - if ndb.useLegacyFormat { - latestVersion = ndb.latestVersion - } else { - latestVersion = ndb.legacyLatestVersion - } + latestVersion := ndb.legacyLatestVersion + /* + if ndb.useLegacyFormat { + latestVersion = ndb.latestVersion + } + */ ndb.mtx.Unlock() if latestVersion != 0 { @@ -823,8 +936,6 @@ func (ndb *nodeDB) GetRoot(version int64) ([]byte, error) { return nil, err } if val == nil { - println("here 2") - fmt.Printf("version %d missing\r\n", version) return nil, ErrVersionDoesNotExist } if len(val) == 0 { // empty root @@ -852,7 +963,6 @@ func (ndb *nodeDB) GetRoot(version int64) ([]byte, error) { return nil, err } if val == nil { - println("here 3") return nil, ErrVersionDoesNotExist } return rnk.GetKey(), nil @@ -890,13 +1000,29 @@ func (ndb *nodeDB) SaveLegacyRoot(version int64, key []byte) error { return ndb.batch.Set(ndb.legacyRootKey(version), key) } +// Saves a single orphan to disk. +func (ndb *nodeDB) saveOrphan(hash []byte, fromVersion, toVersion int64) error { + if fromVersion > toVersion { + return fmt.Errorf("orphan expires before it comes alive. %d > %d", fromVersion, toVersion) + } + key := ndb.legacyOrphanKey(fromVersion, toVersion, hash) + if err := ndb.batch.Set(key, hash); err != nil { + return err + } + return nil +} + // Traverse fast nodes and return error if any, nil otherwise func (ndb *nodeDB) traverseFastNodes(fn func(k, v []byte) error) error { return ndb.traversePrefix(fastKeyFormat.Key(), fn) } -// Traverse all keys and return error if any, nil otherwise +// Traverse orphans ending at a certain version. return error if any, nil otherwise +func (ndb *nodeDB) traverseOrphansVersion(version int64, fn func(k, v []byte) error) error { + return ndb.traversePrefix(legacyOrphanKeyFormat.Key(version), fn) +} +// Traverse all keys and return error if any, nil otherwise func (ndb *nodeDB) traverse(fn func(key, value []byte) error) error { return ndb.traverseRange(nil, nil, fn) } From 07c858ea12bc7c4170c4366e5f91120dd36d0360 Mon Sep 17 00:00:00 2001 From: i-norden Date: Wed, 26 Jun 2024 11:40:14 -0400 Subject: [PATCH 05/18] fix root key access in legacy tests --- import.go | 4 ++-- mutable_tree_test.go | 9 ++++++--- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/import.go b/import.go index 364e33b55..3817e9419 100644 --- a/import.go +++ b/import.go @@ -211,7 +211,7 @@ func (i *Importer) Commit() error { case 0: if i.tree.useLegacyFormat { rootHash = []byte{} - if err := i.batch.Set(i.tree.ndb.legacyNodeKey(GetRootKey(i.version)), []byte{}); err != nil { + if err := i.batch.Set(i.tree.ndb.legacyRootKey(i.version), []byte{}); err != nil { return err } } else { @@ -230,7 +230,7 @@ func (i *Importer) Commit() error { i.stack[0]._hash(i.version) } rootHash = i.stack[0].hash - if err := i.batch.Set(i.tree.ndb.legacyNodeKey(GetRootKey(i.version)), i.tree.ndb.legacyNodeKey(rootHash)); err != nil { + if err := i.batch.Set(i.tree.ndb.legacyRootKey(i.version), i.tree.ndb.legacyNodeKey(rootHash)); err != nil { return err } } else { diff --git a/mutable_tree_test.go b/mutable_tree_test.go index bd4f821e0..cd0a872a4 100644 --- a/mutable_tree_test.go +++ b/mutable_tree_test.go @@ -656,7 +656,7 @@ func prepareLegacyTree(t *testing.T) *MutableTree { require.True(t, ver == 2) require.NoError(t, err) - newTree := NewMutableTree(mdb, 1000, false, log.NewNopLogger()) + newTree := NewLegacyMutableTree(mdb, 1000, false, false, nil, log.NewNopLogger()) return newTree } @@ -737,6 +737,7 @@ func TestMutableTree_DeleteVersion(t *testing.T) { func TestLegacyMutableTree_DeleteVersion(t *testing.T) { tree := prepareLegacyTree(t) + println("break") ver, err := tree.LoadVersion(2) require.True(t, ver == 2) require.NoError(t, err) @@ -2816,7 +2817,8 @@ func TestLegacyMutableTree_InitialVersion_FirstVersion(t *testing.T) { _, version, err := tree.SaveVersion() require.NoError(t, err) require.Equal(t, initialVersion, version) - rootKey := GetRootKey(version) + rootKey, err := tree.ndb.GetRoot(version) + require.NoError(t, err) // the nodes created at the first version are not assigned with the `InitialVersion` node, err := tree.ndb.GetNode(rootKey) require.NoError(t, err) @@ -2828,7 +2830,8 @@ func TestLegacyMutableTree_InitialVersion_FirstVersion(t *testing.T) { _, version, err = tree.SaveVersion() require.NoError(t, err) require.Equal(t, initialVersion+1, version) - rootKey = GetRootKey(version) + rootKey, err = tree.ndb.GetRoot(version) + require.NoError(t, err) // the following versions behaves normally node, err = tree.ndb.GetNode(rootKey) require.NoError(t, err) From 8d3d8a00d294f28780f72113ed477e6b958e3c5a Mon Sep 17 00:00:00 2001 From: i-norden Date: Wed, 26 Jun 2024 13:47:38 -0400 Subject: [PATCH 06/18] nodedb legacy tests and fixes --- nodedb.go | 13 +- nodedb_test.go | 382 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 393 insertions(+), 2 deletions(-) diff --git a/nodedb.go b/nodedb.go index 500dd6866..b4dc9000b 100644 --- a/nodedb.go +++ b/nodedb.go @@ -1293,11 +1293,20 @@ func (ndb *nodeDB) size() int { func (ndb *nodeDB) traverseNodes(fn func(node *Node) error) error { nodes := []*Node{} - if err := ndb.traversePrefix(nodeKeyFormat.Prefix(), func(key, value []byte) error { + var prefix []byte + var nodeMaker func(hash, buf []byte) (*Node, error) + if ndb.useLegacyFormat { + prefix = legacyNodeKeyFormat.Prefix() + nodeMaker = MakeLegacyNode + } else { + prefix = nodeKeyFormat.Prefix() + nodeMaker = MakeNode + } + if err := ndb.traversePrefix(prefix, func(key, value []byte) error { if isRef, _ := isReferenceRoot(value); isRef { return nil } - node, err := MakeNode(key[1:], value) + node, err := nodeMaker(key[1:], value) if err != nil { return err } diff --git a/nodedb_test.go b/nodedb_test.go index a9af51b63..75e938561 100644 --- a/nodedb_test.go +++ b/nodedb_test.go @@ -7,6 +7,8 @@ import ( "testing" "time" + ibytes "github.com/cosmos/iavl/internal/bytes" + log "cosmossdk.io/core/log" "github.com/golang/mock/gomock" "github.com/stretchr/testify/require" @@ -55,6 +57,19 @@ func TestNewNoDbStorage_StorageVersionInDb_Success(t *testing.T) { require.Equal(t, expectedVersion, ndb.storageVersion) } +func TestLegacyNewNoDbStorage_StorageVersionInDb_Success(t *testing.T) { + const expectedVersion = defaultStorageVersionValue + + ctrl := gomock.NewController(t) + dbMock := mock.NewMockDB(ctrl) + + dbMock.EXPECT().Get(gomock.Any()).Return([]byte(expectedVersion), nil).Times(1) + dbMock.EXPECT().NewBatchWithSize(gomock.Any()).Return(nil).Times(1) + + ndb := newLegacyNodeDB(dbMock, 0, DefaultOptions(), false, log.NewNopLogger()) + require.Equal(t, expectedVersion, ndb.storageVersion) +} + func TestNewNoDbStorage_ErrorInConstructor_DefaultSet(t *testing.T) { const expectedVersion = defaultStorageVersionValue @@ -67,6 +82,18 @@ func TestNewNoDbStorage_ErrorInConstructor_DefaultSet(t *testing.T) { require.Equal(t, expectedVersion, ndb.getStorageVersion()) } +func TestLegacyNewNoDbStorage_ErrorInConstructor_DefaultSet(t *testing.T) { + const expectedVersion = defaultStorageVersionValue + + ctrl := gomock.NewController(t) + dbMock := mock.NewMockDB(ctrl) + + dbMock.EXPECT().Get(gomock.Any()).Return(nil, errors.New("some db error")).Times(1) + dbMock.EXPECT().NewBatchWithSize(gomock.Any()).Return(nil).Times(1) + ndb := newLegacyNodeDB(dbMock, 0, DefaultOptions(), false, log.NewNopLogger()) + require.Equal(t, expectedVersion, ndb.getStorageVersion()) +} + func TestNewNoDbStorage_DoesNotExist_DefaultSet(t *testing.T) { const expectedVersion = defaultStorageVersionValue @@ -80,6 +107,19 @@ func TestNewNoDbStorage_DoesNotExist_DefaultSet(t *testing.T) { require.Equal(t, expectedVersion, ndb.getStorageVersion()) } +func TestLegacyNewNoDbStorage_DoesNotExist_DefaultSet(t *testing.T) { + const expectedVersion = defaultStorageVersionValue + + ctrl := gomock.NewController(t) + dbMock := mock.NewMockDB(ctrl) + + dbMock.EXPECT().Get(gomock.Any()).Return(nil, nil).Times(1) + dbMock.EXPECT().NewBatchWithSize(gomock.Any()).Return(nil).Times(1) + + ndb := newLegacyNodeDB(dbMock, 0, DefaultOptions(), false, log.NewNopLogger()) + require.Equal(t, expectedVersion, ndb.getStorageVersion()) +} + func TestSetStorageVersion_Success(t *testing.T) { const expectedVersion = fastStorageVersionValue @@ -98,6 +138,24 @@ func TestSetStorageVersion_Success(t *testing.T) { require.NoError(t, ndb.batch.Write()) } +func TestLegacySetStorageVersion_Success(t *testing.T) { + const expectedVersion = fastStorageVersionValue + + db := dbm.NewMemDB() + + ndb := newLegacyNodeDB(db, 0, DefaultOptions(), false, log.NewNopLogger()) + require.Equal(t, defaultStorageVersionValue, ndb.getStorageVersion()) + + latestVersion, err := ndb.getLatestVersion() + require.NoError(t, err) + + err = ndb.SetFastStorageVersionToBatch(latestVersion) + require.NoError(t, err) + + require.Equal(t, expectedVersion+fastStorageVersionDelimiter+strconv.Itoa(int(latestVersion)), ndb.getStorageVersion()) + require.NoError(t, ndb.batch.Write()) +} + func TestSetStorageVersion_DBFailure_OldKept(t *testing.T) { ctrl := gomock.NewController(t) dbMock := mock.NewMockDB(ctrl) @@ -122,6 +180,30 @@ func TestSetStorageVersion_DBFailure_OldKept(t *testing.T) { require.Equal(t, defaultStorageVersionValue, ndb.getStorageVersion()) } +func TestLegacySetStorageVersion_DBFailure_OldKept(t *testing.T) { + ctrl := gomock.NewController(t) + dbMock := mock.NewMockDB(ctrl) + batchMock := mock.NewMockBatch(ctrl) + + expectedErrorMsg := "some db error" + + expectedFastCacheVersion := 2 + + dbMock.EXPECT().Get(gomock.Any()).Return([]byte(defaultStorageVersionValue), nil).Times(1) + dbMock.EXPECT().NewBatchWithSize(gomock.Any()).Return(batchMock).Times(1) + + batchMock.EXPECT().GetByteSize().Return(100, nil).Times(1) + batchMock.EXPECT().Set(metadataKeyFormat.Key([]byte(storageVersionKey)), []byte(fastStorageVersionValue+fastStorageVersionDelimiter+strconv.Itoa(expectedFastCacheVersion))).Return(errors.New(expectedErrorMsg)).Times(1) + + ndb := newLegacyNodeDB(dbMock, 0, DefaultOptions(), false, log.NewNopLogger()) + require.Equal(t, defaultStorageVersionValue, ndb.getStorageVersion()) + + err := ndb.SetFastStorageVersionToBatch(int64(expectedFastCacheVersion)) + require.Error(t, err) + require.Equal(t, expectedErrorMsg, err.Error()) + require.Equal(t, defaultStorageVersionValue, ndb.getStorageVersion()) +} + func TestSetStorageVersion_InvalidVersionFailure_OldKept(t *testing.T) { ctrl := gomock.NewController(t) dbMock := mock.NewMockDB(ctrl) @@ -143,6 +225,27 @@ func TestSetStorageVersion_InvalidVersionFailure_OldKept(t *testing.T) { require.Equal(t, invalidStorageVersion, ndb.getStorageVersion()) } +func TestLegacySetStorageVersion_InvalidVersionFailure_OldKept(t *testing.T) { + ctrl := gomock.NewController(t) + dbMock := mock.NewMockDB(ctrl) + batchMock := mock.NewMockBatch(ctrl) + + expectedErrorMsg := errInvalidFastStorageVersion + + invalidStorageVersion := fastStorageVersionValue + fastStorageVersionDelimiter + "1" + fastStorageVersionDelimiter + "2" + + dbMock.EXPECT().Get(gomock.Any()).Return([]byte(invalidStorageVersion), nil).Times(1) + dbMock.EXPECT().NewBatchWithSize(gomock.Any()).Return(batchMock).Times(1) + + ndb := newLegacyNodeDB(dbMock, 0, DefaultOptions(), false, log.NewNopLogger()) + require.Equal(t, invalidStorageVersion, ndb.getStorageVersion()) + + err := ndb.SetFastStorageVersionToBatch(0) + require.Error(t, err) + require.Equal(t, expectedErrorMsg, err) + require.Equal(t, invalidStorageVersion, ndb.getStorageVersion()) +} + func TestSetStorageVersion_FastVersionFirst_VersionAppended(t *testing.T) { db := dbm.NewMemDB() ndb := newNodeDB(db, 0, DefaultOptions(), log.NewNopLogger()) @@ -154,6 +257,17 @@ func TestSetStorageVersion_FastVersionFirst_VersionAppended(t *testing.T) { require.Equal(t, fastStorageVersionValue+fastStorageVersionDelimiter+strconv.Itoa(int(ndb.latestVersion)), ndb.storageVersion) } +func TestLegacySetStorageVersion_FastVersionFirst_VersionAppended(t *testing.T) { + db := dbm.NewMemDB() + ndb := newLegacyNodeDB(db, 0, DefaultOptions(), false, log.NewNopLogger()) + ndb.storageVersion = fastStorageVersionValue + ndb.latestVersion = 100 + + err := ndb.SetFastStorageVersionToBatch(ndb.latestVersion) + require.NoError(t, err) + require.Equal(t, fastStorageVersionValue+fastStorageVersionDelimiter+strconv.Itoa(int(ndb.latestVersion)), ndb.storageVersion) +} + func TestSetStorageVersion_FastVersionSecond_VersionAppended(t *testing.T) { db := dbm.NewMemDB() ndb := newNodeDB(db, 0, DefaultOptions(), log.NewNopLogger()) @@ -168,6 +282,20 @@ func TestSetStorageVersion_FastVersionSecond_VersionAppended(t *testing.T) { require.Equal(t, string(storageVersionBytes)+fastStorageVersionDelimiter+strconv.Itoa(int(ndb.latestVersion)), ndb.storageVersion) } +func TestLegacySetStorageVersion_FastVersionSecond_VersionAppended(t *testing.T) { + db := dbm.NewMemDB() + ndb := newLegacyNodeDB(db, 0, DefaultOptions(), false, log.NewNopLogger()) + ndb.latestVersion = 100 + + storageVersionBytes := []byte(fastStorageVersionValue) + storageVersionBytes[len(fastStorageVersionValue)-1]++ // increment last byte + ndb.storageVersion = string(storageVersionBytes) + + err := ndb.SetFastStorageVersionToBatch(ndb.latestVersion) + require.NoError(t, err) + require.Equal(t, string(storageVersionBytes)+fastStorageVersionDelimiter+strconv.Itoa(int(ndb.latestVersion)), ndb.storageVersion) +} + func TestSetStorageVersion_SameVersionTwice(t *testing.T) { db := dbm.NewMemDB() ndb := newNodeDB(db, 0, DefaultOptions(), log.NewNopLogger()) @@ -187,6 +315,25 @@ func TestSetStorageVersion_SameVersionTwice(t *testing.T) { require.Equal(t, newStorageVersion, ndb.storageVersion) } +func TestLegacySetStorageVersion_SameVersionTwice(t *testing.T) { + db := dbm.NewMemDB() + ndb := newLegacyNodeDB(db, 0, DefaultOptions(), false, log.NewNopLogger()) + ndb.latestVersion = 100 + + storageVersionBytes := []byte(fastStorageVersionValue) + storageVersionBytes[len(fastStorageVersionValue)-1]++ // increment last byte + ndb.storageVersion = string(storageVersionBytes) + + err := ndb.SetFastStorageVersionToBatch(ndb.latestVersion) + require.NoError(t, err) + newStorageVersion := string(storageVersionBytes) + fastStorageVersionDelimiter + strconv.Itoa(int(ndb.latestVersion)) + require.Equal(t, newStorageVersion, ndb.storageVersion) + + err = ndb.SetFastStorageVersionToBatch(ndb.latestVersion) + require.NoError(t, err) + require.Equal(t, newStorageVersion, ndb.storageVersion) +} + // Test case where version is incorrect and has some extra garbage at the end func TestShouldForceFastStorageUpdate_DefaultVersion_True(t *testing.T) { db := dbm.NewMemDB() @@ -199,6 +346,17 @@ func TestShouldForceFastStorageUpdate_DefaultVersion_True(t *testing.T) { require.NoError(t, err) } +func TestLegacyShouldForceFastStorageUpdate_DefaultVersion_True(t *testing.T) { + db := dbm.NewMemDB() + ndb := newLegacyNodeDB(db, 0, DefaultOptions(), false, log.NewNopLogger()) + ndb.storageVersion = defaultStorageVersionValue + ndb.latestVersion = 100 + + shouldForce, err := ndb.shouldForceFastStorageUpgrade() + require.False(t, shouldForce) + require.NoError(t, err) +} + func TestShouldForceFastStorageUpdate_FastVersion_Greater_True(t *testing.T) { db := dbm.NewMemDB() ndb := newNodeDB(db, 0, DefaultOptions(), log.NewNopLogger()) @@ -210,6 +368,17 @@ func TestShouldForceFastStorageUpdate_FastVersion_Greater_True(t *testing.T) { require.NoError(t, err) } +func TestLegacyShouldForceFastStorageUpdate_FastVersion_Greater_True(t *testing.T) { + db := dbm.NewMemDB() + ndb := newLegacyNodeDB(db, 0, DefaultOptions(), false, log.NewNopLogger()) + ndb.latestVersion = 100 + ndb.storageVersion = fastStorageVersionValue + fastStorageVersionDelimiter + strconv.Itoa(int(ndb.latestVersion+1)) + + shouldForce, err := ndb.shouldForceFastStorageUpgrade() + require.True(t, shouldForce) + require.NoError(t, err) +} + func TestShouldForceFastStorageUpdate_FastVersion_Smaller_True(t *testing.T) { db := dbm.NewMemDB() ndb := newNodeDB(db, 0, DefaultOptions(), log.NewNopLogger()) @@ -221,6 +390,17 @@ func TestShouldForceFastStorageUpdate_FastVersion_Smaller_True(t *testing.T) { require.NoError(t, err) } +func TestLegacyShouldForceFastStorageUpdate_FastVersion_Smaller_True(t *testing.T) { + db := dbm.NewMemDB() + ndb := newLegacyNodeDB(db, 0, DefaultOptions(), false, log.NewNopLogger()) + ndb.latestVersion = 100 + ndb.storageVersion = fastStorageVersionValue + fastStorageVersionDelimiter + strconv.Itoa(int(ndb.latestVersion-1)) + + shouldForce, err := ndb.shouldForceFastStorageUpgrade() + require.True(t, shouldForce) + require.NoError(t, err) +} + func TestShouldForceFastStorageUpdate_FastVersion_Match_False(t *testing.T) { db := dbm.NewMemDB() ndb := newNodeDB(db, 0, DefaultOptions(), log.NewNopLogger()) @@ -232,6 +412,17 @@ func TestShouldForceFastStorageUpdate_FastVersion_Match_False(t *testing.T) { require.NoError(t, err) } +func TestLegacyShouldForceFastStorageUpdate_FastVersion_Match_False(t *testing.T) { + db := dbm.NewMemDB() + ndb := newLegacyNodeDB(db, 0, DefaultOptions(), false, log.NewNopLogger()) + ndb.latestVersion = 100 + ndb.storageVersion = fastStorageVersionValue + fastStorageVersionDelimiter + strconv.Itoa(int(ndb.latestVersion)) + + shouldForce, err := ndb.shouldForceFastStorageUpgrade() + require.False(t, shouldForce) + require.NoError(t, err) +} + func TestIsFastStorageEnabled_True(t *testing.T) { db := dbm.NewMemDB() ndb := newNodeDB(db, 0, DefaultOptions(), log.NewNopLogger()) @@ -241,6 +432,15 @@ func TestIsFastStorageEnabled_True(t *testing.T) { require.True(t, ndb.hasUpgradedToFastStorage()) } +func TestLegacyIsFastStorageEnabled_True(t *testing.T) { + db := dbm.NewMemDB() + ndb := newLegacyNodeDB(db, 0, DefaultOptions(), false, log.NewNopLogger()) + ndb.latestVersion = 100 + ndb.storageVersion = fastStorageVersionValue + fastStorageVersionDelimiter + strconv.Itoa(int(ndb.latestVersion)) + + require.True(t, ndb.hasUpgradedToFastStorage()) +} + func TestIsFastStorageEnabled_False(t *testing.T) { db := dbm.NewMemDB() ndb := newNodeDB(db, 0, DefaultOptions(), log.NewNopLogger()) @@ -252,6 +452,17 @@ func TestIsFastStorageEnabled_False(t *testing.T) { require.NoError(t, err) } +func TestLegacyIsFastStorageEnabled_False(t *testing.T) { + db := dbm.NewMemDB() + ndb := newLegacyNodeDB(db, 0, DefaultOptions(), false, log.NewNopLogger()) + ndb.latestVersion = 100 + ndb.storageVersion = defaultStorageVersionValue + + shouldForce, err := ndb.shouldForceFastStorageUpgrade() + require.False(t, shouldForce) + require.NoError(t, err) +} + func TestTraverseNodes(t *testing.T) { tree := getTestTree(0) // version 1 @@ -288,6 +499,43 @@ func TestTraverseNodes(t *testing.T) { require.Equal(t, 64, count) } +func TestLegacyTraverseNodes(t *testing.T) { + tree := getLegacyTestTree(0) + // version 1 + for i := 0; i < 20; i++ { + _, err := tree.Set([]byte{byte(i)}, []byte{byte(i)}) + require.NoError(t, err) + } + _, _, err := tree.SaveVersion() + require.NoError(t, err) + // version 2, no commit + _, _, err = tree.SaveVersion() + require.NoError(t, err) + // version 3 + for i := 20; i < 30; i++ { + _, err := tree.Set([]byte{byte(i)}, []byte{byte(i)}) + require.NoError(t, err) + } + _, _, err = tree.SaveVersion() + require.NoError(t, err) + + count := 0 + err = tree.ndb.traverseNodes(func(node *Node) error { + node.isLegacy = true + actualNode, err := tree.ndb.GetNode(node.GetKey()) + if err != nil { + return err + } + if actualNode.String() != node.String() { + return fmt.Errorf("found unexpected node") + } + count++ + return nil + }) + require.NoError(t, err) + require.Equal(t, 64, count) +} + func assertOrphansAndBranches(t *testing.T, ndb *nodeDB, version int64, branches int, orphanKeys [][]byte) { var branchCount, orphanIndex int err := ndb.traverseOrphans(version, version+1, func(node *Node) error { @@ -378,6 +626,80 @@ func TestNodeDB_traverseOrphans(t *testing.T) { assertOrphansAndBranches(t, tree.ndb, 4, 8, [][]byte{{byte(9)}, {byte(10)}, {byte(12)}}) } +func TestLegacyNodeDB_traverseOrphans(t *testing.T) { + tree := getLegacyTestTree(0) + var up bool + var err error + + // version 1 + for i := 0; i < 20; i++ { + up, err = tree.Set([]byte{byte(i)}, []byte{byte(i)}) + require.False(t, up) + require.NoError(t, err) + } + _, _, err = tree.SaveVersion() + require.NoError(t, err) + // note: assertions were constructed by hand after inspecting the output of the graphviz below. + // WriteDOTGraphToFile("/tmp/tree_one.dot", tree.ImmutableTree) + + // version 2 + up, err = tree.Set([]byte{byte(19)}, []byte{byte(0)}) + require.True(t, up) + require.NoError(t, err) + _, _, err = tree.SaveVersion() + require.NoError(t, err) + // WriteDOTGraphToFile("/tmp/tree_two.dot", tree.ImmutableTree) + + assertOrphansAndBranches(t, tree.ndb, 1, 5, [][]byte{{byte(19)}}) + + // version 3 + k, up, err := tree.Remove([]byte{byte(0)}) + require.Equal(t, []byte{byte(0)}, k) + require.True(t, up) + require.NoError(t, err) + + _, _, err = tree.SaveVersion() + require.NoError(t, err) + // WriteDOTGraphToFile("/tmp/tree_three.dot", tree.ImmutableTree) + + assertOrphansAndBranches(t, tree.ndb, 2, 4, [][]byte{{byte(0)}}) + + // version 4 + k, up, err = tree.Remove([]byte{byte(1)}) + require.Equal(t, []byte{byte(1)}, k) + require.True(t, up) + require.NoError(t, err) + k, up, err = tree.Remove([]byte{byte(19)}) + require.Equal(t, []byte{byte(0)}, k) + require.True(t, up) + require.NoError(t, err) + + _, _, err = tree.SaveVersion() + require.NoError(t, err) + // WriteDOTGraphToFile("/tmp/tree_four.dot", tree.ImmutableTree) + + assertOrphansAndBranches(t, tree.ndb, 3, 7, [][]byte{{byte(1)}, {byte(19)}}) + + // version 5 + k, up, err = tree.Remove([]byte{byte(10)}) + require.Equal(t, []byte{byte(10)}, k) + require.True(t, up) + require.NoError(t, err) + k, up, err = tree.Remove([]byte{byte(9)}) + require.Equal(t, []byte{byte(9)}, k) + require.True(t, up) + require.NoError(t, err) + up, err = tree.Set([]byte{byte(12)}, []byte{byte(0)}) + require.True(t, up) + require.NoError(t, err) + + _, _, err = tree.SaveVersion() + require.NoError(t, err) + // WriteDOTGraphToFile("/tmp/tree_five.dot", tree.ImmutableTree) + + assertOrphansAndBranches(t, tree.ndb, 4, 8, [][]byte{{byte(9)}, {byte(10)}, {byte(12)}}) +} + func makeAndPopulateMutableTree(tb testing.TB) *MutableTree { memDB := dbm.NewMemDB() tree := NewMutableTree(memDB, 0, false, log.NewNopLogger(), InitialVersionOption(9)) @@ -437,3 +759,63 @@ func TestDeleteVersionsFromNoDeadlock(t *testing.T) { require.Error(t, err, "") require.Contains(t, err.Error(), fmt.Sprintf("unable to delete version %v with 2 active readers", targetVersion+2)) } + +func TestLegacyDeleteVersionsFromNoDeadlock(t *testing.T) { + const expectedVersion = fastStorageVersionValue + + db := dbm.NewMemDB() + + ndb := newLegacyNodeDB(db, 0, DefaultOptions(), false, log.NewNopLogger()) + require.Equal(t, defaultStorageVersionValue, ndb.getStorageVersion()) + + err := ndb.SetFastStorageVersionToBatch(ndb.latestVersion) + require.NoError(t, err) + + latestVersion, err := ndb.getLatestVersion() + require.NoError(t, err) + require.Equal(t, expectedVersion+fastStorageVersionDelimiter+strconv.Itoa(int(latestVersion)), ndb.getStorageVersion()) + require.NoError(t, ndb.batch.Write()) + + // Reported in https://github.com/cosmos/iavl/issues/842 + // there was a deadlock that triggered on an invalid version being + // checked for deletion. + // Now add in data to trigger the error path. + ndb.versionReaders[latestVersion+1] = 2 + + errCh := make(chan error) + targetVersion := latestVersion - 1 + + go func() { + defer close(errCh) + errCh <- ndb.DeleteVersionsFrom(targetVersion) + }() + + select { + case err = <-errCh: + // Happy path, the mutex was unlocked fast enough. + + case <-time.After(2 * time.Second): + t.Error("code did not return even after 2 seconds") + } + + require.True(t, ndb.mtx.TryLock(), "tryLock failed mutex was still locked") + ndb.mtx.Unlock() // Since TryLock passed, the lock is now solely being held by us. + require.Error(t, err, "") + require.Contains(t, err.Error(), fmt.Sprintf("unable to delete version %v with 2 active readers", targetVersion+2)) +} + +func TestLegacyNoStoreVersion_True(t *testing.T) { + db := dbm.NewMemDB() + err := db.Set(metadataKeyFormat.Key(ibytes.UnsafeStrToBytes(storageVersionKey)), []byte("1.1.1")) + require.NoError(t, err) + ndb := newLegacyNodeDB(db, 0, DefaultOptions(), true, log.NewNopLogger()) + require.Equal(t, ndb.storageVersion, defaultStorageVersionValue) +} + +func TestLegacyNoStoreVersion_False(t *testing.T) { + db := dbm.NewMemDB() + err := db.Set(metadataKeyFormat.Key(ibytes.UnsafeStrToBytes(storageVersionKey)), []byte("1.1.1")) + require.NoError(t, err) + ndb := newLegacyNodeDB(db, 0, DefaultOptions(), false, log.NewNopLogger()) + require.Equal(t, ndb.storageVersion, "1.1.1") +} From dffea95fb115e407b13b254418f2ab65620d0140 Mon Sep 17 00:00:00 2001 From: i-norden Date: Wed, 26 Jun 2024 16:45:31 -0400 Subject: [PATCH 07/18] legacy diff tests --- diff_test.go | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/diff_test.go b/diff_test.go index cf54716c7..b8f647c1c 100644 --- a/diff_test.go +++ b/diff_test.go @@ -39,6 +39,29 @@ func TestDiffRoundTrip(t *testing.T) { require.Equal(t, changeSets, extractChangeSets) } +func TestLegacyDiffRoundTrip(t *testing.T) { + changeSets := genChangeSets(rand.New(rand.NewSource(0)), 300) + + // apply changeSets to tree + db := dbm.NewMemDB() + tree := NewLegacyMutableTree(db, 0, true, false, nil, log.NewNopLogger()) + for i := range changeSets { + v, err := tree.SaveChangeSet(changeSets[i]) + require.NoError(t, err) + require.Equal(t, int64(i+1), v) + } + + // extract change sets from db + var extractChangeSets []*ChangeSet + tree2 := NewLegacyMutableTree(db, 0, true, false, nil, log.NewNopLogger()) + err := tree2.TraverseStateChanges(0, math.MaxInt64, func(_ int64, changeSet *ChangeSet) error { + extractChangeSets = append(extractChangeSets, changeSet) + return nil + }) + require.NoError(t, err) + require.Equal(t, changeSets, extractChangeSets) +} + func genChangeSets(r *rand.Rand, n int) []*ChangeSet { var changeSets []*ChangeSet From 184b1393187962ede399ed721b25cbc8893922d4 Mon Sep 17 00:00:00 2001 From: i-norden Date: Wed, 26 Jun 2024 16:45:49 -0400 Subject: [PATCH 08/18] legacy export tests --- export_test.go | 358 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 358 insertions(+) diff --git a/export_test.go b/export_test.go index 41f988302..14175f354 100644 --- a/export_test.go +++ b/export_test.go @@ -59,6 +59,51 @@ func setupExportTreeBasic(t require.TestingT) *ImmutableTree { return itree } +func setupLegacyExportTreeBasic(t require.TestingT) *ImmutableTree { + tree := NewLegacyMutableTree(dbm.NewMemDB(), 0, false, false, nil, log.NewNopLogger()) + + _, err := tree.Set([]byte("x"), []byte{255}) + require.NoError(t, err) + _, err = tree.Set([]byte("z"), []byte{255}) + require.NoError(t, err) + _, err = tree.Set([]byte("a"), []byte{1}) + require.NoError(t, err) + _, err = tree.Set([]byte("b"), []byte{2}) + require.NoError(t, err) + _, err = tree.Set([]byte("c"), []byte{3}) + require.NoError(t, err) + _, _, err = tree.SaveVersion() + require.NoError(t, err) + + _, _, err = tree.Remove([]byte("x")) + require.NoError(t, err) + _, _, err = tree.Remove([]byte("b")) + require.NoError(t, err) + _, err = tree.Set([]byte("c"), []byte{255}) + require.NoError(t, err) + _, err = tree.Set([]byte("d"), []byte{4}) + require.NoError(t, err) + _, _, err = tree.SaveVersion() + require.NoError(t, err) + + _, err = tree.Set([]byte("b"), []byte{2}) + require.NoError(t, err) + _, err = tree.Set([]byte("c"), []byte{3}) + require.NoError(t, err) + _, err = tree.Set([]byte("e"), []byte{5}) + require.NoError(t, err) + _, _, err = tree.Remove([]byte("z")) + require.NoError(t, err) + _, err = tree.Set([]byte("abc"), []byte{6}) + require.NoError(t, err) + _, version, err := tree.SaveVersion() + require.NoError(t, err) + + itree, err := tree.GetImmutable(version) + require.NoError(t, err) + return itree +} + // setupExportTreeRandom sets up a randomly generated tree. // nolint: dupl func setupExportTreeRandom(t *testing.T) *ImmutableTree { @@ -124,6 +169,69 @@ func setupExportTreeRandom(t *testing.T) *ImmutableTree { return itree } +func setupLegacyExportTreeRandom(t *testing.T) *ImmutableTree { + const ( + randSeed = 49872768940 // For deterministic tests + keySize = 16 + valueSize = 16 + + versions = 8 // number of versions to generate + versionOps = 1024 // number of operations (create/update/delete) per version + updateRatio = 0.4 // ratio of updates out of all operations + deleteRatio = 0.2 // ratio of deletes out of all operations + ) + + r := rand.New(rand.NewSource(randSeed)) + tree := NewLegacyMutableTree(dbm.NewMemDB(), 0, false, false, nil, log.NewNopLogger()) + + var version int64 + keys := make([][]byte, 0, versionOps) + for i := 0; i < versions; i++ { + for j := 0; j < versionOps; j++ { + key := make([]byte, keySize) + value := make([]byte, valueSize) + + // The performance of this is likely to be terrible, but that's fine for small tests + switch { + case len(keys) > 0 && r.Float64() <= deleteRatio: + index := r.Intn(len(keys)) + key = keys[index] + keys = append(keys[:index], keys[index+1:]...) + _, removed, err := tree.Remove(key) + require.NoError(t, err) + require.True(t, removed) + + case len(keys) > 0 && r.Float64() <= updateRatio: + key = keys[r.Intn(len(keys))] + r.Read(value) + updated, err := tree.Set(key, value) + require.NoError(t, err) + require.True(t, updated) + + default: + r.Read(key) + r.Read(value) + // If we get an update, set again + for updated, err := tree.Set(key, value); updated && err == nil; { + key = make([]byte, keySize) + r.Read(key) + } + keys = append(keys, key) + } + } + var err error + _, version, err = tree.SaveVersion() + require.NoError(t, err) + } + + require.EqualValues(t, versions, tree.Version()) + require.GreaterOrEqual(t, tree.Size(), int64(math.Trunc(versions*versionOps*(1-updateRatio-deleteRatio))/2)) + + itree, err := tree.GetImmutable(version) + require.NoError(t, err) + return itree +} + // setupExportTreeSized sets up a single-version tree with a given number // of randomly generated key/value pairs, useful for benchmarking. func setupExportTreeSized(t require.TestingT, treeSize int) *ImmutableTree { @@ -158,6 +266,38 @@ func setupExportTreeSized(t require.TestingT, treeSize int) *ImmutableTree { return itree } +func setupLegacyExportTreeSized(t require.TestingT, treeSize int) *ImmutableTree { + const ( + randSeed = 49872768940 // For deterministic tests + keySize = 16 + valueSize = 16 + ) + + r := rand.New(rand.NewSource(randSeed)) + tree := NewLegacyMutableTree(dbm.NewMemDB(), 0, false, false, nil, log.NewNopLogger()) + + for i := 0; i < treeSize; i++ { + key := make([]byte, keySize) + value := make([]byte, valueSize) + r.Read(key) + r.Read(value) + updated, err := tree.Set(key, value) + require.NoError(t, err) + + if updated { + i-- + } + } + + _, version, err := tree.SaveVersion() + require.NoError(t, err) + + itree, err := tree.GetImmutable(version) + require.NoError(t, err) + + return itree +} + func TestExporter(t *testing.T) { tree := setupExportTreeBasic(t) @@ -191,6 +331,39 @@ func TestExporter(t *testing.T) { assert.Equal(t, expect, actual) } +func TestLegacyExporter(t *testing.T) { + tree := setupLegacyExportTreeBasic(t) + + expect := []*ExportNode{ + {Key: []byte("a"), Value: []byte{1}, Version: 1, Height: 0}, + {Key: []byte("abc"), Value: []byte{6}, Version: 3, Height: 0}, + {Key: []byte("abc"), Value: nil, Version: 3, Height: 1}, + {Key: []byte("b"), Value: []byte{2}, Version: 3, Height: 0}, + {Key: []byte("c"), Value: []byte{3}, Version: 3, Height: 0}, + {Key: []byte("c"), Value: nil, Version: 3, Height: 1}, + {Key: []byte("b"), Value: nil, Version: 3, Height: 2}, + {Key: []byte("d"), Value: []byte{4}, Version: 2, Height: 0}, + {Key: []byte("e"), Value: []byte{5}, Version: 3, Height: 0}, + {Key: []byte("e"), Value: nil, Version: 3, Height: 1}, + {Key: []byte("d"), Value: nil, Version: 3, Height: 3}, + } + + actual := make([]*ExportNode, 0, len(expect)) + exporter, err := tree.Export() + require.NoError(t, err) + defer exporter.Close() + for { + node, err := exporter.Next() + if err == ErrorExportDone { + break + } + require.NoError(t, err) + actual = append(actual, node) + } + + assert.Equal(t, expect, actual) +} + func TestExporterCompress(t *testing.T) { tree := setupExportTreeBasic(t) @@ -226,6 +399,41 @@ func TestExporterCompress(t *testing.T) { assert.Equal(t, expect, actual) } +func TestLegacyExporterCompress(t *testing.T) { + tree := setupLegacyExportTreeBasic(t) + + expect := []*ExportNode{ + {Key: []byte{0, 'a'}, Value: []byte{1}, Version: 1, Height: 0}, + {Key: []byte{1, 'b', 'c'}, Value: []byte{6}, Version: 3, Height: 0}, + {Key: nil, Value: nil, Version: 0, Height: 1}, + {Key: []byte{0, 'b'}, Value: []byte{2}, Version: 3, Height: 0}, + {Key: []byte{0, 'c'}, Value: []byte{3}, Version: 3, Height: 0}, + {Key: nil, Value: nil, Version: 0, Height: 1}, + {Key: nil, Value: nil, Version: 0, Height: 2}, + {Key: []byte{0, 'd'}, Value: []byte{4}, Version: 2, Height: 0}, + {Key: []byte{0, 'e'}, Value: []byte{5}, Version: 3, Height: 0}, + {Key: nil, Value: nil, Version: 0, Height: 1}, + {Key: nil, Value: nil, Version: 0, Height: 3}, + } + + actual := make([]*ExportNode, 0, len(expect)) + innerExporter, err := tree.Export() + require.NoError(t, err) + defer innerExporter.Close() + + exporter := NewCompressExporter(innerExporter) + for { + node, err := exporter.Next() + if err == ErrorExportDone { + break + } + require.NoError(t, err) + actual = append(actual, node) + } + + assert.Equal(t, expect, actual) +} + func TestExporter_Import(t *testing.T) { testcases := map[string]*ImmutableTree{ "empty tree": NewImmutableTree(dbm.NewMemDB(), 0, false, log.NewNopLogger()), @@ -298,6 +506,78 @@ func TestExporter_Import(t *testing.T) { } } +func TestLegacyExporter_Import(t *testing.T) { + testcases := map[string]*ImmutableTree{ + "empty tree": NewLegacyImmutableTree(dbm.NewMemDB(), 0, false, false, nil, log.NewNopLogger()), + "basic tree": setupLegacyExportTreeBasic(t), + } + if !testing.Short() { + testcases["sized tree"] = setupLegacyExportTreeSized(t, 4096) + testcases["random tree"] = setupLegacyExportTreeRandom(t) + } + + for desc, tree := range testcases { + tree := tree + for _, compress := range []bool{false, true} { + if compress { + desc += "-compress" + } + compress := compress + t.Run(desc, func(t *testing.T) { + t.Parallel() + root := tree.Hash() + innerExporter, err := tree.Export() + require.NoError(t, err) + defer innerExporter.Close() + + exporter := NodeExporter(innerExporter) + if compress { + exporter = NewCompressExporter(innerExporter) + } + + newTree := NewLegacyMutableTree(dbm.NewMemDB(), 0, false, false, root, log.NewNopLogger()) + innerImporter, err := newTree.Import(tree.Version()) + require.NoError(t, err) + defer innerImporter.Close() + + importer := NodeImporter(innerImporter) + if compress { + importer = NewCompressImporter(innerImporter) + } + + for { + item, err := exporter.Next() + if err == ErrorExportDone { + err = innerImporter.Commit() + require.NoError(t, err) + break + } + require.NoError(t, err) + err = importer.Add(item) + require.NoError(t, err) + } + + treeHash := tree.Hash() + newTreeHash := newTree.Hash() + + require.Equal(t, treeHash, newTreeHash, "Tree hash mismatch") + require.Equal(t, tree.Size(), newTree.Size(), "Tree size mismatch") + require.Equal(t, tree.Version(), newTree.Version(), "Tree version mismatch") + + tree.Iterate(func(key, value []byte) bool { //nolint:errcheck + index, _, err := tree.GetWithIndex(key) + require.NoError(t, err) + newIndex, newValue, err := newTree.GetWithIndex(key) + require.NoError(t, err) + require.Equal(t, index, newIndex, "Index mismatch for key %v", key) + require.Equal(t, value, newValue, "Value mismatch for key %v", key) + return false + }) + }) + } + } +} + func TestExporter_Close(t *testing.T) { tree := setupExportTreeSized(t, 4096) exporter, err := tree.Export() @@ -322,6 +602,30 @@ func TestExporter_Close(t *testing.T) { exporter.Close() } +func TestLegacyExporter_Close(t *testing.T) { + tree := setupLegacyExportTreeSized(t, 4096) + exporter, err := tree.Export() + require.NoError(t, err) + + node, err := exporter.Next() + require.NoError(t, err) + require.NotNil(t, node) + + exporter.Close() + node, err = exporter.Next() + require.Error(t, err) + require.Equal(t, ErrorExportDone, err) + require.Nil(t, node) + + node, err = exporter.Next() + require.Error(t, err) + require.Equal(t, ErrorExportDone, err) + require.Nil(t, node) + + exporter.Close() + exporter.Close() +} + func TestExporter_DeleteVersionErrors(t *testing.T) { tree := NewMutableTree(dbm.NewMemDB(), 0, false, log.NewNopLogger()) @@ -357,6 +661,41 @@ func TestExporter_DeleteVersionErrors(t *testing.T) { require.NoError(t, err) } +func TestLegacyExporter_DeleteVersionErrors(t *testing.T) { + tree := NewLegacyMutableTree(dbm.NewMemDB(), 0, false, false, nil, log.NewNopLogger()) + + _, err := tree.Set([]byte("a"), []byte{1}) + require.NoError(t, err) + _, _, err = tree.SaveVersion() + require.NoError(t, err) + + _, err = tree.Set([]byte("b"), []byte{2}) + require.NoError(t, err) + _, _, err = tree.SaveVersion() + require.NoError(t, err) + + _, err = tree.Set([]byte("c"), []byte{3}) + require.NoError(t, err) + _, _, err = tree.SaveVersion() + require.NoError(t, err) + + itree, err := tree.GetImmutable(2) + require.NoError(t, err) + exporter, err := itree.Export() + require.NoError(t, err) + defer exporter.Close() + + err = tree.DeleteVersionsTo(1) + require.NoError(t, err) + + err = tree.DeleteVersionsTo(2) + require.Error(t, err) + + exporter.Close() + err = tree.DeleteVersionsTo(2) + require.NoError(t, err) +} + func BenchmarkExport(b *testing.B) { b.StopTimer() tree := setupExportTreeSized(b, 4096) @@ -375,3 +714,22 @@ func BenchmarkExport(b *testing.B) { exporter.Close() } } + +func BenchmarkLegacyExport(b *testing.B) { + b.StopTimer() + tree := setupLegacyExportTreeSized(b, 4096) + b.StartTimer() + for n := 0; n < b.N; n++ { + exporter, err := tree.Export() + require.NoError(b, err) + for { + _, err := exporter.Next() + if err == ErrorExportDone { + break + } else if err != nil { + b.Error(err) + } + } + exporter.Close() + } +} From a87f0c088b857659fe1d6947814bc633a213bfd1 Mon Sep 17 00:00:00 2001 From: i-norden Date: Wed, 26 Jun 2024 16:45:57 -0400 Subject: [PATCH 09/18] legacy import tests --- import_test.go | 263 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 263 insertions(+) diff --git a/import_test.go b/import_test.go index aec9406a9..5eaeac42f 100644 --- a/import_test.go +++ b/import_test.go @@ -70,12 +70,78 @@ func ExampleImporter() { } } +func ExampleLegacyImporter() { + tree := NewLegacyMutableTree(dbm.NewMemDB(), 0, false, false, nil, log.NewNopLogger()) + + _, err := tree.Set([]byte("a"), []byte{1}) + if err != nil { + panic(err) + } + + _, err = tree.Set([]byte("b"), []byte{2}) + if err != nil { + panic(err) + } + _, err = tree.Set([]byte("c"), []byte{3}) + if err != nil { + panic(err) + } + _, version, err := tree.SaveVersion() + if err != nil { + panic(err) + } + + itree, err := tree.GetImmutable(version) + if err != nil { + panic(err) + } + exporter, err := itree.Export() + if err != nil { + panic(err) + } + defer exporter.Close() + exported := []*ExportNode{} + for { + var node *ExportNode + node, err = exporter.Next() + if err == ErrorExportDone { + break + } else if err != nil { + panic(err) + } + exported = append(exported, node) + } + + newTree := NewLegacyMutableTree(dbm.NewMemDB(), 0, false, false, nil, log.NewNopLogger()) + importer, err := newTree.Import(version) + if err != nil { + panic(err) + } + defer importer.Close() + for _, node := range exported { + err = importer.Add(node) + if err != nil { + panic(err) + } + } + err = importer.Commit() + if err != nil { + panic(err) + } +} + func TestImporter_NegativeVersion(t *testing.T) { tree := NewMutableTree(dbm.NewMemDB(), 0, false, log.NewNopLogger()) _, err := tree.Import(-1) require.Error(t, err) } +func TestLegacyImporter_NegativeVersion(t *testing.T) { + tree := NewLegacyMutableTree(dbm.NewMemDB(), 0, false, false, nil, log.NewNopLogger()) + _, err := tree.Import(-1) + require.Error(t, err) +} + func TestImporter_NotEmpty(t *testing.T) { tree := NewMutableTree(dbm.NewMemDB(), 0, false, log.NewNopLogger()) _, err := tree.Set([]byte("a"), []byte{1}) @@ -87,6 +153,17 @@ func TestImporter_NotEmpty(t *testing.T) { require.Error(t, err) } +func TestLegacyImporter_NotEmpty(t *testing.T) { + tree := NewLegacyMutableTree(dbm.NewMemDB(), 0, false, false, nil, log.NewNopLogger()) + _, err := tree.Set([]byte("a"), []byte{1}) + require.NoError(t, err) + _, _, err = tree.SaveVersion() + require.NoError(t, err) + + _, err = tree.Import(1) + require.Error(t, err) +} + func TestImporter_NotEmptyDatabase(t *testing.T) { db := dbm.NewMemDB() @@ -104,6 +181,23 @@ func TestImporter_NotEmptyDatabase(t *testing.T) { require.Error(t, err) } +func TestLegacyImporter_NotEmptyDatabase(t *testing.T) { + db := dbm.NewMemDB() + + tree := NewLegacyMutableTree(db, 0, false, false, nil, log.NewNopLogger()) + _, err := tree.Set([]byte("a"), []byte{1}) + require.NoError(t, err) + _, _, err = tree.SaveVersion() + require.NoError(t, err) + + tree = NewLegacyMutableTree(db, 0, false, false, nil, log.NewNopLogger()) + _, err = tree.Load() + require.NoError(t, err) + + _, err = tree.Import(1) + require.Error(t, err) +} + func TestImporter_NotEmptyUnsaved(t *testing.T) { tree := NewMutableTree(dbm.NewMemDB(), 0, false, log.NewNopLogger()) _, err := tree.Set([]byte("a"), []byte{1}) @@ -113,6 +207,15 @@ func TestImporter_NotEmptyUnsaved(t *testing.T) { require.Error(t, err) } +func TestLegacyImporter_NotEmptyUnsaved(t *testing.T) { + tree := NewLegacyMutableTree(dbm.NewMemDB(), 0, false, false, nil, log.NewNopLogger()) + _, err := tree.Set([]byte("a"), []byte{1}) + require.NoError(t, err) + + _, err = tree.Import(1) + require.Error(t, err) +} + func TestImporter_Add(t *testing.T) { k := []byte("key") v := []byte("value") @@ -150,6 +253,43 @@ func TestImporter_Add(t *testing.T) { } } +func TestLegacyImporter_Add(t *testing.T) { + k := []byte("key") + v := []byte("value") + + testcases := map[string]struct { + node *ExportNode + valid bool + }{ + "nil node": {nil, false}, + "valid": {&ExportNode{Key: k, Value: v, Version: 1, Height: 0}, true}, + "no key": {&ExportNode{Key: nil, Value: v, Version: 1, Height: 0}, false}, + "no value": {&ExportNode{Key: k, Value: nil, Version: 1, Height: 0}, false}, + "version too large": {&ExportNode{Key: k, Value: v, Version: 2, Height: 0}, false}, + "no version": {&ExportNode{Key: k, Value: v, Version: 0, Height: 0}, false}, + // further cases will be handled by Node.validate() + } + for desc, tc := range testcases { + tc := tc // appease scopelint + t.Run(desc, func(t *testing.T) { + tree := NewLegacyMutableTree(dbm.NewMemDB(), 0, false, false, nil, log.NewNopLogger()) + importer, err := tree.Import(1) + require.NoError(t, err) + defer importer.Close() + + err = importer.Add(tc.node) + if tc.valid { + require.NoError(t, err) + } else { + if err == nil { + err = importer.Commit() + } + require.Error(t, err) + } + }) + } +} + func TestImporter_Add_Closed(t *testing.T) { tree := NewMutableTree(dbm.NewMemDB(), 0, false, log.NewNopLogger()) importer, err := tree.Import(1) @@ -161,6 +301,17 @@ func TestImporter_Add_Closed(t *testing.T) { require.Equal(t, ErrNoImport, err) } +func TestLegacyImporter_Add_Closed(t *testing.T) { + tree := NewLegacyMutableTree(dbm.NewMemDB(), 0, false, false, nil, log.NewNopLogger()) + importer, err := tree.Import(1) + require.NoError(t, err) + + importer.Close() + err = importer.Add(&ExportNode{Key: []byte("key"), Value: []byte("value"), Version: 1, Height: 0}) + require.Error(t, err) + require.Equal(t, ErrNoImport, err) +} + func TestImporter_Close(t *testing.T) { tree := NewMutableTree(dbm.NewMemDB(), 0, false, log.NewNopLogger()) importer, err := tree.Import(1) @@ -177,6 +328,22 @@ func TestImporter_Close(t *testing.T) { importer.Close() } +func TestLegacyImporter_Close(t *testing.T) { + tree := NewLegacyMutableTree(dbm.NewMemDB(), 0, false, false, nil, log.NewNopLogger()) + importer, err := tree.Import(1) + require.NoError(t, err) + + err = importer.Add(&ExportNode{Key: []byte("key"), Value: []byte("value"), Version: 1, Height: 0}) + require.NoError(t, err) + + importer.Close() + has, err := tree.Has([]byte("key")) + require.NoError(t, err) + require.False(t, has) + + importer.Close() +} + func TestImporter_Commit(t *testing.T) { tree := NewMutableTree(dbm.NewMemDB(), 0, false, log.NewNopLogger()) importer, err := tree.Import(1) @@ -192,6 +359,21 @@ func TestImporter_Commit(t *testing.T) { require.True(t, has) } +func TestLegacyImporter_Commit(t *testing.T) { + tree := NewLegacyMutableTree(dbm.NewMemDB(), 0, false, false, nil, log.NewNopLogger()) + importer, err := tree.Import(1) + require.NoError(t, err) + + err = importer.Add(&ExportNode{Key: []byte("key"), Value: []byte("value"), Version: 1, Height: 0}) + require.NoError(t, err) + + err = importer.Commit() + require.NoError(t, err) + has, err := tree.Has([]byte("key")) + require.NoError(t, err) + require.True(t, has) +} + func TestImporter_Commit_ForwardVersion(t *testing.T) { tree := NewMutableTree(dbm.NewMemDB(), 0, false, log.NewNopLogger()) importer, err := tree.Import(2) @@ -207,6 +389,21 @@ func TestImporter_Commit_ForwardVersion(t *testing.T) { require.True(t, has) } +func TestLegacyImporter_Commit_ForwardVersion(t *testing.T) { + tree := NewLegacyMutableTree(dbm.NewMemDB(), 0, false, false, nil, log.NewNopLogger()) + importer, err := tree.Import(2) + require.NoError(t, err) + + err = importer.Add(&ExportNode{Key: []byte("key"), Value: []byte("value"), Version: 1, Height: 0}) + require.NoError(t, err) + + err = importer.Commit() + require.NoError(t, err) + has, err := tree.Has([]byte("key")) + require.NoError(t, err) + require.True(t, has) +} + func TestImporter_Commit_Closed(t *testing.T) { tree := NewMutableTree(dbm.NewMemDB(), 0, false, log.NewNopLogger()) importer, err := tree.Import(1) @@ -221,6 +418,20 @@ func TestImporter_Commit_Closed(t *testing.T) { require.Equal(t, ErrNoImport, err) } +func TestLegacyImporter_Commit_Closed(t *testing.T) { + tree := NewLegacyMutableTree(dbm.NewMemDB(), 0, false, false, nil, log.NewNopLogger()) + importer, err := tree.Import(1) + require.NoError(t, err) + + err = importer.Add(&ExportNode{Key: []byte("key"), Value: []byte("value"), Version: 1, Height: 0}) + require.NoError(t, err) + + importer.Close() + err = importer.Commit() + require.Error(t, err) + require.Equal(t, ErrNoImport, err) +} + func TestImporter_Commit_Empty(t *testing.T) { tree := NewMutableTree(dbm.NewMemDB(), 0, false, log.NewNopLogger()) importer, err := tree.Import(3) @@ -232,14 +443,33 @@ func TestImporter_Commit_Empty(t *testing.T) { assert.EqualValues(t, 3, tree.Version()) } +func TestLegacyImporter_Commit_Empty(t *testing.T) { + tree := NewLegacyMutableTree(dbm.NewMemDB(), 0, false, false, nil, log.NewNopLogger()) + importer, err := tree.Import(3) + require.NoError(t, err) + defer importer.Close() + + err = importer.Commit() + require.NoError(t, err) + assert.EqualValues(t, 3, tree.Version()) +} + func BenchmarkImport(b *testing.B) { benchmarkImport(b, 4096) } +func BenchmarkLegacyImport(b *testing.B) { + benchmarkLegacyImport(b, 4096) +} + func BenchmarkImportBatch(b *testing.B) { benchmarkImport(b, maxBatchSize*10) } +func BenchmarkLegacyImportBatch(b *testing.B) { + benchmarkLegacyImport(b, maxBatchSize*10) +} + func benchmarkImport(b *testing.B, nodes int) { b.StopTimer() tree := setupExportTreeSized(b, nodes) @@ -272,3 +502,36 @@ func benchmarkImport(b *testing.B, nodes int) { require.NoError(b, err) } } + +func benchmarkLegacyImport(b *testing.B, nodes int) { + b.StopTimer() + tree := setupLegacyExportTreeSized(b, nodes) + exported := make([]*ExportNode, 0, nodes) + exporter, err := tree.Export() + require.NoError(b, err) + for { + item, err := exporter.Next() + if err == ErrorExportDone { + break + } else if err != nil { + b.Error(err) + } + exported = append(exported, item) + } + exporter.Close() + b.StartTimer() + + for n := 0; n < b.N; n++ { + newTree := NewLegacyMutableTree(dbm.NewMemDB(), 0, false, false, nil, log.NewNopLogger()) + importer, err := newTree.Import(tree.Version()) + require.NoError(b, err) + for _, item := range exported { + err = importer.Add(item) + if err != nil { + b.Error(err) + } + } + err = importer.Commit() + require.NoError(b, err) + } +} From 050ed0d64a9071ee7557ce01f0616e6be87e9f7e Mon Sep 17 00:00:00 2001 From: i-norden Date: Wed, 26 Jun 2024 16:46:11 -0400 Subject: [PATCH 10/18] legacy import support --- import.go | 32 +++++++++++++++++++++----------- 1 file changed, 21 insertions(+), 11 deletions(-) diff --git a/import.go b/import.go index 3817e9419..e1a6146b7 100644 --- a/import.go +++ b/import.go @@ -31,6 +31,7 @@ type Importer struct { // inflightCommit tracks a batch commit, if any. inflightCommit <-chan error + useLegacy bool } // newImporter creates a new Importer for an empty MutableTree. @@ -47,13 +48,13 @@ func newImporter(tree *MutableTree, version int64) (*Importer, error) { if !tree.IsEmpty() { return nil, errors.New("tree must be empty") } - return &Importer{ - tree: tree, - version: version, - batch: tree.ndb.db.NewBatch(), - stack: make([]*Node, 0, 8), - nonces: make([]uint32, version+1), + tree: tree, + version: version, + batch: tree.ndb.db.NewBatch(), + stack: make([]*Node, 0, 8), + nonces: make([]uint32, version+1), + useLegacy: tree.useLegacyFormat, }, nil } @@ -149,6 +150,7 @@ func (i *Importer) Add(exportNode *ExportNode) error { key: exportNode.Key, value: exportNode.Value, subtreeHeight: exportNode.Height, + isLegacy: i.useLegacy, } // We build the tree from the bottom-left up. The stack is used to store unresolved left @@ -167,6 +169,12 @@ func (i *Importer) Add(exportNode *ExportNode) error { node.leftNode = leftNode node.rightNode = rightNode + if leftNode.isLegacy && len(leftNode.hash) == 0 { + leftNode._hash(leftNode.nodeKey.version) + } + if rightNode.isLegacy && len(rightNode.hash) == 0 { + rightNode._hash(rightNode.nodeKey.version) + } node.leftNodeKey = leftNode.GetKey() node.rightNodeKey = rightNode.GetKey() node.size = leftNode.size + rightNode.size @@ -221,15 +229,17 @@ func (i *Importer) Commit() error { } case 1: i.stack[0].nodeKey.nonce = 1 + if i.tree.useLegacyFormat { + if len(i.stack[0].hash) == 0 { + i.stack[0]._hash(i.version) + } + rootHash = i.stack[0].hash + } if err := i.writeNode(i.stack[0]); err != nil { return err } if i.stack[0].nodeKey.version < i.version { // it means there is no update in the given version if i.tree.useLegacyFormat { - if len(i.stack[0].hash) == 0 { - i.stack[0]._hash(i.version) - } - rootHash = i.stack[0].hash if err := i.batch.Set(i.tree.ndb.legacyRootKey(i.version), i.tree.ndb.legacyNodeKey(rootHash)); err != nil { return err } @@ -250,7 +260,7 @@ func (i *Importer) Commit() error { } i.tree.ndb.resetLatestVersion(i.version) - if i.tree.useLegacyFormat { + if i.tree.useLegacyFormat && len(rootHash) != 0 { _, err = i.tree.LoadVersionByRootHash(i.version, rootHash) if err != nil { return err From 9b20ca5e41cf413cc22bb0eb945e82fc3ec88fce Mon Sep 17 00:00:00 2001 From: i-norden Date: Wed, 26 Jun 2024 16:46:22 -0400 Subject: [PATCH 11/18] iterator tests --- iterator_test.go | 329 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 329 insertions(+) diff --git a/iterator_test.go b/iterator_test.go index f9a1855ea..8059e12b9 100644 --- a/iterator_test.go +++ b/iterator_test.go @@ -86,6 +86,47 @@ func TestUnsavedFastIterator_NewIterator_NilAdditions_Failure(t *testing.T) { }) } +func TestLegacyUnsavedFastIterator_NewIterator_NilAdditions_Failure(t *testing.T) { + start, end := []byte{'a'}, []byte{'c'} + ascending := true + + performTest := func(t *testing.T, itr corestore.Iterator) { + require.NotNil(t, itr) + require.False(t, itr.Valid()) + actualsStart, actualEnd := itr.Domain() + require.Equal(t, start, actualsStart) + require.Equal(t, end, actualEnd) + require.Error(t, itr.Error()) + } + + t.Run("Nil additions given", func(t *testing.T) { + tree := NewLegacyMutableTree(dbm.NewMemDB(), 0, false, false, nil, log.NewNopLogger()) + itr := NewUnsavedFastIterator(start, end, ascending, tree.ndb, nil, tree.unsavedFastNodeRemovals) + performTest(t, itr) + require.ErrorIs(t, errUnsavedFastIteratorNilAdditionsGiven, itr.Error()) + }) + + t.Run("Nil removals given", func(t *testing.T) { + tree := NewLegacyMutableTree(dbm.NewMemDB(), 0, false, false, nil, log.NewNopLogger()) + itr := NewUnsavedFastIterator(start, end, ascending, tree.ndb, tree.unsavedFastNodeAdditions, nil) + performTest(t, itr) + require.ErrorIs(t, errUnsavedFastIteratorNilRemovalsGiven, itr.Error()) + }) + + t.Run("All nil", func(t *testing.T) { + itr := NewUnsavedFastIterator(start, end, ascending, nil, nil, nil) + performTest(t, itr) + require.ErrorIs(t, errFastIteratorNilNdbGiven, itr.Error()) + }) + + t.Run("Additions and removals are nil", func(t *testing.T) { + tree := NewLegacyMutableTree(dbm.NewMemDB(), 0, false, false, nil, log.NewNopLogger()) + itr := NewUnsavedFastIterator(start, end, ascending, tree.ndb, nil, nil) + performTest(t, itr) + require.ErrorIs(t, errUnsavedFastIteratorNilAdditionsGiven, itr.Error()) + }) +} + func TestIterator_Empty_Invalid(t *testing.T) { config := &iteratorTestConfig{ startByteToSet: 'a', @@ -116,6 +157,36 @@ func TestIterator_Empty_Invalid(t *testing.T) { }) } +func TestLegacyIterator_Empty_Invalid(t *testing.T) { + config := &iteratorTestConfig{ + startByteToSet: 'a', + endByteToSet: 'z', + startIterate: []byte("a"), + endIterate: []byte("a"), + ascending: true, + } + + performTest := func(t *testing.T, itr corestore.Iterator, mirror [][]string) { + require.Equal(t, 0, len(mirror)) + require.False(t, itr.Valid()) + } + + t.Run("Iterator", func(t *testing.T) { + itr, mirror := setupLegacyIteratorAndMirror(t, config) + performTest(t, itr, mirror) + }) + + t.Run("Fast Iterator", func(t *testing.T) { + itr, mirror := setupLegacyFastIteratorAndMirror(t, config) + performTest(t, itr, mirror) + }) + + t.Run("Unsaved Fast Iterator", func(t *testing.T) { + itr, mirror := setupLegacyUnsavedFastIterator(t, config) + performTest(t, itr, mirror) + }) +} + func TestIterator_Basic_Ranged_Ascending_Success(t *testing.T) { config := &iteratorTestConfig{ startByteToSet: 'a', @@ -127,6 +198,17 @@ func TestIterator_Basic_Ranged_Ascending_Success(t *testing.T) { iteratorSuccessTest(t, config) } +func TestLegacyIterator_Basic_Ranged_Ascending_Success(t *testing.T) { + config := &iteratorTestConfig{ + startByteToSet: 'a', + endByteToSet: 'z', + startIterate: []byte("e"), + endIterate: []byte("w"), + ascending: true, + } + legacyIteratorSuccessTest(t, config) +} + func TestIterator_Basic_Ranged_Descending_Success(t *testing.T) { config := &iteratorTestConfig{ startByteToSet: 'a', @@ -138,6 +220,17 @@ func TestIterator_Basic_Ranged_Descending_Success(t *testing.T) { iteratorSuccessTest(t, config) } +func TestLegacyIterator_Basic_Ranged_Descending_Success(t *testing.T) { + config := &iteratorTestConfig{ + startByteToSet: 'a', + endByteToSet: 'z', + startIterate: []byte("e"), + endIterate: []byte("w"), + ascending: false, + } + legacyIteratorSuccessTest(t, config) +} + func TestIterator_Basic_Full_Ascending_Success(t *testing.T) { config := &iteratorTestConfig{ startByteToSet: 'a', @@ -150,6 +243,18 @@ func TestIterator_Basic_Full_Ascending_Success(t *testing.T) { iteratorSuccessTest(t, config) } +func TestLegacyIterator_Basic_Full_Ascending_Success(t *testing.T) { + config := &iteratorTestConfig{ + startByteToSet: 'a', + endByteToSet: 'z', + startIterate: nil, + endIterate: nil, + ascending: true, + } + + legacyIteratorSuccessTest(t, config) +} + func TestIterator_Basic_Full_Descending_Success(t *testing.T) { config := &iteratorTestConfig{ startByteToSet: 'a', @@ -161,6 +266,17 @@ func TestIterator_Basic_Full_Descending_Success(t *testing.T) { iteratorSuccessTest(t, config) } +func TestLegacyIterator_Basic_Full_Descending_Success(t *testing.T) { + config := &iteratorTestConfig{ + startByteToSet: 'a', + endByteToSet: 'z', + startIterate: nil, + endIterate: nil, + ascending: false, + } + legacyIteratorSuccessTest(t, config) +} + func TestIterator_WithDelete_Full_Ascending_Success(t *testing.T) { config := &iteratorTestConfig{ startByteToSet: 'a', @@ -217,6 +333,62 @@ func TestIterator_WithDelete_Full_Ascending_Success(t *testing.T) { }) } +func TestLegacyIterator_WithDelete_Full_Ascending_Success(t *testing.T) { + config := &iteratorTestConfig{ + startByteToSet: 'a', + endByteToSet: 'z', + startIterate: nil, + endIterate: nil, + ascending: false, + } + + tree, mirror := getRandomizedLegacyTreeAndMirror(t) + + _, _, err := tree.SaveVersion() + require.NoError(t, err) + + randomizeTreeAndMirror(t, tree, mirror) + + _, _, err = tree.SaveVersion() + require.NoError(t, err) + + err = tree.DeleteVersionsTo(1) + require.NoError(t, err) + + latestVersion, err := tree.ndb.getLatestVersion() + require.NoError(t, err) + immutableTree, err := tree.GetImmutable(latestVersion) + require.NoError(t, err) + + // sort mirror for assertion + sortedMirror := make([][]string, 0, len(mirror)) + for k, v := range mirror { + sortedMirror = append(sortedMirror, []string{k, v}) + } + + sort.Slice(sortedMirror, func(i, j int) bool { + return sortedMirror[i][0] > sortedMirror[j][0] + }) + + t.Run("Iterator", func(t *testing.T) { + itr := NewIterator(config.startIterate, config.endIterate, config.ascending, immutableTree) + require.True(t, itr.Valid()) + assertIterator(t, itr, sortedMirror, config.ascending) + }) + + t.Run("Fast Iterator", func(t *testing.T) { + itr := NewFastIterator(config.startIterate, config.endIterate, config.ascending, immutableTree.ndb) + require.True(t, itr.Valid()) + assertIterator(t, itr, sortedMirror, config.ascending) + }) + + t.Run("Unsaved Fast Iterator", func(t *testing.T) { + itr := NewUnsavedFastIterator(config.startIterate, config.endIterate, config.ascending, immutableTree.ndb, tree.unsavedFastNodeAdditions, tree.unsavedFastNodeRemovals) + require.True(t, itr.Valid()) + assertIterator(t, itr, sortedMirror, config.ascending) + }) +} + func iteratorSuccessTest(t *testing.T, config *iteratorTestConfig) { performTest := func(t *testing.T, itr corestore.Iterator, mirror [][]string) { actualStart, actualEnd := itr.Domain() @@ -247,6 +419,36 @@ func iteratorSuccessTest(t *testing.T, config *iteratorTestConfig) { }) } +func legacyIteratorSuccessTest(t *testing.T, config *iteratorTestConfig) { + performTest := func(t *testing.T, itr corestore.Iterator, mirror [][]string) { + actualStart, actualEnd := itr.Domain() + require.Equal(t, config.startIterate, actualStart) + require.Equal(t, config.endIterate, actualEnd) + + require.NoError(t, itr.Error()) + + assertIterator(t, itr, mirror, config.ascending) + } + + t.Run("Iterator", func(t *testing.T) { + itr, mirror := setupLegacyIteratorAndMirror(t, config) + require.True(t, itr.Valid()) + performTest(t, itr, mirror) + }) + + t.Run("Fast Iterator", func(t *testing.T) { + itr, mirror := setupLegacyFastIteratorAndMirror(t, config) + require.True(t, itr.Valid()) + performTest(t, itr, mirror) + }) + + t.Run("Unsaved Fast Iterator", func(t *testing.T) { + itr, mirror := setupLegacyUnsavedFastIterator(t, config) + require.True(t, itr.Valid()) + performTest(t, itr, mirror) + }) +} + func setupIteratorAndMirror(t *testing.T, config *iteratorTestConfig) (corestore.Iterator, [][]string) { tree := NewMutableTree(dbm.NewMemDB(), 0, false, log.NewNopLogger()) @@ -263,6 +465,22 @@ func setupIteratorAndMirror(t *testing.T, config *iteratorTestConfig) (corestore return itr, mirror } +func setupLegacyIteratorAndMirror(t *testing.T, config *iteratorTestConfig) (corestore.Iterator, [][]string) { + tree := NewLegacyMutableTree(dbm.NewMemDB(), 0, false, false, nil, log.NewNopLogger()) + + mirror := setupMirrorForIterator(t, config, tree) + _, _, err := tree.SaveVersion() + require.NoError(t, err) + + latestVersion, err := tree.ndb.getLatestVersion() + require.NoError(t, err) + immutableTree, err := tree.GetImmutable(latestVersion) + require.NoError(t, err) + + itr := NewIterator(config.startIterate, config.endIterate, config.ascending, immutableTree) + return itr, mirror +} + func setupFastIteratorAndMirror(t *testing.T, config *iteratorTestConfig) (corestore.Iterator, [][]string) { tree := NewMutableTree(dbm.NewMemDB(), 0, false, log.NewNopLogger()) @@ -274,6 +492,17 @@ func setupFastIteratorAndMirror(t *testing.T, config *iteratorTestConfig) (cores return itr, mirror } +func setupLegacyFastIteratorAndMirror(t *testing.T, config *iteratorTestConfig) (corestore.Iterator, [][]string) { + tree := NewLegacyMutableTree(dbm.NewMemDB(), 0, false, false, nil, log.NewNopLogger()) + + mirror := setupMirrorForIterator(t, config, tree) + _, _, err := tree.SaveVersion() + require.NoError(t, err) + + itr := NewFastIterator(config.startIterate, config.endIterate, config.ascending, tree.ndb) + return itr, mirror +} + func setupUnsavedFastIterator(t *testing.T, config *iteratorTestConfig) (corestore.Iterator, [][]string) { tree := NewMutableTree(dbm.NewMemDB(), 0, false, log.NewNopLogger()) @@ -328,6 +557,60 @@ func setupUnsavedFastIterator(t *testing.T, config *iteratorTestConfig) (coresto return itr, mirror } +func setupLegacyUnsavedFastIterator(t *testing.T, config *iteratorTestConfig) (corestore.Iterator, [][]string) { + tree := NewLegacyMutableTree(dbm.NewMemDB(), 0, false, false, nil, log.NewNopLogger()) + + // For unsaved fast iterator, we would like to test the state where + // there are saved fast nodes as well as some unsaved additions and removals. + // So, we split the byte range in half where the first half is saved and the second half is unsaved. + breakpointByte := (config.endByteToSet + config.startByteToSet) / 2 + + firstHalfConfig := *config + firstHalfConfig.endByteToSet = breakpointByte // exclusive + + secondHalfConfig := *config + secondHalfConfig.startByteToSet = breakpointByte + + // First half of the mirror + mirror := setupMirrorForIterator(t, &firstHalfConfig, tree) + _, _, err := tree.SaveVersion() + require.NoError(t, err) + + // No unsaved additions or removals should be present after saving + require.Equal(t, 0, syncMapCount(tree.unsavedFastNodeAdditions)) + require.Equal(t, 0, syncMapCount(tree.unsavedFastNodeRemovals)) + + // Ensure that there are unsaved additions and removals present + secondHalfMirror := setupMirrorForIterator(t, &secondHalfConfig, tree) + + require.True(t, syncMapCount(tree.unsavedFastNodeAdditions) >= len(secondHalfMirror)) + require.Equal(t, 0, syncMapCount(tree.unsavedFastNodeRemovals)) + + // Merge the two halves + if config.ascending { + mirror = append(mirror, secondHalfMirror...) + } else { + mirror = append(secondHalfMirror, mirror...) + } + + if len(mirror) > 0 { + // Remove random keys + for i := 0; i < len(mirror)/4; i++ { + randIndex := rand.Intn(len(mirror)) + keyToRemove := mirror[randIndex][0] + + _, removed, err := tree.Remove([]byte(keyToRemove)) + require.NoError(t, err) + require.True(t, removed) + + mirror = append(mirror[:randIndex], mirror[randIndex+1:]...) + } + } + + itr := NewUnsavedFastIterator(config.startIterate, config.endIterate, config.ascending, tree.ndb, tree.unsavedFastNodeAdditions, tree.unsavedFastNodeRemovals) + return itr, mirror +} + func TestNodeIterator_Success(t *testing.T) { tree, mirror := getRandomizedTreeAndMirror(t) @@ -364,6 +647,42 @@ func TestNodeIterator_Success(t *testing.T) { require.Equal(t, nodeCount, updateCount+skipCount) } +func TestLegacyNodeIterator_Success(t *testing.T) { + tree, mirror := getRandomizedLegacyTreeAndMirror(t) + + _, _, err := tree.SaveVersion() + require.NoError(t, err) + + randomizeTreeAndMirror(t, tree, mirror) + + _, _, err = tree.SaveVersion() + require.NoError(t, err) + + // check if the iterating count is same with the entire node count of the tree + itr, err := NewNodeIterator(tree.root.GetKey(), tree.ndb) + require.NoError(t, err) + nodeCount := 0 + for ; itr.Valid(); itr.Next(false) { + nodeCount++ + } + require.Equal(t, int64(nodeCount), tree.Size()*2-1) + + // check if the skipped node count is right + itr, err = NewNodeIterator(tree.root.GetKey(), tree.ndb) + require.NoError(t, err) + updateCount := 0 + skipCount := 0 + for itr.Valid() { + node := itr.GetNode() + updateCount++ + if node.nodeKey.version < tree.Version() { + skipCount += int(node.size*2 - 2) // the size of the subtree without the root + } + itr.Next(node.nodeKey.version < tree.Version()) + } + require.Equal(t, nodeCount, updateCount+skipCount) +} + func TestNodeIterator_WithEmptyRoot(t *testing.T) { itr, err := NewNodeIterator(nil, newNodeDB(dbm.NewMemDB(), 0, DefaultOptions(), log.NewNopLogger())) require.NoError(t, err) @@ -374,6 +693,16 @@ func TestNodeIterator_WithEmptyRoot(t *testing.T) { require.False(t, itr.Valid()) } +func TestLegacyNodeIterator_WithEmptyRoot(t *testing.T) { + itr, err := NewNodeIterator(nil, newLegacyNodeDB(dbm.NewMemDB(), 0, DefaultOptions(), false, log.NewNopLogger())) + require.NoError(t, err) + require.False(t, itr.Valid()) + + itr, err = NewNodeIterator([]byte{}, newLegacyNodeDB(dbm.NewMemDB(), 0, DefaultOptions(), false, log.NewNopLogger())) + require.NoError(t, err) + require.False(t, itr.Valid()) +} + func syncMapCount(m *sync.Map) int { count := 0 m.Range(func(_, _ interface{}) bool { From 1b875235a7732e419ea786e946bdd2e8c060157a Mon Sep 17 00:00:00 2001 From: i-norden Date: Wed, 26 Jun 2024 16:46:36 -0400 Subject: [PATCH 12/18] legacy node tests --- node_test.go | 153 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 153 insertions(+) diff --git a/node_test.go b/node_test.go index 12dfcb051..ba5083ed0 100644 --- a/node_test.go +++ b/node_test.go @@ -39,6 +39,33 @@ func TestNode_encodedSize(t *testing.T) { require.Equal(t, 39, node.encodedSize()) } +func TestLegacyNode_encodedSize(t *testing.T) { + nodeKey := &NodeKey{ + version: 1, + nonce: 1, + } + node := &Node{ + key: iavlrand.RandBytes(10), + value: iavlrand.RandBytes(10), + subtreeHeight: 0, + size: 100, + hash: iavlrand.RandBytes(20), + nodeKey: nodeKey, + leftNodeKey: iavlrand.RandBytes(32), + leftNode: nil, + rightNodeKey: iavlrand.RandBytes(32), + rightNode: nil, + isLegacy: true, + } + + // leaf node + require.Equal(t, 26, node.encodedSize()) + + // non-leaf node + node.subtreeHeight = 1 + require.Equal(t, 81, node.encodedSize()) +} + func TestNode_encode_decode(t *testing.T) { childNodeKey := &NodeKey{ version: 1, @@ -113,6 +140,81 @@ func TestNode_encode_decode(t *testing.T) { } } +func TestLegacyNode_encode_decode(t *testing.T) { + iavlrand.Seed(1337) + childNodeHashKey := iavlrand.RandBytes(32) + childNodeHash := []byte{0x7f, 0x68, 0x90, 0xca, 0x16, 0xde, 0xa6, 0xe8, 0x89, 0x3d, 0x96, 0xf0, 0xa3, 0xd, 0xa, 0x14, 0xe5, 0x55, 0x59, 0xfc, 0x9b, 0x83, 0x4, 0x91, 0xe3, 0xd2, 0x45, 0x1c, 0x81, 0xf6, 0xd1, 0xe} + testcases := map[string]struct { + node *Node + expectHex string + expectError bool + }{ + "nil": {nil, "", true}, + "inner": {&Node{ + subtreeHeight: 3, + size: 7, + key: []byte("key"), + nodeKey: &NodeKey{ + version: 2, + nonce: 0, + }, + leftNodeKey: childNodeHashKey, + rightNodeKey: childNodeHashKey, + hash: []byte{0x7f, 0x68, 0x90, 0xca, 0x16, 0xde, 0xa6, 0xe8, 0x89, 0x3d, 0x96, 0xf0, 0xa3, 0xd, 0xa, 0x14, 0xe5, 0x55, 0x59, 0xfc, 0x9b, 0x83, 0x4, 0x91, 0xe3, 0xd2, 0x45, 0x1c, 0x81, 0xf6, 0xd1, 0xe}, + isLegacy: true, + }, "060e04036b65792026429483d02d520734e5408754139293c0b70564faffec9d46f109731ea3f4bb2026429483d02d520734e5408754139293c0b70564faffec9d46f109731ea3f4bb", false}, + "inner hybrid": {&Node{ + subtreeHeight: 3, + size: 7, + key: []byte("key"), + nodeKey: &NodeKey{ + version: 2, + nonce: 0, + }, + leftNodeKey: childNodeHashKey, + rightNodeKey: childNodeHash, + hash: []byte{0x7f, 0x68, 0x90, 0xca, 0x16, 0xde, 0xa6, 0xe8, 0x89, 0x3d, 0x96, 0xf0, 0xa3, 0xd, 0xa, 0x14, 0xe5, 0x55, 0x59, 0xfc, 0x9b, 0x83, 0x4, 0x91, 0xe3, 0xd2, 0x45, 0x1c, 0x81, 0xf6, 0xd1, 0xe}, + isLegacy: true, + }, "060e04036b65792026429483d02d520734e5408754139293c0b70564faffec9d46f109731ea3f4bb207f6890ca16dea6e8893d96f0a30d0a14e55559fc9b830491e3d2451c81f6d10e", false}, + "leaf": {&Node{ + subtreeHeight: 0, + size: 1, + key: []byte("key"), + value: []byte("value"), + nodeKey: &NodeKey{ + version: 3, + nonce: 0, + }, + hash: []byte{0x7f, 0x68, 0x90, 0xca, 0x16, 0xde, 0xa6, 0xe8, 0x89, 0x3d, 0x96, 0xf0, 0xa3, 0xd, 0xa, 0x14, 0xe5, 0x55, 0x59, 0xfc, 0x9b, 0x83, 0x4, 0x91, 0xe3, 0xd2, 0x45, 0x1c, 0x81, 0xf6, 0xd1, 0xe}, + isLegacy: true, + }, "000206036b65790576616c7565", false}, + } + for name, tc := range testcases { + tc := tc + t.Run(name, func(t *testing.T) { + var buf bytes.Buffer + err := tc.node.writeLegacyBytes(&buf) + if tc.expectError { + require.Error(t, err) + return + } + require.NoError(t, err) + require.Equal(t, tc.expectHex, hex.EncodeToString(buf.Bytes())) + + node, err := MakeLegacyNode(tc.node.GetKey(), buf.Bytes()) + require.NoError(t, err) + // since key and value is always decoded to []byte{} we augment the expected struct here + if tc.node.key == nil { + tc.node.key = []byte{} + } + if tc.node.value == nil && tc.node.subtreeHeight == 0 { + tc.node.value = []byte{} + } + require.Equal(t, tc.node, node) + }) + } +} + func TestNode_validate(t *testing.T) { k := []byte("key") v := []byte("value") @@ -164,6 +266,57 @@ func TestNode_validate(t *testing.T) { } } +func TestLegacyNode_validate(t *testing.T) { + k := []byte("key") + v := []byte("value") + nk := &NodeKey{ + version: 1, + nonce: 1, + } + c := &Node{key: []byte("child"), value: []byte("x"), size: 1} + + testcases := map[string]struct { + node *Node + valid bool + }{ + "nil node": {nil, false}, + "leaf": {&Node{key: k, value: v, nodeKey: nk, size: 1}, true}, + "leaf with nil key": {&Node{key: nil, value: v, size: 1}, false}, + "leaf with empty key": {&Node{key: []byte{}, value: v, nodeKey: nk, size: 1}, true}, + "leaf with nil value": {&Node{key: k, value: nil, size: 1}, false}, + "leaf with empty value": {&Node{key: k, value: []byte{}, nodeKey: nk, size: 1}, true}, + "leaf with version 0": {&Node{key: k, value: v, size: 1}, false}, + "leaf with version -1": {&Node{key: k, value: v, size: 1}, false}, + "leaf with size 0": {&Node{key: k, value: v, size: 0}, false}, + "leaf with size 2": {&Node{key: k, value: v, size: 2}, false}, + "leaf with size -1": {&Node{key: k, value: v, size: -1}, false}, + "leaf with left node key": {&Node{key: k, value: v, size: 1, leftNodeKey: iavlrand.RandBytes(32)}, false}, + "leaf with left child": {&Node{key: k, value: v, size: 1, leftNode: c}, false}, + "leaf with right node key": {&Node{key: k, value: v, size: 1, rightNodeKey: iavlrand.RandBytes(32)}, false}, + "leaf with right child": {&Node{key: k, value: v, size: 1, rightNode: c}, false}, + "inner": {&Node{key: k, size: 1, subtreeHeight: 1, nodeKey: nk, leftNodeKey: iavlrand.RandBytes(32), rightNodeKey: iavlrand.RandBytes(32)}, true}, + "inner with nil key": {&Node{key: nil, value: v, size: 1, subtreeHeight: 1, leftNodeKey: iavlrand.RandBytes(32), rightNodeKey: iavlrand.RandBytes(32)}, false}, + "inner with value": {&Node{key: k, value: v, size: 1, subtreeHeight: 1, leftNodeKey: iavlrand.RandBytes(32), rightNodeKey: iavlrand.RandBytes(32)}, false}, + "inner with empty value": {&Node{key: k, value: []byte{}, size: 1, subtreeHeight: 1, leftNodeKey: iavlrand.RandBytes(32), rightNodeKey: iavlrand.RandBytes(32)}, false}, + "inner with left child": {&Node{key: k, size: 1, subtreeHeight: 1, nodeKey: nk, leftNodeKey: iavlrand.RandBytes(32)}, true}, + "inner with right child": {&Node{key: k, size: 1, subtreeHeight: 1, nodeKey: nk, rightNodeKey: iavlrand.RandBytes(32)}, true}, + "inner with no child": {&Node{key: k, size: 1, subtreeHeight: 1}, false}, + "inner with height 0": {&Node{key: k, size: 1, subtreeHeight: 0, leftNodeKey: iavlrand.RandBytes(32), rightNodeKey: iavlrand.RandBytes(32)}, false}, + } + + for desc, tc := range testcases { + tc := tc // appease scopelint + t.Run(desc, func(t *testing.T) { + err := tc.node.validate() + if tc.valid { + assert.NoError(t, err) + } else { + assert.Error(t, err) + } + }) + } +} + func BenchmarkNode_encodedSize(b *testing.B) { nk := &NodeKey{ version: rand.Int63n(10000000), From 652973810af7131d3dc1037036b3c7c149ccbc6e Mon Sep 17 00:00:00 2001 From: i-norden Date: Wed, 26 Jun 2024 16:47:02 -0400 Subject: [PATCH 13/18] proof tests --- proof_iavl_test.go | 51 +++++++++++++ proof_ics23_test.go | 173 ++++++++++++++++++++++++++++++++++++++++++++ proof_test.go | 63 ++++++++++++++++ 3 files changed, 287 insertions(+) diff --git a/proof_iavl_test.go b/proof_iavl_test.go index ea95fffb5..845584ab9 100644 --- a/proof_iavl_test.go +++ b/proof_iavl_test.go @@ -60,3 +60,54 @@ func TestProofOp(t *testing.T) { }) } } + +func TestLegacyProofOp(t *testing.T) { + tree := NewLegacyMutableTree(dbm.NewMemDB(), 0, false, false, nil, log.NewNopLogger()) + keys := []byte{0x0a, 0x11, 0x2e, 0x32, 0x50, 0x72, 0x99, 0xa1, 0xe4, 0xf7} // 10 total. + for _, ikey := range keys { + key := []byte{ikey} + _, err := tree.Set(key, key) + require.NoError(t, err) + } + + testcases := []struct { + key byte + expectPresent bool + }{ + {0x00, false}, + {0x0a, true}, + {0x0b, false}, + {0x11, true}, + {0x60, false}, + {0x72, true}, + {0x99, true}, + {0xaa, false}, + {0xe4, true}, + {0xf7, true}, + {0xff, false}, + } + + for _, tc := range testcases { + tc := tc + t.Run(fmt.Sprintf("%02x", tc.key), func(t *testing.T) { + key := []byte{tc.key} + if tc.expectPresent { + proof, err := tree.GetMembershipProof(key) + require.NoError(t, err) + + // Verify that proof is valid. + res, err := tree.VerifyMembership(proof, key) + require.NoError(t, err) + require.True(t, res) + } else { + proof, err := tree.GetNonMembershipProof(key) + require.NoError(t, err) + + // Verify that proof is valid. + res, err := tree.VerifyNonMembership(proof, key) + require.NoError(t, err) + require.True(t, res) + } + }) + } +} diff --git a/proof_ics23_test.go b/proof_ics23_test.go index b9e017725..43d39467f 100644 --- a/proof_ics23_test.go +++ b/proof_ics23_test.go @@ -46,6 +46,38 @@ func TestGetMembership(t *testing.T) { } } +func TestLegacyGetMembership(t *testing.T) { + cases := map[string]struct { + size int + loc Where + }{ + "small left": {size: 100, loc: Left}, + "small middle": {size: 100, loc: Middle}, + "small right": {size: 100, loc: Right}, + "big left": {size: 5431, loc: Left}, + "big middle": {size: 5431, loc: Middle}, + "big right": {size: 5431, loc: Right}, + } + + for name, tc := range cases { + tc := tc + t.Run(name, func(t *testing.T) { + tree, allkeys, err := BuildLegacyTree(tc.size, 0) + require.NoError(t, err, "Creating tree: %+v", err) + + key := GetKey(allkeys, tc.loc) + val, err := tree.Get(key) + require.NoError(t, err) + proof, err := tree.GetMembershipProof(key) + require.NoError(t, err, "Creating Proof: %+v", err) + + root := tree.WorkingHash() + valid := ics23.VerifyMembership(ics23.IavlSpec, root, proof, key, val) + require.True(t, valid, "Membership Proof Invalid") + }) + } +} + func TestGetNonMembership(t *testing.T) { cases := map[string]struct { size int @@ -98,6 +130,58 @@ func TestGetNonMembership(t *testing.T) { } } +func TestLegacyGetNonMembership(t *testing.T) { + cases := map[string]struct { + size int + loc Where + }{ + "small left": {size: 100, loc: Left}, + "small middle": {size: 100, loc: Middle}, + "small right": {size: 100, loc: Right}, + "big left": {size: 5431, loc: Left}, + "big middle": {size: 5431, loc: Middle}, + "big right": {size: 5431, loc: Right}, + } + + performTest := func(tree *MutableTree, allKeys [][]byte, loc Where) { + key := GetNonKey(allKeys, loc) + + proof, err := tree.GetNonMembershipProof(key) + require.NoError(t, err, "Creating Proof: %+v", err) + + root := tree.WorkingHash() + valid := ics23.VerifyNonMembership(ics23.IavlSpec, root, proof, key) + require.True(t, valid, "Non Membership Proof Invalid") + } + + for name, tc := range cases { + tc := tc + t.Run("fast-"+name, func(t *testing.T) { + tree, allkeys, err := BuildLegacyTree(tc.size, 0) + require.NoError(t, err, "Creating tree: %+v", err) + // Save version to enable fast cache + _, _, err = tree.SaveVersion() + require.NoError(t, err) + + isFastCacheEnabled, err := tree.IsFastCacheEnabled() + require.NoError(t, err) + require.True(t, isFastCacheEnabled) + + performTest(tree, allkeys, tc.loc) + }) + + t.Run("regular-"+name, func(t *testing.T) { + tree, allkeys, err := BuildLegacyTree(tc.size, 0) + require.NoError(t, err, "Creating tree: %+v", err) + isFastCacheEnabled, err := tree.IsFastCacheEnabled() + require.NoError(t, err) + require.False(t, isFastCacheEnabled) + + performTest(tree, allkeys, tc.loc) + }) + } +} + func BenchmarkGetNonMembership(b *testing.B) { cases := []struct { size int @@ -162,6 +246,70 @@ func BenchmarkGetNonMembership(b *testing.B) { }) } +func BenchmarkLegacyGetNonMembership(b *testing.B) { + cases := []struct { + size int + loc Where + }{ + {size: 100, loc: Left}, + {size: 100, loc: Middle}, + {size: 100, loc: Right}, + {size: 5431, loc: Left}, + {size: 5431, loc: Middle}, + {size: 5431, loc: Right}, + } + + performTest := func(tree *MutableTree, allKeys [][]byte, loc Where) { + key := GetNonKey(allKeys, loc) + + proof, err := tree.GetNonMembershipProof(key) + require.NoError(b, err, "Creating Proof: %+v", err) + + b.StopTimer() + root := tree.WorkingHash() + valid := ics23.VerifyNonMembership(ics23.IavlSpec, root, proof, key) + require.True(b, valid) + b.StartTimer() + } + + b.Run("fast", func(b *testing.B) { + for i := 0; i < b.N; i++ { + b.StopTimer() + caseIdx := mrand.Intn(len(cases)) + tc := cases[caseIdx] + + tree, allkeys, err := BuildLegacyTree(tc.size, 100000) + require.NoError(b, err, "Creating tree: %+v", err) + // Save version to enable fast cache + _, _, err = tree.SaveVersion() + require.NoError(b, err) + + isFastCacheEnabled, err := tree.IsFastCacheEnabled() + require.NoError(b, err) + require.True(b, isFastCacheEnabled) + b.StartTimer() + performTest(tree, allkeys, tc.loc) + } + }) + + b.Run("regular", func(b *testing.B) { + for i := 0; i < b.N; i++ { + b.StopTimer() + caseIdx := mrand.Intn(len(cases)) + tc := cases[caseIdx] + + tree, allkeys, err := BuildLegacyTree(tc.size, 100000) + require.NoError(b, err, "Creating tree: %+v", err) + isFastCacheEnabled, err := tree.IsFastCacheEnabled() + require.NoError(b, err) + require.False(b, isFastCacheEnabled) + + b.StartTimer() + performTest(tree, allkeys, tc.loc) + } + }) +} + // Test Helpers // Where selects a location for a key - Left, Right, or Middle @@ -226,6 +374,31 @@ func BuildTree(size int, cacheSize int) (itree *MutableTree, keys [][]byte, err return tree, keys, nil } +// BuildLegacyTree creates random key/values and stores in tree +// returns a list of all keys in sorted order +func BuildLegacyTree(size int, cacheSize int) (itree *MutableTree, keys [][]byte, err error) { + tree := NewLegacyMutableTree(dbm.NewMemDB(), cacheSize, false, false, nil, log.NewNopLogger()) + + // insert lots of info and store the bytes + keys = make([][]byte, size) + for i := 0; i < size; i++ { + key := make([]byte, 4) + // create random 4 byte key + rand.Read(key) //nolint:errcheck + value := "value_for_key:" + string(key) + _, err = tree.Set(key, []byte(value)) + if err != nil { + return nil, nil, err + } + keys[i] = key + } + sort.Slice(keys, func(i, j int) bool { + return bytes.Compare(keys[i], keys[j]) < 0 + }) + + return tree, keys, nil +} + // sink is kept as a global to ensure that value checks and assignments to it can't be // optimized away, and this will help us ensure that benchmarks successfully run. var sink interface{} diff --git a/proof_test.go b/proof_test.go index 0c475c95b..df8022357 100644 --- a/proof_test.go +++ b/proof_test.go @@ -39,6 +39,33 @@ func TestTreeGetProof(t *testing.T) { require.True(res) } +func TestLegacyTreeGetProof(t *testing.T) { + require := require.New(t) + tree := getLegacyTestTree(0) + for _, ikey := range []byte{0x11, 0x32, 0x50, 0x72, 0x99} { + key := []byte{ikey} + tree.Set(key, []byte(iavlrand.RandStr(8))) + } + + key := []byte{0x32} + proof, err := tree.GetMembershipProof(key) + require.NoError(err) + require.NotNil(proof) + + res, err := tree.VerifyMembership(proof, key) + require.NoError(err, "%+v", err) + require.True(res) + + key = []byte{0x1} + proof, err = tree.GetNonMembershipProof(key) + require.NoError(err) + require.NotNil(proof) + + res, err = tree.VerifyNonMembership(proof, key) + require.NoError(err, "%+v", err) + require.True(res) +} + func TestTreeKeyExistsProof(t *testing.T) { tree := getTestTree(0) @@ -75,6 +102,42 @@ func TestTreeKeyExistsProof(t *testing.T) { } } +func TestLegacyTreeKeyExistsProof(t *testing.T) { + tree := getLegacyTestTree(0) + + // should get error + _, err := tree.GetProof([]byte("foo")) + assert.Error(t, err) + + // insert lots of info and store the bytes + allkeys := make([][]byte, 200) + for i := 0; i < 200; i++ { + key := iavlrand.RandStr(20) + value := "value_for_" + key + tree.Set([]byte(key), []byte(value)) + allkeys[i] = []byte(key) + } + sortByteSlices(allkeys) // Sort all keys + + // query random key fails + _, err = tree.GetMembershipProof([]byte("foo")) + require.Error(t, err) + + // valid proof for real keys + for _, key := range allkeys { + proof, err := tree.GetMembershipProof(key) + require.NoError(t, err) + require.Equal(t, + append([]byte("value_for_"), key...), + proof.GetExist().Value, + ) + + res, err := tree.VerifyMembership(proof, key) + require.NoError(t, err) + require.True(t, res) + } +} + //---------------------------------------- // Contract: !bytes.Equal(input, output) && len(input) >= len(output) From b27ebef7956a4f8e8db3c83757bec68bec1d5585 Mon Sep 17 00:00:00 2001 From: i-norden Date: Wed, 26 Jun 2024 16:47:32 -0400 Subject: [PATCH 14/18] tree fuzz and dot tests --- tree_dotgraph_test.go | 11 +++++++++++ tree_fuzz_test.go | 20 ++++++++++++++++++++ 2 files changed, 31 insertions(+) diff --git a/tree_dotgraph_test.go b/tree_dotgraph_test.go index 41efdfcbe..2bb93ed99 100644 --- a/tree_dotgraph_test.go +++ b/tree_dotgraph_test.go @@ -15,3 +15,14 @@ func TestWriteDOTGraph(_ *testing.T) { } WriteDOTGraph(io.Discard, tree.ImmutableTree, []PathToLeaf{}) } + +func TestLegacyWriteDOTGraph(_ *testing.T) { + tree := getLegacyTestTree(0) + for _, ikey := range []byte{ + 0x0a, 0x11, 0x2e, 0x32, 0x50, 0x72, 0x99, 0xa1, 0xe4, 0xf7, + } { + key := []byte{ikey} + tree.Set(key, key) //nolint:errcheck + } + WriteDOTGraph(io.Discard, tree.ImmutableTree, []PathToLeaf{}) +} diff --git a/tree_fuzz_test.go b/tree_fuzz_test.go index 4abe43162..e4c9b0fc5 100644 --- a/tree_fuzz_test.go +++ b/tree_fuzz_test.go @@ -125,3 +125,23 @@ func TestMutableTreeFuzz(t *testing.T) { } } } + +func TestLegacyMutableTreeFuzz(t *testing.T) { + maxIterations := testFuzzIterations + progsPerIteration := 100000 + iterations := 0 + + for size := 5; iterations < maxIterations; size++ { + for i := 0; i < progsPerIteration/size; i++ { + tree := getLegacyTestTree(0) + program := genRandomProgram(size) + err := program.Execute(tree) + if err != nil { + str, err := tree.String() + require.Nil(t, err) + t.Fatalf("Error after %d iterations (size %d): %s\n%s", iterations, size, err.Error(), str) + } + iterations++ + } + } +} From 29077fac95894b7b87959fbfe54dfd59581d4f41 Mon Sep 17 00:00:00 2001 From: i-norden Date: Wed, 26 Jun 2024 16:48:03 -0400 Subject: [PATCH 15/18] immutable tree legacy support --- immutable_tree.go | 95 ++++++++++++++++++++++++++++++++++++++++---- mutable_tree.go | 20 +++++++++- mutable_tree_test.go | 1 - node.go | 15 ++++++- 4 files changed, 119 insertions(+), 12 deletions(-) diff --git a/immutable_tree.go b/immutable_tree.go index 240cc913f..53a5d3cf8 100644 --- a/immutable_tree.go +++ b/immutable_tree.go @@ -23,6 +23,8 @@ type ImmutableTree struct { ndb *nodeDB version int64 skipFastStorageUpgrade bool + useLegacyFormat bool // If true, save nodes to the DB with the legacy format + rootHash []byte } // NewImmutableTree creates both in-memory and persistent instances @@ -45,6 +47,29 @@ func NewImmutableTree(db dbm.DB, cacheSize int, skipFastStorageUpgrade bool, lg } } +// NewLegacyImmutableTree creates both in-memory and persistent instances +func NewLegacyImmutableTree(db dbm.DB, cacheSize int, skipFastStorageUpgrade bool, noStoreVersion bool, + rootHash []byte, lg log.Logger, options ...Option) *ImmutableTree { + opts := DefaultOptions() + for _, opt := range options { + opt(&opts) + } + + if db == nil { + // In-memory Tree. + return &ImmutableTree{} + } + + return &ImmutableTree{ + logger: lg, + // NodeDB-backed Tree. + ndb: newLegacyNodeDB(db, cacheSize, opts, noStoreVersion, lg), + skipFastStorageUpgrade: skipFastStorageUpgrade, + useLegacyFormat: true, + rootHash: rootHash, + } +} + // String returns a string representation of Tree. func (t *ImmutableTree) String() string { leaves := []string{} @@ -145,7 +170,15 @@ func (t *ImmutableTree) Height() int8 { // Has returns whether or not a key exists. func (t *ImmutableTree) Has(key []byte) (bool, error) { if t.root == nil { - return false, nil + if t.rootHash != nil { + root, err := t.ndb.GetNode(t.rootHash) + if err != nil { + return false, err + } + t.root = root + } else { + return false, nil + } } return t.root.has(t, key) } @@ -169,7 +202,15 @@ func (t *ImmutableTree) Export() (*Exporter, error) { // It's neighbor has index 1 and so on. func (t *ImmutableTree) GetWithIndex(key []byte) (int64, []byte, error) { if t.root == nil { - return 0, nil, nil + if t.rootHash != nil { + root, err := t.ndb.GetNode(t.rootHash) + if err != nil { + return 0, nil, err + } + t.root = root + } else { + return 0, nil, nil + } } return t.root.get(t, key) } @@ -180,7 +221,15 @@ func (t *ImmutableTree) GetWithIndex(key []byte) (int64, []byte, error) { // If tree.skipFastStorageUpgrade is true, this will work almost the same as GetWithIndex. func (t *ImmutableTree) Get(key []byte) ([]byte, error) { if t.root == nil { - return nil, nil + if t.rootHash != nil { + root, err := t.ndb.GetNode(t.rootHash) + if err != nil { + return nil, err + } + t.root = root + } else { + return nil, nil + } } if !t.skipFastStorageUpgrade { @@ -219,7 +268,15 @@ func (t *ImmutableTree) Get(key []byte) ([]byte, error) { // GetByIndex gets the key and value at the specified index. func (t *ImmutableTree) GetByIndex(index int64) (key []byte, value []byte, err error) { if t.root == nil { - return nil, nil, nil + if t.rootHash != nil { + root, err := t.ndb.GetNode(t.rootHash) + if err != nil { + return nil, nil, err + } + t.root = root + } else { + return nil, nil, nil + } } return t.root.getByIndex(t, index) @@ -229,7 +286,15 @@ func (t *ImmutableTree) GetByIndex(index int64) (key []byte, value []byte, err e // since they may point to data stored within IAVL. Returns true if stopped by callback, false otherwise func (t *ImmutableTree) Iterate(fn func(key []byte, value []byte) bool) (bool, error) { if t.root == nil { - return false, nil + if t.rootHash != nil { + root, err := t.ndb.GetNode(t.rootHash) + if err != nil { + return false, err + } + t.root = root + } else { + return false, nil + } } itr, err := t.Iterator(nil, nil, true) @@ -266,7 +331,15 @@ func (t *ImmutableTree) Iterator(start, end []byte, ascending bool) (corestore.I // values must not be modified, since they may point to data stored within IAVL. func (t *ImmutableTree) IterateRange(start, end []byte, ascending bool, fn func(key []byte, value []byte) bool) (stopped bool) { if t.root == nil { - return false + if t.rootHash != nil { + root, err := t.ndb.GetNode(t.rootHash) + if err != nil { + return false + } + t.root = root + } else { + return false + } } return t.root.traverseInRange(t, start, end, ascending, false, false, func(node *Node) bool { if node.subtreeHeight == 0 { @@ -281,7 +354,15 @@ func (t *ImmutableTree) IterateRange(start, end []byte, ascending bool, fn func( // values must not be modified, since they may point to data stored within IAVL. func (t *ImmutableTree) IterateRangeInclusive(start, end []byte, ascending bool, fn func(key, value []byte, version int64) bool) (stopped bool) { if t.root == nil { - return false + if t.rootHash != nil { + root, err := t.ndb.GetNode(t.rootHash) + if err != nil { + return false + } + t.root = root + } else { + return false + } } return t.root.traverseInRange(t, start, end, ascending, true, false, func(node *Node) bool { if node.subtreeHeight == 0 { diff --git a/mutable_tree.go b/mutable_tree.go index 71df0bc79..01e079455 100644 --- a/mutable_tree.go +++ b/mutable_tree.go @@ -236,7 +236,15 @@ func (tree *MutableTree) Import(version int64) (*Importer, error) { // since they may point to data stored within IAVL. Returns true if stopped by callnack, false otherwise func (tree *MutableTree) Iterate(fn func(key []byte, value []byte) bool) (stopped bool, err error) { if tree.root == nil { - return false, nil + if tree.rootHash != nil { + root, err := tree.ndb.GetNode(tree.rootHash) + if err != nil { + return false, err + } + tree.root = root + } else { + return false, nil + } } if tree.skipFastStorageUpgrade { @@ -367,7 +375,15 @@ func (tree *MutableTree) recursiveSetLeaf(node *Node, key []byte, value []byte) // after this call, since it may point to data stored inside IAVL. func (tree *MutableTree) Remove(key []byte) ([]byte, bool, error) { if tree.root == nil { - return nil, false, nil + if tree.rootHash != nil { + root, err := tree.ndb.GetNode(tree.rootHash) + if err != nil { + return nil, false, err + } + tree.root = root + } else { + return nil, false, nil + } } newRoot, _, value, removed, err := tree.recursiveRemove(tree.root, key) if err != nil { diff --git a/mutable_tree_test.go b/mutable_tree_test.go index cd0a872a4..7efba522b 100644 --- a/mutable_tree_test.go +++ b/mutable_tree_test.go @@ -737,7 +737,6 @@ func TestMutableTree_DeleteVersion(t *testing.T) { func TestLegacyMutableTree_DeleteVersion(t *testing.T) { tree := prepareLegacyTree(t) - println("break") ver, err := tree.LoadVersion(2) require.True(t, ver == 2) require.NoError(t, err) diff --git a/node.go b/node.go index 905528ffd..0fdff571a 100644 --- a/node.go +++ b/node.go @@ -244,6 +244,7 @@ func MakeLegacyNode(hash, buf []byte) (*Node, error) { nodeKey: &NodeKey{version: ver}, key: key, hash: hash, + isLegacy: true, } // Read node body. @@ -561,7 +562,16 @@ func (node *Node) encodedSize() int { encoding.EncodeBytesSize(node.key) if node.isLeaf() { n += encoding.EncodeBytesSize(node.value) - } else { + } + if node.isLegacy { + n += encoding.EncodeVarintSize(node.nodeKey.version) + if !node.isLeaf() { + n += encoding.EncodeBytesSize(node.leftNodeKey) + + encoding.EncodeBytesSize(node.rightNodeKey) + } + return n + } + if !node.isLeaf() { n += encoding.EncodeBytesSize(node.hash) if node.leftNodeKey != nil { nk := GetNodeKey(node.leftNodeKey) @@ -694,6 +704,7 @@ func (node *Node) writeLegacyBytes(w io.Writer) error { } } else { if len(node.leftNodeKey) != hashSize { + fmt.Printf("left node key: %x\r\n", node.leftNodeKey) return errors.New("node provided to writeLegacyBytes does not have a hash for leftNodeKey") } err = encoding.EncodeBytes(w, node.leftNodeKey) @@ -701,7 +712,7 @@ func (node *Node) writeLegacyBytes(w io.Writer) error { return fmt.Errorf("writing left hash, %w", err) } - if len(node.leftNodeKey) != 32 { + if len(node.leftNodeKey) != hashSize { return errors.New("node provided to writeLegacyBytes does not have a hash for rightNodeKey") } err = encoding.EncodeBytes(w, node.rightNodeKey) From 3b31a0af66600135f97495155eb42d7628eb38a2 Mon Sep 17 00:00:00 2001 From: i-norden Date: Wed, 26 Jun 2024 16:49:06 -0400 Subject: [PATCH 16/18] legacy tree random tests (only ones still failing...) --- tree_random_test.go | 242 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 242 insertions(+) diff --git a/tree_random_test.go b/tree_random_test.go index 7e2c6905a..1a5eb6e5c 100644 --- a/tree_random_test.go +++ b/tree_random_test.go @@ -45,6 +45,34 @@ func TestRandomOperations(t *testing.T) { } } +func TestLegacyRandomOperations(t *testing.T) { + // In short mode (specifically, when running in CI with the race detector), + // we only run the first couple of seeds. + seeds := []int64{ + 498727689, + 756509998, + 480459882, + 324736440, + 581827344, + 470870060, + 390970079, + 846023066, + 518638291, + 957382170, + } + + for i, seed := range seeds { + i, seed := i, seed + t.Run(fmt.Sprintf("Seed %v", seed), func(t *testing.T) { + if testing.Short() && i >= 2 { + t.Skip("Skipping seed in short mode") + } + t.Parallel() // comment out to disable parallel tests, or use -parallel 1 + testLegacyRandomOperations(t, seed) + }) + } +} + // Randomized test that runs all sorts of random operations, mirrors them in a known-good // map, and verifies the state of the tree against the map. func testRandomOperations(t *testing.T, randSeed int64) { @@ -261,6 +289,220 @@ func testRandomOperations(t *testing.T, randSeed int64) { t.Logf("Final version %v deleted, no stray database entries", prevVersion) } +func testLegacyRandomOperations(t *testing.T, randSeed int64) { + const ( + keySize = 16 // before base64-encoding + valueSize = 16 // before base64-encoding + + versions = 32 // number of final versions to generate + reloadChance = 0.1 // chance of tree reload after save + deleteChance = 0.2 // chance of random version deletion after save + revertChance = 0.05 // chance to revert tree to random version with LoadVersionForOverwriting + syncChance = 0.2 // chance of enabling sync writes on tree load + cacheChance = 0.4 // chance of enabling caching + cacheSizeMax = 256 // maximum size of cache (will be random from 1) + + versionOps = 64 // number of operations (create/update/delete) per version + updateRatio = 0.4 // ratio of updates out of all operations + deleteRatio = 0.2 // ratio of deletes out of all operations + ) + + r := rand.New(rand.NewSource(randSeed)) + + // loadTree loads the last persisted version of a tree with random pruning settings. + loadTree := func(levelDB dbm.DB) (tree *MutableTree, version int64, _ *Options) { //nolint:unparam + var err error + + sync := r.Float64() < syncChance + + // set the cache size regardless of whether caching is enabled. This ensures we always + // call the RNG the same number of times, such that changing settings does not affect + // the RNG sequence. + cacheSize := int(r.Int63n(cacheSizeMax + 1)) + if !(r.Float64() < cacheChance) { + cacheSize = 0 + } + tree = NewLegacyMutableTree(levelDB, cacheSize, false, false, nil, log.NewNopLogger(), SyncOption(sync)) + version, err = tree.Load() + require.NoError(t, err) + t.Logf("Loaded version %v (sync=%v cache=%v)", version, sync, cacheSize) + return + } + + // generates random keys and values + randString := func(size int) string { //nolint:unparam + buf := make([]byte, size) + r.Read(buf) + return base64.StdEncoding.EncodeToString(buf) + } + + // Use the same on-disk database for the entire run. + tempdir, err := os.MkdirTemp("", "iavl") + require.NoError(t, err) + defer os.RemoveAll(tempdir) + + levelDB, err := dbm.NewGoLevelDB("test", tempdir) + require.NoError(t, err) + + tree, version, _ := loadTree(levelDB) + + // Set up a mirror of the current IAVL state, as well as the history of saved mirrors + // on disk and in memory. Since pruning was removed we currently persist all versions, + // thus memMirrors is never used, but it is left here for the future when it is re-introduces. + mirror := make(map[string]string, versionOps) + mirrorKeys := make([]string, 0, versionOps) + diskMirrors := make(map[int64]map[string]string) + memMirrors := make(map[int64]map[string]string) + + for version < versions { + for i := 0; i < versionOps; i++ { + switch { + case len(mirror) > 0 && r.Float64() < deleteRatio: + index := r.Intn(len(mirrorKeys)) + key := mirrorKeys[index] + mirrorKeys = append(mirrorKeys[:index], mirrorKeys[index+1:]...) + _, removed, err := tree.Remove([]byte(key)) + require.NoError(t, err) + require.True(t, removed) + delete(mirror, key) + + case len(mirror) > 0 && r.Float64() < updateRatio: + key := mirrorKeys[r.Intn(len(mirrorKeys))] + value := randString(valueSize) + updated, err := tree.Set([]byte(key), []byte(value)) + require.NoError(t, err) + require.True(t, updated) + mirror[key] = value + + default: + key := randString(keySize) + value := randString(valueSize) + for has, err := tree.Has([]byte(key)); has && err == nil; { + key = randString(keySize) + } + updated, err := tree.Set([]byte(key), []byte(value)) + require.NoError(t, err) + require.False(t, updated) + mirror[key] = value + mirrorKeys = append(mirrorKeys, key) + } + } + _, version, err = tree.SaveVersion() + require.NoError(t, err) + + t.Logf("Saved tree at version %v with %v keys and %v versions", + version, tree.Size(), len(tree.AvailableVersions())) + + // Verify that the version matches the mirror. + assertMirror(t, tree, mirror, 0) + + // Save the mirror as a disk mirror, since we currently persist all versions. + diskMirrors[version] = copyMirror(mirror) + + // Delete random versions if requested, but never the latest version. + if r.Float64() < deleteChance { + versions := getMirrorVersions(diskMirrors, memMirrors) + if len(versions) > 1 { + to := versions[r.Intn(len(versions)-1)] + t.Logf("Deleting versions to %v", to) + err = tree.DeleteVersionsTo(int64(to)) + require.NoError(t, err) + for version := versions[0]; version <= to; version++ { + delete(diskMirrors, int64(version)) + delete(memMirrors, int64(version)) + } + } + } + + // Reload tree from last persisted version if requested, checking that it matches the + // latest disk mirror version and discarding memory mirrors. + if r.Float64() < reloadChance { + tree, version, _ = loadTree(levelDB) + assertMaxVersion(t, tree, version, diskMirrors) + memMirrors = make(map[int64]map[string]string) + mirror = copyMirror(diskMirrors[version]) + mirrorKeys = getMirrorKeys(mirror) + } + + // Revert tree to historical version if requested, deleting all subsequent versions. + if r.Float64() < revertChance { + versions := getMirrorVersions(diskMirrors, memMirrors) + if len(versions) > 1 { + version = int64(versions[r.Intn(len(versions)-1)]) + t.Logf("Reverting to version %v", version) + err = tree.LoadVersionForOverwriting(version) + require.NoError(t, err, "Failed to revert to version %v", version) + if m, ok := diskMirrors[version]; ok { + mirror = copyMirror(m) + } else if m, ok := memMirrors[version]; ok { + mirror = copyMirror(m) + } else { + t.Fatalf("Mirror not found for revert target %v", version) + } + mirrorKeys = getMirrorKeys(mirror) + for v := range diskMirrors { + if v > version { + delete(diskMirrors, v) + } + } + for v := range memMirrors { + if v > version { + delete(memMirrors, v) + } + } + } + } + + // Verify all historical versions. + assertVersions(t, tree, diskMirrors, memMirrors) + + for diskVersion, diskMirror := range diskMirrors { + assertMirror(t, tree, diskMirror, diskVersion) + } + + for memVersion, memMirror := range memMirrors { + assertMirror(t, tree, memMirror, memVersion) + } + } + + // Once we're done, delete all prior versions. + remaining := tree.AvailableVersions() + remaining = remaining[:len(remaining)-1] + + if len(remaining) > 0 { + t.Logf("Deleting versions to %v", remaining[len(remaining)-1]) + err = tree.DeleteVersionsTo(int64(remaining[len(remaining)-1])) + require.NoError(t, err) + } + + require.EqualValues(t, []int{int(version)}, tree.AvailableVersions()) + assertMirror(t, tree, mirror, version) + assertMirror(t, tree, mirror, 0) + assertOrphans(t, tree, 0) + t.Logf("Final version %v is correct, with no stray orphans", version) + + // Now, let's delete all remaining key/value pairs, and make sure no stray + // data is left behind in the database. + prevVersion := tree.Version() + keys := [][]byte{} + _, err = tree.Iterate(func(key, _ []byte) bool { + keys = append(keys, key) + return false + }) + require.NoError(t, err) + for _, key := range keys { + _, removed, err := tree.Remove(key) + require.NoError(t, err) + require.True(t, removed) + } + _, _, err = tree.SaveVersion() + require.NoError(t, err) + err = tree.DeleteVersionsTo(prevVersion) + require.NoError(t, err) + assertEmptyDatabase(t, tree) + t.Logf("Final version %v deleted, no stray database entries", prevVersion) +} + // Checks that the database is empty, only containing a single root entry // at the given version. func assertEmptyDatabase(t *testing.T, tree *MutableTree) { From f8ac7321a31384bb6050bad48655d5dd1e224da5 Mon Sep 17 00:00:00 2001 From: i-norden Date: Mon, 1 Jul 2024 11:26:54 -0400 Subject: [PATCH 17/18] immutable tree fixes --- immutable_tree.go | 8 ++- mutable_tree.go | 107 ++++++++++++++++++++++++++++++++--- nodedb.go | 141 +++++++++++++++++++++++++++++++++++++++------- 3 files changed, 227 insertions(+), 29 deletions(-) diff --git a/immutable_tree.go b/immutable_tree.go index 53a5d3cf8..a6e690e5d 100644 --- a/immutable_tree.go +++ b/immutable_tree.go @@ -385,7 +385,13 @@ func (t *ImmutableTree) IsFastCacheEnabled() (bool, error) { } func (t *ImmutableTree) isLatestTreeVersion() (bool, error) { - latestVersion, err := t.ndb.getLatestVersion() + var latestVersion int64 + var err error + if t.useLegacyFormat { + latestVersion, err = t.ndb.getLegacyLatestVersion() + } else { + latestVersion, err = t.ndb.getLatestVersion() + } if err != nil { return false, err } diff --git a/mutable_tree.go b/mutable_tree.go index 01e079455..3b7522466 100644 --- a/mutable_tree.go +++ b/mutable_tree.go @@ -78,7 +78,7 @@ func NewLegacyMutableTree(db dbm.DB, cacheSize int, skipFastStorageUpgrade, noSt } ndb := newLegacyNodeDB(db, cacheSize, opts, noStoreVersion, lg) - head := &ImmutableTree{ndb: ndb, skipFastStorageUpgrade: skipFastStorageUpgrade} + head := &ImmutableTree{ndb: ndb, skipFastStorageUpgrade: skipFastStorageUpgrade, useLegacyFormat: true} return &MutableTree{ logger: lg, @@ -117,7 +117,6 @@ func (tree *MutableTree) VersionExists(version int64) bool { if err != nil { return false } - return firstVersion <= version && version <= latestVersion } @@ -150,6 +149,10 @@ func (tree *MutableTree) AvailableVersions() []int { firstVersion = legacyLatestVersion } + if tree.useLegacyFormat { + latestVersion = legacyLatestVersion + } + for version := firstVersion; version <= latestVersion; version++ { res = append(res, int(version)) } @@ -526,6 +529,7 @@ func (tree *MutableTree) LoadVersion(targetVersion int64) (int64, error) { if targetVersion <= 0 { targetVersion = latestVersion } + if !tree.VersionExists(targetVersion) { return 0, ErrVersionDoesNotExist } @@ -560,6 +564,81 @@ func (tree *MutableTree) LoadVersion(targetVersion int64) (int64, error) { return latestVersion, nil } +func (tree *MutableTree) LoadLegacyVersion(targetVersion int64) (int64, error) { + firstVersion, err := tree.ndb.getFirstLegacyVersion() + if err != nil { + return 0, err + } + + if firstVersion > 0 && firstVersion < int64(tree.ndb.opts.InitialVersion) { + return firstVersion, fmt.Errorf("initial version set to %v, but found earlier version %v", + tree.ndb.opts.InitialVersion, firstVersion) + } + + latestVersion, err := tree.ndb.getLegacyLatestVersion() + if err != nil { + return 0, err + } + + if firstVersion > 0 && firstVersion < int64(tree.ndb.opts.InitialVersion) { + return latestVersion, fmt.Errorf("initial version set to %v, but found earlier version %v", + tree.ndb.opts.InitialVersion, firstVersion) + } + + if latestVersion < targetVersion { + return latestVersion, fmt.Errorf("wanted to load target %d but only found up to %d", targetVersion, latestVersion) + } + + if firstVersion == 0 { + if targetVersion <= 0 { + if !tree.skipFastStorageUpgrade { + tree.mtx.Lock() + defer tree.mtx.Unlock() + _, err := tree.enableFastStorageAndCommitIfNotEnabled() + return 0, err + } + return 0, nil + } + return 0, fmt.Errorf("no versions found while trying to load %v", targetVersion) + } + + if targetVersion <= 0 { + targetVersion = latestVersion + } + if !tree.VersionExists(targetVersion) { + return 0, ErrVersionDoesNotExist + } + rootNodeKey, err := tree.ndb.GetRoot(targetVersion) + if err != nil { + return 0, err + } + + iTree := &ImmutableTree{ + ndb: tree.ndb, + version: targetVersion, + skipFastStorageUpgrade: tree.skipFastStorageUpgrade, + } + + if rootNodeKey != nil { + iTree.root, err = tree.ndb.GetNode(rootNodeKey) + if err != nil { + return 0, err + } + } + + tree.ImmutableTree = iTree + tree.lastSaved = iTree.clone() + if !tree.skipFastStorageUpgrade { + // Attempt to upgrade + // hanging issue is in here + if _, err := tree.enableFastStorageAndCommitIfNotEnabled(); err != nil { + return 0, err + } + } + + return latestVersion, nil +} + // LoadVersionByRootHash loads a tree using the provided version and roothash func (tree *MutableTree) LoadVersionByRootHash(version int64, rootHash []byte) (int64, error) { if len(rootHash) == 0 { @@ -598,8 +677,14 @@ func (tree *MutableTree) LoadVersionByRootHash(version int64, rootHash []byte) ( // loadVersionForOverwriting attempts to load a tree at a previously committed // version, or the latest version below it. Any versions greater than targetVersion will be deleted. func (tree *MutableTree) LoadVersionForOverwriting(targetVersion int64) error { - if _, err := tree.LoadVersion(targetVersion); err != nil { - return err + if tree.useLegacyFormat { + if _, err := tree.LoadLegacyVersion(targetVersion); err != nil { + return err + } + } else { + if _, err := tree.LoadVersion(targetVersion); err != nil { + return err + } } if err := tree.ndb.DeleteVersionsFrom(targetVersion + 1); err != nil { @@ -661,7 +746,6 @@ func (tree *MutableTree) enableFastStorageAndCommitIfNotEnabled() (bool, error) return false, err } } - if err := tree.enableFastStorageAndCommit(); err != nil { tree.ndb.storageVersion = defaultStorageVersionValue return false, err @@ -686,7 +770,12 @@ func (tree *MutableTree) enableFastStorageAndCommit() error { return err } - latestVersion, err := tree.ndb.getLatestVersion() + var latestVersion int64 + if tree.useLegacyFormat { + latestVersion, err = tree.ndb.getLegacyLatestVersion() + } else { + latestVersion, err = tree.ndb.getLatestVersion() + } if err != nil { return err } @@ -851,7 +940,11 @@ func (tree *MutableTree) SaveVersion() ([]byte, int64, error) { return nil, version, err } - tree.ndb.resetLatestVersion(version) + if tree.ndb.useLegacyFormat { + tree.ndb.resetLegacyLatestVersion(version) + } else { + tree.ndb.resetLatestVersion(version) + } tree.version = version // set new working tree diff --git a/nodedb.go b/nodedb.go index b4dc9000b..96da5e564 100644 --- a/nodedb.go +++ b/nodedb.go @@ -330,7 +330,13 @@ func (ndb *nodeDB) shouldForceFastStorageUpgrade() (bool, error) { versions := strings.Split(ndb.storageVersion, fastStorageVersionDelimiter) if len(versions) == 2 { - latestVersion, err := ndb.getLatestVersion() + var latestVersion int64 + var err error + if ndb.useLegacyFormat { + latestVersion, err = ndb.getLegacyLatestVersion() + } else { + latestVersion, err = ndb.getLatestVersion() + } if err != nil { // TODO: should be true or false as default? (removed panic here) return false, err @@ -491,7 +497,7 @@ func (ndb *nodeDB) deleteLegacyOrphans(version int64) error { // can delete the orphan. Otherwise, we shorten its lifetime, by // moving its endpoint to the previous version. if predecessor < fromVersion || fromVersion == toVersion { - if err := ndb.batch.Delete(ndb.nodeKey(hash)); err != nil { + if err := ndb.batch.Delete(ndb.legacyNodeKey(hash)); err != nil { return err } ndb.nodeCache.Remove(hash) @@ -571,6 +577,7 @@ func (ndb *nodeDB) deleteLegacyVersions(legacyLatestVersion int64) error { // deleteLegacyVersion deletes a legacy tree version from disk. // calls deleteOrphans(version), deleteRoot(version, checkLatestVersion) func (ndb *nodeDB) deleteLegacyVersion(version int64, checkLatestVersion bool) error { + // TODO: HERE if ndb.versionReaders[version] > 0 { return fmt.Errorf("unable to delete version %v, it has %v active readers", version, ndb.versionReaders[version]) } @@ -589,6 +596,46 @@ func (ndb *nodeDB) deleteLegacyVersion(version int64, checkLatestVersion bool) e // DeleteVersionsFrom permanently deletes all tree versions from the given version upwards. func (ndb *nodeDB) DeleteVersionsFrom(fromVersion int64) error { + if ndb.useLegacyFormat { + latest, err := ndb.getLegacyLatestVersion() + if err != nil { + return err + } + if latest < fromVersion { + return nil + } + + ndb.mtx.Lock() + for v, r := range ndb.versionReaders { + if v >= fromVersion && r != 0 { + ndb.mtx.Unlock() // Unlock before exiting + return fmt.Errorf("unable to delete version %v with %v active readers", v, r) + } + } + ndb.mtx.Unlock() + // TODO: HERE + // Delete the nodes for new format + if err := ndb.traverseRange(legacyRootKeyFormat.Key(fromVersion), legacyRootKeyFormat.Key(latest+1), func(k, v []byte) error { + var version int64 + legacyRootKeyFormat.Scan(k, &version) + // delete the legacy nodes + if err := ndb.deleteLegacyNodes(version, v); err != nil { + return err + } + if err := ndb.deleteLegacyOrphans(version); err != nil { + return err + } + // it will skip the orphans because orphans will be removed at once in `deleteLegacyVersions` + // delete the legacy root + return ndb.batch.Delete(k) + }); err != nil { + return err + } + + ndb.resetLegacyLatestVersion(fromVersion - 1) + + return nil + } latest, err := ndb.getLatestVersion() if err != nil { return err @@ -647,6 +694,39 @@ func (ndb *nodeDB) DeleteVersionsFrom(fromVersion int64) error { // DeleteVersionsTo deletes the oldest versions up to the given version from disk. func (ndb *nodeDB) DeleteVersionsTo(toVersion int64) error { + if ndb.useLegacyFormat { + legacyLatestVersion, err := ndb.getLegacyLatestVersion() + if err != nil { + return err + } + + first, err := ndb.getFirstLegacyVersion() + if err != nil { + return err + } + + if legacyLatestVersion <= toVersion { + return fmt.Errorf("latest version %d is less than or equal to toVersion %d", legacyLatestVersion, toVersion) + } + + ndb.mtx.Lock() + for v, r := range ndb.versionReaders { + if v >= first && v <= toVersion && r != 0 { + ndb.mtx.Unlock() + return fmt.Errorf("unable to delete version %d with %d active readers", v, r) + } + } + ndb.mtx.Unlock() + + for version := first; version <= toVersion; version++ { + if err := ndb.deleteLegacyVersion(version, true); err != nil { + return err + } + ndb.resetFirstVersion(version + 1) + } + + return nil + } legacyLatestVersion, err := ndb.getLegacyLatestVersion() if err != nil { return err @@ -654,7 +734,7 @@ func (ndb *nodeDB) DeleteVersionsTo(toVersion int64) error { // If the legacy version is greater than the toVersion, we don't need to delete anything. // It will delete the legacy versions at once. - if !ndb.useLegacyFormat && legacyLatestVersion > toVersion { + if legacyLatestVersion > toVersion { return nil } @@ -681,8 +761,8 @@ func (ndb *nodeDB) DeleteVersionsTo(toVersion int64) error { } ndb.mtx.Unlock() - // Delete the legacy versions unless we are using the legacy format - if !ndb.useLegacyFormat && legacyLatestVersion >= first { + // Delete the legacy versions + if legacyLatestVersion >= first { // Delete the last version for the legacyLastVersion if err := ndb.traverseOrphans(legacyLatestVersion, legacyLatestVersion+1, func(orphan *Node) error { return ndb.batch.Delete(ndb.legacyNodeKey(orphan.hash)) @@ -700,14 +780,8 @@ func (ndb *nodeDB) DeleteVersionsTo(toVersion int64) error { } for version := first; version <= toVersion; version++ { - if ndb.useLegacyFormat { - if err := ndb.deleteLegacyVersion(version, true); err != nil { - return err - } - } else { - if err := ndb.deleteVersion(version); err != nil { - return err - } + if err := ndb.deleteVersion(version); err != nil { + return err } ndb.resetFirstVersion(version + 1) } @@ -788,9 +862,41 @@ func (ndb *nodeDB) getFirstVersion() (int64, error) { return latestVersion, nil } +func (ndb *nodeDB) getFirstLegacyVersion() (int64, error) { + ndb.mtx.Lock() + firstVersion := ndb.firstVersion + ndb.mtx.Unlock() + + if firstVersion > 0 { + return firstVersion, nil + } + + // Find the first version + latestVersion, err := ndb.getLegacyLatestVersion() + if err != nil { + return 0, err + } + for firstVersion < latestVersion { + version := (latestVersion + firstVersion) >> 1 + has, err := ndb.hasLegacyVersion(version) + if err != nil { + return 0, err + } + if has { + latestVersion = version + } else { + firstVersion = version + 1 + } + } + + ndb.resetFirstVersion(latestVersion) + + return latestVersion, nil +} + // deleteRoot deletes the root entry from disk, but not the node it points to. func (ndb *nodeDB) deleteLegacyRoot(version int64, checkLatestVersion bool) error { - latestVersion, err := ndb.getLatestVersion() + latestVersion, err := ndb.getLegacyLatestVersion() if err != nil { return err } @@ -813,15 +919,8 @@ func (ndb *nodeDB) resetFirstVersion(version int64) { func (ndb *nodeDB) getLegacyLatestVersion() (int64, error) { ndb.mtx.Lock() - // consider applying latestVersion = ndb.latestVersion here for legacy mode latestVersion := ndb.legacyLatestVersion - /* - if ndb.useLegacyFormat { - latestVersion = ndb.latestVersion - } - */ ndb.mtx.Unlock() - if latestVersion != 0 { return latestVersion, nil } From 23d0dcfeb8f2a4959fa8fe366070f56d7b06a282 Mon Sep 17 00:00:00 2001 From: i-norden Date: Mon, 1 Jul 2024 11:27:26 -0400 Subject: [PATCH 18/18] tree tests --- iterator_test.go | 2 +- mutable_tree_test.go | 14 +- nodedb_test.go | 10 +- tree_test.go | 1810 +++++++++++++++++++++++++++++++++++++++++- 4 files changed, 1779 insertions(+), 57 deletions(-) diff --git a/iterator_test.go b/iterator_test.go index 8059e12b9..49fdd8729 100644 --- a/iterator_test.go +++ b/iterator_test.go @@ -355,7 +355,7 @@ func TestLegacyIterator_WithDelete_Full_Ascending_Success(t *testing.T) { err = tree.DeleteVersionsTo(1) require.NoError(t, err) - latestVersion, err := tree.ndb.getLatestVersion() + latestVersion, err := tree.ndb.getLegacyLatestVersion() require.NoError(t, err) immutableTree, err := tree.GetImmutable(latestVersion) require.NoError(t, err) diff --git a/mutable_tree_test.go b/mutable_tree_test.go index 7efba522b..c36fd0781 100644 --- a/mutable_tree_test.go +++ b/mutable_tree_test.go @@ -427,13 +427,13 @@ func TestLegacyMutableTree_DeleteVersionsTo(t *testing.T) { // ensure even versions have been deleted for v := int64(1); v <= versionToDelete; v++ { - _, err := tree.LoadVersion(v) + _, err := tree.LoadLegacyVersion(v) require.Error(t, err) } // ensure odd number versions exist and we can query for all set entries for _, v := range []int64{9, 10} { - _, err := tree.LoadVersion(v) + _, err := tree.LoadLegacyVersion(v) require.NoError(t, err) for _, e := range versionEntries[v] { @@ -1623,7 +1623,7 @@ func TestLegacyUpgradeStorageToFast_DbErrorEnableFastStorage_Failure(t *testing. // rIterMock is used to get the latest version from disk. We are mocking that rIterMock returns latestTreeVersion from disk rIterMock.EXPECT().Valid().Return(true).Times(1) - rIterMock.EXPECT().Key().Return(nodeKeyFormat.Key(GetRootKey(1))) + rIterMock.EXPECT().Key().Return(legacyRootKeyFormat.Key(1)) rIterMock.EXPECT().Close().Return(nil).Times(1) expectedError := errors.New("some db error") @@ -1720,7 +1720,7 @@ func TestLegacyFastStorageReUpgradeProtection_NoForceUpgrade_Success(t *testing. // rIterMock is used to get the latest version from disk. We are mocking that rIterMock returns latestTreeVersion from disk rIterMock.EXPECT().Valid().Return(true).Times(1) - rIterMock.EXPECT().Key().Return(nodeKeyFormat.Key(GetRootKey(1))) + rIterMock.EXPECT().Key().Return(legacyRootKeyFormat.Key(1)) rIterMock.EXPECT().Close().Return(nil).Times(1) batchMock := mock.NewMockBatch(ctrl) @@ -1734,7 +1734,7 @@ func TestLegacyFastStorageReUpgradeProtection_NoForceUpgrade_Success(t *testing. // Pretend that we called Load and have the latest state in the tree tree.version = latestTreeVersion - latestVersion, err := tree.ndb.getLatestVersion() + latestVersion, err := tree.ndb.getLegacyLatestVersion() require.NoError(t, err) require.Equal(t, latestVersion, int64(latestTreeVersion)) @@ -1883,7 +1883,7 @@ func TestLegacyFastStorageReUpgradeProtection_ForceUpgradeFirstTime_NoForceSecon // rIterMock is used to get the latest version from disk. We are mocking that rIterMock returns latestTreeVersion from disk rIterMock.EXPECT().Valid().Return(true).Times(1) - rIterMock.EXPECT().Key().Return(nodeKeyFormat.Key(GetRootKey(latestTreeVersion))) + rIterMock.EXPECT().Key().Return(legacyRootKeyFormat.Key(latestTreeVersion)) rIterMock.EXPECT().Close().Return(nil).Times(1) fastNodeKeyToDelete := []byte("some_key") @@ -1928,7 +1928,7 @@ func TestLegacyFastStorageReUpgradeProtection_ForceUpgradeFirstTime_NoForceSecon // Pretend that we called Load and have the latest state in the tree tree.version = latestTreeVersion - latestVersion, err := tree.ndb.getLatestVersion() + latestVersion, err := tree.ndb.getLegacyLatestVersion() require.NoError(t, err) require.Equal(t, latestVersion, int64(latestTreeVersion)) diff --git a/nodedb_test.go b/nodedb_test.go index 75e938561..d7463700d 100644 --- a/nodedb_test.go +++ b/nodedb_test.go @@ -415,8 +415,8 @@ func TestShouldForceFastStorageUpdate_FastVersion_Match_False(t *testing.T) { func TestLegacyShouldForceFastStorageUpdate_FastVersion_Match_False(t *testing.T) { db := dbm.NewMemDB() ndb := newLegacyNodeDB(db, 0, DefaultOptions(), false, log.NewNopLogger()) - ndb.latestVersion = 100 - ndb.storageVersion = fastStorageVersionValue + fastStorageVersionDelimiter + strconv.Itoa(int(ndb.latestVersion)) + ndb.legacyLatestVersion = 100 + ndb.storageVersion = fastStorageVersionValue + fastStorageVersionDelimiter + strconv.Itoa(int(ndb.legacyLatestVersion)) shouldForce, err := ndb.shouldForceFastStorageUpgrade() require.False(t, shouldForce) @@ -435,8 +435,8 @@ func TestIsFastStorageEnabled_True(t *testing.T) { func TestLegacyIsFastStorageEnabled_True(t *testing.T) { db := dbm.NewMemDB() ndb := newLegacyNodeDB(db, 0, DefaultOptions(), false, log.NewNopLogger()) - ndb.latestVersion = 100 - ndb.storageVersion = fastStorageVersionValue + fastStorageVersionDelimiter + strconv.Itoa(int(ndb.latestVersion)) + ndb.legacyLatestVersion = 100 + ndb.storageVersion = fastStorageVersionValue + fastStorageVersionDelimiter + strconv.Itoa(int(ndb.legacyLatestVersion)) require.True(t, ndb.hasUpgradedToFastStorage()) } @@ -455,7 +455,7 @@ func TestIsFastStorageEnabled_False(t *testing.T) { func TestLegacyIsFastStorageEnabled_False(t *testing.T) { db := dbm.NewMemDB() ndb := newLegacyNodeDB(db, 0, DefaultOptions(), false, log.NewNopLogger()) - ndb.latestVersion = 100 + ndb.legacyLatestVersion = 100 ndb.storageVersion = defaultStorageVersionValue shouldForce, err := ndb.shouldForceFastStorageUpgrade() diff --git a/tree_test.go b/tree_test.go index 28c5bfcb1..b054c7c73 100644 --- a/tree_test.go +++ b/tree_test.go @@ -107,6 +107,66 @@ func TestVersionedRandomTree(t *testing.T) { require.Equal(tree.nodeSize(), len(nodes)) } +func Test1LegacyVersionedRandomTree(t *testing.T) { + require := require.New(t) + SetupTest() + d, closeDB := getTestDB() + defer closeDB() + + tree := NewLegacyMutableTree(d, 100, false, false, nil, log.NewNopLogger()) + versions := 50 + keysPerVersion := 30 + + // Create a tree of size 1000 with 100 versions. + for i := 1; i <= versions; i++ { + for j := 0; j < keysPerVersion; j++ { + k := []byte(iavlrand.RandStr(8)) + v := []byte(iavlrand.RandStr(8)) + tree.Set(k, v) + } + tree.SaveVersion() + } + + leafNodes, err := tree.ndb.leafNodes() + require.Nil(err) + require.Equal(versions*keysPerVersion, len(leafNodes), "wrong number of nodes") + + // Before deleting old versions, we should have equal or more nodes in the + // db than in the current tree version. + nodes, err := tree.ndb.nodes() + require.Nil(err) + require.True(len(nodes) >= tree.nodeSize()) + + // Ensure it returns all versions in sorted order + available := tree.AvailableVersions() + assert.Equal(t, versions, len(available)) + assert.Equal(t, 1, available[0]) + assert.Equal(t, versions, available[len(available)-1]) + + err = tree.DeleteVersionsTo(int64(versions - 1)) + require.Nil(err) + + // require.Len(tree.versions, 1, "tree must have one version left") + tr, err := tree.GetImmutable(int64(versions)) + require.NoError(err, "GetImmutable should not error for version %d", versions) + require.Equal(tr.root, tree.root) + + // we should only have one available version now + available = tree.AvailableVersions() + assert.Equal(t, 1, len(available)) + assert.Equal(t, versions, available[0]) + + // After cleaning up all previous versions, we should have as many nodes + // in the db as in the current tree version. + leafNodes, err = tree.ndb.leafNodes() + require.Nil(err) + require.Len(leafNodes, int(tree.Size())) + + nodes, err = tree.ndb.nodes() + require.Nil(err) + require.Equal(tree.nodeSize(), len(nodes)) +} + // nolint: dupl func TestTreeHash(t *testing.T) { const ( @@ -175,6 +235,73 @@ func TestTreeHash(t *testing.T) { require.EqualValues(t, versions, tree.Version()) } +func TestLegacyTreeHash(t *testing.T) { + const ( + randSeed = 49872768940 // For deterministic tests + keySize = 16 + valueSize = 16 + + versions = 4 // number of versions to generate + versionOps = 4096 // number of operations (create/update/delete) per version + updateRatio = 0.4 // ratio of updates out of all operations + deleteRatio = 0.2 // ratio of deletes out of all operations + ) + + // expected hashes for each version + expectHashes := []string{ + "58ec30fa27f338057e5964ed9ec3367e59b2b54bec4c194f10fde7fed16c2a1c", + "91ad3ace227372f0064b2d63e8493ce8f4bdcbd16c7a8e4f4d54029c9db9570c", + "92c25dce822c5968c228cfe7e686129ea281f79273d4a8fcf6f9130a47aa5421", + "e44d170925554f42e00263155c19574837a38e3efed8910daccc7fa12f560fa0", + } + require.Len(t, expectHashes, versions, "must have expected hashes for all versions") + + r := rand.New(rand.NewSource(randSeed)) + tree := NewLegacyMutableTree(dbm.NewMemDB(), 0, false, false, nil, log.NewNopLogger()) + + keys := make([][]byte, 0, versionOps) + for i := 0; i < versions; i++ { + for j := 0; j < versionOps; j++ { + key := make([]byte, keySize) + value := make([]byte, valueSize) + + // The performance of this is likely to be terrible, but that's fine for small tests + switch { + case len(keys) > 0 && r.Float64() <= deleteRatio: + index := r.Intn(len(keys)) + key = keys[index] + keys = append(keys[:index], keys[index+1:]...) + _, removed, err := tree.Remove(key) + require.NoError(t, err) + require.True(t, removed) + + case len(keys) > 0 && r.Float64() <= updateRatio: + key = keys[r.Intn(len(keys))] + r.Read(value) + updated, err := tree.Set(key, value) + require.NoError(t, err) + require.True(t, updated) + + default: + r.Read(key) + r.Read(value) + // If we get an update, set again + for updated, err := tree.Set(key, value); err == nil && updated; { + key = make([]byte, keySize) + r.Read(key) + } + keys = append(keys, key) + } + } + hash, version, err := tree.SaveVersion() + require.NoError(t, err) + require.EqualValues(t, i+1, version) + require.Equal(t, expectHashes[i], hex.EncodeToString(hash)) + } + + require.EqualValues(t, versions, tree.Version()) +} + func TestVersionedRandomTreeSmallKeys(t *testing.T) { require := require.New(t) d, closeDB := getTestDB() @@ -223,6 +350,54 @@ func TestVersionedRandomTreeSmallKeys(t *testing.T) { } } +func TestLegacyVersionedRandomTreeSmallKeys(t *testing.T) { + require := require.New(t) + d, closeDB := getTestDB() + defer closeDB() + + tree := NewLegacyMutableTree(d, 100, false, false, nil, log.NewNopLogger()) + singleVersionTree := getLegacyTestTree(0) + versions := 20 + keysPerVersion := 50 + + for i := 1; i <= versions; i++ { + for j := 0; j < keysPerVersion; j++ { + // Keys of size one are likely to be overwritten. + k := []byte(iavlrand.RandStr(1)) + v := []byte(iavlrand.RandStr(8)) + tree.Set(k, v) + singleVersionTree.Set(k, v) + } + tree.SaveVersion() + } + singleVersionTree.SaveVersion() + + for i := 1; i < versions; i++ { + tree.DeleteVersionsTo(int64(i)) + } + + // After cleaning up all previous versions, we should have as many nodes + // in the db as in the current tree version. The simple tree must be equal + // too. + leafNodes, err := tree.ndb.leafNodes() + require.Nil(err) + + nodes, err := tree.ndb.nodes() + require.Nil(err) + + require.Len(leafNodes, int(tree.Size())) + require.Len(nodes, tree.nodeSize()) + require.Len(nodes, singleVersionTree.nodeSize()) + + // Try getting random keys. + for i := 0; i < keysPerVersion; i++ { + val, err := tree.Get([]byte(iavlrand.RandStr(1))) + require.NoError(err) + require.NotNil(val) + require.NotEmpty(val) + } +} + func TestVersionedRandomTreeSmallKeysRandomDeletes(t *testing.T) { require := require.New(t) d, closeDB := getTestDB() @@ -271,6 +446,54 @@ func TestVersionedRandomTreeSmallKeysRandomDeletes(t *testing.T) { } } +func TestLegacyVersionedRandomTreeSmallKeysRandomDeletes(t *testing.T) { + require := require.New(t) + d, closeDB := getTestDB() + defer closeDB() + + tree := NewLegacyMutableTree(d, 100, false, false, nil, log.NewNopLogger()) + singleVersionTree := getLegacyTestTree(0) + versions := 30 + keysPerVersion := 50 + + for i := 1; i <= versions; i++ { + for j := 0; j < keysPerVersion; j++ { + // Keys of size one are likely to be overwritten. + k := []byte(iavlrand.RandStr(1)) + v := []byte(iavlrand.RandStr(8)) + tree.Set(k, v) + singleVersionTree.Set(k, v) + } + tree.SaveVersion() + } + singleVersionTree.SaveVersion() + + for _, i := range iavlrand.RandPerm(versions - 1) { + tree.DeleteVersionsTo(int64(i + 1)) + } + + // After cleaning up all previous versions, we should have as many nodes + // in the db as in the current tree version. The simple tree must be equal + // too. + leafNodes, err := tree.ndb.leafNodes() + require.Nil(err) + + nodes, err := tree.ndb.nodes() + require.Nil(err) + + require.Len(leafNodes, int(tree.Size())) + require.Len(nodes, tree.nodeSize()) + require.Len(nodes, singleVersionTree.nodeSize()) + + // Try getting random keys. + for i := 0; i < keysPerVersion; i++ { + val, err := tree.Get([]byte(iavlrand.RandStr(1))) + require.NoError(err) + require.NotNil(val) + require.NotEmpty(val) + } +} + func TestVersionedTreeSpecial1(t *testing.T) { tree := getTestTree(100) @@ -295,6 +518,30 @@ func TestVersionedTreeSpecial1(t *testing.T) { require.Equal(t, tree.nodeSize(), len(nodes)) } +func TestLegacyVersionedTreeSpecial1(t *testing.T) { + tree := getLegacyTestTree(100) + + tree.Set([]byte("C"), []byte("so43QQFN")) + tree.SaveVersion() + + tree.Set([]byte("A"), []byte("ut7sTTAO")) + tree.SaveVersion() + + tree.Set([]byte("X"), []byte("AoWWC1kN")) + tree.SaveVersion() + + tree.Set([]byte("T"), []byte("MhkWjkVy")) + tree.SaveVersion() + + tree.DeleteVersionsTo(1) + tree.DeleteVersionsTo(2) + tree.DeleteVersionsTo(3) + + nodes, err := tree.ndb.nodes() + require.Nil(t, err) + require.Equal(t, tree.nodeSize(), len(nodes)) +} + func TestVersionedRandomTreeSpecial2(t *testing.T) { require := require.New(t) tree := getTestTree(100) @@ -314,6 +561,25 @@ func TestVersionedRandomTreeSpecial2(t *testing.T) { require.Len(nodes, tree.nodeSize()) } +func TestLegacyVersionedRandomTreeSpecial2(t *testing.T) { + require := require.New(t) + tree := getLegacyTestTree(100) + + tree.Set([]byte("OFMe2Yvm"), []byte("ez2OtQtE")) + tree.Set([]byte("WEN4iN7Y"), []byte("kQNyUalI")) + tree.SaveVersion() + + tree.Set([]byte("1yY3pXHr"), []byte("udYznpII")) + tree.Set([]byte("7OSHNE7k"), []byte("ff181M2d")) + tree.SaveVersion() + + tree.DeleteVersionsTo(1) + + nodes, err := tree.ndb.nodes() + require.NoError(err) + require.Len(nodes, tree.nodeSize()) +} + func TestVersionedEmptyTree(t *testing.T) { require := require.New(t) d, closeDB := getTestDB() @@ -370,16 +636,72 @@ func TestVersionedEmptyTree(t *testing.T) { require.Error(err, "GetImmutable should fail for version 2") } -func TestVersionedTree(t *testing.T) { +func TestLegacyVersionedEmptyTree(t *testing.T) { require := require.New(t) d, closeDB := getTestDB() defer closeDB() - tree := NewMutableTree(d, 0, false, log.NewNopLogger()) + tree := NewLegacyMutableTree(d, 0, false, false, nil, log.NewNopLogger()) - // We start with empty database. - require.Equal(0, tree.ndb.size()) - require.True(tree.IsEmpty()) + hash, v, err := tree.SaveVersion() + require.NoError(err) + require.Equal("e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", hex.EncodeToString(hash)) + require.EqualValues(1, v) + + hash, v, err = tree.SaveVersion() + require.NoError(err) + require.Equal("e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", hex.EncodeToString(hash)) + require.EqualValues(2, v) + + hash, v, err = tree.SaveVersion() + require.NoError(err) + require.Equal("e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", hex.EncodeToString(hash)) + require.EqualValues(3, v) + + hash, v, err = tree.SaveVersion() + require.NoError(err) + require.Equal("e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", hex.EncodeToString(hash)) + require.EqualValues(4, v) + + require.EqualValues(4, tree.Version()) + + require.True(tree.VersionExists(1)) + require.True(tree.VersionExists(3)) + + // Test the empty root loads correctly. + it, err := tree.GetImmutable(3) + require.NoError(err) + require.Nil(it.root) + + require.NoError(tree.DeleteVersionsTo(3)) + + require.False(tree.VersionExists(1)) + require.False(tree.VersionExists(3)) + + tree.Set([]byte("k"), []byte("v")) + + // Now reload the tree. + tree = NewLegacyMutableTree(d, 0, false, false, nil, log.NewNopLogger()) + tree.Load() + + require.False(tree.VersionExists(1)) + require.False(tree.VersionExists(2)) + require.False(tree.VersionExists(3)) + + _, err = tree.GetImmutable(2) + require.Error(err, "GetImmutable should fail for version 2") +} + +func TestVersionedTree(t *testing.T) { + require := require.New(t) + d, closeDB := getTestDB() + defer closeDB() + + tree := NewMutableTree(d, 0, false, log.NewNopLogger()) + + // We start with empty database. + require.Equal(0, tree.ndb.size()) + require.True(tree.IsEmpty()) require.False(tree.IsFastCacheEnabled()) // version 0 @@ -589,6 +911,225 @@ func TestVersionedTree(t *testing.T) { require.Nil(val) } +func TestLegacyVersionedTree(t *testing.T) { + require := require.New(t) + d, closeDB := getTestDB() + defer closeDB() + + tree := NewLegacyMutableTree(d, 0, false, false, nil, log.NewNopLogger()) + + // We start with empty database. + require.Equal(0, tree.ndb.size()) + require.True(tree.IsEmpty()) + require.False(tree.IsFastCacheEnabled()) + + // version 0 + + tree.Set([]byte("key1"), []byte("val0")) + tree.Set([]byte("key2"), []byte("val0")) + + // Still zero keys, since we haven't written them. + nodes, err := tree.ndb.leafNodes() + require.NoError(err) + require.Len(nodes, 0) + require.False(tree.IsEmpty()) + + // Now let's write the keys to storage. + hash1, v, err := tree.SaveVersion() + require.NoError(err) + require.False(tree.IsEmpty()) + require.EqualValues(1, v) + + // -----1----- + // key1 = val0 version=1 + // key2 = val0 version=1 + // key2 (root) version=1 + // ----------- + + nodes1, err := tree.ndb.leafNodes() + require.NoError(err) + require.Len(nodes1, 2, "db should have a size of 2") + + // version 1 + + tree.Set([]byte("key1"), []byte("val1")) + tree.Set([]byte("key2"), []byte("val1")) + tree.Set([]byte("key3"), []byte("val1")) + nodes, err = tree.ndb.leafNodes() + require.NoError(err) + require.Len(nodes, len(nodes1)) + + hash2, v2, err := tree.SaveVersion() + require.NoError(err) + require.False(bytes.Equal(hash1, hash2)) + require.EqualValues(v+1, v2) + + // Recreate a new tree and load it, to make sure it works in this + // scenario. + tree = NewLegacyMutableTree(d, 100, false, false, nil, log.NewNopLogger()) + _, err = tree.Load() + require.NoError(err) + + // require.Len(tree.versions, 2, "wrong number of versions") + require.EqualValues(v2, tree.Version()) + + // -----1----- + // key1 = val0 + // key2 = val0 + // -----2----- + // key1 = val1 + // key2 = val1 + // key3 = val1 + // ----------- + + nodes2, err := tree.ndb.leafNodes() + require.NoError(err) + require.Len(nodes2, 5, "db should have grown in size") + orphans, err := tree.ndb.orphans() + require.NoError(err) + require.Len(orphans, 3, "db should have three orphans") + + // Create three more orphans. + tree.Remove([]byte("key1")) // orphans both leaf node and inner node containing "key1" and "key2" + tree.Set([]byte("key2"), []byte("val2")) + + hash3, v3, _ := tree.SaveVersion() + require.EqualValues(3, v3) + + // -----1----- + // key1 = val0 (replaced) + // key2 = val0 (replaced) + // -----2----- + // key1 = val1 (removed) + // key2 = val1 (replaced) + // key3 = val1 + // -----3----- + // key2 = val2 + // ----------- + + nodes3, err := tree.ndb.leafNodes() + require.NoError(err) + require.Len(nodes3, 6, "wrong number of nodes") + + orphans, err = tree.ndb.orphans() + require.NoError(err) + require.Len(orphans, 7, "wrong number of orphans") + + hash4, _, _ := tree.SaveVersion() + require.EqualValues(hash3, hash4) + require.NotNil(hash4) + + tree = NewLegacyMutableTree(d, 100, false, false, nil, log.NewNopLogger()) + _, err = tree.Load() + require.NoError(err) + + // ------------ + // DB UNCHANGED + // ------------ + + nodes4, err := tree.ndb.leafNodes() + require.NoError(err) + require.Len(nodes4, len(nodes3), "db should not have changed in size") + + tree.Set([]byte("key1"), []byte("val0")) + + // "key2" + val, err := tree.GetVersioned([]byte("key2"), 0) + require.NoError(err) + require.Nil(val) + + val, err = tree.GetVersioned([]byte("key2"), 1) + require.NoError(err) + require.Equal("val0", string(val)) + + val, err = tree.GetVersioned([]byte("key2"), 2) + require.NoError(err) + require.Equal("val1", string(val)) + + val, err = tree.Get([]byte("key2")) + require.NoError(err) + require.Equal("val2", string(val)) + + // "key1" + val, err = tree.GetVersioned([]byte("key1"), 1) + require.NoError(err) + require.Equal("val0", string(val)) + + val, err = tree.GetVersioned([]byte("key1"), 2) + require.NoError(err) + require.Equal("val1", string(val)) + + val, err = tree.GetVersioned([]byte("key1"), 3) + require.NoError(err) + require.Nil(val) + + val, err = tree.GetVersioned([]byte("key1"), 4) + require.NoError(err) + require.Nil(val) + + val, err = tree.Get([]byte("key1")) + require.NoError(err) + require.Equal("val0", string(val)) + + // "key3" + val, err = tree.GetVersioned([]byte("key3"), 0) + require.NoError(err) + require.Nil(val) + + val, err = tree.GetVersioned([]byte("key3"), 2) + require.NoError(err) + require.Equal("val1", string(val)) + + val, err = tree.GetVersioned([]byte("key3"), 3) + require.NoError(err) + require.Equal("val1", string(val)) + + // Delete a version. After this the keys in that version should not be found. + tree.DeleteVersionsTo(2) + + // -----1----- + // key1 = val0 + // key2 = val0 + // -----2----- + // key3 = val1 + // -----3----- + // key2 = val2 + // ----------- + + nodes5, err := tree.ndb.leafNodes() + require.NoError(err) + + require.True(len(nodes5) < len(nodes4), "db should have shrunk after delete %d !< %d", len(nodes5), len(nodes4)) + + val, err = tree.GetVersioned([]byte("key2"), 2) + require.NoError(err) + require.Nil(val) + + val, err = tree.GetVersioned([]byte("key3"), 2) + require.NoError(err) + require.Nil(val) + + // But they should still exist in the latest version. + + val, err = tree.Get([]byte("key2")) + require.NoError(err) + require.Equal("val2", string(val)) + + val, err = tree.Get([]byte("key3")) + require.NoError(err) + require.Equal("val1", string(val)) + + // Version 1 should not be available. + + val, err = tree.GetVersioned([]byte("key1"), 1) + require.NoError(err) + require.Nil(val) + + val, err = tree.GetVersioned([]byte("key2"), 1) + require.NoError(err) + require.Nil(val) +} + func TestVersionedTreeVersionDeletingEfficiency(t *testing.T) { d, closeDB := getTestDB() defer closeDB() @@ -637,55 +1178,195 @@ func TestVersionedTreeVersionDeletingEfficiency(t *testing.T) { require.Equal(t, tree2.nodeSize(), tree.nodeSize()) } +func TestLegacyVersionedTreeVersionDeletingEfficiency(t *testing.T) { + d, closeDB := getTestDB() + defer closeDB() + + tree := NewLegacyMutableTree(d, 0, false, false, nil, log.NewNopLogger()) + + tree.Set([]byte("key0"), []byte("val0")) + tree.Set([]byte("key1"), []byte("val0")) + tree.Set([]byte("key2"), []byte("val0")) + tree.SaveVersion() + + leafNodes, err := tree.ndb.leafNodes() + require.Nil(t, err) + require.Len(t, leafNodes, 3) + + tree.Set([]byte("key1"), []byte("val1")) + tree.Set([]byte("key2"), []byte("val1")) + tree.Set([]byte("key3"), []byte("val1")) + tree.SaveVersion() + + leafNodes, err = tree.ndb.leafNodes() + require.Nil(t, err) + require.Len(t, leafNodes, 6) + + tree.Set([]byte("key0"), []byte("val2")) + tree.Remove([]byte("key1")) + tree.Set([]byte("key2"), []byte("val2")) + tree.SaveVersion() + + leafNodes, err = tree.ndb.leafNodes() + require.Nil(t, err) + require.Len(t, leafNodes, 8) + + tree.DeleteVersionsTo(2) + + leafNodes, err = tree.ndb.leafNodes() + require.Nil(t, err) + require.Len(t, leafNodes, 3) + + tree2 := getLegacyTestTree(0) + tree2.Set([]byte("key0"), []byte("val2")) + tree2.Set([]byte("key2"), []byte("val2")) + tree2.Set([]byte("key3"), []byte("val1")) + tree2.SaveVersion() + + require.Equal(t, tree2.nodeSize(), tree.nodeSize()) +} + func TestVersionedTreeOrphanDeleting(t *testing.T) { tree := getTestTree(0) - tree.Set([]byte("key0"), []byte("val0")) + tree.Set([]byte("key0"), []byte("val0")) + tree.Set([]byte("key1"), []byte("val0")) + tree.Set([]byte("key2"), []byte("val0")) + tree.SaveVersion() + + tree.Set([]byte("key1"), []byte("val1")) + tree.Set([]byte("key2"), []byte("val1")) + tree.Set([]byte("key3"), []byte("val1")) + tree.SaveVersion() + + tree.Set([]byte("key0"), []byte("val2")) + tree.Remove([]byte("key1")) + tree.Set([]byte("key2"), []byte("val2")) + tree.SaveVersion() + + tree.DeleteVersionsTo(2) + + val, err := tree.Get([]byte("key0")) + require.NoError(t, err) + require.Equal(t, val, []byte("val2")) + + val, err = tree.Get([]byte("key1")) + require.NoError(t, err) + require.Nil(t, val) + + val, err = tree.Get([]byte("key2")) + require.NoError(t, err) + require.Equal(t, val, []byte("val2")) + + val, err = tree.Get([]byte("key3")) + require.NoError(t, err) + require.Equal(t, val, []byte("val1")) + + tree.DeleteVersionsTo(1) + + leafNodes, err := tree.ndb.leafNodes() + require.Nil(t, err) + require.Len(t, leafNodes, 3) +} + +func TestLegacyVersionedTreeOrphanDeleting(t *testing.T) { + tree := getLegacyTestTree(0) + + tree.Set([]byte("key0"), []byte("val0")) + tree.Set([]byte("key1"), []byte("val0")) + tree.Set([]byte("key2"), []byte("val0")) + tree.SaveVersion() + + tree.Set([]byte("key1"), []byte("val1")) + tree.Set([]byte("key2"), []byte("val1")) + tree.Set([]byte("key3"), []byte("val1")) + tree.SaveVersion() + + tree.Set([]byte("key0"), []byte("val2")) + tree.Remove([]byte("key1")) + tree.Set([]byte("key2"), []byte("val2")) + tree.SaveVersion() + + tree.DeleteVersionsTo(2) + + val, err := tree.Get([]byte("key0")) + require.NoError(t, err) + require.Equal(t, val, []byte("val2")) + + val, err = tree.Get([]byte("key1")) + require.NoError(t, err) + require.Nil(t, val) + + val, err = tree.Get([]byte("key2")) + require.NoError(t, err) + require.Equal(t, val, []byte("val2")) + + val, err = tree.Get([]byte("key3")) + require.NoError(t, err) + require.Equal(t, val, []byte("val1")) + + tree.DeleteVersionsTo(1) + + leafNodes, err := tree.ndb.leafNodes() + require.Nil(t, err) + require.Len(t, leafNodes, 3) +} + +func TestVersionedTreeSpecialCase(t *testing.T) { + require := require.New(t) + d, closeDB := getTestDB() + defer closeDB() + + tree := NewMutableTree(d, 0, false, log.NewNopLogger()) + + tree.Set([]byte("key1"), []byte("val0")) + tree.Set([]byte("key2"), []byte("val0")) + tree.SaveVersion() + + tree.Set([]byte("key1"), []byte("val1")) + tree.Set([]byte("key2"), []byte("val1")) + tree.SaveVersion() + + tree.Set([]byte("key2"), []byte("val2")) + tree.SaveVersion() + + tree.DeleteVersionsTo(2) + + val, err := tree.GetVersioned([]byte("key2"), 1) + require.NoError(err) + require.Nil(val) +} + +func TestLegacyVersionedTreeSpecialCase(t *testing.T) { + require := require.New(t) + d, closeDB := getTestDB() + defer closeDB() + + tree := NewLegacyMutableTree(d, 0, false, false, nil, log.NewNopLogger()) + tree.Set([]byte("key1"), []byte("val0")) tree.Set([]byte("key2"), []byte("val0")) tree.SaveVersion() tree.Set([]byte("key1"), []byte("val1")) tree.Set([]byte("key2"), []byte("val1")) - tree.Set([]byte("key3"), []byte("val1")) tree.SaveVersion() - tree.Set([]byte("key0"), []byte("val2")) - tree.Remove([]byte("key1")) tree.Set([]byte("key2"), []byte("val2")) tree.SaveVersion() tree.DeleteVersionsTo(2) - val, err := tree.Get([]byte("key0")) - require.NoError(t, err) - require.Equal(t, val, []byte("val2")) - - val, err = tree.Get([]byte("key1")) - require.NoError(t, err) - require.Nil(t, val) - - val, err = tree.Get([]byte("key2")) - require.NoError(t, err) - require.Equal(t, val, []byte("val2")) - - val, err = tree.Get([]byte("key3")) - require.NoError(t, err) - require.Equal(t, val, []byte("val1")) - - tree.DeleteVersionsTo(1) - - leafNodes, err := tree.ndb.leafNodes() - require.Nil(t, err) - require.Len(t, leafNodes, 3) + val, err := tree.GetVersioned([]byte("key2"), 1) + require.NoError(err) + require.Nil(val) } -func TestVersionedTreeSpecialCase(t *testing.T) { +func TestVersionedTreeSpecialCase2(t *testing.T) { require := require.New(t) - d, closeDB := getTestDB() - defer closeDB() - tree := NewMutableTree(d, 0, false, log.NewNopLogger()) + d := dbm.NewMemDB() + tree := NewMutableTree(d, 100, false, log.NewNopLogger()) tree.Set([]byte("key1"), []byte("val0")) tree.Set([]byte("key2"), []byte("val0")) @@ -698,18 +1379,22 @@ func TestVersionedTreeSpecialCase(t *testing.T) { tree.Set([]byte("key2"), []byte("val2")) tree.SaveVersion() - tree.DeleteVersionsTo(2) + tree = NewMutableTree(d, 100, false, log.NewNopLogger()) + _, err := tree.Load() + require.NoError(err) + + require.NoError(tree.DeleteVersionsTo(2)) val, err := tree.GetVersioned([]byte("key2"), 1) require.NoError(err) require.Nil(val) } -func TestVersionedTreeSpecialCase2(t *testing.T) { +func TestLegacyVersionedTreeSpecialCase2(t *testing.T) { require := require.New(t) d := dbm.NewMemDB() - tree := NewMutableTree(d, 100, false, log.NewNopLogger()) + tree := NewLegacyMutableTree(d, 100, false, false, nil, log.NewNopLogger()) tree.Set([]byte("key1"), []byte("val0")) tree.Set([]byte("key2"), []byte("val0")) @@ -722,7 +1407,7 @@ func TestVersionedTreeSpecialCase2(t *testing.T) { tree.Set([]byte("key2"), []byte("val2")) tree.SaveVersion() - tree = NewMutableTree(d, 100, false, log.NewNopLogger()) + tree = NewLegacyMutableTree(d, 100, false, false, nil, log.NewNopLogger()) _, err := tree.Load() require.NoError(err) @@ -763,6 +1448,36 @@ func TestVersionedTreeSpecialCase3(t *testing.T) { require.Equal(tree.nodeSize(), len(nodes)) } +func TestLegacyVersionedTreeSpecialCase3(t *testing.T) { + require := require.New(t) + tree := getLegacyTestTree(0) + + tree.Set([]byte("m"), []byte("liWT0U6G")) + tree.Set([]byte("G"), []byte("7PxRXwUA")) + tree.SaveVersion() + + tree.Set([]byte("7"), []byte("XRLXgf8C")) + tree.SaveVersion() + + tree.Set([]byte("r"), []byte("bBEmIXBU")) + tree.SaveVersion() + + tree.Set([]byte("i"), []byte("kkIS35te")) + tree.SaveVersion() + + tree.Set([]byte("k"), []byte("CpEnpzKJ")) + tree.SaveVersion() + + tree.DeleteVersionsTo(1) + tree.DeleteVersionsTo(2) + tree.DeleteVersionsTo(3) + tree.DeleteVersionsTo(4) + + nodes, err := tree.ndb.nodes() + require.NoError(err) + require.Equal(tree.nodeSize(), len(nodes)) +} + func TestVersionedTreeSaveAndLoad(t *testing.T) { require := require.New(t) d := dbm.NewMemDB() @@ -811,6 +1526,54 @@ func TestVersionedTreeSaveAndLoad(t *testing.T) { require.Len(nodes, ntree.nodeSize()) } +func TestLegacyVersionedTreeSaveAndLoad(t *testing.T) { + require := require.New(t) + d := dbm.NewMemDB() + tree := NewLegacyMutableTree(d, 0, false, false, nil, log.NewNopLogger()) + + // Loading with an empty root is a no-op. + tree.Load() + + tree.Set([]byte("C"), []byte("so43QQFN")) + tree.SaveVersion() + + tree.Set([]byte("A"), []byte("ut7sTTAO")) + tree.SaveVersion() + + tree.Set([]byte("X"), []byte("AoWWC1kN")) + tree.SaveVersion() + + tree.SaveVersion() + tree.SaveVersion() + tree.SaveVersion() + + preHash := tree.Hash() + require.NotNil(preHash) + + require.Equal(int64(6), tree.Version()) + + // Reload the tree, to test that roots and orphans are properly loaded. + ntree := NewLegacyMutableTree(d, 0, false, false, nil, log.NewNopLogger()) + ntree.Load() + + require.False(ntree.IsEmpty()) + require.Equal(int64(6), ntree.Version()) + + postHash := ntree.Hash() + require.Equal(preHash, postHash) + + ntree.Set([]byte("T"), []byte("MhkWjkVy")) + ntree.SaveVersion() + + ntree.DeleteVersionsTo(6) + + require.False(ntree.IsEmpty()) + require.Equal(int64(4), ntree.Size()) + nodes, err := tree.ndb.nodes() + require.NoError(err) + require.Len(nodes, ntree.nodeSize()) +} + func TestVersionedTreeErrors(t *testing.T) { require := require.New(t) tree := getTestTree(100) @@ -839,6 +1602,34 @@ func TestVersionedTreeErrors(t *testing.T) { require.Error(err) } +func TestLegacyVersionedTreeErrors(t *testing.T) { + require := require.New(t) + tree := getLegacyTestTree(100) + + // Can't delete non-existent versions. + require.Error(tree.DeleteVersionsTo(1)) + require.Error(tree.DeleteVersionsTo(99)) + + tree.Set([]byte("key"), []byte("val")) + + // Saving with content is ok. + _, _, err := tree.SaveVersion() + require.NoError(err) + + // Can't delete current version. + require.Error(tree.DeleteVersionsTo(1)) + + // Trying to get a key from a version which doesn't exist. + val, err := tree.GetVersioned([]byte("key"), 404) + require.NoError(err) + require.Nil(val) + + // Same thing with proof. We get an error because a proof couldn't be + // constructed. + _, err = tree.GetVersionedProof([]byte("key"), 404) + require.Error(err) +} + func TestVersionedCheckpointsSpecialCase(t *testing.T) { require := require.New(t) tree := getTestTree(0) @@ -866,6 +1657,33 @@ func TestVersionedCheckpointsSpecialCase(t *testing.T) { require.Equal([]byte("val1"), val) } +func TestLegacyVersionedCheckpointsSpecialCase(t *testing.T) { + require := require.New(t) + tree := getLegacyTestTree(0) + key := []byte("k") + + tree.Set(key, []byte("val1")) + + tree.SaveVersion() + // ... + tree.SaveVersion() + // ... + tree.SaveVersion() + // ... + // This orphans "k" at version 1. + tree.Set(key, []byte("val2")) + tree.SaveVersion() + + // When version 1 is deleted, the orphans should move to the next + // checkpoint, which is version 10. + tree.DeleteVersionsTo(1) + + val, err := tree.GetVersioned(key, 2) + require.Nil(err) + require.NotEmpty(val) + require.Equal([]byte("val1"), val) +} + func TestVersionedCheckpointsSpecialCase2(_ *testing.T) { tree := getTestTree(0) @@ -886,6 +1704,26 @@ func TestVersionedCheckpointsSpecialCase2(_ *testing.T) { tree.DeleteVersionsTo(2) } +func TestLegacyVersionedCheckpointsSpecialCase2(_ *testing.T) { + tree := getLegacyTestTree(0) + + tree.Set([]byte("U"), []byte("XamDUtiJ")) + tree.Set([]byte("A"), []byte("UkZBuYIU")) + tree.Set([]byte("H"), []byte("7a9En4uw")) + tree.Set([]byte("V"), []byte("5HXU3pSI")) + tree.SaveVersion() + + tree.Set([]byte("U"), []byte("Replaced")) + tree.Set([]byte("A"), []byte("Replaced")) + tree.SaveVersion() + + tree.Set([]byte("X"), []byte("New")) + tree.SaveVersion() + + tree.DeleteVersionsTo(1) + tree.DeleteVersionsTo(2) +} + func TestVersionedCheckpointsSpecialCase3(_ *testing.T) { tree := getTestTree(0) @@ -906,6 +1744,26 @@ func TestVersionedCheckpointsSpecialCase3(_ *testing.T) { tree.GetVersioned([]byte("m"), 1) } +func TestLegacyVersionedCheckpointsSpecialCase3(_ *testing.T) { + tree := getLegacyTestTree(0) + + tree.Set([]byte("n"), []byte("2wUCUs8q")) + tree.Set([]byte("l"), []byte("WQ7mvMbc")) + tree.SaveVersion() + + tree.Set([]byte("N"), []byte("ved29IqU")) + tree.Set([]byte("v"), []byte("01jquVXU")) + tree.SaveVersion() + + tree.Set([]byte("l"), []byte("bhIpltPM")) + tree.Set([]byte("B"), []byte("rj97IKZh")) + tree.SaveVersion() + + tree.DeleteVersionsTo(2) + + tree.GetVersioned([]byte("m"), 1) +} + func TestVersionedCheckpointsSpecialCase4(t *testing.T) { tree := NewMutableTree(dbm.NewMemDB(), 0, false, log.NewNopLogger()) @@ -942,8 +1800,61 @@ func TestVersionedCheckpointsSpecialCase4(t *testing.T) { require.Nil(t, val) } -func TestVersionedCheckpointsSpecialCase5(_ *testing.T) { - tree := getTestTree(0) +func TestLegacyVersionedCheckpointsSpecialCase4(t *testing.T) { + tree := NewLegacyMutableTree(dbm.NewMemDB(), 0, false, false, nil, log.NewNopLogger()) + + tree.Set([]byte("U"), []byte("XamDUtiJ")) + tree.Set([]byte("A"), []byte("UkZBuYIU")) + tree.Set([]byte("H"), []byte("7a9En4uw")) + tree.Set([]byte("V"), []byte("5HXU3pSI")) + tree.SaveVersion() + + tree.Remove([]byte("U")) + tree.Remove([]byte("A")) + tree.SaveVersion() + + tree.Set([]byte("X"), []byte("New")) + tree.SaveVersion() + + val, err := tree.GetVersioned([]byte("A"), 2) + require.NoError(t, err) + require.Nil(t, val) + + val, err = tree.GetVersioned([]byte("A"), 1) + require.NoError(t, err) + require.NotEmpty(t, val) + + tree.DeleteVersionsTo(1) + tree.DeleteVersionsTo(2) + + val, err = tree.GetVersioned([]byte("A"), 2) + require.NoError(t, err) + require.Nil(t, val) + + val, err = tree.GetVersioned([]byte("A"), 1) + require.NoError(t, err) + require.Nil(t, val) +} + +func TestVersionedCheckpointsSpecialCase5(_ *testing.T) { + tree := getTestTree(0) + + tree.Set([]byte("R"), []byte("ygZlIzeW")) + tree.SaveVersion() + + tree.Set([]byte("j"), []byte("ZgmCWyo2")) + tree.SaveVersion() + + tree.Set([]byte("R"), []byte("vQDaoz6Z")) + tree.SaveVersion() + + tree.DeleteVersionsTo(1) + + tree.GetVersioned([]byte("R"), 2) +} + +func TestLegacyVersionedCheckpointsSpecialCase5(_ *testing.T) { + tree := getLegacyTestTree(0) tree.Set([]byte("R"), []byte("ygZlIzeW")) tree.SaveVersion() @@ -991,6 +1902,38 @@ func TestVersionedCheckpointsSpecialCase6(_ *testing.T) { tree.GetVersioned([]byte("4"), 1) } +func TestLegacyVersionedCheckpointsSpecialCase6(_ *testing.T) { + tree := getLegacyTestTree(0) + + tree.Set([]byte("Y"), []byte("MW79JQeV")) + tree.Set([]byte("7"), []byte("Kp0ToUJB")) + tree.Set([]byte("Z"), []byte("I26B1jPG")) + tree.Set([]byte("6"), []byte("ZG0iXq3h")) + tree.Set([]byte("2"), []byte("WOR27LdW")) + tree.Set([]byte("4"), []byte("MKMvc6cn")) + tree.SaveVersion() + + tree.Set([]byte("1"), []byte("208dOu40")) + tree.Set([]byte("G"), []byte("7isI9OQH")) + tree.Set([]byte("8"), []byte("zMC1YwpH")) + tree.SaveVersion() + + tree.Set([]byte("7"), []byte("bn62vWbq")) + tree.Set([]byte("5"), []byte("wZuLGDkZ")) + tree.SaveVersion() + + tree.DeleteVersionsTo(1) + tree.DeleteVersionsTo(2) + + tree.GetVersioned([]byte("Y"), 1) + tree.GetVersioned([]byte("7"), 1) + tree.GetVersioned([]byte("Z"), 1) + tree.GetVersioned([]byte("6"), 1) + tree.GetVersioned([]byte("s"), 1) + tree.GetVersioned([]byte("2"), 1) + tree.GetVersioned([]byte("4"), 1) +} + func TestVersionedCheckpointsSpecialCase7(_ *testing.T) { tree := getTestTree(100) @@ -1024,6 +1967,39 @@ func TestVersionedCheckpointsSpecialCase7(_ *testing.T) { tree.GetVersioned([]byte("A"), 3) } +func TestLegacyVersionedCheckpointsSpecialCase7(_ *testing.T) { + tree := getLegacyTestTree(100) + + tree.Set([]byte("n"), []byte("OtqD3nyn")) + tree.Set([]byte("W"), []byte("kMdhJjF5")) + tree.Set([]byte("A"), []byte("BM3BnrIb")) + tree.Set([]byte("I"), []byte("QvtCH970")) + tree.Set([]byte("L"), []byte("txKgOTqD")) + tree.Set([]byte("Y"), []byte("NAl7PC5L")) + tree.SaveVersion() + + tree.Set([]byte("7"), []byte("qWcEAlyX")) + tree.SaveVersion() + + tree.Set([]byte("M"), []byte("HdQwzA64")) + tree.Set([]byte("3"), []byte("2Naa77fo")) + tree.Set([]byte("A"), []byte("SRuwKOTm")) + tree.Set([]byte("I"), []byte("oMX4aAOy")) + tree.Set([]byte("4"), []byte("dKfvbEOc")) + tree.SaveVersion() + + tree.Set([]byte("D"), []byte("3U4QbXCC")) + tree.Set([]byte("B"), []byte("FxExhiDq")) + tree.SaveVersion() + + tree.Set([]byte("A"), []byte("tWQgbFCY")) + tree.SaveVersion() + + tree.DeleteVersionsTo(4) + + tree.GetVersioned([]byte("A"), 3) +} + func TestVersionedTreeEfficiency(t *testing.T) { require := require.New(t) tree := NewMutableTree(dbm.NewMemDB(), 0, false, log.NewNopLogger()) @@ -1071,6 +2047,53 @@ func TestVersionedTreeEfficiency(t *testing.T) { require.Equal(keysAdded-tree.nodeSize(), keysDeleted) } +func TestLegacyVersionedTreeEfficiency(t *testing.T) { + require := require.New(t) + tree := NewLegacyMutableTree(dbm.NewMemDB(), 0, false, false, nil, log.NewNopLogger()) + versions := 20 + keysPerVersion := 100 + keysAddedPerVersion := map[int]int{} + + keysAdded := 0 + for i := 1; i <= versions; i++ { + for j := 0; j < keysPerVersion; j++ { + // Keys of size one are likely to be overwritten. + tree.Set([]byte(iavlrand.RandStr(1)), []byte(iavlrand.RandStr(8))) + } + nodes, err := tree.ndb.nodes() + require.NoError(err) + sizeBefore := len(nodes) + tree.SaveVersion() + _, err = tree.ndb.nodes() + require.NoError(err) + nodes, err = tree.ndb.nodes() + require.NoError(err) + sizeAfter := len(nodes) + change := sizeAfter - sizeBefore + keysAddedPerVersion[i] = change + keysAdded += change + } + + keysDeleted := 0 + for i := 1; i < versions; i++ { + if tree.VersionExists(int64(i)) { + nodes, err := tree.ndb.nodes() + require.NoError(err) + sizeBefore := len(nodes) + tree.DeleteVersionsTo(int64(i)) + nodes, err = tree.ndb.nodes() + require.NoError(err) + sizeAfter := len(nodes) + + change := sizeBefore - sizeAfter + keysDeleted += change + + require.InDelta(change, keysAddedPerVersion[i], float64(keysPerVersion)/5) + } + } + require.Equal(keysAdded-tree.nodeSize(), keysDeleted) +} + func TestVersionedTreeProofs(t *testing.T) { require := require.New(t) tree := getTestTree(0) @@ -1149,6 +2172,84 @@ func TestVersionedTreeProofs(t *testing.T) { require.True(res) } +func TestLegacyVersionedTreeProofs(t *testing.T) { + require := require.New(t) + tree := getLegacyTestTree(0) + + tree.Set([]byte("k1"), []byte("v1")) + tree.Set([]byte("k2"), []byte("v1")) + tree.Set([]byte("k3"), []byte("v1")) + _, _, err := tree.SaveVersion() + require.NoError(err) + + // fmt.Println("TREE VERSION 1") + // printNode(tree.ndb, tree.root, 0) + // fmt.Println("TREE VERSION 1 END") + + root1 := tree.Hash() + + tree.Set([]byte("k2"), []byte("v2")) + tree.Set([]byte("k4"), []byte("v2")) + _, _, err = tree.SaveVersion() + require.NoError(err) + + // fmt.Println("TREE VERSION 2") + // printNode(tree.ndb, tree.root, 0) + // fmt.Println("TREE VERSION END") + + root2 := tree.Hash() + require.NotEqual(root1, root2) + + tree.Remove([]byte("k2")) + _, _, err = tree.SaveVersion() + require.NoError(err) + + root3 := tree.Hash() + require.NotEqual(root2, root3) + + iTree, err := tree.GetImmutable(1) + require.NoError(err) + + proof, err := tree.GetVersionedProof([]byte("k2"), 1) + require.NoError(err) + require.EqualValues(proof.GetExist().Value, []byte("v1")) + res, err := iTree.VerifyProof(proof, []byte("k2")) + require.NoError(err) + require.True(res) + + proof, err = tree.GetVersionedProof([]byte("k4"), 1) + require.NoError(err) + require.EqualValues(proof.GetNonexist().Key, []byte("k4")) + res, err = iTree.VerifyProof(proof, []byte("k4")) + require.NoError(err) + require.True(res) + + iTree, err = tree.GetImmutable(2) + require.NoError(err) + proof, err = tree.GetVersionedProof([]byte("k2"), 2) + require.NoError(err) + require.EqualValues(proof.GetExist().Value, []byte("v2")) + res, err = iTree.VerifyProof(proof, []byte("k2")) + require.NoError(err) + require.True(res) + + proof, err = tree.GetVersionedProof([]byte("k1"), 2) + require.NoError(err) + require.EqualValues(proof.GetExist().Value, []byte("v1")) + res, err = iTree.VerifyProof(proof, []byte("k1")) + require.NoError(err) + require.True(res) + + iTree, err = tree.GetImmutable(3) + require.NoError(err) + proof, err = tree.GetVersionedProof([]byte("k2"), 3) + require.NoError(err) + require.EqualValues(proof.GetNonexist().Key, []byte("k2")) + res, err = iTree.VerifyProof(proof, []byte("k2")) + require.NoError(err) + require.True(res) +} + func TestOrphans(t *testing.T) { // If you create a sequence of saved versions // Then randomly delete versions other than the first and last until only those two remain @@ -1173,6 +2274,30 @@ func TestOrphans(t *testing.T) { } } +func TestLegacyOrphans(t *testing.T) { + // If you create a sequence of saved versions + // Then randomly delete versions other than the first and last until only those two remain + // Any remaining orphan nodes should either have fromVersion == firstVersion || toVersion == lastVersion + require := require.New(t) + tree := NewLegacyMutableTree(dbm.NewMemDB(), 100, false, false, nil, log.NewNopLogger()) + + NUMVERSIONS := 100 + NUMUPDATES := 100 + + for i := 0; i < NUMVERSIONS; i++ { + for j := 1; j < NUMUPDATES; j++ { + tree.Set(iavlrand.RandBytes(2), iavlrand.RandBytes(2)) + } + _, _, err := tree.SaveVersion() + require.NoError(err, "SaveVersion should not error") + } + + for v := 1; v < NUMVERSIONS; v++ { + err := tree.DeleteVersionsTo(int64(v)) + require.NoError(err, "DeleteVersion should not error") + } +} + func TestVersionedTreeHash(t *testing.T) { require := require.New(t) tree := getTestTree(0) @@ -1203,18 +2328,75 @@ func TestVersionedTreeHash(t *testing.T) { require.True(res) } +func TestLegacyVersionedTreeHash(t *testing.T) { + require := require.New(t) + tree := getLegacyTestTree(0) + + hash := tree.Hash() + require.Equal("e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", hex.EncodeToString(hash)) + tree.Set([]byte("I"), []byte("D")) + hash = tree.Hash() + require.Equal("e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", hex.EncodeToString(hash)) + + hash1, _, err := tree.SaveVersion() + require.NoError(err) + + tree.Set([]byte("I"), []byte("F")) + hash = tree.Hash() + require.EqualValues(hash1, hash) + + _, _, err = tree.SaveVersion() + require.NoError(err) + + proof, err := tree.GetVersionedProof([]byte("I"), 2) + require.NoError(err) + require.EqualValues([]byte("F"), proof.GetExist().Value) + iTree, err := tree.GetImmutable(2) + require.NoError(err) + res, err := iTree.VerifyProof(proof, []byte("I")) + require.NoError(err) + require.True(res) +} + func TestNilValueSemantics(t *testing.T) { require := require.New(t) tree := getTestTree(0) - _, err := tree.Set([]byte("k"), nil) - require.Error(err) + _, err := tree.Set([]byte("k"), nil) + require.Error(err) +} + +func TestLegacyNilValueSemantics(t *testing.T) { + require := require.New(t) + tree := getLegacyTestTree(0) + + _, err := tree.Set([]byte("k"), nil) + require.Error(err) +} + +func TestCopyValueSemantics(t *testing.T) { + require := require.New(t) + + tree := getTestTree(0) + + val := []byte("v1") + + tree.Set([]byte("k"), val) + v, err := tree.Get([]byte("k")) + require.NoError(err) + require.Equal([]byte("v1"), v) + + val[1] = '2' + + val, err = tree.Get([]byte("k")) + require.NoError(err) + require.Equal([]byte("v2"), val) } -func TestCopyValueSemantics(t *testing.T) { +func TestLegacyCopyValueSemantics(t *testing.T) { require := require.New(t) - tree := getTestTree(0) + tree := getLegacyTestTree(0) val := []byte("v1") @@ -1262,6 +2444,38 @@ func TestRollback(t *testing.T) { require.Equal([]byte("v"), val) } +func TestLegacyRollback(t *testing.T) { + require := require.New(t) + + tree := getLegacyTestTree(0) + + tree.Set([]byte("k"), []byte("v")) + tree.SaveVersion() + + tree.Set([]byte("r"), []byte("v")) + tree.Set([]byte("s"), []byte("v")) + + tree.Rollback() + + tree.Set([]byte("t"), []byte("v")) + + tree.SaveVersion() + + require.Equal(int64(2), tree.Size()) + + val, err := tree.Get([]byte("r")) + require.NoError(err) + require.Nil(val) + + val, err = tree.Get([]byte("s")) + require.NoError(err) + require.Nil(val) + + val, err = tree.Get([]byte("t")) + require.NoError(err) + require.Equal([]byte("v"), val) +} + func TestLoadVersion(t *testing.T) { tree := getTestTree(0) maxVersions := 10 @@ -1301,6 +2515,45 @@ func TestLoadVersion(t *testing.T) { require.Equal(t, version, int64(maxVersions)) } +func TestLegacyLoadVersion(t *testing.T) { + tree := getLegacyTestTree(0) + maxVersions := 10 + + version, err := tree.LoadVersion(0) + require.NoError(t, err, "unexpected error") + require.Equal(t, version, int64(0), "expected latest version to be zero") + + for i := 0; i < maxVersions; i++ { + tree.Set([]byte(fmt.Sprintf("key_%d", i+1)), []byte(fmt.Sprintf("value_%d", i+1))) + + _, _, err = tree.SaveVersion() + require.NoError(t, err, "SaveVersion should not fail") + } + + // require the ability to load the latest version + version, err = tree.LoadVersion(int64(maxVersions)) + require.NoError(t, err, "unexpected error when lazy loading version") + require.Equal(t, version, int64(maxVersions)) + + value, err := tree.Get([]byte(fmt.Sprintf("key_%d", maxVersions))) + require.NoError(t, err) + require.Equal(t, value, []byte(fmt.Sprintf("value_%d", maxVersions)), "unexpected value") + + // require the ability to load an older version + version, err = tree.LoadVersion(int64(maxVersions - 1)) + require.NoError(t, err, "unexpected error when loading version") + require.Equal(t, version, int64(maxVersions)) + + value, err = tree.Get([]byte(fmt.Sprintf("key_%d", maxVersions-1))) + require.NoError(t, err) + require.Equal(t, value, []byte(fmt.Sprintf("value_%d", maxVersions-1)), "unexpected value") + + // require the inability to load a non-valid version + version, err = tree.LoadVersion(int64(maxVersions + 1)) + require.Error(t, err, "expected error when loading version") + require.Equal(t, version, int64(maxVersions)) +} + func TestOverwrite(t *testing.T) { require := require.New(t) @@ -1333,6 +2586,38 @@ func TestOverwrite(t *testing.T) { require.NoError(err, "SaveVersion should not fail, overwrite was idempotent") } +func TestLegacyOverwrite(t *testing.T) { + require := require.New(t) + + mdb := dbm.NewMemDB() + tree := NewLegacyMutableTree(mdb, 0, false, false, nil, log.NewNopLogger()) + + // Set one kv pair and save version 1 + tree.Set([]byte("key1"), []byte("value1")) + _, _, err := tree.SaveVersion() + require.NoError(err, "SaveVersion should not fail") + + // Set another kv pair and save version 2 + tree.Set([]byte("key2"), []byte("value2")) + _, _, err = tree.SaveVersion() + require.NoError(err, "SaveVersion should not fail") + + // Reload tree at version 1 + tree = NewLegacyMutableTree(mdb, 0, false, false, nil, log.NewNopLogger()) + _, err = tree.LoadVersion(int64(1)) + require.NoError(err, "LoadVersion should not fail") + + // Attempt to put a different kv pair into the tree and save + tree.Set([]byte("key2"), []byte("different value 2")) + _, _, err = tree.SaveVersion() + require.Error(err, "SaveVersion should fail because of changed value") + + // Replay the original transition from version 1 to version 2 and attempt to save + tree.Set([]byte("key2"), []byte("value2")) + _, _, err = tree.SaveVersion() + require.NoError(err, "SaveVersion should not fail, overwrite was idempotent") +} + func TestOverwriteEmpty(t *testing.T) { require := require.New(t) @@ -1367,6 +2652,40 @@ func TestOverwriteEmpty(t *testing.T) { require.EqualValues(2, version) } +func TestLegacyOverwriteEmpty(t *testing.T) { + require := require.New(t) + + mdb := dbm.NewMemDB() + tree := NewLegacyMutableTree(mdb, 0, false, false, nil, log.NewNopLogger()) + + // Save empty version 1 + _, _, err := tree.SaveVersion() + require.NoError(err) + + // Save empty version 2 + _, _, err = tree.SaveVersion() + require.NoError(err) + + // Save a key in version 3 + tree.Set([]byte("key"), []byte("value")) + _, _, err = tree.SaveVersion() + require.NoError(err) + + // Load version 1 and attempt to save a different key + _, err = tree.LoadVersion(1) + require.NoError(err) + tree.Set([]byte("foo"), []byte("bar")) + _, _, err = tree.SaveVersion() + require.Error(err) + + // However, deleting the key and saving an empty version should work, + // since it's the same as the existing version. + tree.Remove([]byte("foo")) + _, version, err := tree.SaveVersion() + require.NoError(err) + require.EqualValues(2, version) +} + func TestLoadVersionForOverwriting(t *testing.T) { require := require.New(t) @@ -1431,6 +2750,70 @@ func TestLoadVersionForOverwriting(t *testing.T) { require.NoError(err, "SaveVersion should not fail.") } +func TestLegacyLoadVersionForOverwriting(t *testing.T) { + require := require.New(t) + + mdb := dbm.NewMemDB() + tree := NewLegacyMutableTree(mdb, 0, false, false, nil, log.NewNopLogger()) + + maxLength := 100 + for count := 1; count <= maxLength; count++ { + countStr := strconv.Itoa(count) + // Set one kv pair and save version + tree.Set([]byte("key"+countStr), []byte("value"+countStr)) + _, _, err := tree.SaveVersion() + require.NoError(err, "SaveVersion should not fail") + } + + tree = NewLegacyMutableTree(mdb, 0, false, false, nil, log.NewNopLogger()) + require.Error(tree.LoadVersionForOverwriting(int64(maxLength * 2))) + + tree = NewLegacyMutableTree(mdb, 0, false, false, nil, log.NewNopLogger()) + err := tree.LoadVersionForOverwriting(int64(maxLength / 2)) + require.NoError(err, "LoadVersion should not fail") + + for version := 1; version <= maxLength/2; version++ { + exist := tree.VersionExists(int64(version)) + require.True(exist, "versions no more than 50 should exist") + } + + for version := (maxLength / 2) + 1; version <= maxLength; version++ { + exist := tree.VersionExists(int64(version)) + require.False(exist, "versions more than 50 should have been deleted") + } + + tree.Set([]byte("key49"), []byte("value49 different")) + _, _, err = tree.SaveVersion() + require.NoError(err, "SaveVersion should not fail, overwrite was allowed") + + tree.Set([]byte("key50"), []byte("value50 different")) + _, _, err = tree.SaveVersion() + require.NoError(err, "SaveVersion should not fail, overwrite was allowed") + + // Reload tree at version 50, the latest tree version is 52 + tree = NewLegacyMutableTree(mdb, 0, false, false, nil, log.NewNopLogger()) + _, err = tree.LoadVersion(int64(maxLength / 2)) + require.NoError(err, "LoadVersion should not fail") + + tree.Set([]byte("key49"), []byte("value49 different")) + _, _, err = tree.SaveVersion() + require.NoError(err, "SaveVersion should not fail, write the same value") + + tree.Set([]byte("key50"), []byte("value50 different different")) + _, _, err = tree.SaveVersion() + require.Error(err, "SaveVersion should fail, overwrite was not allowed") + + tree.Set([]byte("key50"), []byte("value50 different")) + _, _, err = tree.SaveVersion() + require.NoError(err, "SaveVersion should not fail, write the same value") + + // The tree version now is 52 which is equal to latest version. + // Now any key value can be written into the tree + tree.Set([]byte("key any value"), []byte("value any value")) + _, _, err = tree.SaveVersion() + require.NoError(err, "SaveVersion should not fail.") +} + // BENCHMARKS func BenchmarkTreeLoadAndDelete(b *testing.B) { @@ -1538,6 +2921,68 @@ func TestLoadVersionForOverwritingCase2(t *testing.T) { require.NoError(err, "SaveVersion should not fail") } +func TestLegacyLoadVersionForOverwritingCase2(t *testing.T) { + require := require.New(t) + + tree := NewLegacyMutableTree(dbm.NewMemDB(), 0, false, false, nil, log.NewNopLogger()) + + for i := byte(0); i < 20; i++ { + tree.Set([]byte{i}, []byte{i}) + } + + _, _, err := tree.SaveVersion() + require.NoError(err, "SaveVersion should not fail") + + for i := byte(0); i < 20; i++ { + tree.Set([]byte{i}, []byte{i + 1}) + } + + _, _, err = tree.SaveVersion() + require.NoError(err, "SaveVersion should not fail with the same key") + + for i := byte(0); i < 20; i++ { + tree.Set([]byte{i}, []byte{i + 2}) + } + tree.SaveVersion() + + removedNodes := []*Node{} + + nodes, err := tree.ndb.nodes() + require.NoError(err) + for _, n := range nodes { + if n.nodeKey.version > 1 { + removedNodes = append(removedNodes, n) + } + } + + err = tree.LoadVersionForOverwriting(1) + require.NoError(err, "LoadVersionForOverwriting should not fail") + + for i := byte(0); i < 20; i++ { + v, err := tree.Get([]byte{i}) + require.NoError(err) + require.Equal([]byte{i}, v) + } + + for _, n := range removedNodes { + has, _ := tree.ndb.Has(n.GetKey()) + require.False(has, "LoadVersionForOverwriting should remove useless nodes") + } + + tree.Set([]byte{0x2}, []byte{0x3}) + + _, _, err = tree.SaveVersion() + require.NoError(err, "SaveVersion should not fail") + + err = tree.DeleteVersionsTo(1) + require.NoError(err, "DeleteVersion should not fail") + + tree.Set([]byte{0x1}, []byte{0x3}) + + _, _, err = tree.SaveVersion() + require.NoError(err, "SaveVersion should not fail") +} + func TestLoadVersionForOverwritingCase3(t *testing.T) { require := require.New(t) @@ -1586,6 +3031,54 @@ func TestLoadVersionForOverwritingCase3(t *testing.T) { } } +func TestLegacyLoadVersionForOverwritingCase3(t *testing.T) { + require := require.New(t) + + tree := NewLegacyMutableTree(dbm.NewMemDB(), 0, false, false, nil, log.NewNopLogger()) + + for i := byte(0); i < 20; i++ { + tree.Set([]byte{i}, []byte{i}) + } + _, _, err := tree.SaveVersion() + require.NoError(err) + + for i := byte(0); i < 20; i++ { + tree.Set([]byte{i}, []byte{i + 1}) + } + _, _, err = tree.SaveVersion() + require.NoError(err) + + removedNodes := []*Node{} + + nodes, err := tree.ndb.nodes() + require.NoError(err) + for _, n := range nodes { + if n.nodeKey.version > 1 { + removedNodes = append(removedNodes, n) + } + } + + for i := byte(0); i < 20; i++ { + tree.Remove([]byte{i}) + } + _, _, err = tree.SaveVersion() + require.NoError(err) + + err = tree.LoadVersionForOverwriting(1) + require.NoError(err) + for _, n := range removedNodes { + has, err := tree.ndb.Has(n.GetKey()) + require.NoError(err) + require.False(has, "LoadVersionForOverwriting should remove useless nodes") + } + + for i := byte(0); i < 20; i++ { + v, err := tree.Get([]byte{i}) + require.NoError(err) + require.Equal([]byte{i}, v) + } +} + func TestIterate_ImmutableTree_Version1(t *testing.T) { tree, mirror := getRandomizedTreeAndMirror(t) @@ -1598,6 +3091,18 @@ func TestIterate_ImmutableTree_Version1(t *testing.T) { assertImmutableMirrorIterate(t, immutableTree, mirror) } +func TestLegacyIterate_ImmutableTree_Version1(t *testing.T) { + tree, mirror := getRandomizedLegacyTreeAndMirror(t) + + _, _, err := tree.SaveVersion() + require.NoError(t, err) + + immutableTree, err := tree.GetImmutable(1) + require.NoError(t, err) + + assertImmutableMirrorIterate(t, immutableTree, mirror) +} + func TestIterate_ImmutableTree_Version2(t *testing.T) { tree, mirror := getRandomizedTreeAndMirror(t) @@ -1615,6 +3120,23 @@ func TestIterate_ImmutableTree_Version2(t *testing.T) { assertImmutableMirrorIterate(t, immutableTree, mirror) } +func TestLegacyIterate_ImmutableTree_Version2(t *testing.T) { + tree, mirror := getRandomizedLegacyTreeAndMirror(t) + + _, _, err := tree.SaveVersion() + require.NoError(t, err) + + randomizeTreeAndMirror(t, tree, mirror) + + _, _, err = tree.SaveVersion() + require.NoError(t, err) + + immutableTree, err := tree.GetImmutable(2) + require.NoError(t, err) + + assertImmutableMirrorIterate(t, immutableTree, mirror) +} + func TestGetByIndex_ImmutableTree(t *testing.T) { tree, mirror := getRandomizedTreeAndMirror(t) mirrorKeys := getSortedMirrorKeys(mirror) @@ -1640,6 +3162,31 @@ func TestGetByIndex_ImmutableTree(t *testing.T) { } } +func TestLegacyGetByIndex_ImmutableTree(t *testing.T) { + tree, mirror := getRandomizedLegacyTreeAndMirror(t) + mirrorKeys := getSortedMirrorKeys(mirror) + + _, _, err := tree.SaveVersion() + require.NoError(t, err) + + immutableTree, err := tree.GetImmutable(1) + require.NoError(t, err) + + isFastCacheEnabled, err := immutableTree.IsFastCacheEnabled() + require.NoError(t, err) + require.True(t, isFastCacheEnabled) + + for index, expectedKey := range mirrorKeys { + expectedValue := mirror[expectedKey] + + actualKey, actualValue, err := immutableTree.GetByIndex(int64(index)) + require.NoError(t, err) + + require.Equal(t, expectedKey, string(actualKey)) + require.Equal(t, expectedValue, string(actualValue)) + } +} + func TestGetWithIndex_ImmutableTree(t *testing.T) { tree, mirror := getRandomizedTreeAndMirror(t) mirrorKeys := getSortedMirrorKeys(mirror) @@ -1665,6 +3212,31 @@ func TestGetWithIndex_ImmutableTree(t *testing.T) { } } +func TestLegacyGetWithIndex_ImmutableTree(t *testing.T) { + tree, mirror := getRandomizedLegacyTreeAndMirror(t) + mirrorKeys := getSortedMirrorKeys(mirror) + + _, _, err := tree.SaveVersion() + require.NoError(t, err) + + immutableTree, err := tree.GetImmutable(1) + require.NoError(t, err) + + isFastCacheEnabled, err := immutableTree.IsFastCacheEnabled() + require.NoError(t, err) + require.True(t, isFastCacheEnabled) + + for expectedIndex, key := range mirrorKeys { + expectedValue := mirror[key] + + actualIndex, actualValue, err := immutableTree.GetWithIndex([]byte(key)) + require.NoError(t, err) + + require.Equal(t, expectedValue, string(actualValue)) + require.Equal(t, int64(expectedIndex), actualIndex) + } +} + func Benchmark_GetWithIndex(b *testing.B) { db := dbm.NewMemDB() @@ -1819,6 +3391,62 @@ func TestNodeCacheStatisic(t *testing.T) { } } +func TestLegacyNodeCacheStatisic(t *testing.T) { + const numKeyVals = 100000 + testcases := map[string]struct { + cacheSize int + expectFastCacheHitCnt int + expectFastCacheMissCnt int + expectCacheHitCnt int + expectCacheMissCnt int + }{ + "with_cache": { + cacheSize: numKeyVals, + expectFastCacheHitCnt: numKeyVals, + expectFastCacheMissCnt: 0, + expectCacheHitCnt: 1, + expectCacheMissCnt: 0, + }, + "without_cache": { + cacheSize: 0, + expectFastCacheHitCnt: 100000, // this value is hardcoded in nodedb for fast cache. + expectFastCacheMissCnt: 0, + expectCacheHitCnt: 0, + expectCacheMissCnt: 1, + }, + } + + for name, tc := range testcases { + tc := tc + t.Run(name, func(_ *testing.T) { + stat := &Statistics{} + db := dbm.NewMemDB() + mt := NewLegacyMutableTree(db, tc.cacheSize, false, false, nil, log.NewNopLogger(), StatOption(stat)) + + for i := 0; i < numKeyVals; i++ { + key := []byte(strconv.Itoa(i)) + _, err := mt.Set(key, iavlrand.RandBytes(10)) + require.NoError(t, err) + } + _, ver, _ := mt.SaveVersion() + it, err := mt.GetImmutable(ver) + require.NoError(t, err) + + for i := 0; i < numKeyVals; i++ { + key := []byte(strconv.Itoa(i)) + val, err := it.Get(key) + require.NoError(t, err) + require.NotNil(t, val) + require.NotEmpty(t, val) + } + require.Equal(t, tc.expectFastCacheHitCnt, int(stat.GetFastCacheHitCnt())) + require.Equal(t, tc.expectFastCacheMissCnt, int(stat.GetFastCacheMissCnt())) + require.Equal(t, tc.expectCacheHitCnt, int(stat.GetCacheHitCnt())) + require.Equal(t, tc.expectCacheMissCnt, int(stat.GetCacheMissCnt())) + }) + } +} + func TestEmptyVersionDelete(t *testing.T) { db := dbm.NewMemDB() defer db.Close() @@ -1849,6 +3477,36 @@ func TestEmptyVersionDelete(t *testing.T) { require.Len(t, versions, 5) } +func TestLegacyEmptyVersionDelete(t *testing.T) { + db := dbm.NewMemDB() + defer db.Close() + + tree := NewLegacyMutableTree(db, 0, false, false, nil, log.NewNopLogger()) + + _, err := tree.Set([]byte("key1"), []byte("value1")) + require.NoError(t, err) + + toVersion := 10 + for i := 0; i < toVersion; i++ { + _, _, err = tree.SaveVersion() + require.NoError(t, err) + } + + require.NoError(t, tree.DeleteVersionsTo(5)) + + // Load the tree from disk. + tree = NewLegacyMutableTree(db, 0, false, false, nil, log.NewNopLogger()) + v, err := tree.Load() + require.NoError(t, err) + require.Equal(t, int64(toVersion), v) + // Version 1 is only meaningful, so it should not be deleted. + require.Equal(t, tree.root.GetKey(), (&NodeKey{version: 1, nonce: 0}).GetKey()) + // it is expected that the version 1 is deleted. + versions := tree.AvailableVersions() + require.Equal(t, 6, versions[0]) + require.Len(t, versions, 5) +} + func TestReferenceRoot(t *testing.T) { db := dbm.NewMemDB() defer db.Close() @@ -1881,6 +3539,38 @@ func TestReferenceRoot(t *testing.T) { require.Equal(t, tree.root.key, []byte("key2")) } +func TestLegacyReferenceRoot(t *testing.T) { + db := dbm.NewMemDB() + defer db.Close() + + tree := NewLegacyMutableTree(db, 0, false, false, nil, log.NewNopLogger()) + + _, err := tree.Set([]byte("key1"), []byte("value1")) + require.NoError(t, err) + + _, err = tree.Set([]byte("key2"), []byte("value2")) + require.NoError(t, err) + + _, _, err = tree.SaveVersion() + require.NoError(t, err) + + _, _, err = tree.Remove([]byte("key1")) + require.NoError(t, err) + + // the root will be the leaf node of key2 + _, _, err = tree.SaveVersion() + require.NoError(t, err) + + // Load the tree from disk. + tree = NewLegacyMutableTree(db, 0, false, false, nil, log.NewNopLogger()) + _, err = tree.Load() + require.NoError(t, err) + require.Equal(t, int64(2), tree.Version()) + // check the root of version 2 is the leaf node of key2 + require.Equal(t, tree.root.GetKey(), (&NodeKey{version: 1, nonce: 3}).GetKey()) + require.Equal(t, tree.root.key, []byte("key2")) +} + func TestWorkingHashWithInitialVersion(t *testing.T) { db := dbm.NewMemDB() defer db.Close() @@ -1912,3 +3602,35 @@ func TestWorkingHashWithInitialVersion(t *testing.T) { require.NoError(t, err) require.Equal(t, commitHash1, commitHash) } + +func TestLegacyWorkingHashWithInitialVersion(t *testing.T) { + db := dbm.NewMemDB() + defer db.Close() + + initialVersion := int64(100) + tree := NewLegacyMutableTree(db, 0, false, false, nil, log.NewNopLogger()) + tree.SetInitialVersion(uint64(initialVersion)) + + v := tree.WorkingVersion() + require.Equal(t, initialVersion, v) + + _, err := tree.Set([]byte("key1"), []byte("value1")) + require.NoError(t, err) + + workingHash := tree.WorkingHash() + commitHash, _, err := tree.SaveVersion() + require.NoError(t, err) + require.Equal(t, commitHash, workingHash) + + db = dbm.NewMemDB() + + // without WorkingHash + tree = NewLegacyMutableTree(db, 0, false, false, nil, log.NewNopLogger(), InitialVersionOption(uint64(initialVersion))) + + _, err = tree.Set([]byte("key1"), []byte("value1")) + require.NoError(t, err) + + commitHash1, _, err := tree.SaveVersion() + require.NoError(t, err) + require.Equal(t, commitHash1, commitHash) +}