Skip to content
Open
Show file tree
Hide file tree
Changes from 18 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,5 @@ cpu*.pdf
mem*.pdf

# IDE files
.idea/*
.idea/*
.vscode/*
4 changes: 2 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ Special thanks to external contributors on this release: @odeke-em

### Bug Fixes

- [\#347](https://github.com/cosmos/iavl/pull/347) Fix another integer overflow in `decodeBytes()` that can cause panics for certain inputs. The `ValueOp` and `AbsenceOp` proof decoders are vulnerable to this via malicious inputs since 0.15.0.
- [\#347](https://github.com/cosmos/iavl/pull/347) Fix another integer overflow in `utils.DecodeBytes()` that can cause panics for certain inputs. The `ValueOp` and `AbsenceOp` proof decoders are vulnerable to this via malicious inputs since 0.15.0.

- [\#349](https://github.com/cosmos/iavl/pull/349) Fix spurious blank lines in `PathToLeaf.String()`.

Expand All @@ -63,7 +63,7 @@ Special thanks to external contributors on this release: @odeke-em

### Bug Fixes

- [\#340](https://github.com/cosmos/iavl/pull/340) Fix integer overflow in `decodeBytes()` that can cause panics on 64-bit systems and out-of-memory issues on 32-bit systems. The `ValueOp` and `AbsenceOp` proof decoders are vulnerable to this via malicious inputs. The bug was introduced in 0.15.0.
- [\#340](https://github.com/cosmos/iavl/pull/340) Fix integer overflow in `utils.DecodeBytes()` that can cause panics on 64-bit systems and out-of-memory issues on 32-bit systems. The `ValueOp` and `AbsenceOp` proof decoders are vulnerable to this via malicious inputs. The bug was introduced in 0.15.0.

## 0.15.0 (November 23, 2020)

Expand Down
36 changes: 35 additions & 1 deletion basic_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,7 @@ func TestUnit(t *testing.T) {
t.Fatalf("Expected %v new hashes, got %v", hashCount, count)
}
// nuke hashes and reconstruct hash, ensure it's the same.
tree.root.traverse(tree, true, func(node *Node) bool {
tree.traverse(tree.root, true, func(node *Node) bool {
node.hash = nil
return false
})
Expand Down Expand Up @@ -526,3 +526,37 @@ func TestTreeProof(t *testing.T) {
}
}
}

// 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{}

func BenchmarkConvertLeafOp(b *testing.B) {
var versions = []int64{
0,
1,
100,
127,
128,
1 << 29,
-0,
-1,
-100,
-127,
-128,
-1 << 29,
}

b.ReportAllocs()
b.ResetTimer()

for i := 0; i < b.N; i++ {
for _, version := range versions {
sink = convertLeafOp(version)
}
}
if sink == nil {
b.Fatal("Benchmark wasn't run")
}
sink = nil
}
40 changes: 20 additions & 20 deletions docs/node/node.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ type Node struct {
key []byte // key for the node.
value []byte // value of leaf node. If inner node, value = nil
version int64 // The version of the IAVL that this node was first added in.
height int8 // The height of the node. Leaf nodes have height 0
depth int8 // The depth of the node. Leaf nodes have depth 0
size int64 // The number of leaves that are under the current node. Leaf nodes have size = 1
hash []byte // hash of above field and leftHash, rightHash
leftHash []byte // hash of left child
Expand All @@ -25,52 +25,52 @@ Inner nodes have keys equal to the highest key on their left branch and have val

The version of a node is the first version of the IAVL tree that the node gets added in. Future versions of the IAVL may point to this node if they also contain the node, however the node's version itself does not change.

Size is the number of leaves under a given node. With a full subtree, `node.size = 2^(node.height)`.
Size is the number of leaves under a given node. With a full subtree, `node.size = 2^(node.subtreeHeight)`.

### Marshaling

Every node is persisted by encoding the key, version, height, size and hash. If the node is a leaf node, then the value is persisted as well. If the node is not a leaf node, then the leftHash and rightHash are persisted as well.
Every node is persisted by encoding the key, version, depth, size and hash. If the node is a leaf node, then the value is persisted as well. If the node is not a leaf node, then the leftHash and rightHash are persisted as well.

```golang
// Writes the node as a serialized byte slice to the supplied io.Writer.
func (node *Node) writeBytes(w io.Writer) error {
cause := encodeVarint(w, node.height)
cause := utils.EncodeVarint(w, node.subtreeHeight)
if cause != nil {
return errors.Wrap(cause, "writing height")
return errors.Wrap(cause, "writing depth")
}
cause = encodeVarint(w, node.size)
cause = utils.EncodeVarint(w, node.size)
if cause != nil {
return errors.Wrap(cause, "writing size")
}
cause = encodeVarint(w, node.version)
cause = utils.EncodeVarint(w, node.version)
if cause != nil {
return errors.Wrap(cause, "writing version")
}

// Unlike writeHashBytes, key is written for inner nodes.
cause = encodeBytes(w, node.key)
cause = utils.EncodeBytes(w, node.key)
if cause != nil {
return errors.Wrap(cause, "writing key")
}

if node.isLeaf() {
cause = encodeBytes(w, node.value)
cause = utils.EncodeBytes(w, node.value)
if cause != nil {
return errors.Wrap(cause, "writing value")
}
} else {
if node.leftHash == nil {
panic("node.leftHash was nil in writeBytes")
}
cause = encodeBytes(w, node.leftHash)
cause = utils.EncodeBytes(w, node.leftHash)
if cause != nil {
return errors.Wrap(cause, "writing left hash")
}

if node.rightHash == nil {
panic("node.rightHash was nil in writeBytes")
}
cause = encodeBytes(w, node.rightHash)
cause = utils.EncodeBytes(w, node.rightHash)
if cause != nil {
return errors.Wrap(cause, "writing right hash")
}
Expand All @@ -81,48 +81,48 @@ func (node *Node) writeBytes(w io.Writer) error {

### Hashes

A node's hash is calculated by hashing the height, size, and version of the node. If the node is a leaf node, then the key and value are also hashed. If the node is an inner node, the leftHash and rightHash are included in hash but the key is not.
A node's hash is calculated by hashing the depth, size, and version of the node. If the node is a leaf node, then the key and value are also hashed. If the node is an inner node, the leftHash and rightHash are included in hash but the key is not.

```golang
// Writes the node's hash to the given io.Writer. This function expects
// child hashes to be already set.
func (node *Node) writeHashBytes(w io.Writer) error {
err := encodeVarint(w, node.height)
err := utils.EncodeVarint(w, node.subtreeHeight)
if err != nil {
return errors.Wrap(err, "writing height")
return errors.Wrap(err, "writing depth")
}
err = encodeVarint(w, node.size)
err = utils.EncodeVarint(w, node.size)
if err != nil {
return errors.Wrap(err, "writing size")
}
err = encodeVarint(w, node.version)
err = utils.EncodeVarint(w, node.version)
if err != nil {
return errors.Wrap(err, "writing version")
}

// Key is not written for inner nodes, unlike writeBytes.

if node.isLeaf() {
err = encodeBytes(w, node.key)
err = utils.EncodeBytes(w, node.key)
if err != nil {
return errors.Wrap(err, "writing key")
}
// Indirection needed to provide proofs without values.
// (e.g. proofLeafNode.ValueHash)
valueHash := tmhash.Sum(node.value)
err = encodeBytes(w, valueHash)
err = utils.EncodeBytes(w, valueHash)
if err != nil {
return errors.Wrap(err, "writing value")
}
} else {
if node.leftHash == nil || node.rightHash == nil {
panic("Found an empty child hash")
}
err = encodeBytes(w, node.leftHash)
err = utils.EncodeBytes(w, node.leftHash)
if err != nil {
return errors.Wrap(err, "writing left hash")
}
err = encodeBytes(w, node.rightHash)
err = utils.EncodeBytes(w, node.rightHash)
if err != nil {
return errors.Wrap(err, "writing right hash")
}
Expand Down
2 changes: 1 addition & 1 deletion docs/node/nodedb.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ func (ndb *nodeDB) deleteOrphans(version int64) {
// can delete the orphan. Otherwise, we shorten its lifetime, by
// moving its endpoint to the previous version.
if predecessor < fromVersion || fromVersion == toVersion {
ndb.batch.Delete(ndb.nodeKey(hash))
ndb.batch.Delete(getNodeKey(hash))
ndb.uncacheNode(hash)
} else {
ndb.saveOrphan(hash, fromVersion, predecessor)
Expand Down
2 changes: 1 addition & 1 deletion docs/proof/proof.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ In reality, IAVL nodes contain more data than shown here - for details please re
overview.

A cryptographically secure hash is generated for each node in the tree by hashing the node's key
and value (if leaf node), version, and height, as well as the hashes of each direct child (if
and value (if leaf node), version, and subtreeHeight, as well as the hashes of each direct child (if
any). This implies that the hash of any given node also depends on the hashes of all descendants
of the node. In turn, this implies that the hash of the root node depends on the hashes of all
nodes (and therefore all data) in the tree.
Expand Down
2 changes: 1 addition & 1 deletion docs/tree/export_import.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ This would produce the following export:

When importing, the tree must be rebuilt in the same order, such that the missing attributes (e.g. `hash` and `size`) can be generated. This is possible because children are always given before their parents. We can therefore first generate the hash and size of the left and right leaf nodes, and then use these to recursively generate the hash and size of the parent.

One way to do this is to keep a stack of orphaned children, and then pop those children once we build their parent, which then becomes a new child on the stack. We know that we encounter a parent because its height is higher than the child or children on top of the stack. We need a stack because we may need to recursively build a right branch while holding an orphaned left child. For the above export this would look like the following (in `key:height=value` format):
One way to do this is to keep a stack of orphaned children, and then pop those children once we build their parent, which then becomes a new child on the stack. We know that we encounter a parent because its depth is higher than the child or children on top of the stack. We need a stack because we may need to recursively build a right branch while holding an orphaned left child. For the above export this would look like the following (in `key:depth=value` format):

```
| Stack | Import node |
Expand Down
2 changes: 1 addition & 1 deletion docs/tree/immutable_tree.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ type ImmutableTree struct {

Using the root and the nodeDB, the ImmutableTree can retrieve any node that is a part of the IAVL tree at this version.

Users can get information about the IAVL tree by calling getter functions such as `Size()` and `Height()` which will return the tree's size and height by querying the root node's size and height.
Users can get information about the IAVL tree by calling getter functions such as `Size()` and `Height()` which will return the tree's size and depth by querying the root node's size and depth.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Users can get information about the IAVL tree by calling getter functions such as `Size()` and `Height()` which will return the tree's size and depth by querying the root node's size and depth.
Users can get information about the IAVL tree by calling getter functions such as `Size()` and `Depth()` which will return the tree's size and depth by querying the root node's size and depth.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it actually be depth though? It seems to be returning "subtree height"


### Get

Expand Down
14 changes: 7 additions & 7 deletions docs/tree/mutable_tree.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ newNode := NewNode(key, value, newVersion)
// original leaf node: originalLeaf gets replaced by inner node below
Node{
key: setKey, // inner node key is equal to left child's key
height: 1, // height=1 since node is parent of leaves
depth: 1, // depth=1 since node is parent of leaves
size: 2, // 2 leaf nodes under this node
leftNode: newNode, // left Node is the new added leaf node
rightNode: originalLeaf, // right Node is the original leaf node
Expand All @@ -59,7 +59,7 @@ newNode := NewNode(key, value, latestVersion+1)
// original leaf node: originalLeaf gets replaced by inner node below
Node{
key: leafKey, // inner node key is equal to left child's key
height: 1, // height=1 since node is parent of leaves
depth: 1, // depth=1 since node is parent of leaves
size: 2, // 2 leaf nodes under this node
leftNode: originalLeaf, // left Node is the original leaf node
rightNode: newNode, // right Node is the new added leaf node
Expand All @@ -69,7 +69,7 @@ Node{

Any node that gets recursed upon during a Set call is necessarily orphaned since it will either have a new value (in the case of an update) or it will have a new descendant. The recursive calls accumulate a list of orphans as it descends down the IAVL tree. This list of orphans is ultimately added to the mutable tree's orphan list at the end of the Set call.

After each set, the current working tree has its height and size recalculated. If the height of the left branch and right branch of the working tree differs by more than one, then the mutable tree has to be balanced before the Set call can return.
After each set, the current working tree has its depth and size recalculated. If the depth of the left branch and right branch of the working tree differs by more than one, then the mutable tree has to be balanced before the Set call can return.

### Remove

Expand Down Expand Up @@ -111,11 +111,11 @@ ReplaceNode = RightLeaf
orphaned = [LeftLeaf, recurseNode]
```

If recurseNode is an inner node that got called in the recursiveRemove, but is not a direct parent of the removed leaf. Then an updated version of the node will exist in the tree. Notably, it will have an incremented version, a new hash (as explained in the `NewHash` section), and recalculated height and size.
If recurseNode is an inner node that got called in the recursiveRemove, but is not a direct parent of the removed leaf. Then an updated version of the node will exist in the tree. Notably, it will have an incremented version, a new hash (as explained in the `NewHash` section), and recalculated depth and size.

The ReplaceNode will be a cloned version of `recurseNode` with an incremented version. The hash will be updated given the NewHash of recurseNode's left child or right child (depending on which branch got recurse upon).

The height and size of the ReplaceNode will have to be calculated since these values can change after the `remove`.
The depth and size of the ReplaceNode will have to be calculated since these values can change after the `remove`.

It's possible that the subtree for `ReplaceNode` will have to be rebalanced (see `Balance` section). If this is the case, this will also update `ReplaceNode`'s hash since the structure of `ReplaceNode`'s subtree will change.

Expand Down Expand Up @@ -145,9 +145,9 @@ If the `removeKey` does not exist in the IAVL tree, then the orphans list is `ni

### Balance

Anytime a node is unbalanced such that the height of its left branch and the height of its right branch differs by more than 1, the IAVL tree will rebalance itself.
Anytime a node is unbalanced such that the depth of its left branch and the depth of its right branch differs by more than 1, the IAVL tree will rebalance itself.

This is acheived by rotating the subtrees until there is no more than one height difference between two branches of any subtree in the IAVL.
This is acheived by rotating the subtrees until there is no more than one depth difference between two branches of any subtree in the IAVL.

Since Balance is mutating the structure of the tree, any displaced nodes will be orphaned.

Expand Down
4 changes: 2 additions & 2 deletions export.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,12 +50,12 @@ func newExporter(tree *ImmutableTree) *Exporter {

// export exports nodes
func (e *Exporter) export(ctx context.Context) {
e.tree.root.traversePost(e.tree, true, func(node *Node) bool {
e.tree.traversePost(e.tree.root, true, func(node *Node) bool {
exportNode := &ExportNode{
Key: node.key,
Value: node.value,
Version: node.version,
Height: node.height,
Height: node.subtreeHeight,
}

select {
Expand Down
9 changes: 5 additions & 4 deletions fast_iterator.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package iavl
import (
"errors"

"github.com/cosmos/iavl/types"
dbm "github.com/tendermint/tm-db"
)

Expand All @@ -22,7 +23,7 @@ type FastIterator struct {

ndb *nodeDB

nextFastNode *FastNode
nextFastNode *types.FastNode

fastIterator dbm.Iterator
}
Expand Down Expand Up @@ -82,15 +83,15 @@ func (iter *FastIterator) Valid() bool {
// Key implements dbm.Iterator
func (iter *FastIterator) Key() []byte {
if iter.valid {
return iter.nextFastNode.key
return iter.nextFastNode.GetKey()
}
return nil
}

// Value implements dbm.Iterator
func (iter *FastIterator) Value() []byte {
if iter.valid {
return iter.nextFastNode.value
return iter.nextFastNode.GetValue()
}
return nil
}
Expand All @@ -116,7 +117,7 @@ func (iter *FastIterator) Next() {

iter.valid = iter.valid && iter.fastIterator.Valid()
if iter.valid {
iter.nextFastNode, iter.err = DeserializeFastNode(iter.fastIterator.Key()[1:], iter.fastIterator.Value())
iter.nextFastNode, iter.err = types.DeserializeFastNode(iter.fastIterator.Key()[1:], iter.fastIterator.Value())
iter.valid = iter.err == nil
}
}
Expand Down
Loading