Skip to content

Commit 13ed7b6

Browse files
committed
prefix: add membership proofs
1 parent a5c502b commit 13ed7b6

3 files changed

Lines changed: 175 additions & 43 deletions

File tree

prefix/storage.go

Lines changed: 0 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -3,19 +3,12 @@ package prefix
33
import (
44
"context"
55
"errors"
6-
"slices"
76
)
87

98
type Storage interface {
109
// Load retrieves the node with the given label.
1110
Load(ctx context.Context, label Label) (*Node, error)
1211

13-
// LoadPath loads the siblings of the path to reach the given node
14-
// (intuitively, the inclusion proof). If the node is not present, the
15-
// sequence stops with what would be its sibling if it were present. The
16-
// returned nodes are ordered from the node sibling up to the root's child.
17-
LoadPath(ctx context.Context, label Label) ([]*Node, error)
18-
1912
// Store stores the given nodes. If a node with the same label already
2013
// exists, it is replaced. The nodes can be in any order.
2114
Store(ctx context.Context, nodes ...*Node) error
@@ -40,29 +33,6 @@ func (s *memoryStorage) Load(ctx context.Context, label Label) (*Node, error) {
4033
return nil, ErrNodeNotFound
4134
}
4235

43-
func (s *memoryStorage) LoadPath(ctx context.Context, label Label) ([]*Node, error) {
44-
var nodes []*Node
45-
node := s.nodes[RootLabel]
46-
for node.Label != label {
47-
if !label.HasPrefix(node.Label) {
48-
if node.Label != EmptyNodeLabel {
49-
nodes = append(nodes, node)
50-
}
51-
break
52-
}
53-
switch label.SideOf(node.Label) {
54-
case Left:
55-
nodes = append(nodes, s.nodes[node.Right])
56-
node = s.nodes[node.Left]
57-
case Right:
58-
nodes = append(nodes, s.nodes[node.Left])
59-
node = s.nodes[node.Right]
60-
}
61-
}
62-
slices.Reverse(nodes)
63-
return nodes, nil
64-
}
65-
6636
func (s *memoryStorage) Store(ctx context.Context, nodes ...*Node) error {
6737
for _, node := range nodes {
6838
s.nodes[node.Label] = node

prefix/tree.go

Lines changed: 99 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import (
1818
"encoding/binary"
1919
"errors"
2020
"fmt"
21+
"slices"
2122
"strings"
2223
)
2324

@@ -127,10 +128,86 @@ func InitStorage(ctx context.Context, h HashFunc, s Storage) error {
127128
return s.Store(ctx, newEmptyNode(h), newRootNode(h))
128129
}
129130

131+
func (t *Tree) RootHash(ctx context.Context) ([32]byte, error) {
132+
root, err := t.s.Load(ctx, RootLabel)
133+
if err != nil {
134+
return [32]byte{}, fmt.Errorf("failed to load root: %w", err)
135+
}
136+
return root.Hash, nil
137+
}
138+
139+
type ProofNode struct {
140+
Label Label
141+
Hash [32]byte
142+
}
143+
144+
func (t *Tree) Lookup(ctx context.Context, label [32]byte) ([]ProofNode, error) {
145+
l := Label{256, label}
146+
if _, err := t.s.Load(ctx, l); err != nil {
147+
return nil, fmt.Errorf("failed to load node %s: %w", l, err)
148+
}
149+
path, err := loadPath(ctx, t.s, l)
150+
if err != nil {
151+
return nil, fmt.Errorf("failed to load path for node %s: %w", l, err)
152+
}
153+
proof := make([]ProofNode, 0, len(path))
154+
for _, sibling := range path {
155+
proof = append(proof, ProofNode{
156+
Label: sibling.Label,
157+
Hash: sibling.Hash,
158+
})
159+
}
160+
return proof, nil
161+
}
162+
163+
// loadPath loads the siblings of the path to reach the given node
164+
// (intuitively, the inclusion proof). If the node is not present, the
165+
// sequence stops with what would be its sibling if it were present. The
166+
// returned nodes are ordered from the node sibling up to the root's child.
167+
func loadPath(ctx context.Context, s Storage, label Label) ([]*Node, error) {
168+
// If the Storage has a custom implementation of LoadPath, use it.
169+
if s, ok := s.(interface {
170+
LoadPath(context.Context, Label) ([]*Node, error)
171+
}); ok {
172+
return s.LoadPath(ctx, label)
173+
}
174+
var nodes []*Node
175+
node, err := s.Load(ctx, RootLabel)
176+
if err != nil {
177+
return nil, fmt.Errorf("failed to load root: %w", err)
178+
}
179+
for node.Label != label {
180+
if !label.HasPrefix(node.Label) {
181+
if node.Label != EmptyNodeLabel {
182+
nodes = append(nodes, node)
183+
}
184+
break
185+
}
186+
left, err := s.Load(ctx, node.Left)
187+
if err != nil {
188+
return nil, fmt.Errorf("failed to load left node %s: %w", node.Left, err)
189+
}
190+
right, err := s.Load(ctx, node.Right)
191+
if err != nil {
192+
return nil, fmt.Errorf("failed to load left node %s: %w", node.Right, err)
193+
}
194+
switch label.SideOf(node.Label) {
195+
case Left:
196+
nodes = append(nodes, right)
197+
node = left
198+
case Right:
199+
nodes = append(nodes, left)
200+
node = right
201+
}
202+
}
203+
slices.Reverse(nodes)
204+
return nodes, nil
205+
}
206+
130207
func (t *Tree) Insert(ctx context.Context, label, value [32]byte) error {
131208
leaf := newLeaf(t.h, label, value)
132209

133-
path, err := t.s.LoadPath(ctx, leaf.Label)
210+
path, err := loadPath(ctx, t.s, leaf.Label)
134211
if err != nil {
135212
return err
136213
}
@@ -148,3 +225,24 @@ func (t *Tree) Insert(ctx context.Context, label, value [32]byte) error {
148225

149226
return t.s.Store(ctx, changed...)
150227
}
228+
229+
func VerifyMembershipProof(h HashFunc, label, value [32]byte, proof []ProofNode, root [32]byte) error {
230+
node := newLeaf(h, label, value)
231+
for _, sibling := range proof {
232+
var err error
233+
node, err = newParentNode(h, node, &Node{
234+
Label: sibling.Label,
235+
Hash: sibling.Hash,
236+
})
237+
if err != nil {
238+
return fmt.Errorf("failed to compute parent node: %w", err)
239+
}
240+
}
241+
if node.Label != RootLabel {
242+
return fmt.Errorf("proof does not lead to root, got %s", node.Label)
243+
}
244+
if node.Hash != root {
245+
return fmt.Errorf("proof does not match root hash, got %x, want %x", node.Hash, root)
246+
}
247+
return nil
248+
}

prefix/tree_test.go

Lines changed: 76 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"encoding/binary"
77
"encoding/hex"
88
"math/rand/v2"
9+
"runtime"
910
"testing"
1011

1112
"lukechampine.com/blake3"
@@ -48,9 +49,8 @@ func testFullTree(t *testing.T, newStorage func(t *testing.T) Storage) {
4849
fatalIfErr(t, tree.Insert(t.Context(), label, value))
4950
}
5051

51-
root, err := store.Load(t.Context(), RootLabel)
52+
rootHash, err := tree.RootHash(t.Context())
5253
fatalIfErr(t, err)
53-
rootHash := root.Hash
5454

5555
store = newStorage(t)
5656
fatalIfErr(t, InitStorage(t.Context(), blake3.Sum256, store))
@@ -63,10 +63,10 @@ func testFullTree(t *testing.T, newStorage func(t *testing.T) Storage) {
6363
fatalIfErr(t, tree.Insert(t.Context(), label, value))
6464
}
6565

66-
root, err = store.Load(t.Context(), RootLabel)
66+
rootHash1, err := tree.RootHash(t.Context())
6767
fatalIfErr(t, err)
68-
if root.Hash != rootHash {
69-
t.Fatalf("after inserting in reverse order: got %x, want %x", root.Hash, rootHash)
68+
if rootHash1 != rootHash {
69+
t.Fatalf("after inserting in reverse order: got %x, want %x", rootHash1, rootHash)
7070
}
7171

7272
store = newStorage(t)
@@ -80,35 +80,39 @@ func testFullTree(t *testing.T, newStorage func(t *testing.T) Storage) {
8080
fatalIfErr(t, tree.Insert(t.Context(), label, value))
8181
}
8282

83-
root, err = store.Load(t.Context(), RootLabel)
83+
rootHash1, err = tree.RootHash(t.Context())
8484
fatalIfErr(t, err)
85-
if root.Hash != rootHash {
86-
t.Fatalf("after inserting in random order: got %x, want %x", root.Hash, rootHash)
85+
if rootHash1 != rootHash {
86+
t.Fatalf("after inserting in random order: got %x, want %x", rootHash1, rootHash)
8787
}
8888
}
8989

9090
func TestAccumulated(t *testing.T) {
9191
testAllStorage(t, testAccumulated)
9292
}
9393
func testAccumulated(t *testing.T, newStorage func(t *testing.T) Storage) {
94+
if _, ok := newStorage(t).(*prefixsqlite.Storage); ok && testing.Short() {
95+
t.Skip("skipping accumulated test for sqlite storage in short mode")
96+
}
97+
9498
source := blake3.New(0, nil).XOF()
9599
sink := blake3.New(32, nil)
96100

97101
for range 100 {
98102
store := newStorage(t)
99103
fatalIfErr(t, InitStorage(t.Context(), blake3.Sum256, store))
100104
tree := NewTree(blake3.Sum256, store)
101-
root, err := store.Load(t.Context(), RootLabel)
105+
rootHash, err := tree.RootHash(t.Context())
102106
fatalIfErr(t, err)
103-
sink.Write(root.Hash[:])
107+
sink.Write(rootHash[:])
104108
for range 1000 {
105109
var label, value [32]byte
106110
source.Read(label[:])
107111
source.Read(value[:])
108112
fatalIfErr(t, tree.Insert(t.Context(), label, value))
109-
root, err := store.Load(t.Context(), RootLabel)
113+
rootHash, err := tree.RootHash(t.Context())
110114
fatalIfErr(t, err)
111-
sink.Write(root.Hash[:])
115+
sink.Write(rootHash[:])
112116
}
113117
}
114118

@@ -119,6 +123,66 @@ func testAccumulated(t *testing.T, newStorage func(t *testing.T) Storage) {
119123
}
120124
}
121125

126+
func TestMemoryUsage(t *testing.T) {
127+
if testing.Short() {
128+
t.Skip("skipping memory usage test in short mode")
129+
}
130+
131+
store := NewMemoryStorage()
132+
fatalIfErr(t, InitStorage(t.Context(), blake3.Sum256, store))
133+
tree := NewTree(blake3.Sum256, store)
134+
135+
runtime.GC()
136+
var start runtime.MemStats
137+
runtime.ReadMemStats(&start)
138+
139+
source := blake3.New(0, nil).XOF()
140+
for n := range 1000000 {
141+
var label, value [32]byte
142+
source.Read(label[:])
143+
source.Read(value[:])
144+
fatalIfErr(t, tree.Insert(t.Context(), label, value))
145+
146+
switch n + 1 {
147+
case 1000, 10000, 100000, 1000000:
148+
runtime.GC()
149+
var m runtime.MemStats
150+
runtime.ReadMemStats(&m)
151+
t.Logf("Memory usage after inserting % 8d nodes: % 10d bytes", n+1, int64(m.Alloc)-int64(start.Alloc))
152+
}
153+
}
154+
}
155+
156+
func TestMembershipProof(t *testing.T) {
157+
testAllStorage(t, testMembershipProof)
158+
}
159+
func testMembershipProof(t *testing.T, newStorage func(t *testing.T) Storage) {
160+
store := newStorage(t)
161+
fatalIfErr(t, InitStorage(t.Context(), blake3.Sum256, store))
162+
tree := NewTree(blake3.Sum256, store)
163+
164+
var entries [][32]byte
165+
for _, n := range rand.Perm(100) {
166+
var label [32]byte
167+
binary.LittleEndian.PutUint16(label[:], uint16(n))
168+
value := blake3.Sum256(label[:])
169+
fatalIfErr(t, tree.Insert(t.Context(), label, value))
170+
entries = append(entries, label)
171+
172+
rootHash, err := tree.RootHash(t.Context())
173+
fatalIfErr(t, err)
174+
175+
for _, label := range entries {
176+
value := blake3.Sum256(label[:])
177+
proof, err := tree.Lookup(t.Context(), label)
178+
fatalIfErr(t, err)
179+
if err := VerifyMembershipProof(blake3.Sum256, label, value, proof, rootHash); err != nil {
180+
t.Fatalf("membership proof for %x with %d entries failed: %v", label, len(entries), err)
181+
}
182+
}
183+
}
184+
}
185+
122186
func fatalIfErr(t *testing.T, err error) {
123187
if err != nil {
124188
t.Helper()

0 commit comments

Comments
 (0)