diff --git a/deepsubtree.go b/deepsubtree.go index bd9ab422e..2957a882a 100644 --- a/deepsubtree.go +++ b/deepsubtree.go @@ -7,7 +7,6 @@ import ( "fmt" "strings" - "github.com/chrispappas/golang-generics-set/set" ics23 "github.com/confio/ics23/go" dbm "github.com/tendermint/tm-db" ) @@ -21,14 +20,15 @@ const ( // a subset of nodes of an IAVL tree type DeepSubTree struct { *MutableTree - // witnessData WitnessData - // counter int + initialRootHash []byte // Initial Root Hash when Deep Subtree is initialized for an already existing tree + witnessData []WitnessData // Represents a trace operation along with inclusion proofs required for said operation + operationCounter int // Keeps track of which operation in the witness data list the Deep Subtree is on } // NewDeepSubTree returns a new deep subtree with the specified cache size, datastore, and version. func NewDeepSubTree(db dbm.DB, cacheSize int, skipFastStorageUpgrade bool, version int64) *DeepSubTree { ndb := newNodeDB(db, cacheSize, nil) - head := &ImmutableTree{ndb: ndb, version: version} + head := &ImmutableTree{ndb: ndb, version: version, skipFastStorageUpgrade: skipFastStorageUpgrade} mutableTree := &MutableTree{ ImmutableTree: head, lastSaved: head.clone(), @@ -40,7 +40,27 @@ func NewDeepSubTree(db dbm.DB, cacheSize int, skipFastStorageUpgrade bool, versi ndb: ndb, skipFastStorageUpgrade: skipFastStorageUpgrade, } - return &DeepSubTree{MutableTree: mutableTree} + return &DeepSubTree{MutableTree: mutableTree, initialRootHash: nil, witnessData: nil, operationCounter: 0} +} + +// Setter for witness data. Also, resets the operation counter back to 0. +func (dst *DeepSubTree) SetWitnessData(witnessData []WitnessData) { + dst.witnessData = witnessData + dst.operationCounter = 0 +} + +// Returns the initial root hash if it is initialized and Deep Subtree root is nil. +// Otherwise, returns the Deep Subtree working hash is considered the initial root hash. +func (dst *DeepSubTree) GetInitialRootHash() ([]byte, error) { + if dst.root == nil && dst.initialRootHash != nil { + return dst.initialRootHash, nil + } + return dst.WorkingHash() +} + +// Setter for initial root hash +func (dst *DeepSubTree) SetInitialRootHash(initialRootHash []byte) { + dst.initialRootHash = initialRootHash } func (node *Node) updateInnerNodeKey() { @@ -63,7 +83,7 @@ func (dst *DeepSubTree) buildTree(rootHash []byte) error { if !bytes.Equal(workingHash, rootHash) { if dst.root != nil { return fmt.Errorf( - "deep Subtree rootHash: %s does not match expected rootHash: %s", + "deep subtree rootHash: %s does not match expected rootHash: %s", workingHash, rootHash, ) @@ -119,10 +139,58 @@ func (dst *DeepSubTree) linkNode(node *Node) error { return nil } -// Set sets a key in the working tree with the given value. -// Assumption: Node with given key already exists and is a leaf node. -// Modified version of set taken from mutable_tree.go +// Verifies the given operation matches up with the witness data. +// Also, verifies and adds existence proofs related to the operation. +func (dst *DeepSubTree) verifyOperationAndProofs(operation Operation, key []byte, value []byte) error { + if dst.witnessData == nil { + return errors.New("witness data in deep subtree is nil") + } + if dst.operationCounter >= len(dst.witnessData) { + return fmt.Errorf( + "operation counter in witness data: %d should be less than length of witness data: %d", + dst.operationCounter, + len(dst.witnessData), + ) + } + traceOp := dst.witnessData[dst.operationCounter] + if traceOp.Operation != operation || !bytes.Equal(traceOp.Key, key) || !bytes.Equal(traceOp.Value, value) { + return fmt.Errorf( + "traceOp in witnessData (%s, %s, %s) does not match up with executed operation (%s, %s, %s)", + traceOp.Operation, string(traceOp.Key), string(traceOp.Value), + operation, string(key), string(value), + ) + } + rootHash, err := dst.GetInitialRootHash() + if err != nil { + return err + } + + // Verify proofs against current rootHash + for _, proof := range traceOp.Proofs { + err := proof.Verify(ics23.IavlSpec, rootHash, proof.Key, proof.Value) + if err != nil { + return err + } + } + err = dst.AddExistenceProofs(traceOp.Proofs, rootHash) + if err != nil { + return err + } + dst.operationCounter++ + return nil +} + +// Verifies the Set operation with witness data and perform the given write operation func (dst *DeepSubTree) Set(key []byte, value []byte) (updated bool, err error) { + err = dst.verifyOperationAndProofs("write", key, value) + if err != nil { + return false, err + } + return dst.set(key, value) +} + +// Sets a key in the working tree with the given value. +func (dst *DeepSubTree) set(key []byte, value []byte) (updated bool, err error) { if value == nil { return updated, fmt.Errorf("attempt to store nil value at key '%s'", key) } @@ -132,7 +200,6 @@ func (dst *DeepSubTree) Set(key []byte, value []byte) (updated bool, err error) return updated, nil } - // TODO: verify operation is on top, look at the witness data and add the relevant existence proofs dst.root, updated, err = dst.recursiveSet(dst.root, key, value) if err != nil { return updated, err @@ -224,9 +291,37 @@ func (dst *DeepSubTree) recursiveSet(node *Node, key []byte, value []byte) ( return newNode, updated, err } -// Remove tries to remove a key from the tree and if removed, returns its -// value, nodes orphaned and 'true'. +// Verifies the Get operation with witness data and perform the given read operation +func (dst *DeepSubTree) Get(key []byte) (value []byte, err error) { + err = dst.verifyOperationAndProofs("read", key, nil) + if err != nil { + return nil, err + } + return dst.get(key) +} + +// Get returns the value of the specified key if it exists, or nil otherwise. +// The returned value must not be modified, since it may point to data stored within IAVL. +func (dst *DeepSubTree) get(key []byte) ([]byte, error) { + if dst.root == nil { + return nil, nil + } + + return dst.ImmutableTree.Get(key) +} + +// Verifies the Remove operation with witness data and perform the given delete operation func (dst *DeepSubTree) Remove(key []byte) (value []byte, removed bool, err error) { + err = dst.verifyOperationAndProofs("delete", key, nil) + if err != nil { + return nil, false, err + } + return dst.remove(key) +} + +// Remove tries to remove a key from the tree and if removed, returns its +// value, and 'true'. +func (dst *DeepSubTree) remove(key []byte) (value []byte, removed bool, err error) { if dst.root == nil { return nil, false, nil } @@ -338,59 +433,6 @@ func (dst *DeepSubTree) recursiveRemove(node *Node, key []byte) (newHash []byte, return nil, nil, nil, fmt.Errorf("node with key: %s not found", key) } -func (tree *MutableTree) getExistenceProofsNeededForSet(key []byte, value []byte) ([]*ics23.ExistenceProof, error) { - _, err := tree.Set(key, value) - - if err != nil { - return nil, err - } - - keysAccessed := tree.ndb.keysAccessed.Values() - tree.ndb.keysAccessed = make(set.Set[string]) - - tree.Rollback() - - return tree.reapInclusionProofs(keysAccessed) -} - -func (tree *MutableTree) getExistenceProofsNeededForRemove(key []byte) ([]*ics23.ExistenceProof, error) { - ics23proof, err := tree.GetMembershipProof(key) - if err != nil { - return nil, err - } - - _, _, err = tree.Remove(key) - if err != nil { - return nil, err - } - - keysAccessed := tree.ndb.keysAccessed.Values() - tree.ndb.keysAccessed = make(set.Set[string]) - - tree.Rollback() - - keysAccessed = append(keysAccessed, string(key)) - - existenceProofs, err := tree.reapInclusionProofs(keysAccessed) - if err != nil { - return nil, err - } - existenceProofs = append(existenceProofs, ics23proof.GetExist()) - return existenceProofs, nil -} - -func (tree *MutableTree) reapInclusionProofs(keysAccessed []string) ([]*ics23.ExistenceProof, error) { - existenceProofs := make([]*ics23.ExistenceProof, 0) - for _, key := range keysAccessed { - ics23proof, err := tree.GetMembershipProof([]byte(key)) - if err != nil { - return nil, err - } - existenceProofs = append(existenceProofs, ics23proof.GetExist()) - } - return existenceProofs, nil -} - func recomputeHash(node *Node) error { if node.leftHash == nil && node.leftNode != nil { leftHash, err := node.leftNode._hash() @@ -502,18 +544,24 @@ func (dst *DeepSubTree) AddExistenceProofs(existenceProofs []*ics23.ExistencePro return err } } - if rootHash == nil { - workingHash, err := dst.WorkingHash() - if err != nil { - return err - } - rootHash = workingHash + err := dst.buildTree(rootHash) + if err != nil { + return err } + return nil +} - err := dst.buildTree(rootHash) +func (dst *DeepSubTree) saveNodeIfNeeded(node *Node) error { + has, err := dst.ndb.Has(node.hash) if err != nil { return err } + if !has { + err = dst.ndb.SaveNode(node) + if err != nil { + return err + } + } return nil } @@ -522,7 +570,7 @@ func (dst *DeepSubTree) addExistenceProof(proof *ics23.ExistenceProof) error { if err != nil { return err } - err = dst.ndb.SaveNode(leaf) + err = dst.saveNodeIfNeeded(leaf) if err != nil { return err } @@ -535,16 +583,7 @@ func (dst *DeepSubTree) addExistenceProof(proof *ics23.ExistenceProof) error { } prevHash = inner.hash - has, err := dst.ndb.Has(inner.hash) - if err != nil { - return err - } - if !has { - err = dst.ndb.SaveNode(inner) - if err != nil { - return err - } - } + dst.saveNodeIfNeeded(inner) } return nil } diff --git a/deepsubtree_test.go b/deepsubtree_test.go index 4fdb07ba0..7492eee84 100644 --- a/deepsubtree_test.go +++ b/deepsubtree_test.go @@ -20,6 +20,7 @@ type op int const ( Set op = iota Remove + Get Noop ) @@ -84,8 +85,9 @@ func TestDeepSubtreeStepByStep(t *testing.T) { rootHash, err := tree.WorkingHash() require.NoError(err) - dst := NewDeepSubTree(db.NewMemDB(), 100, false, 0) + dst := NewDeepSubTree(db.NewMemDB(), 100, false, tree.version) require.NoError(err) + dst.SetInitialRootHash(tree.root.hash) // insert key/value pairs in tree allkeys := [][]byte{ @@ -116,6 +118,8 @@ func TestDeepSubtreeWithUpdates(t *testing.T) { tree, err := getTestTree(5) require.NoError(err) + tree.SetTracingEnabled(true) + tree.Set([]byte("e"), []byte{5}) tree.Set([]byte("d"), []byte{4}) tree.Set([]byte("c"), []byte{3}) @@ -140,9 +144,9 @@ func TestDeepSubtreeWithUpdates(t *testing.T) { tree := getTree() rootHash, err := tree.WorkingHash() require.NoError(err) - mutableTree, err := NewMutableTree(db.NewMemDB(), 100, true) + dst := NewDeepSubTree(db.NewMemDB(), 100, true, tree.version) require.NoError(err) - dst := DeepSubTree{mutableTree} + dst.SetInitialRootHash(tree.root.hash) for _, subsetKey := range subsetKeys { ics23proof, err := tree.GetMembershipProof(subsetKey) require.NoError(err) @@ -151,23 +155,21 @@ func TestDeepSubtreeWithUpdates(t *testing.T) { }, rootHash) require.NoError(err) } - dst.SaveVersion() areEqual, err := haveEqualRoots(dst.MutableTree, tree) require.NoError(err) require.True(areEqual) + tc := testContext{ + tree: tree, + dst: dst, + } + values := [][]byte{{10}, {20}} for i, subsetKey := range subsetKeys { - dst.Set(subsetKey, values[i]) - dst.SaveVersion() - tree.Set(subsetKey, values[i]) - tree.SaveVersion() + err := tc.setInDST(subsetKey, values[i]) + require.Nil(err) } - - areEqual, err = haveEqualRoots(dst.MutableTree, tree) - require.NoError(err) - require.True(areEqual) } } @@ -179,6 +181,8 @@ func TestDeepSubtreeWWithAddsAndDeletes(t *testing.T) { tree, err := getTestTree(5) require.NoError(err) + tree.SetTracingEnabled(true) + tree.Set([]byte("b"), []byte{2}) tree.Set([]byte("a"), []byte{1}) @@ -193,9 +197,9 @@ func TestDeepSubtreeWWithAddsAndDeletes(t *testing.T) { } rootHash, err := tree.WorkingHash() require.NoError(err) - mutableTree, err := NewMutableTree(db.NewMemDB(), 100, true) + dst := NewDeepSubTree(db.NewMemDB(), 100, true, tree.version) require.NoError(err) - dst := DeepSubTree{mutableTree} + dst.SetInitialRootHash(tree.root.hash) for _, subsetKey := range subsetKeys { ics23proof, err := tree.GetMembershipProof(subsetKey) require.NoError(err) @@ -211,52 +215,26 @@ func TestDeepSubtreeWWithAddsAndDeletes(t *testing.T) { valuesToAdd := [][]byte{ {3}, {4}, } - // Add non-existence proofs for keys we expect to add later - for i, keyToAdd := range keysToAdd { - existenceProofs, err := tree.getExistenceProofsNeededForSet(keyToAdd, valuesToAdd[i]) - require.NoError(err) - err = dst.AddExistenceProofs(existenceProofs, rootHash) - require.NoError(err) + tc := testContext{ + tree: tree, + dst: dst, } - dst.SaveVersion() - - areEqual, err := haveEqualRoots(dst.MutableTree, tree) - require.NoError(err) - require.True(areEqual) - require.Equal(len(keysToAdd), len(valuesToAdd)) // Add all the keys we intend to add and check root hashes stay equal - for i := range keysToAdd { - keyToAdd := keysToAdd[i] - valueToAdd := valuesToAdd[i] - dst.Set(keyToAdd, valueToAdd) - dst.SaveVersion() - tree.Set(keyToAdd, valueToAdd) - tree.SaveVersion() - - areEqual, err := haveEqualRoots(dst.MutableTree, tree) - require.NoError(err) - require.True(areEqual) + for i, keyToAdd := range keysToAdd { + err := tc.setInDST(keyToAdd, valuesToAdd[i]) + require.Nil(err) } + require.Equal(len(keysToAdd), len(valuesToAdd)) + // Delete all the keys we added and check root hashes stay equal for i := range keysToAdd { keyToDelete := keysToAdd[i] - existenceProofs, err := tree.getExistenceProofsNeededForRemove(keyToDelete) - require.NoError(err) - err = dst.AddExistenceProofs(existenceProofs, nil) - require.NoError(err) - - dst.Remove(keyToDelete) - dst.SaveVersion() - tree.Remove(keyToDelete) - tree.SaveVersion() - - areEqual, err := haveEqualRoots(dst.MutableTree, tree) - require.NoError(err) - require.True(areEqual) + err := tc.removeInDST(keyToDelete) + require.Nil(err) } } @@ -275,20 +253,20 @@ type testContext struct { keys set.Set[string] } -// Generates random new key half times and an existing key for the other half times. -func (tc *testContext) getKey(genRandom bool) (key []byte, err error) { +// Returns random new key half times if genRandom is true. +// Otherwise, returns a randomly picked existing key. +func (tc *testContext) getKey(genRandom bool, addsNewKey bool) (key []byte, err error) { tree, r, keys := tc.tree, tc.r, tc.keys if genRandom && readByte(r) < math.MaxUint8/2 { k := make([]byte, readByte(r)/2+1) r.Read(k) - val, err := tree.Get(k) + _, err := tree.Get(k) if err != nil { return nil, err } - if val != nil { - return nil, nil + if addsNewKey { + keys.Add(string(k)) } - keys.Add(string(k)) return k, nil } if keys.Len() == 0 { @@ -299,19 +277,22 @@ func (tc *testContext) getKey(genRandom bool) (key []byte, err error) { return []byte(kString), nil } +// Performs the Set operation on full IAVL tree first, gets the witness data generated from +// the operatio, and uses that witness data to peform the same operation on the Deep Subtree func (tc *testContext) setInDST(key []byte, value []byte) error { if key == nil { return nil } tree, dst := tc.tree, tc.dst - existenceProofs, err := tc.tree.getExistenceProofsNeededForSet(key, value) - if err != nil { - return err - } - err = dst.AddExistenceProofs(existenceProofs, nil) + + // Set key-value pair in IAVL tree + _, err := tree.Set(key, value) if err != nil { return err } + tree.SaveVersion() + witness := tree.witnessData[len(tree.witnessData)-1] + dst.SetWitnessData([]WitnessData{witness}) // Set key-value pair in DST _, err = dst.Set(key, value) @@ -320,10 +301,6 @@ func (tc *testContext) setInDST(key []byte, value []byte) error { } dst.SaveVersion() - // Set key-value pair in IAVL tree - tree.Set(key, value) - tree.SaveVersion() - areEqual, err := haveEqualRoots(dst.MutableTree, tree) if err != nil { return err @@ -334,16 +311,23 @@ func (tc *testContext) setInDST(key []byte, value []byte) error { return nil } +// Performs the Remove operation on full IAVL tree first, gets the witness data generated from +// the operatio, and uses that witness data to peform the same operation on the Deep Subtree func (tc *testContext) removeInDST(key []byte) error { if key == nil { return nil } tree, dst := tc.tree, tc.dst - existenceProofs, err := tree.getExistenceProofsNeededForRemove(key) + + // Set key-value pair in IAVL tree + _, _, err := tree.Remove(key) if err != nil { return err } - err = dst.AddExistenceProofs(existenceProofs, nil) + tree.SaveVersion() + witness := tree.witnessData[len(tree.witnessData)-1] + dst.SetWitnessData([]WitnessData{witness}) + if err != nil { return err } @@ -356,9 +340,6 @@ func (tc *testContext) removeInDST(key []byte) error { } dst.SaveVersion() - tree.Remove(key) - tree.SaveVersion() - areEqual, err := haveEqualRoots(dst.MutableTree, tree) if err != nil { return err @@ -369,17 +350,56 @@ func (tc *testContext) removeInDST(key []byte) error { return nil } +// Performs the Get operation on full IAVL tree first, gets the witness data generated from +// the operation, and uses that witness data to peform the same operation on the Deep Subtree +func (tc *testContext) getInDST(key []byte) error { + if key == nil { + return nil + } + tree, dst := tc.tree, tc.dst + + // Set key-value pair in IAVL tree + treeValue, err := tree.Get(key) + if err != nil { + return err + } + witness := tree.witnessData[len(tree.witnessData)-1] + dst.SetWitnessData([]WitnessData{witness}) + + if err != nil { + return err + } + dstValue, err := dst.Get(key) + if err != nil { + return err + } + if !bytes.Equal(dstValue, treeValue) { + return fmt.Errorf("Get: Values retrieved to get key: %s do not match", string(key)) + } + + return nil +} + +// Fuzz tests different combinations of Get, Remove, Set operations generated in +// a random order with keys related to operations chosen randomly func FuzzBatchAddReverse(f *testing.F) { f.Fuzz(func(t *testing.T, input []byte) { require := require.New(t) - if len(input) < 1000 { + if len(input) < 100 { return } tree, err := NewMutableTreeWithOpts(db.NewMemDB(), cacheSize, nil, true) require.NoError(err) + tree.SetTracingEnabled(true) dst := NewDeepSubTree(db.NewMemDB(), cacheSize, true, 0) r := bytes.NewReader(input) keys := make(set.Set[string]) + tc := testContext{ + r, + tree, + dst, + keys, + } for i := 0; r.Len() != 0; i++ { b, err := r.ReadByte() if err != nil { @@ -387,17 +407,11 @@ func FuzzBatchAddReverse(f *testing.F) { } op := op(int(b) % int(Noop)) require.NoError(err) - tc := testContext{ - r, - tree, - dst, - keys, - } switch op { case Set: - keyToAdd, err := tc.getKey(true) + keyToAdd, err := tc.getKey(true, true) require.NoError(err) - // fmt.Printf("%d: Add: %s, %t\n", i, string(keyToAdd), isNewKey) + t.Logf("%d: Add: %s\n", i, string(keyToAdd)) value := make([]byte, 32) binary.BigEndian.PutUint64(value, uint64(i)) err = tc.setInDST(keyToAdd, value) @@ -405,14 +419,22 @@ func FuzzBatchAddReverse(f *testing.F) { t.Error(err) } case Remove: - keyToDelete, err := tc.getKey(false) + keyToDelete, err := tc.getKey(false, false) require.NoError(err) - // fmt.Printf("%d: Remove: %s\n", i, string(keyToDelete)) + t.Logf("%d: Remove: %s\n", i, string(keyToDelete)) err = tc.removeInDST(keyToDelete) if err != nil { t.Error(err) } keys.Delete(string(keyToDelete)) + case Get: + keyToGet, err := tc.getKey(true, false) + require.NoError(err) + t.Logf("%d: Get: %s\n", i, string(keyToGet)) + err = tc.getInDST(keyToGet) + if err != nil { + t.Error(err) + } } } t.Log("Done") diff --git a/mutable_tree.go b/mutable_tree.go index 2fb9a9809..ed8ac73b7 100644 --- a/mutable_tree.go +++ b/mutable_tree.go @@ -8,6 +8,7 @@ import ( "sync" "github.com/chrispappas/golang-generics-set/set" + ics23 "github.com/confio/ics23/go" "github.com/pkg/errors" dbm "github.com/tendermint/tm-db" @@ -39,7 +40,9 @@ type MutableTree struct { ndb *nodeDB skipFastStorageUpgrade bool // If true, the tree will work like no fast storage and always not upgrade fast storage - mtx sync.Mutex + mtx sync.Mutex + tracingEnabled bool + witnessData []WitnessData } // NewMutableTree returns a new tree with the specified cache size and datastore. @@ -62,9 +65,28 @@ func NewMutableTreeWithOpts(db dbm.DB, cacheSize int, opts *Options, skipFastSto unsavedFastNodeRemovals: make(map[string]interface{}), ndb: ndb, skipFastStorageUpgrade: skipFastStorageUpgrade, + witnessData: make([]WitnessData, 0), + tracingEnabled: false, }, nil } +// Sets tracingEnabled to given boolean and also resets any existing witness data +func (tree *MutableTree) SetTracingEnabled(tracingEnabled bool) { + tree.tracingEnabled = tracingEnabled + tree.ndb.setTracingEnabled(tracingEnabled) + tree.resetWitnessData() +} + +// Resets witness data inside tree +func (tree *MutableTree) resetWitnessData() { + tree.witnessData = make([]WitnessData, 0) +} + +// Getter for witness data +func (tree *MutableTree) GetWitnessData() []WitnessData { + return tree.witnessData +} + // IsEmpty returns whether or not the tree has any keys. Only trees that are // not empty can be saved. func (tree *MutableTree) IsEmpty() bool { @@ -126,11 +148,56 @@ func (tree *MutableTree) prepareOrphansSlice() []*Node { return make([]*Node, 0, tree.Height()+3) } -// Set sets a key in the working tree. Nil values are invalid. The given +// Return a list of existences proofs for all keys in the given set +func (tree *MutableTree) reapExistenceProofs(keysAccessed []string) ([]*ics23.ExistenceProof, error) { + existenceProofs := make([]*ics23.ExistenceProof, 0, len(keysAccessed)) + for _, key := range keysAccessed { + ics23proof, err := tree.GetMembershipProof([]byte(key)) + if err != nil { + return nil, err + } + existenceProofs = append(existenceProofs, ics23proof.GetExist()) + } + return existenceProofs, nil +} + +// Wrapper around setOp to add operation related data to the tree's witness data +// when tracing is enabled +func (tree *MutableTree) Set(key, value []byte) (updated bool, err error) { + if !tree.tracingEnabled { + return tree.setOp(key, value) + } + savedTree := tree.ImmutableTree.clone() + _, err = tree.setOp(key, value) + + if err != nil { + return false, err + } + + tree.ImmutableTree = savedTree + tree.orphans = map[string]int64{} + + keysAccessed := tree.ndb.keysAccessed.Values() + + existenceProofs, err := tree.reapExistenceProofs(keysAccessed) + if err != nil { + return false, err + } + tree.witnessData = append(tree.witnessData, WitnessData{ + Operation: "write", + Key: key, + Value: value, + Proofs: existenceProofs, + }) + return tree.setOp(key, value) +} + +// setOp sets a key in the working tree. Nil values are invalid. The given // key/value byte slices must not be modified after this call, since they point // to slices stored within IAVL. It returns true when an existing value was // updated, while false means it was a new key. -func (tree *MutableTree) Set(key, value []byte) (updated bool, err error) { +func (tree *MutableTree) setOp(key, value []byte) (updated bool, err error) { + tree.ndb.keysAccessed = make(set.Set[string]) var orphaned []*Node orphaned, updated, err = tree.set(key, value) if err != nil { @@ -140,12 +207,42 @@ func (tree *MutableTree) Set(key, value []byte) (updated bool, err error) { if err != nil { return updated, err } + if !updated { + tree.ndb.keysAccessed.Delete(string(key)) + } return updated, nil } -// Get returns the value of the specified key if it exists, or nil otherwise. -// The returned value must not be modified, since it may point to data stored within IAVL. +// Wrapper around getOp to add operation related data to the tree's witness data +// when tracing is enabled func (tree *MutableTree) Get(key []byte) ([]byte, error) { + if !tree.tracingEnabled { + return tree.getOp(key) + } + value, err := tree.getOp(key) + if err != nil { + return nil, err + } + + keysAccessed := tree.ndb.keysAccessed.Values() + + existenceProofs, err := tree.reapExistenceProofs(keysAccessed) + if err != nil { + return nil, err + } + tree.witnessData = append(tree.witnessData, WitnessData{ + Operation: "read", + Key: key, + Proofs: existenceProofs, + }) + return value, nil +} + +// getOp returns the value of the specified key if it exists, or nil otherwise. +// The returned value must not be modified, since it may point to data stored within IAVL. +func (tree *MutableTree) getOp(key []byte) ([]byte, error) { + tree.ndb.keysAccessed = make(set.Set[string]) + if tree.root == nil { return nil, nil } @@ -243,7 +340,7 @@ func (tree *MutableTree) recursiveSet(node *Node, key []byte, value []byte, orph newSelf *Node, updated bool, err error, ) { version := tree.version + 1 - + node.addTrace(tree.ImmutableTree, node.key) if node.isLeaf() { if !tree.skipFastStorageUpgrade { tree.addUnsavedAddition(key, NewFastNode(key, value, version)) @@ -317,9 +414,44 @@ func (tree *MutableTree) recursiveSet(node *Node, key []byte, value []byte, orph } } -// Remove removes a key from the working tree. The given key byte slice should not be modified -// after this call, since it may point to data stored inside IAVL. +// Wrapper around removeOp to add operation related data to the tree's witness data +// when tracing is enabled func (tree *MutableTree) Remove(key []byte) ([]byte, bool, error) { + if !tree.tracingEnabled { + return tree.removeOp(key) + } + ics23proof, err := tree.GetMembershipProof(key) + if err != nil { + return nil, false, err + } + savedTree := tree.ImmutableTree.clone() + _, _, err = tree.removeOp(key) + if err != nil { + return nil, false, err + } + + keysAccessed := tree.ndb.keysAccessed.Values() + + tree.ImmutableTree = savedTree + tree.orphans = map[string]int64{} + + existenceProofs, err := tree.reapExistenceProofs(keysAccessed) + existenceProofs = append(existenceProofs, ics23proof.GetExist()) + if err != nil { + return nil, false, err + } + tree.witnessData = append(tree.witnessData, WitnessData{ + Operation: "delete", + Key: key, + Proofs: existenceProofs, + }) + return tree.removeOp(key) +} + +// removeOp removes a key from the working tree. The given key byte slice should not be modified +// after this call, since it may point to data stored inside IAVL. +func (tree *MutableTree) removeOp(key []byte) ([]byte, bool, error) { + tree.ndb.keysAccessed = make(set.Set[string]) val, orphaned, removed, err := tree.remove(key) if err != nil { return nil, false, err @@ -371,7 +503,7 @@ func (tree *MutableTree) remove(key []byte) (value []byte, orphaned []*Node, rem // - the orphaned nodes. func (tree *MutableTree) recursiveRemove(node *Node, key []byte, orphans *[]*Node) (newHash []byte, newSelf *Node, newKey []byte, newValue []byte, err error) { version := tree.version + 1 - + node.addTrace(tree.ImmutableTree, node.key) if node.isLeaf() { if bytes.Equal(key, node.key) { *orphans = append(*orphans, node) @@ -828,7 +960,6 @@ func (tree *MutableTree) SaveVersion() ([]byte, int64, error) { if version == 1 && tree.ndb.opts.InitialVersion > 0 { version = int64(tree.ndb.opts.InitialVersion) } - tree.ndb.keysAccessed = make(set.Set[string]) if tree.VersionExists(version) { // If the version already exists, return an error as we're attempting to overwrite. // However, the same hash means idempotent (i.e. no-op). diff --git a/node.go b/node.go index 607a164d2..5f5c68326 100644 --- a/node.go +++ b/node.go @@ -180,6 +180,7 @@ func (node *Node) has(t *ImmutableTree, key []byte) (has bool, err error) { // The index is the index in the list of leaf nodes sorted lexicographically by key. The leftmost leaf has index 0. // It's neighbor has index 1 and so on. func (node *Node) get(t *ImmutableTree, key []byte) (index int64, value []byte, err error) { + node.addTrace(t, node.key) if node.isLeaf() { switch bytes.Compare(node.key, key) { case -1: @@ -190,7 +191,6 @@ func (node *Node) get(t *ImmutableTree, key []byte) (index int64, value []byte, return 0, node.value, nil } } - if bytes.Compare(key, node.key) < 0 { leftNode, err := node.getLeftNode(t) if err != nil { @@ -465,8 +465,16 @@ func (node *Node) writeBytes(w io.Writer) error { return nil } +func (node *Node) addTrace(t *ImmutableTree, key []byte) { + if t == nil || t.ndb == nil { + return + } + t.ndb.addTrace(key) +} + func (node *Node) getLeftNode(t *ImmutableTree) (*Node, error) { if node.leftNode != nil { + node.addTrace(t, node.leftNode.key) return node.leftNode, nil } leftNode, err := t.ndb.GetNode(node.leftHash) @@ -479,6 +487,7 @@ func (node *Node) getLeftNode(t *ImmutableTree) (*Node, error) { func (node *Node) getRightNode(t *ImmutableTree) (*Node, error) { if node.rightNode != nil { + node.addTrace(t, node.rightNode.key) return node.rightNode, nil } rightNode, err := t.ndb.GetNode(node.rightHash) diff --git a/nodedb.go b/nodedb.go index 5fd973e82..0395eb323 100644 --- a/nodedb.go +++ b/nodedb.go @@ -80,7 +80,8 @@ type nodeDB struct { latestVersion int64 // Latest version of nodeDB. 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. - keysAccessed set.Set[string] + keysAccessed set.Set[string] // Set of keys accessed so far, used when tracing is enabled + tracingEnabled bool } func newNodeDB(db dbm.DB, cacheSize int, opts *Options) *nodeDB { @@ -105,13 +106,22 @@ func newNodeDB(db dbm.DB, cacheSize int, opts *Options) *nodeDB { versionReaders: make(map[int64]uint32, 8), storageVersion: string(storeVersion), keysAccessed: make(set.Set[string]), + tracingEnabled: false, } } -// Adds the given into a set of keys accessed +// Adds the given into a set of keys accessed when tracing is enabled // Note: Used by Deep Subtrees to know which keys to add existence proofs for func (ndb *nodeDB) addTrace(key []byte) { - ndb.keysAccessed.Add(string(key)) + if ndb.tracingEnabled && ndb.keysAccessed != nil { + ndb.keysAccessed.Add(string(key)) + } +} + +// Sets tracingEnabled to given boolean and also resets keysAccessed +func (ndb *nodeDB) setTracingEnabled(tracingEnabled bool) { + ndb.tracingEnabled = tracingEnabled + ndb.keysAccessed = make(set.Set[string]) } // GetNode gets a node from memory or disk. If it is an inner node, it does not diff --git a/testdata/fuzz/FuzzBatchAddReverse/a3b3957bd77c6d899517002007edae0b695d75270c4a25dbe8a5987cf79dbb3d b/testdata/fuzz/FuzzBatchAddReverse/a3b3957bd77c6d899517002007edae0b695d75270c4a25dbe8a5987cf79dbb3d new file mode 100644 index 000000000..2177c6d0d --- /dev/null +++ b/testdata/fuzz/FuzzBatchAddReverse/a3b3957bd77c6d899517002007edae0b695d75270c4a25dbe8a5987cf79dbb3d @@ -0,0 +1,2 @@ +go test fuzz v1 +[]byte("00\x000000000000000000000000000000000000000000000000000000010000000000000000010000000000000000a00000000000000000000000000\xf8a000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000Ha0000010000000000000000000000000000002001") diff --git a/trace_ops.go b/trace_ops.go new file mode 100644 index 000000000..45ff9faee --- /dev/null +++ b/trace_ops.go @@ -0,0 +1,24 @@ +package iavl + +import ( + ics23 "github.com/confio/ics23/go" +) + +const ( + WriteOp Operation = "write" + ReadOp Operation = "read" + DeleteOp Operation = "delete" +) + +type ( + // operation represents an tree operation + Operation string +) + +// Witness data represents a trace operation along with inclusion proofs required for said operation +type WitnessData struct { + Operation Operation + Key []byte + Value []byte + Proofs []*ics23.ExistenceProof +}