Skip to content

Commit d95600c

Browse files
committed
SubtreeInclusionProof Tests
1 parent 7a046fc commit d95600c

2 files changed

Lines changed: 256 additions & 4 deletions

File tree

proof/proof.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -59,17 +59,17 @@ func SubtreeInclusion(index, start, end uint64) (Nodes, error) {
5959
if start >= end {
6060
return Nodes{}, fmt.Errorf("start %d larger or equal than end %d", start, end)
6161
}
62-
if index < start || index > end {
63-
return Nodes{}, fmt.Errorf("index %d out of bounds for subtree range [%d, %d)", index, start, end)
62+
if index < start || index >= end {
63+
return Nodes{}, fmt.Errorf("index %d out of bounds for subtree [%d, %d)", index, start, end)
6464
}
6565
if bc := uint64(1) << bits.Len64(end-start-1); start%bc != 0 {
6666
return Nodes{}, fmt.Errorf("start %d not a multiple of bit_ceil(end - start) = %d", start, bc)
6767
}
6868

69-
// Shift the subtree to the left such that it starts at 0.
69+
// Shift the subtree to the left, such that it starts at 0.
7070
p := nodes(index-start, 0, end-start)
7171

72-
// Shift all nodes back to the right.
72+
// Shift nodes back to the right, in line with the original subtree position.
7373
for n := range p.IDs {
7474
p.IDs[n].Index += start >> p.IDs[n].Level
7575
}

proof/proof_test.go

Lines changed: 252 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,178 @@ func TestInclusion(t *testing.T) {
130130
}
131131
}
132132

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

423+
func TestInclusionSubtreeSucceedsUpToTreeSize(t *testing.T) {
424+
const maxSize = uint64(555)
425+
for sbe := uint64(1); sbe <= maxSize; sbe++ {
426+
for sbs := uint64(1); sbs < sbe; sbs++ {
427+
for i := sbe; i < sbe; i++ {
428+
if _, err := SubtreeInclusion(i, sbs, sbe); err != nil {
429+
t.Errorf("SubtreeInclusion(ts:%d, i:%d) = %v", sbe, i, err)
430+
}
431+
}
432+
}
433+
}
434+
}
435+
251436
func TestConsistencySucceedsUpToTreeSize(t *testing.T) {
252437
const maxSize = uint64(100)
253438
for s1 := uint64(1); s1 < maxSize; s1++ {
@@ -305,6 +490,73 @@ func TestEphem(t *testing.T) {
305490
}
306491
}
307492

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

0 commit comments

Comments
 (0)