Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(examples): add merkle tree package #631

Merged
merged 7 commits into from
May 24, 2023
Merged
Show file tree
Hide file tree
Changes from all 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
20 changes: 20 additions & 0 deletions examples/gno.land/p/demo/merkle/README.md
Copy link
Member

Choose a reason for hiding this comment

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

maybe add a go equivalent to the js code?

Copy link
Member Author

Choose a reason for hiding this comment

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

the merkle.gno is 100% go compatible.. with a quick look at the gno code, you can have the go code... is it really mendatory ?

Copy link
Member

Choose a reason for hiding this comment

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

No, not necessarily. But it seems weird then that there is an example of JS code inside of the directory of a gno package :)

Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# p/demo/merkle

This package implement a merkle tree that is complient with [merkletreejs](https://github.com/merkletreejs/merkletreejs)

## [merkletreejs](https://github.com/merkletreejs/merkletreejs)

```javascript
const { MerkleTree } = require("merkletreejs");
const SHA256 = require("crypto-js/sha256");

let leaves = [];
for (let i = 0; i < 10; i++) {
leaves.push(SHA256(`node_${i}`));
}

const tree = new MerkleTree(leaves, SHA256);
const root = tree.getRoot().toString("hex");

console.log(root); // cd8a40502b0b92bf58e7432a5abb2d8b60121cf2b7966d6ebaf103f907a1bc21
```
132 changes: 132 additions & 0 deletions examples/gno.land/p/demo/merkle/merkle.gno
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
package merkle

import (
"bytes"
"crypto/sha256"
thehowl marked this conversation as resolved.
Show resolved Hide resolved
"encoding/hex"
"errors"
)

type Hashable interface {
Bytes() []byte
}

type nodes []Node

type Node struct {
hash []byte

position uint8
}

func (n Node) Hash() string {
return hex.EncodeToString(n.hash[:])
}

type Tree struct {
layers []nodes
}

// Root return the merkle root of the tree
func (t *Tree) Root() string {
for _, l := range t.layers {
if len(l) == 1 {
return l[0].Hash()
}
}
return ""
}

// NewTree create a new Merkle Tree
func NewTree(data []Hashable) *Tree {
tree := &Tree{}

leaves := make([]Node, len(data))

for i, d := range data {
hash := sha256.Sum256(d.Bytes())
leaves[i] = Node{hash: hash[:]}
}

tree.layers = []nodes{nodes(leaves)}

var buff bytes.Buffer
for len(leaves) > 1 {
level := make([]Node, 0, len(leaves)/2+1)
for i := 0; i < len(leaves); i += 2 {
buff.Reset()

if i < len(leaves)-1 {
buff.Write(leaves[i].hash)
buff.Write(leaves[i+1].hash)
hash := sha256.Sum256(buff.Bytes())
level = append(level, Node{
hash: hash[:],
})
} else {
level = append(level, leaves[i])
}
}
leaves = level
tree.layers = append(tree.layers, level)
}
return tree
}

// Proof return a MerkleProof
func (t *Tree) Proof(data Hashable) ([]Node, error) {
targetHash := sha256.Sum256(data.Bytes())
targetIndex := -1

for i, layer := range t.layers[0] {
if bytes.Equal(targetHash[:], layer.hash) {
targetIndex = i
break
}
}

if targetIndex == -1 {
return nil, errors.New("target not found")
}

proofs := make([]Node, 0, len(t.layers))

for _, layer := range t.layers {
var pairIndex int

if targetIndex%2 == 0 {
pairIndex = targetIndex + 1
} else {
pairIndex = targetIndex - 1
}
if pairIndex < len(layer) {
proofs = append(proofs, Node{
hash: layer[pairIndex].hash,
position: uint8(targetIndex) % 2,
})
}
targetIndex /= 2
}
return proofs, nil
}

// Verify if a merkle proof is valid
func (t *Tree) Verify(leaf Hashable, proofs []Node) bool {
return Verify(t.Root(), leaf, proofs)
}

// Verify if a merkle proof is valid
func Verify(root string, leaf Hashable, proofs []Node) bool {
hash := sha256.Sum256(leaf.Bytes())

for i := 0; i < len(proofs); i += 1 {
var h []byte
if proofs[i].position == 0 {
h = append(hash[:], proofs[i].hash...)
} else {
h = append(proofs[i].hash, hash[:]...)
}
hash = sha256.Sum256(h)
}
return hex.EncodeToString(hash[:]) == root
}
70 changes: 70 additions & 0 deletions examples/gno.land/p/demo/merkle/merkle_test.gno
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package merkle

import (
"fmt"
"testing"
"time"
)

type testData struct {
content string
}

func (d testData) Bytes() []byte {
return []byte(d.content)
}

func TestMerkleTree(t *testing.T) {
tests := []struct {
size int
expected string
}{
{
size: 1,
expected: "cf9f824bce7f5bc63d557b23591f58577f53fe29f974a615bdddbd0140f912f4",
},
{
size: 3,
expected: "1a4a5f0fa267244bf9f74a63fdf2a87eed5e97e4bd104a9e94728c8fb5442177",
},
{
size: 10,
expected: "cd8a40502b0b92bf58e7432a5abb2d8b60121cf2b7966d6ebaf103f907a1bc21",
},
{
size: 1000,
expected: "fa533d2efdf12be26bc410dfa42936ac63361324e35e9b1ff54d422a1dd2388b",
},
}

for _, test := range tests {
var leaves []Hashable
for i := 0; i < test.size; i++ {
leaves = append(leaves, testData{fmt.Sprintf("node_%d", i)})
}

tree := NewTree(leaves)

if tree == nil {
t.Error("Merkle tree creation failed")
}

root := tree.Root()

if root != test.expected {
t.Fatalf("merkle.Tree.Root(), expected: %s; got: %s", test.expected, root)
}

for _, leaf := range leaves {
proofs, err := tree.Proof(leaf)
if err != nil {
t.Fatal("failed to proof leaf: %v, on tree: %v", leaf, test)
}

ok := Verify(root, leaf, proofs)
if !ok {
t.Fatal("failed to verify leaf: %v, on tree: %v", leaf, tree)
}
}
}
}
2 changes: 2 additions & 0 deletions gnovm/pkg/gnolang/precompile.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,11 @@ var stdlibWhitelist = []string{
"context",
"crypto/md5",
"crypto/sha1",
"crypto/sha256",
"encoding/json",
"encoding/base64",
"encoding/binary",
"encoding/hex",
moul marked this conversation as resolved.
Show resolved Hide resolved
"encoding/xml",
"errors",
"flag",
Expand Down