Skip to content
This repository has been archived by the owner on Feb 27, 2023. It is now read-only.

Implement compute-optimised Merkle tree #5

Merged
merged 84 commits into from
Aug 13, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
84 commits
Select commit Hold shift + click to select a range
8b47625
Implement treehasher
musalbas Jun 26, 2020
ba45c6d
fix digestLeaf method signature
liamsi Jun 26, 2020
6c3df4d
Fix a bunch of failures
liamsi Jun 26, 2020
51dfc3c
Merge pull request #3 from liamsi/ismail/fix_digest_leaf
musalbas Jun 29, 2020
513a548
run go fmt
musalbas Jun 29, 2020
d1eb1ea
Add placeholder values (incomplete
musalbas Jul 2, 2020
19635d2
Revert "Add placeholder values (incomplete"
musalbas Jul 3, 2020
9681cb8
update sidesNodesForRoot, add placeholder value and remove default va…
musalbas Jul 3, 2020
8a53820
fix sideNodesForRoot
musalbas Jul 6, 2020
291f69a
Updates
musalbas Jul 7, 2020
3811693
refactor sidenodes function
musalbas Jul 7, 2020
88434f9
format
musalbas Jul 7, 2020
9580c06
Update treehasher.go
musalbas Jul 7, 2020
f97d2e2
Add commentary
musalbas Jul 8, 2020
1ee39e6
implement update logic
musalbas Jul 8, 2020
7fa4dd3
Implement getter
musalbas Jul 8, 2020
5544283
Fix value putting
musalbas Jul 8, 2020
668ffc9
fix value parsing
musalbas Jul 8, 2020
5e2b3cc
passing SMT test
musalbas Jul 8, 2020
f35ba1d
Allow for flexible left/right
musalbas Jul 8, 2020
09ba921
bugfix
musalbas Jul 8, 2020
b36d8b2
Implement deletion; basic test cases pass.
musalbas Jul 9, 2020
dc0cbdb
add additional test
musalbas Jul 9, 2020
44c8c16
Improve test comments
musalbas Jul 13, 2020
8690733
Add better tests
musalbas Jul 13, 2020
335b1a9
fix tests: did not catch err
musalbas Jul 13, 2020
d005205
fix nil check
musalbas Jul 13, 2020
e25d724
improve tests
musalbas Jul 13, 2020
ffbd556
fix test
musalbas Jul 13, 2020
5f09ca0
partial test fix
musalbas Jul 20, 2020
3992ea7
Revert "partial test fix"
musalbas Jul 20, 2020
ed9e1d2
typo fix
musalbas Jul 21, 2020
9896367
Fix bug in update algorithm
musalbas Jul 21, 2020
d4dd523
Add bulk tester
musalbas Jul 21, 2020
db5b71f
typo fix
musalbas Jul 21, 2020
92d305c
Fix bug in deletion algorithm
musalbas Jul 21, 2020
c3d9853
Add test case for extreme deletions
musalbas Jul 21, 2020
e3e77ed
Add test repeats due to randomness.
musalbas Jul 21, 2020
93a6d46
Add test to check that each leaf is at the correct height.
musalbas Jul 21, 2020
ec93126
format
musalbas Jul 21, 2020
c8ff1fa
Catch error in test
musalbas Jul 21, 2020
33f6620
Move bulk tests to seperate file.
musalbas Jul 21, 2020
cb12ad1
working non-compact proofs for non-empty keys
musalbas Jul 21, 2020
e365be9
Change leafs to be = h(k) || h(v) instead of = h(k) || v
musalbas Jul 22, 2020
578066f
Refactor proof code
musalbas Jul 23, 2020
d026f85
Improve commentary
musalbas Jul 23, 2020
f348d29
Working non-membership proofs.
musalbas Jul 23, 2020
deffc31
Move proof sanity check to seperate function
musalbas Jul 23, 2020
d3cbada
Implement compact proofs.
musalbas Jul 23, 2020
511bad2
Re-implement DeepSparseMerkleSubTree
musalbas Jul 23, 2020
6a2864c
go fmt
musalbas Jul 24, 2020
a36c5d7
Break up long comments
musalbas Jul 24, 2020
a67555f
Normalise variable names for leafData / valueHash / value
musalbas Jul 24, 2020
6f9e4ed
Add sanity check for sidenode size in verifier
musalbas Jul 24, 2020
aeb2921
optimise Prover so that the leaf doesn't have to be gotten twice from…
musalbas Jul 24, 2020
2d392fe
Add test for edge case where two leafs and direct neighbours
musalbas Jul 24, 2020
2bd1752
Add Merkle proof checking to bulk test.
musalbas Jul 31, 2020
45dab71
Refactor proof tests, add base case tests
musalbas Aug 4, 2020
cb65f31
Remove t.Logs
musalbas Aug 4, 2020
a1b11b0
add invalid proof check
musalbas Aug 4, 2020
5300f62
Test random proofs
musalbas Aug 4, 2020
d03c67a
Add test cases for proof sanity checks
musalbas Aug 4, 2020
87df869
fmt
musalbas Aug 4, 2020
78cfaed
run bad proofs through verify
musalbas Aug 4, 2020
8c49f13
add test for default val
musalbas Aug 4, 2020
801c3de
bulk check empty values
musalbas Aug 4, 2020
9738347
fix test
musalbas Aug 4, 2020
8549c13
Fix test
musalbas Aug 5, 2020
cbedd41
Rename AddBranches to UpdateBranch
musalbas Aug 5, 2020
00530c1
Revert "Rename AddBranches to UpdateBranch"
musalbas Aug 11, 2020
08f5179
Re-implement AddBranch correctly.
musalbas Aug 12, 2020
4ec60e8
Improve deep tree tests.
musalbas Aug 12, 2020
cb611da
Add tests for bad cases
musalbas Aug 12, 2020
8aae2d6
Update readme
musalbas Aug 12, 2020
13ee731
Grammar fix.
musalbas Aug 12, 2020
fdc27b7
Split compact proofs into different type.
musalbas Aug 12, 2020
a95d676
Satisfy golint
musalbas Aug 12, 2020
5ccf7f1
Improve test coverage for compacting a proof.
musalbas Aug 12, 2020
2653c67
fix test
musalbas Aug 12, 2020
cdc57b2
Call ProveCompactForRoot in ProveCompact.
musalbas Aug 12, 2020
358c966
use err in mapstore test
musalbas Aug 12, 2020
cd51e37
Merge branch 'master' into optimised_compute
musalbas Aug 12, 2020
0ab2273
Move err check to the right place
musalbas Aug 13, 2020
da2e197
Merge branch 'optimised_compute' of github.com:lazyledger/smt into op…
musalbas Aug 13, 2020
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
4 changes: 1 addition & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,13 +1,11 @@
# smt

A Go library that implements a Sparse Merkle tree for a key-value map.
A Go library that implements a Sparse Merkle tree for a key-value map. The tree implements the same optimisations specified in the [Libra whitepaper](https://developers.libra.org/docs/assets/papers/the-libra-blockchain/2020-05-26.pdf), to reduce the number of hash operations required per tree operation to O(k) where k is the number of non-empty elements in the tree.
musalbas marked this conversation as resolved.
Show resolved Hide resolved

[![Build Status](https://travis-ci.org/lazyledger/smt.svg?branch=master)](https://travis-ci.org/lazyledger/smt)
[![Coverage Status](https://coveralls.io/repos/github/lazyledger/smt/badge.svg?branch=master)](https://coveralls.io/github/lazyledger/smt?branch=master)
[![GoDoc](https://godoc.org/github.com/lazyledger/smt?status.svg)](https://godoc.org/github.com/lazyledger/smt)

Thanks to Vitalik Buterin who provided the [original Python prototype](https://ethresear.ch/t/data-availability-proof-friendly-state-tree-transitions/1453/6) that some of this library is inspired from.

## Example

```go
Expand Down
136 changes: 136 additions & 0 deletions bulk_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
package smt

import (
"bytes"
"crypto/sha256"
"math/rand"
"reflect"
"testing"
)

// Test all tree operations in bulk.
func TestSparseMerkleTree(t *testing.T) {
for i := 0; i < 5; i++ {
// Test more inserts/updates than deletions.
bulkOperations(t, 200, 100, 100, 50)
}
for i := 0; i < 5; i++ {
// Test extreme deletions.
bulkOperations(t, 200, 100, 100, 500)
}
}

// Test all tree operations in bulk, with specified ratio probabilities of insert, update and delete.
func bulkOperations(t *testing.T, operations int, insert int, update int, delete int) {
sm := NewSimpleMap()
smt := NewSparseMerkleTree(sm, sha256.New())

max := insert + update + delete
kv := make(map[string]string)

for i := 0; i < operations; i++ {
n := rand.Intn(max)
if n < insert { // Insert
keyLen := 16 + rand.Intn(32)
key := make([]byte, keyLen)
rand.Read(key)

valLen := 1 + rand.Intn(64)
val := make([]byte, valLen)
rand.Read(val)

kv[string(key)] = string(val)
_, err := smt.Update(key, val)
if err != nil {
t.Errorf("error: %v", err)
}
} else if n > insert && n < insert+update { // Update
keys := reflect.ValueOf(kv).MapKeys()
if len(keys) == 0 {
continue
}
key := []byte(keys[rand.Intn(len(keys))].Interface().(string))

valLen := 1 + rand.Intn(64)
val := make([]byte, valLen)
rand.Read(val)

kv[string(key)] = string(val)
_, err := smt.Update(key, val)
if err != nil {
t.Errorf("error: %v", err)
}
} else { // Delete
keys := reflect.ValueOf(kv).MapKeys()
if len(keys) == 0 {
continue
}
key := []byte(keys[rand.Intn(len(keys))].Interface().(string))

kv[string(key)] = ""
_, err := smt.Update(key, defaultValue)
if err != nil {
t.Errorf("error: %v", err)
}
}

bulkCheckAll(t, smt, &kv)
}
}

func bulkCheckAll(t *testing.T, smt *SparseMerkleTree, kv *map[string]string) {
for k, v := range *kv {
value, err := smt.Get([]byte(k))
if err != nil {
t.Errorf("error: %v", err)
}
if !bytes.Equal([]byte(v), value) {
t.Error("got incorrect value when bulk testing operations")
}

// Generate and verify a Merkle proof for this key.
proof, err := smt.Prove([]byte(k))
if err != nil {
t.Errorf("error: %v", err)
}
if !VerifyProof(proof, smt.Root(), []byte(k), []byte(v), smt.th.hasher) {
t.Error("Merkle proof failed to verify")
}
compactProof, err := smt.ProveCompact([]byte(k))
if err != nil {
t.Errorf("error: %v", err)
}
if !VerifyCompactProof(compactProof, smt.Root(), []byte(k), []byte(v), smt.th.hasher) {
t.Error("Merkle proof failed to verify")
}

if v == "" {
continue
}

// Check that the key is at the correct height in the tree.
largestCommonPrefix := 0
for k2, v2 := range *kv {
if v2 == "" {
continue
}
commonPrefix := countCommonPrefix(smt.th.path([]byte(k)), smt.th.path([]byte(k2)))
if commonPrefix != smt.depth() && commonPrefix > largestCommonPrefix {
largestCommonPrefix = commonPrefix
}
}
sideNodes, _, _, err := smt.sideNodesForRoot(smt.th.path([]byte(k)), smt.Root())
if err != nil {
t.Errorf("error: %v", err)
}
numSideNodes := 0
for _, v := range sideNodes {
if v != nil {
numSideNodes++
}
}
if numSideNodes != largestCommonPrefix+1 && (numSideNodes != 0 && largestCommonPrefix != 0) {
t.Error("leaf is at unexpected height")
}
}
}
53 changes: 35 additions & 18 deletions deepsubtree.go
Original file line number Diff line number Diff line change
@@ -1,31 +1,48 @@
package smt

import(
"hash"
import (
"hash"
)

// BadProofError is returned when an invalid Merkle proof is supplied.
type BadProofError struct{}

func (e *BadProofError) Error() string {
return "bad proof"
}
Comment on lines +7 to +12
Copy link
Member

@liamsi liamsi Aug 13, 2020

Choose a reason for hiding this comment

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

Similar to the other place, this is equivalent to a sentinel error:

Suggested change
// BadProofError is returned when an invalid Merkle proof is supplied.
type BadProofError struct{}
func (e *BadProofError) Error() string {
return "bad proof"
}
// ErrBadProof is returned when an invalid Merkle proof is supplied.
var ErrBadProof = errors.New("bad proof")

Also, it is more common / go-idiomatic to have a Err prefix.

Copy link
Member Author

Choose a reason for hiding this comment

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

Isn't it better to have a special type for the error, so that users of the API can differentiate between error types?

Copy link
Member

@liamsi liamsi Aug 13, 2020

Choose a reason for hiding this comment

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

In go it is very common to use sentinel values for cases like this (this custom error is just returns a string with no further fields or context). See for instance the io package: https://golang.org/pkg/io/#pkg-variables

The custom error doesn't provide any additional value compared to a sentinel error (not to say that in other cases it could make more sense to use a custom error).

Also, worth looking at: https://blog.golang.org/go1.13-errors


// DeepSparseMerkleSubTree is a deep Sparse Merkle subtree for working on only a few leafs.
type DeepSparseMerkleSubTree struct {
*SparseMerkleTree
*SparseMerkleTree
}

// NewDeepSparseMerkleSubTree creates a new deep Sparse Merkle subtree on an empty MapStore.
func NewDeepSparseMerkleSubTree(ms MapStore, hasher hash.Hash) *DeepSparseMerkleSubTree {
smt := &SparseMerkleTree{
hasher: hasher,
ms: ms,
}
func NewDeepSparseMerkleSubTree(ms MapStore, hasher hash.Hash, root []byte) *DeepSparseMerkleSubTree {
smt := &SparseMerkleTree{
th: *newTreeHasher(hasher),
ms: ms,
}

return &DeepSparseMerkleSubTree{SparseMerkleTree: smt}
smt.SetRoot(root)

return &DeepSparseMerkleSubTree{SparseMerkleTree: smt}
}

// AddBranches adds new branches to the tree.
// These branches are generated by smt.ProveForRoot, and should be verified by VerifyProof first.
// Set updateRoot to true if the current root of the tree should be updated.
func (dsmst *DeepSparseMerkleSubTree) AddBranches(proof [][]byte, key []byte, value []byte, updateRoot bool) ([]byte, error) {
newRoot, err := dsmst.updateWithSideNodes(dsmst.digest(key), value, proof)
if err == nil && updateRoot {
dsmst.SetRoot(newRoot)
}
return newRoot, err
// AddBranch adds a branch to the tree.
// These branches are generated by smt.ProveForRoot.
// If the proof is invalid, a BadProofError is returned.
func (dsmst *DeepSparseMerkleSubTree) AddBranch(proof SparseMerkleProof, key []byte, value []byte) error {
result, updates := verifyProofWithUpdates(proof, dsmst.Root(), key, value, dsmst.th.hasher)
if !result {
return &BadProofError{}
}

for _, update := range updates {
err := dsmst.ms.Put(update[0], update[1])
if err != nil {
return err
}
}

return nil
}
156 changes: 99 additions & 57 deletions deepsubtree_test.go
Original file line number Diff line number Diff line change
@@ -1,62 +1,104 @@
package smt

import(
"crypto/sha256"
"testing"
"bytes"
import (
"bytes"
"crypto/sha256"
"testing"
)

func TestDeepSparseMerkleSubTree(t *testing.T) {
dsmst := NewDeepSparseMerkleSubTree(NewSimpleMap(), sha256.New())
smt := NewSparseMerkleTree(NewSimpleMap(), sha256.New())

smt.Update([]byte("testKey1"), []byte("testValue1"))
smt.Update([]byte("testKey2"), []byte("testValue2"))
smt.Update([]byte("testKey3"), []byte("testValue3"))
smt.Update([]byte("testKey4"), []byte("testValue4"))

proof1, _ := smt.Prove([]byte("testKey1"))
proof2, _ := smt.Prove([]byte("testKey2"))
dsmst.AddBranches(proof1, []byte("testKey1"), []byte("testValue1"), true)
dsmst.AddBranches(proof2, []byte("testKey2"), []byte("testValue2"), true)

value, err := dsmst.Get([]byte("testKey1"))
if err != nil {
t.Error("returned error when getting value in deep subtree")
}
if bytes.Compare(value, []byte("testValue1")) != 0 {
t.Error("did not get correct value in deep subtree")
}
value, err = dsmst.Get([]byte("testKey2"))
if err != nil {
t.Error("returned error when getting value in deep subtree")
}
if bytes.Compare(value, []byte("testValue2")) != 0 {
t.Error("did not get correct value in deep subtree")
}

dsmst.Update([]byte("testKey1"), []byte("testValue3"))
dsmst.Update([]byte("testKey2"), []byte("testValue4"))

value, err = dsmst.Get([]byte("testKey1"))
if err != nil {
t.Error("returned error when getting value in deep subtree")
}
if bytes.Compare(value, []byte("testValue3")) != 0 {
t.Error("did not get correct value in deep subtree")
}
value, err = dsmst.Get([]byte("testKey2"))
if err != nil {
t.Error("returned error when getting value in deep subtree")
}
if bytes.Compare(value, []byte("testValue4")) != 0 {
t.Error("did not get correct value in deep subtree")
}

smt.Update([]byte("testKey1"), []byte("testValue3"))
smt.Update([]byte("testKey2"), []byte("testValue4"))

if bytes.Compare(smt.Root(), dsmst.Root()) != 0 {
t.Error("roots of identical standard tree and subtree do not match")
}
func TestDeepSparseMerkleSubTreeBasic(t *testing.T) {
smt := NewSparseMerkleTree(NewSimpleMap(), sha256.New())

smt.Update([]byte("testKey1"), []byte("testValue1"))
smt.Update([]byte("testKey2"), []byte("testValue2"))
smt.Update([]byte("testKey3"), []byte("testValue3"))
smt.Update([]byte("testKey4"), []byte("testValue4"))
smt.Update([]byte("testKey6"), []byte("testValue6"))

proof1, _ := smt.Prove([]byte("testKey1"))
proof2, _ := smt.Prove([]byte("testKey2"))
proof5, _ := smt.Prove([]byte("testKey5"))

dsmst := NewDeepSparseMerkleSubTree(NewSimpleMap(), sha256.New(), smt.Root())
dsmst.AddBranch(proof1, []byte("testKey1"), []byte("testValue1"))
dsmst.AddBranch(proof2, []byte("testKey2"), []byte("testValue2"))
dsmst.AddBranch(proof5, []byte("testKey5"), defaultValue)

value, err := dsmst.Get([]byte("testKey1"))
if err != nil {
t.Error("returned error when getting value in deep subtree")
}
if bytes.Compare(value, []byte("testValue1")) != 0 {
t.Error("did not get correct value in deep subtree")
}
value, err = dsmst.Get([]byte("testKey2"))
if err != nil {
t.Error("returned error when getting value in deep subtree")
}
if bytes.Compare(value, []byte("testValue2")) != 0 {
t.Error("did not get correct value in deep subtree")
}
value, err = dsmst.Get([]byte("testKey5"))
if err != nil {
t.Error("returned error when getting value in deep subtree")
}
if bytes.Compare(value, defaultValue) != 0 {
t.Error("did not get correct value in deep subtree")
}
value, err = dsmst.Get([]byte("testKey6"))
if err == nil {
t.Error("did not error when getting non-added value in deep subtree")
}

dsmst.Update([]byte("testKey1"), []byte("testValue3"))
dsmst.Update([]byte("testKey2"), defaultValue)
dsmst.Update([]byte("testKey5"), []byte("testValue5"))

value, err = dsmst.Get([]byte("testKey1"))
if err != nil {
t.Error("returned error when getting value in deep subtree")
}
if bytes.Compare(value, []byte("testValue3")) != 0 {
t.Error("did not get correct value in deep subtree")
}
value, err = dsmst.Get([]byte("testKey2"))
if err != nil {
t.Error("returned error when getting value in deep subtree")
}
if bytes.Compare(value, defaultValue) != 0 {
t.Error("did not get correct value in deep subtree")
}
value, err = dsmst.Get([]byte("testKey5"))
if err != nil {
t.Error("returned error when getting value in deep subtree")
}
if bytes.Compare(value, []byte("testValue5")) != 0 {
t.Error("did not get correct value in deep subtree")
}

smt.Update([]byte("testKey1"), []byte("testValue3"))
smt.Update([]byte("testKey2"), defaultValue)
smt.Update([]byte("testKey5"), []byte("testValue5"))

if bytes.Compare(smt.Root(), dsmst.Root()) != 0 {
t.Error("roots of identical standard tree and subtree do not match")
}
}

func TestDeepSparseMerkleSubTreeBadInput(t *testing.T) {
smt := NewSparseMerkleTree(NewSimpleMap(), sha256.New())

smt.Update([]byte("testKey1"), []byte("testValue1"))
smt.Update([]byte("testKey2"), []byte("testValue2"))
smt.Update([]byte("testKey3"), []byte("testValue3"))
smt.Update([]byte("testKey4"), []byte("testValue4"))

badProof, _ := smt.Prove([]byte("testKey1"))
badProof.SideNodes[0][0] = byte(0)

dsmst := NewDeepSparseMerkleSubTree(NewSimpleMap(), sha256.New(), smt.Root())
err := dsmst.AddBranch(badProof, []byte("testKey1"), []byte("testValue1"))
if _, ok := err.(*BadProofError); !ok {
t.Error("did not return BadProofError for bad proof input")
}
}
Loading