Skip to content

Commit 4443702

Browse files
committed
SubtreeInclusionProof Tests
1 parent 5afe747 commit 4443702

2 files changed

Lines changed: 263 additions & 6 deletions

File tree

proof/proof.go

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -62,23 +62,23 @@ func SubtreeInclusion(index, start, end uint64) (Nodes, error) {
6262
if start >= end {
6363
return Nodes{}, fmt.Errorf("start %d greater than or equal to end %d", start, end)
6464
}
65-
if index < start || index > end {
66-
return Nodes{}, fmt.Errorf("index %d out of bounds for subtree range [%d, %d)", index, start, end)
65+
if index < start || index >= end {
66+
return Nodes{}, fmt.Errorf("index %d out of bounds for subtree [%d, %d)", index, start, end)
6767
}
6868
if err := checkSubtreeAlignment(start, end); err != nil {
6969
return Nodes{}, err
7070
}
7171

72-
// Shift the subtree to the left such that it starts at 0.
73-
p := nodes(index-start, 0, end-start)
72+
// Shift the subtree to the left, such that it starts at 0.
73+
p := nodes(index-start, 0, end-start).skipFirst()
7474

75-
// Shift all nodes back to the right.
75+
// Shift nodes back to the right, in line with the original subtree position.
7676
for n := range p.IDs {
7777
p.IDs[n].Index += start >> p.IDs[n].Level
7878
}
7979
p.ephem.Index += start >> p.ephem.Level
8080

81-
return p.skipFirst(), nil
81+
return p, nil
8282
}
8383

8484
// Consistency returns the information on how to fetch and construct a

proof/proof_test.go

Lines changed: 257 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ package proof
1616

1717
import (
1818
"fmt"
19+
"math/bits"
1920
"testing"
2021

2122
"github.com/google/go-cmp/cmp"
@@ -130,6 +131,179 @@ func TestInclusion(t *testing.T) {
130131
}
131132
}
132133

134+
// TestSubtreeInclusion contains subtree inclusion proof tests. For reference, consider the
135+
// following example of a tree from RFC 6962:
136+
//
137+
// aaaaa <== Level 4
138+
// / \
139+
// ... ...
140+
// / \
141+
// / \
142+
// / \
143+
// aaaa bbbb <== Level 3
144+
// / \ / \
145+
// / \ / \
146+
// / \ / \
147+
// / \ / \
148+
// / \ / \
149+
// aaa bbb ccc ddd <== Level 2
150+
// / \ / \ / \ / \
151+
// / \ / \ / \ / \
152+
// / \ / \ / \ / \
153+
// aa bb cc dd ee ff gg hh ii <== Level 1
154+
// / \ / \ / \ / \ / \ / \ / \ / \ / \
155+
// a b c d e f g h i j k l m n o p q r <== Level 0
156+
// | | | | | | | | | | | | | | | | | |
157+
// d0 d1 d2 d3 d4 d5 d6 d7 d8 d9 d10 d12 d14 d16
158+
// | | | |
159+
// d11 d13 d15 d17
160+
//
161+
// Our storage node layers are always populated from the bottom up, hence the
162+
// gaps above ii.
163+
func TestSubtreeInclusion(t *testing.T) {
164+
id := compact.NewNodeID
165+
nodes := func(ids ...compact.NodeID) Nodes {
166+
return Nodes{IDs: ids}
167+
}
168+
rehash := func(begin, end int, ids ...compact.NodeID) Nodes {
169+
return Nodes{IDs: ids, begin: begin, end: end}
170+
}
171+
for _, tc := range []struct {
172+
index uint64 // Leaf index in the requested tree.
173+
start uint64 // The smallest index of the subtree.
174+
end uint64 // The largest index of the subtree + 1.
175+
want Nodes
176+
wantErr bool
177+
}{
178+
// Errors.
179+
{start: 0, end: 0, index: 0, wantErr: true}, // everything at 0
180+
{start: 1, end: 1, index: 0, wantErr: true}, // start = end
181+
{start: 2, end: 1, index: 0, wantErr: true}, // start > end
182+
{start: 1, end: 2, index: 0, wantErr: true}, // index out of bounds left
183+
{start: 0, end: 2, index: 3, wantErr: true}, // index out of bounds right
184+
{start: 0, end: 3, index: 3, wantErr: true}, // index out of bounds right
185+
{start: 3, end: 5, index: 3, wantErr: true}, // start not multiple of bit_ceil(len)
186+
{start: 1, end: 1<<63 + 2, index: 1, wantErr: true}, // start not multiple of bit_ceil(len) with big tree
187+
188+
// Small trees.
189+
{start: 0, end: 1, index: 0, want: Nodes{IDs: []compact.NodeID{}}},
190+
{start: 0, end: 2, index: 0, want: nodes(id(0, 1))}, // b
191+
{start: 0, end: 2, index: 1, want: nodes(id(0, 0))}, // a
192+
{start: 0, end: 3, index: 1, want: rehash(1, 2, id(0, 0), id(0, 2))}, // a c
193+
194+
// Small subtrees.
195+
// Small tree shifted by bit_ceil(len).
196+
{start: 1, end: 2, index: 1, want: Nodes{IDs: []compact.NodeID{}}},
197+
{start: 2, end: 3, index: 2, want: Nodes{IDs: []compact.NodeID{}}},
198+
{start: 7, end: 8, index: 7, want: Nodes{IDs: []compact.NodeID{}}},
199+
{start: 2, end: 4, index: 2, want: nodes(id(0, 3))}, // d
200+
{start: 2, end: 4, index: 3, want: nodes(id(0, 2))}, // c
201+
{start: 4, end: 7, index: 4, want: rehash(1, 2,
202+
id(0, 5), id(0, 6))}, // f, j
203+
{start: 4, end: 7, index: 6, want: nodes(id(1, 2))}, // i
204+
205+
// Tree of size 7.
206+
{start: 0, end: 7, index: 0, want: rehash(2, 4, // bbb=hash(cc,g)
207+
id(0, 1), id(1, 1), id(0, 6), id(1, 2))}, // b bb g cc
208+
{start: 0, end: 7, index: 1, want: rehash(2, 4, // bbb=hash(cc,g)
209+
id(0, 0), id(1, 1), id(0, 6), id(1, 2))}, // a bb g cc
210+
{start: 0, end: 7, index: 2, want: rehash(2, 4, // bbb=hash(cc,g)
211+
id(0, 3), id(1, 0), id(0, 6), id(1, 2))}, // d aa g cc
212+
{start: 0, end: 7, index: 3, want: rehash(2, 4, // bbb=hash(cc,g)
213+
id(0, 2), id(1, 0), id(0, 6), id(1, 2))}, // c aa g cc
214+
{start: 0, end: 7, index: 4, want: rehash(1, 2,
215+
id(0, 5), id(0, 6), id(2, 0))}, // f g aaa
216+
{start: 0, end: 7, index: 5, want: rehash(1, 2,
217+
id(0, 4), id(0, 6), id(2, 0))}, // e g aaa
218+
{start: 0, end: 7, index: 6, want: nodes(id(1, 2), id(2, 0))}, // i k
219+
220+
// Subtree of size 7.
221+
// Tree of size 7 shifted by bit_ceil(len).
222+
{start: 8, end: 15, index: 8, want: rehash(2, 4, // ddd=hash(gg,o)
223+
id(0, 9), id(1, 5), id(0, 14), id(1, 6))}, // j ff o gg
224+
{start: 8, end: 15, index: 9, want: rehash(2, 4, // ddd=hash(gg,o)
225+
id(0, 8), id(1, 5), id(0, 14), id(1, 6))}, // j ff o gg
226+
{start: 8, end: 15, index: 10, want: rehash(2, 4, // ddd=hash(gg,o)
227+
id(0, 11), id(1, 4), id(0, 14), id(1, 6))}, // l ee o gg
228+
{start: 8, end: 15, index: 11, want: rehash(2, 4, // ddd=hash(gg, o)
229+
id(0, 10), id(1, 4), id(0, 14), id(1, 6))}, // k ee o gg
230+
{start: 8, end: 15, index: 12, want: rehash(1, 2,
231+
id(0, 13), id(0, 14), id(2, 2))}, // n o ccc
232+
{start: 8, end: 15, index: 13, want: rehash(1, 2,
233+
id(0, 12), id(0, 14), id(2, 2))}, // m o ccc
234+
{start: 8, end: 15, index: 14, want: nodes(id(1, 6), id(2, 2))}, // gg ccc
235+
236+
// Smaller trees within a bigger stored tree.
237+
// start = 0
238+
{start: 0, end: 4, index: 2, want: nodes(id(0, 3), id(1, 0))}, // d aa
239+
{start: 0, end: 5, index: 3, want: rehash(2, 3, id(0, 2), id(1, 0), id(0, 4))}, // c aa e
240+
{start: 0, end: 6, index: 3, want: rehash(2, 3, id(0, 2), id(1, 0), id(1, 2))}, // c aa i
241+
{start: 0, end: 6, index: 4, want: nodes(id(0, 5), id(2, 0))}, // f aaa
242+
{start: 0, end: 7, index: 1, want: rehash(2, 4, // bbb=hash(cc,g)
243+
id(0, 0), id(1, 1), id(0, 6), id(1, 2))}, // a bb g cc
244+
{start: 0, end: 7, index: 3, want: rehash(2, 4, // bbb=hash(cc,g)
245+
id(0, 2), id(1, 0), id(0, 6), id(1, 2))}, // c aa g cc
246+
// Shifted by bit_ceil(len).
247+
{start: 4, end: 8, index: 6, want: nodes(id(0, 7), id(1, 2))}, // h cc
248+
{start: 8, end: 13, index: 11, want: rehash(2, 3, id(0, 10), id(1, 4), id(0, 12))}, // k ee m
249+
{start: 8, end: 14, index: 11, want: rehash(2, 3, id(0, 10), id(1, 4), id(1, 6))}, // k, ee, gg
250+
{start: 8, end: 14, index: 12, want: nodes(id(0, 13), id(2, 2))}, // n ccc
251+
{start: 8, end: 15, index: 9, want: rehash(2, 4, // ddd=hash(gg,o)
252+
id(0, 8), id(1, 5), id(0, 14), id(1, 6))}, // i ff o gg
253+
{start: 8, end: 15, index: 11, want: rehash(2, 4, // bbb=hash(cc,g)
254+
id(0, 10), id(1, 4), id(0, 14), id(1, 6))}, // k ff q gg
255+
256+
// Some rehashes in the middle of the returned list.
257+
{start: 0, end: 15, index: 10, want: rehash(2, 4,
258+
id(0, 11), id(1, 4),
259+
id(0, 14), id(1, 6),
260+
id(3, 0),
261+
)},
262+
{start: 16, end: 31, index: 26, want: rehash(2, 4,
263+
id(0, 27), id(1, 12),
264+
id(0, 30), id(1, 14),
265+
id(3, 2),
266+
)},
267+
{start: 0, end: 31, index: 24, want: rehash(2, 4,
268+
id(0, 25), id(1, 13),
269+
id(0, 30), id(1, 14),
270+
id(3, 2), id(4, 0),
271+
)},
272+
{start: 32, end: 63, index: 56, want: rehash(2, 4,
273+
id(0, 57), id(1, 29),
274+
id(0, 62), id(1, 30),
275+
id(3, 6), id(4, 2),
276+
)},
277+
{start: 0, end: 95, index: 81, want: rehash(3, 6,
278+
id(0, 80), id(1, 41), id(2, 21),
279+
id(0, 94), id(1, 46), id(2, 22),
280+
id(4, 4), id(6, 0),
281+
)},
282+
{start: 128, end: 223, index: 209, want: rehash(3, 6,
283+
id(0, 208), id(1, 105), id(2, 53),
284+
id(0, 222), id(1, 110), id(2, 54),
285+
id(4, 12), id(6, 2),
286+
)},
287+
} {
288+
t.Run(fmt.Sprintf("%d:%d:%d", tc.start, tc.end, tc.index), func(t *testing.T) {
289+
proof, err := SubtreeInclusion(tc.index, tc.start, tc.end)
290+
if tc.wantErr {
291+
if err == nil {
292+
t.Fatal("accepted bad params")
293+
}
294+
return
295+
} else if err != nil {
296+
t.Fatalf("Inclusion: %v", err)
297+
}
298+
// Ignore the ephemeral node, it is tested separately.
299+
proof.ephem = compact.NodeID{}
300+
if diff := cmp.Diff(tc.want, proof, cmp.AllowUnexported(Nodes{})); diff != "" {
301+
t.Errorf("paths mismatch:\n%v", diff)
302+
}
303+
})
304+
}
305+
}
306+
133307
// TestConsistency contains consistency proof tests. For reference, consider
134308
// the following example:
135309
//
@@ -248,6 +422,22 @@ func TestInclusionSucceedsUpToTreeSize(t *testing.T) {
248422
}
249423
}
250424

