Skip to content

Commit 2097c8d

Browse files
committed
SubtreeInclusionProof Tests
1 parent 02d6bfc commit 2097c8d

2 files changed

Lines changed: 264 additions & 4 deletions

File tree

proof/proof.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -59,8 +59,8 @@ 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 shift := bits.Len64(end - start - 1); shift >= 64 {
6666
if start != 0 {
@@ -70,10 +70,10 @@ func SubtreeInclusion(index, start, end uint64) (Nodes, error) {
7070
return Nodes{}, fmt.Errorf("start %d not a multiple of bit_ceil(end - start) = %d", start, bc)
7171
}
7272

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

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

proof/proof_test.go

Lines changed: 260 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,25 @@ 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+
bc := uint64(1) << bits.Len64(sbe-sbs-1)
430+
wantErr := sbs%bc != 0
431+
for i := sbs; i < sbe; i++ {
432+
_, err := SubtreeInclusion(i, sbs, sbe)
433+
if !wantErr && err != nil {
434+
t.Errorf("SubtreeInclusion(i:%d, sbs:%d, sbe: %d) = %v", i, sbs, sbe, err)
435+
}
436+
if wantErr && err == nil {
437+
t.Errorf("SubtreeInclusion(i:%d, sbs:%d, sbe: %d) = %v", i, sbs, sbe, err)
438+
}
439+
}
440+
}
441+
}
442+
}
443+
251444
func TestConsistencySucceedsUpToTreeSize(t *testing.T) {
252445
const maxSize = uint64(100)
253446
for s1 := uint64(1); s1 < maxSize; s1++ {
@@ -305,6 +498,73 @@ func TestEphem(t *testing.T) {
305498
}
306499
}
307500

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

0 commit comments

Comments
 (0)