Skip to content

Commit 6f8b72c

Browse files
committed
Implement get-compact-range using RFC 6962 methods
This is a proof of concept change demonstrating that it is possible to obtain arbitrary compact ranges from a Merkle tree log that restricts itself only to endpoints represented in RFC 6962, in constant time interaction complexity. Specifically, it is possible to obtain comact range [begin, end) by calling "get consistency proof" endpoints <= 2 times for carefully crafted tree sizes. In a few cases where it is impossible to get certain hashes, this approach falls back to calling the "get entries" endpoint 1 time to obtain between 1-3 entries and reconstruct the compact range. Overall, the interaction with the log is limited by 2 calls, and each call is limited in size.
1 parent c120179 commit 6f8b72c

File tree

5 files changed

+271
-0
lines changed

5 files changed

+271
-0
lines changed

exp/README.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
Experimental
2+
------------
3+
4+
This directory contains a Go module witth experimental features not included
5+
into the main Go module of this repository. These must be used with caution.
6+
7+
The idea of this module is similar to Go's https://pkg.go.dev/golang.org/x/exp.

exp/get_compact_range.go

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
package merkle
2+
3+
import (
4+
"fmt"
5+
6+
"github.com/transparency-dev/merkle/compact"
7+
"github.com/transparency-dev/merkle/proof"
8+
)
9+
10+
type HashGetter interface {
11+
GetConsistencyProof(first, second uint64) ([][]byte, error)
12+
GetLeafHashes(begin, end uint64) ([][]byte, error)
13+
}
14+
15+
func GetCompactRange(rf *compact.RangeFactory, begin, end, size uint64, hg HashGetter) (*compact.Range, error) {
16+
if begin > size || end > size {
17+
return nil, fmt.Errorf("[%d, %d) out of range in %d", begin, end, size)
18+
}
19+
if begin >= end {
20+
return rf.NewEmptyRange(begin), nil
21+
}
22+
23+
if size <= 3 || end == 1 {
24+
hashes, err := hg.GetLeafHashes(begin, end)
25+
if err != nil {
26+
return nil, fmt.Errorf("GetLeafHashes(%d, %d): %v", begin, end, err)
27+
}
28+
if got, want := uint64(len(hashes)), end-begin; got != want {
29+
return nil, fmt.Errorf("GetLeafHashes(%d, %d): %d hashes, want %d", begin, end, got, want)
30+
}
31+
r := rf.NewEmptyRange(begin)
32+
for _, h := range hashes {
33+
if err := r.Append(h, nil); err != nil {
34+
return nil, fmt.Errorf("Append: %v", err)
35+
}
36+
}
37+
return r, nil
38+
}
39+
// size >= 4 && end >= 2
40+
41+
known := make(map[compact.NodeID][]byte)
42+
43+
store := func(nodes proof.Nodes, hashes [][]byte) error {
44+
_, b, e := nodes.Ephem()
45+
wantSize := len(nodes.IDs) - (e - b)
46+
if b != e {
47+
wantSize++
48+
}
49+
if got := len(hashes); got != wantSize {
50+
return fmt.Errorf("proof size mismatch: got %d, want %d", got, wantSize)
51+
}
52+
53+
idx := 0
54+
for _, hash := range hashes {
55+
if idx == b && b+1 < e {
56+
idx = e - 1
57+
continue
58+
}
59+
known[nodes.IDs[idx]] = hash
60+
idx++
61+
}
62+
return nil
63+
}
64+
65+
newRange := func(begin, end uint64) (*compact.Range, error) {
66+
size := compact.RangeSize(begin, end)
67+
ids := compact.RangeNodes(begin, end, make([]compact.NodeID, 0, size))
68+
hashes := make([][]byte, 0, len(ids))
69+
for _, id := range ids {
70+
if hash, ok := known[id]; ok {
71+
hashes = append(hashes, hash)
72+
} else {
73+
return nil, fmt.Errorf("hash not known: %+v", id)
74+
}
75+
}
76+
return rf.NewRange(begin, end, hashes)
77+
}
78+
79+
fetch := func(first, second uint64) error {
80+
nodes, err := proof.Consistency(first, second)
81+
if err != nil {
82+
return fmt.Errorf("proof.Consistency: %v", err)
83+
}
84+
hashes, err := hg.GetConsistencyProof(first, second)
85+
if err != nil {
86+
return fmt.Errorf("GetConsistencyProof(%d, %d): %v", first, second, err)
87+
}
88+
store(nodes, hashes)
89+
return nil
90+
}
91+
92+
mid, _ := compact.Decompose(begin, end)
93+
mid += begin
94+
if err := fetch(begin, mid); err != nil {
95+
return nil, err
96+
}
97+
98+
if begin == 0 && end == 2 || end == 3 {
99+
if err := fetch(3, 4); err != nil {
100+
return nil, err
101+
}
102+
}
103+
if end <= 3 {
104+
return newRange(begin, end)
105+
}
106+
// end >= 4
107+
108+
if (end-1)&(end-2) != 0 { // end-1 is not a power of 2.
109+
if err := fetch(end-1, end); err != nil {
110+
return nil, err
111+
}
112+
r, err := newRange(begin, end-1)
113+
if err != nil {
114+
return nil, err
115+
}
116+
if err := r.Append(known[compact.NewNodeID(0, end-1)], nil); err != nil {
117+
return nil, fmt.Errorf("Append: %v", err)
118+
}
119+
return r, nil
120+
}
121+
122+
// At this point: end >= 4, end-1 is a power of 2; thus, end-2 is not a power of 2.
123+
if err := fetch(end-2, end); err != nil {
124+
return nil, err
125+
}
126+
r := rf.NewEmptyRange(begin)
127+
if end-2 > begin {
128+
var err error
129+
if r, err = newRange(begin, end-2); err != nil {
130+
return nil, err
131+
}
132+
}
133+
for index := r.End(); index < end; index++ {
134+
if err := r.Append(known[compact.NewNodeID(0, index)], nil); err != nil {
135+
return nil, fmt.Errorf("Append: %v", err)
136+
}
137+
}
138+
return r, nil
139+
}