425+
func TestInclusionSubtreeSucceedsUpToTreeSize(t *testing.T) {
426+
const maxSize = uint64(555)
427+
for sbe := uint64(1); sbe <= maxSize; sbe++ {
428+
for sbs := uint64(0); sbs < sbe; sbs++ {
429+
if bc := uint64(1) << bits.Len64(sbe-sbs-1); sbs%bc != 0 {
430+
continue
431+
}
432+
for i := sbs; i < sbe; i++ {
433+
if _, err := SubtreeInclusion(i, sbs, sbe); err != nil {
434+
t.Errorf("SubtreeInclusion(i:%d, sbs:%d, sbe: %d) = %v", i, sbs, sbe, err)
435+
}
436+
}
437+
}
438+
}
439+
}
440+
251441
func TestConsistencySucceedsUpToTreeSize(t *testing.T) {
252442
const maxSize = uint64(100)
253443
for s1 := uint64(1); s1 < maxSize; s1++ {
@@ -305,6 +495,73 @@ func TestEphem(t *testing.T) {
305495
}
306496
}
307497

498+
func TestEphemSubtree(t *testing.T) {
499+
id := compact.NewNodeID
500+
for _, tc := range []struct {
501+
index uint64
502+
start uint64
503+
end uint64
504+
want compact.NodeID
505+
}{
506+
// Edge case: For perfect trees (resp. subtree) the ephemeral node is the
507+
// sibling of the root (resp subtree root). However, it will not be used in
508+
// the proof, as the corresponding subtree is empty.
509+
{index: 3, start: 0, end: 32, want: id(5, 1)},
510+
{index: 35, start: 32, end: 64, want: id(5, 2)},
511+
512+
// start = 0
513+
{index: 0, start: 0, end: 9, want: id(3, 1)},
514+
{index: 0, start: 0, end: 13, want: id(3, 1)},
515+
{index: 7, start: 0, end: 13, want: id(3, 1)},
516+
{index: 8, start: 0, end: 13, want: id(2, 3)},
517+
{index: 11, start: 0, end: 13, want: id(2, 3)},
518+
// More edge cases when the computed ephemeral node is not used in the
519+
// proof, because it is fully outside the tree border.
520+
{index: 12, start: 0, end: 13, want: id(0, 13)},
521+
{index: 13, start: 0, end: 14, want: id(1, 7)},
522+
// Shifted by bit_ceil(len).
523+
{index: 16, start: 16, end: 25, want: id(3, 3)},
524+
{index: 16, start: 16, end: 29, want: id(3, 3)},
525+
{index: 23, start: 16, end: 29, want: id(3, 3)},
526+
{index: 24, start: 16, end: 29, want: id(2, 7)},
527+
{index: 27, start: 16, end: 29, want: id(2, 7)},
528+
// More edge cases when the computed ephemeral node is not used in the
529+
// proof, because it is fully outside the tree border.
530+
{index: 28, start: 16, end: 29, want: id(0, 29)},
531+
{index: 29, start: 16, end: 30, want: id(1, 15)},
532+
533+
// There is only one node (level 0, index 1024) in the right subtree, but
534+
// the ephemeral node is at level 10 rather than level 0. This is because
535+
// for the purposes of the proof this node is *effectively* at level 10.
536+
{index: 123, start: 0, end: 1025, want: id(10, 1)},
537+
// Shifted by bit_ceil(len).
538+
{index: 2171, start: 2048, end: 3073, want: id(10, 3)},
539+
540+
{index: 0, start: 0, end: 0xFFFF, want: id(15, 1)},
541+
{index: 0xF000, start: 0, end: 0xFFFF, want: id(11, 0x1F)},
542+
{index: 0xFF00, start: 0, end: 0xFFFF, want: id(7, 0x1FF)},
543+
{index: 0xFFF0, start: 0, end: 0xFFFF, want: id(3, 0x1FFF)},
544+
{index: 0xFFFF - 1, start: 0, end: 0xFFFF, want: id(0, 0xFFFF)},
545+
// Shifted by bit_ceil(len).
546+
{index: 0x10000, start: 0x10000, end: 0x1FFFF, want: id(15, 3)},
547+
{index: 0x1F000, start: 0x10000, end: 0x1FFFF, want: id(11, 0x3F)},
548+
{index: 0x1FF00, start: 0x10000, end: 0x1FFFF, want: id(7, 0x3FF)},
549+
{index: 0x1FFF0, start: 0x10000, end: 0x1FFFF, want: id(3, 0x3FFF)},
550+
{index: 0x1FFFF - 1, start: 0x10000, end: 0x1FFFF, want: id(0, 0x1FFFF)},
551+
} {
552+
t.Run(fmt.Sprintf("%d:%d:%d", tc.index, tc.start, tc.end), func(t *testing.T) {
553+
nodes, err := SubtreeInclusion(tc.index, tc.start, tc.end)
554+
if err != nil {
555+
t.Fatalf("SubtreeInclusion: %v", err)
556+
}
557+
got, _, _ := nodes.Ephem()
558+
if want := tc.want; got != want {
559+
t.Errorf("Ephem: got %+v, want %+v", got, want)
560+
}
561+
})
562+
}
563+
}
564+
308565
func TestRehash(t *testing.T) {
309566
th := rfc6962.DefaultHasher
310567
h := [][]byte{

0 commit comments

Comments
 (0)