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

[Exploration] Using Celestia's SMT for Pocket Network V1? #74

Draft
wants to merge 26 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
b574a50
Add Makefile with test_all property
Olshansk Aug 5, 2022
cef5d99
General comment updates before adding treehasher test
Olshansk Oct 1, 2022
0e2da02
Added TestTreeHasherPath
Olshansk Oct 1, 2022
fed5f94
Slight simplifcation to digestLeaf
Olshansk Oct 1, 2022
915a4e9
Added tests and updated code for treeHasher
Olshansk Oct 1, 2022
0d8e5f1
Do not expose updateForRoot function
Olshansk Oct 1, 2022
9cbd11d
Do not expose setRoot function
Olshansk Oct 1, 2022
0e0808b
Do not expose deleteForRoot function
Olshansk Oct 1, 2022
1ce3acc
Delete deleteForRoot altogether
Olshansk Oct 1, 2022
3ea32eb
Added questions to deleteWithSideNodes
Olshansk Oct 1, 2022
dcb4743
Minor optimization to countCommonPrefix
Olshansk Oct 1, 2022
9fb2fdb
Specified return types for sideNodesForRoot
Olshansk Oct 1, 2022
d2f0789
Remove getSiblingData from interface in sideNodesForRoot
Olshansk Oct 1, 2022
3c5608f
Finished reading (but don't fully understand) sideNodesForRoot and up…
Olshansk Oct 1, 2022
f1bd40b
Finished reading through smt.go
Olshansk Oct 1, 2022
093f1fb
Added personal checklist
Olshansk Oct 1, 2022
b0bd128
Very minor cleanup to MapStore
Olshansk Oct 1, 2022
fdb7138
Don't pass around a pointer to maps in the tests
Olshansk Oct 1, 2022
e5af727
Add questions and improve readability in smt.go
Olshansk Oct 2, 2022
88ec5fa
Add questions and improve readability in treehasher.go
Olshansk Oct 2, 2022
8a1793b
Add TODOs in README
Olshansk Oct 2, 2022
77c9425
Commit before improving readability of updateWithSideNodes
Olshansk Oct 2, 2022
ee24777
INterim commit before dsplot
Olshansk Oct 23, 2022
ef6cebd
Got some sort of visualization going
Olshansk Oct 24, 2022
a8c621a
Remove OLSH logs
Olshansk Oct 24, 2022
4765876
Some old changes
Olshansk Feb 25, 2023
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
Binary file added .DS_Store
Binary file not shown.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
.vscode
33 changes: 33 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
.SILENT:

help:
printf "Available targets\n\n"
awk '/^[a-zA-Z\-\_0-9]+:/ { \
helpMessage = match(lastLine, /^## (.*)/); \
if (helpMessage) { \
helpCommand = substr($$1, 0, index($$1, ":")-1); \
helpMessage = substr(lastLine, RSTART + 3, RLENGTH); \
printf "%-30s %s\n", helpCommand, helpMessage; \
} \
} \
{ lastLine = $$0 }' $(MAKEFILE_LIST)

.PHONY: test_all
## Run all the unit tests
test_all:
go test -v -count=1 ./...

.PHONY: test_smt
## Run all the ^TestSparseMerkleTree unit tests
test_smt:
go test -v -count=1 -run TestSparseMerkleTree ./...

.PHONY: test_th
## Run all the ^TestTreeHasher unit tests
test_th:
go test -v -count=1 -run TestTreeHasher ./...

.PHONY: test_ms
## Run all the ^TestMapStore unit tests
test_ms:
go test -v -count=1 -run TestMapStore ./...
97 changes: 73 additions & 24 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,44 +1,93 @@
# smt
# Sparse Merkle Tree (smt)

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][libra whitepaper], 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.
A Go library that implements a Sparse Merkle Tree for a key-value map. The tree implements the same optimisations specified in the [Jellyfish Merkle Tree whitepaper][jmt whitepaper] originally designed for the [Libra blockchain][libra whitepaper]. It reduces the number of hash operations required per tree operation to `O(k)` where `k` is the number of non-empty elements in the tree.
Copy link
Member

@musalbas musalbas Oct 31, 2022

Choose a reason for hiding this comment

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

This library is not based on JMT, just an optimized SMT. The JMT was introduced after the original Libra whitepaper, which is basically an optimized SMT but with further optimizations on the storage layer (storing it as a 16-ary tree, etc), which we don't make here yet. The original Libra whitepaper mentioned an optimized SMT, but did not introduce the JMT (with extra storage optimizations).

Copy link
Author

Choose a reason for hiding this comment

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

Seems like a JMT is just an SMT with r=16 + RocksDB + versioned keys.

Screen Shot 2022-10-31 at 6 39 04 PM