exp/get_compact_range_test.go

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
package merkle_test
2+
3+
import (
4+
"fmt"
5+
"testing"
6+
7+
"github.com/google/go-cmp/cmp"
8+
"github.com/transparency-dev/merkle"
9+
"github.com/transparency-dev/merkle/compact"
10+
"github.com/transparency-dev/merkle/proof"
11+
)
12+
13+
func TestGetCompactRange(t *testing.T) {
14+
rf := compact.RangeFactory{Hash: func(left, right []byte) []byte {
15+
return append(append(make([]byte, 0, len(left)+len(right)), left...), right...)
16+
}}
17+
tr := newTree(t, 256, &rf)
18+
19+
test := func(begin, end, size uint64) {
20+
t.Run(fmt.Sprintf("%d:%d_%d", size, begin, end), func(t *testing.T) {
21+
got, err := merkle.GetCompactRange(&rf, begin, end, size, tr)
22+
if err != nil {
23+
t.Fatalf("GetCompactRange: %v", err)
24+
}
25+
want, err := tr.getCompactRange(begin, end)
26+
if err != nil {
27+
t.Fatalf("GetCompactRange: %v", err)
28+
}
29+
if diff := cmp.Diff(got, want); diff != "" {
30+
t.Fatalf("Diff: %s", diff)
31+
}
32+
})
33+
}
34+
35+
for begin := uint64(0); begin <= tr.size; begin++ {
36+
for end := begin; end <= tr.size; end++ {
37+
for size := end; size < end+5 && size < tr.size; size++ {
38+
test(begin, end, size)
39+
}
40+
test(begin, end, tr.size)
41+
}
42+
}
43+
}
44+
45+
type tree struct {
46+
rf *compact.RangeFactory
47+
size uint64
48+
nodes map[compact.NodeID][]byte
49+
}
50+
51+
func newTree(t *testing.T, size uint64, rf *compact.RangeFactory) *tree {
52+
hash := func(leaf uint64) []byte {
53+
if leaf >= 256 {
54+
t.Fatalf("leaf %d not supported in this test", leaf)
55+
}
56+
return []byte{byte(leaf)}
57+
}
58+
59+
nodes := make(map[compact.NodeID][]byte, size*2-1)
60+
r := rf.NewEmptyRange(0)
61+
for i := uint64(0); i < size; i++ {
62+
nodes[compact.NewNodeID(0, i)] = hash(i)
63+
if err := r.Append(hash(i), func(id compact.NodeID, hash []byte) {
64+
nodes[id] = hash
65+
}); err != nil {
66+
t.Fatalf("Append: %v", err)
67+
}
68+
}
69+
return &tree{rf: rf, size: size, nodes: nodes}
70+
}
71+
72+
func (t *tree) GetConsistencyProof(first, second uint64) ([][]byte, error) {
73+
if first > t.size || second > t.size {
74+
return nil, fmt.Errorf("%d or %d is beyond %d", first, second, t.size)
75+
}
76+
nodes, err := proof.Consistency(first, second)
77+
if err != nil {
78+
return nil, err
79+
}
80+
hashes, err := t.getNodes(nodes.IDs)
81+
if err != nil {
82+
return nil, err
83+
}
84+
return nodes.Rehash(hashes, t.rf.Hash)
85+
}
86+
87+
func (t *tree) GetLeafHashes(begin, end uint64) ([][]byte, error) {
88+
if begin >= end {
89+
return nil, nil
90+
}
91+
ids := make([]compact.NodeID, 0, end-begin)
92+
for i := begin; i < end; i++ {
93+
ids = append(ids, compact.NewNodeID(0, i))
94+
}
95+
return t.getNodes(ids)
96+
}
97+
98+
func (t *tree) getCompactRange(begin, end uint64) (*compact.Range, error) {
99+
hashes, err := t.getNodes(compact.RangeNodes(begin, end))
100+
if err != nil {
101+
return nil, err
102+
}
103+
return t.rf.NewRange(begin, end, hashes)
104+
}
105+
106+
func (t *tree) getNodes(ids []compact.NodeID) ([][]byte, error) {
107+
hashes := make([][]byte, len(ids))
108+
for i, id := range ids {
109+
if hash, ok := t.nodes[id]; ok {
110+
hashes[i] = hash
111+
} else {
112+
return nil, fmt.Errorf("node %+v not found", id)
113+
}
114+
}
115+
return hashes, nil
116+
}

exp/go.mod

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
module github.com/transparency-dev/merkle/exp
2+
3+
go 1.16
4+
5+
require github.com/transparency-dev/merkle v0.0.0-20220425113829-c120179f55ad // indirect

exp/go.sum

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
2+
github.com/transparency-dev/merkle v0.0.0-20220425113829-c120179f55ad h1:82yvTO+VijfWulMsMQvqQSZ0zNEAgmEUeBG+ArrO9Js=
3+
github.com/transparency-dev/merkle v0.0.0-20220425113829-c120179f55ad/go.mod h1:B8FIw5LTq6DaULoHsVFRzYIUDkl8yuSwCdZnOZGKL/A=
4+
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=

0 commit comments

Comments
 (0)