[![Tests](https://github.com/celestiaorg/smt/actions/workflows/test.yml/badge.svg)](https://github.com/celestiaorg/smt/actions/workflows/test.yml)
[![codecov](https://codecov.io/gh/celestiaorg/smt/branch/master/graph/badge.svg?token=U3GGEDSA94)](https://codecov.io/gh/celestiaorg/smt)
[![GoDoc](https://godoc.org/github.com/celestiaorg/smt?status.svg)](https://godoc.org/github.com/celestiaorg/smt)

## Installation

```bash
go get github.com/celestiaorg/smt@master
```

## Example

```go
package main

import (
"crypto/sha256"
"fmt"
"crypto/sha256"
"fmt"

"github.com/celestiaorg/smt"
"github.com/celestiaorg/smt"
)

func main() {
// Initialise two new key-value store to store the nodes and values of the tree
nodeStore := smt.NewSimpleMap()
valueStore := smt.NewSimpleMap()
// Initialise the tree
tree := smt.NewSparseMerkleTree(nodeStore, valueStore, sha256.New())

// Update the key "foo" with the value "bar"
_, _ = tree.Update([]byte("foo"), []byte("bar"))

// Generate a Merkle proof for foo=bar
proof, _ := tree.Prove([]byte("foo"))
root := tree.Root() // We also need the current tree root for the proof

// Verify the Merkle proof for foo=bar
if smt.VerifyProof(proof, root, []byte("foo"), []byte("bar"), sha256.New()) {
fmt.Println("Proof verification succeeded.")
} else {
fmt.Println("Proof verification failed.")
}
// Initialise 2 new key-value store to stores the nodes and values of the tree
nodeStore := smt.NewSimpleMap() // Mapping from hash -> data;
valueStore := smt.NewSimpleMap() // Mapping from node_path -> node_value; a path can be retrieved using the digest of the key

// Initialise the smt
tree := smt.NewSparseMerkleTree(nodeStore, valueStore, sha256.New())

// Update the key "foo" with the value "bar"
_, _ = tree.Update([]byte("foo"), []byte("bar"))

// Generate a Merkle proof for foo=bar
proof, _ := tree.Prove([]byte("foo"))
root := tree.Root() // We also need the current tree root for the proof

// Verify the Merkle proof for foo=bar
if smt.VerifyProof(proof, root, []byte("foo"), []byte("bar"), sha256.New()) {
fmt.Println("Proof verification succeeded.")
} else {
fmt.Println("Proof verification failed.")
}
}
```

## Development

Run `make` to see all the options available

## General Improvements / TODOs

- [ ] Use the `require` test module to simplify unit tests; can be done with a single clever regex find+replace
- [ ] Create types for `sideNodes`, `root`, etc...
- [ ] Add an interface for `SparseMerkleProof` so we can return nils and not access vars directly
- [ ] Add an interface for `SparseMerkleTree` so it's clear how we should interact with it
- [ ] If we create an interface for `TreeHasher`, we can embed it in `SparseMerkleTree` and then avoid the need to write things like `smt.th.path(...)` everywhere and use `smt.path(...)` directly.
- [ ] Consider splitting `smt.go` into `smt_ops.go` and `smt_proofs.go`
- [ ] Functions like `sideNodesForRoot` and `updateWithSideNodes` need to be split into smaller more compartmentalized functions

[libra whitepaper]: https://diem-developers-components.netlify.app/papers/the-diem-blockchain/2020-05-26.pdf
[jmt whitepaper]: https://developers.diem.com/papers/jellyfish-merkle-tree/2021-01-14.pdf

### [Delete me later] personal checklist

- [x] ├── LICENSE
- [x] ├── Makefile
- [x] ├── README.md
- [ ] ├── bench_test.go
- [ ] ├── bulk_test.go
- [ ] ├── deepsubtree.go
- [ ] ├── deepsubtree_test.go
- [ ] ├── fuzz
- [ ] │   ├── delete
- [ ] │   │   └── fuzz.go
- [ ] │   └── fuzz.go
- [x] ├── go.mod
- [x] ├── go.sum
- [x] ├── mapstore.go
- [x] ├── mapstore_test.go
- [x] ├── options.go
- [ ] ├── oss-fuzz-build.sh
- [ ] ├── proofs.go
- [ ] ├── proofs_test.go
- [x] ├── smt.go
- [ ] ├── smt_test.go
- [x] ├── treehasher.go
- [x] ├── treehasher_test.go
- [x] └── utils.go
Binary file added bits.csv
Binary file not shown.
57 changes: 34 additions & 23 deletions bulk_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import (
"math/rand"
"reflect"
"testing"

"github.com/stretchr/testify/require"
)

// Test all tree operations in bulk.
Expand All @@ -21,7 +23,7 @@ func TestSparseMerkleTree(t *testing.T) {
}

// 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) {
func bulkOperations(t *testing.T, operations, insert, update, delete int) (*SparseMerkleTree, *SimpleMap, *SimpleMap, map[string]string) {
smn, smv := NewSimpleMap(), NewSimpleMap()
smt := NewSparseMerkleTree(smn, smv, sha256.New())

Expand Down Expand Up @@ -74,12 +76,13 @@ func bulkOperations(t *testing.T, operations int, insert int, update int, delete
}
}

bulkCheckAll(t, smt, &kv)
bulkCheckAll(t, smt, kv)
}
return smt, smn, smv, kv
}

func bulkCheckAll(t *testing.T, smt *SparseMerkleTree, kv *map[string]string) {
for k, v := range *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)
Expand Down Expand Up @@ -109,28 +112,36 @@ func bulkCheckAll(t *testing.T, smt *SparseMerkleTree, kv *map[string]string) {
}

// 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
}
largestCommonPrefix := getLargestCommonPrefix(t, smt, kv, k)
numSideNodes := getNumSideNodes(t, smt, kv, k)
if (numSideNodes != largestCommonPrefix+1) && numSideNodes != 0 && largestCommonPrefix != 0 {
t.Error("leaf is at unexpected height")
}
sideNodes, _, _, _, err := smt.sideNodesForRoot(smt.th.path([]byte(k)), smt.Root(), false)
if err != nil {
t.Errorf("error: %v", err)
}
}

func getNumSideNodes(t *testing.T, smt *SparseMerkleTree, kv map[string]string, key string) (numSideNodes int) {
path := smt.th.path([]byte(key))
sideNodes, _, _, _, err := smt.sideNodesForRoot(path, smt.Root())
require.NoError(t, err)
for _, v := range sideNodes {
if v != nil {
numSideNodes++
}
numSideNodes := 0
for _, v := range sideNodes {
if v != nil {
numSideNodes++
}
}
return
}

func getLargestCommonPrefix(_ *testing.T, smt *SparseMerkleTree, kv map[string]string, key string) (largestCommonPrefix int) {
path := smt.th.path([]byte(key))
for k, v := range kv {
if v == "" {
continue
}
if numSideNodes != largestCommonPrefix+1 && (numSideNodes != 0 && largestCommonPrefix != 0) {
t.Error("leaf is at unexpected height")
commonPrefix := countCommonPrefix(path, smt.th.path([]byte(k)))
if commonPrefix != smt.depth() && commonPrefix > largestCommonPrefix {
largestCommonPrefix = commonPrefix
}
}
return
}
2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
module github.com/celestiaorg/smt

go 1.14

require github.com/stretchr/testify v1.8.0
14 changes: 14 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
9 changes: 4 additions & 5 deletions mapstore.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@ import (

// MapStore is a key-value store.
type MapStore interface {
Get(key []byte) ([]byte, error) // Get gets the value for a key.
Set(key []byte, value []byte) error // Set updates the value for a key.
Delete(key []byte) error // Delete deletes a key.
Get(key []byte) ([]byte, error) // Get gets the value for a key.
Set(key, value []byte) error // Set updates the value for a key.
Delete(key []byte) error // Delete deletes a key.
}

// InvalidKeyError is thrown when a key that does not exist is being accessed.
Expand Down Expand Up @@ -48,8 +48,7 @@ func (sm *SimpleMap) Set(key []byte, value []byte) error {

// Delete deletes a key.
func (sm *SimpleMap) Delete(key []byte) error {
_, ok := sm.m[string(key)]
if ok {
if _, ok := sm.m[string(key)]; ok {
delete(sm.m, string(key))
return nil
}
Expand Down
14 changes: 8 additions & 6 deletions mapstore_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,26 +6,28 @@ import (
"testing"
)

func TestSimpleMap(t *testing.T) {
func TestMapStoreSimpleMap(t *testing.T) {
sm := NewSimpleMap()
h := sha256.New()

var value []byte
var err error

h.Write([]byte("test"))
key := h.Sum(nil)

// Tests for Get.
_, err = sm.Get(h.Sum(nil))
_, err = sm.Get(key)
if err == nil {
t.Error("did not return an error when getting a non-existent key")
}

// Tests for Put.
err = sm.Set(h.Sum(nil), []byte("hello"))
err = sm.Set(key, []byte("hello"))
if err != nil {
t.Error("updating a key returned an error")
}
value, err = sm.Get(h.Sum(nil))
value, err = sm.Get(key)
if err != nil {
t.Error("getting a key returned an error")
}
Expand All @@ -34,11 +36,11 @@ func TestSimpleMap(t *testing.T) {
}

// Tests for Del.
err = sm.Delete(h.Sum(nil))
err = sm.Delete(key)
if err != nil {
t.Error("deleting a key returned an error")
}
_, err = sm.Get(h.Sum(nil))
_, err = sm.Get(key)
if err == nil {
t.Error("failed to delete key")
}
Expand Down
Loading