Skip to content

Commit 1dc9004

Browse files
committed
SubtreeConsistency
1 parent 2acfad1 commit 1dc9004

2 files changed

Lines changed: 259 additions & 2 deletions

File tree

proof/proof.go

Lines changed: 58 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -73,8 +73,8 @@ func SubtreeInclusion(index, start, end uint64) (Nodes, error) {
7373
for n := range p.IDs {
7474
p.IDs[n].Index += start >> p.IDs[n].Level
7575
}
76-
// For consistency, always shift p.ephem, regardless of whether it will be
77-
// used by the proof.
76+
// p.ephem might not be used by the resulting proof, but shift it
77+
// unconditionally for uniformity.
7878
p.ephem.Index += start >> p.ephem.Level
7979

8080
return p, nil
@@ -110,6 +110,62 @@ func Consistency(size1, size2 uint64) (Nodes, error) {
110110
return p, nil
111111
}
112112

113+
// SubtreeConsistency returns the information on how to fetch and construct a
114+
// consistency proof between a Merkle subtree covering [start, end) and log
115+
// Merkle tree of a given size. It requires:
116+
// - 0 <= start < end <= size
117+
// - start to be a multiple of the smallest power of two greater than or equal to
118+
// (end - start)
119+
func SubtreeConsistency(start, end, size uint64) (Nodes, error) {
120+
if err := isSubtreeValid(start, end); err != nil {
121+
return Nodes{}, fmt.Errorf("subtree invalid: %v", err)
122+
}
123+
if end > size {
124+
return Nodes{}, fmt.Errorf("subtree end %d strictly greater than tree size %d", end, size)
125+
}
126+
if start == 0 && end == size {
127+
return Nodes{IDs: []compact.NodeID{}}, nil
128+
}
129+
130+
// If end == size, prove inclusion of [start, end) into the tree.
131+
if end == size {
132+
// Find the subtree's root, the lowest common ancestor of entries |start| and
133+
// |end-1|.
134+
level := uint(bits.Len64((end - 1) ^ start))
135+
index := (end - 1) >> level
136+
137+
// Shift the tree down by |level|.
138+
p := nodes(index, 0, index+1)
139+
// The first node of the proof is the subtree's root. It is already known
140+
// by the client and can be skipped.
141+
p = p.skipFirst()
142+
143+
// Shift the nodes back up.
144+
for n := range p.IDs {
145+
p.IDs[n].Level += level
146+
}
147+
// p.ephem might not be used by the resulting proof, but shift it
148+
// unconditionally for uniformity.
149+
p.ephem.Level += level
150+
return p, nil
151+
}
152+
153+
// Find the root of the biggest perfect subtree of [start, end) ending at end.
154+
level := uint(bits.TrailingZeros64(end - start))
155+
index := (end - 1) >> level
156+
157+
// The consistency proof consists of this node (except if the subtree is full,
158+
// in which case adding this node would be redundant because the client is
159+
// assumed to know it from a checkpoint), and nodes of the inclusion proof
160+
// of this node in the tree of the given size.
161+
p := nodes(index, level, size)
162+
// Handle the case when the subtree size is a power of 2.
163+
if (end-start)&(end-start-1) == 0 {
164+
return p.skipFirst(), nil
165+
}
166+
return p, nil
167+
}
168+
113169
// nodes returns the node IDs necessary to prove that the (level, index) node
114170
// is included in the Merkle tree of the given size.
115171
func nodes(index uint64, level uint, size uint64) Nodes {

proof/proof_test.go

Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -412,6 +412,191 @@ func TestConsistency(t *testing.T) {
412412
}
413413
}
414414

415+
// TestSubtreeConsistency contains consistency proof tests. For reference, consider
416+
// the following example:
417+
//
418+
// aaaaa <== Level 4
419+
// / \
420+
// ... ...
421+
// / \
422+
// / \
423+
// / \
424+
// aaaa bbbb <== Level 3
425+
// / \ / \
426+
// / \ / \
427+
// / \ / \
428+
// / \ / \
429+
// / \ / \
430+
// aaa bbb ccc ddd <== Level 2
431+
// / \ / \ / \ / \
432+
// / \ / \ / \ / \
433+
// / \ / \ / \ / \
434+
// aa bb cc dd ee ff gg hh ii <== Level 1
435+
// / \ / \ / \ / \ / \ / \ / \ / \ / \
436+
// a b c d e f g h i j k l m n o p q r <== Level 0
437+
// | | | | | | | | | | | | | | | | | |
438+
// d0 d1 d2 d3 d4 d5 d6 d7 d8 d9 d10 d12 d14 d16
439+
// | | | |
440+
// d11 d13 d15 d17
441+
//
442+
// The consistency proof between tree size 5 and 7 consists of nodes e, f, g,
443+
// and aaa. The node g is taken instead of its missing parent.
444+
func TestSubtreeConsistency(t *testing.T) {
445+
id := compact.NewNodeID
446+
nodes := func(ids ...compact.NodeID) Nodes {
447+
return Nodes{IDs: ids}
448+
}
449+
rehash := func(begin, end int, ids ...compact.NodeID) Nodes {
450+
return Nodes{IDs: ids, begin: begin, end: end}
451+
}
452+
for _, tc := range []struct {
453+
start uint64
454+
end uint64
455+
size uint64
456+
want Nodes
457+
wantErr bool
458+
}{
459+
// Errors.
460+
{start: 0, end: 0, size: 0, wantErr: true}, // start = end = 0
461+
{start: 1, end: 1, size: 1, wantErr: true}, // start = end
462+
{start: 2, end: 1, size: 0, wantErr: true}, // start > end
463+
{start: 0, end: 5, size: 0, wantErr: true}, // end > size
464+
{start: 0, end: 9, size: 8, wantErr: true}, // end > size
465+
{start: 3, end: 5, size: 3, wantErr: true}, // start not multiple of bit_ceil(end-start)
466+
{start: 1, end: 1<<63 + 2, size: 1<<63 + 2, wantErr: true}, // start not multiple of bit_ceil(len) with big tree
467+
468+
// Small trees.
469+
// start = 0
470+
{start: 0, end: 1, size: 2, want: nodes(id(0, 1))}, // b
471+
{start: 0, end: 1, size: 4, want: nodes(id(0, 1), id(1, 1))}, // b bb
472+
{start: 0, end: 1, size: 6, want: rehash(2, 3, id(0, 1), id(1, 1), id(1, 2))}, // b bb cc
473+
{start: 0, end: 2, size: 3, want: rehash(0, 1, id(0, 2))}, // c
474+
{start: 0, end: 2, size: 8, want: nodes(id(1, 1), id(2, 1))}, // bb bbb
475+
{start: 0, end: 3, size: 7, want: rehash(3, 5, // bbb=hash(cc,g)
476+
id(0, 2), id(0, 3), id(1, 0), id(0, 6), id(1, 2))}, // c d aa g cc
477+
{start: 0, end: 4, size: 7, want: rehash(0, 2, // bbb=hash(cc,g)
478+
id(0, 6), id(1, 2))}, // g cc
479+
{start: 0, end: 5, size: 7, want: rehash(2, 3,
480+
id(0, 4), id(0, 5), id(0, 6), id(2, 0))}, // e f g aaa
481+
{start: 0, end: 6, size: 7, want: rehash(1, 2,
482+
id(1, 2), id(0, 6), id(2, 0))}, // cc g aaa
483+
{start: 0, end: 6, size: 8, want: nodes(
484+
id(1, 2), id(1, 3), id(2, 0))}, // cc h aaa
485+
{start: 0, end: 7, size: 8, want: nodes(
486+
id(0, 6), id(0, 7), id(1, 2), id(2, 0))}, // g h cc aaa
487+
// start > 0
488+
{start: 1, end: 2, size: 3, want: rehash(1, 2, id(0, 0), id(0, 2))}, // a c
489+
{start: 1, end: 2, size: 5, want: rehash(2, 3, id(0, 0), id(1, 1), id(0, 4))}, // a bb e
490+
{start: 2, end: 4, size: 5, want: rehash(1, 2, id(1, 0), id(0, 4))}, // aa e
491+
{start: 1, end: 2, size: 7, want: rehash(2, 4, id(0, 0), id(1, 1), id(0, 6), id(1, 2))}, // a bb g cc
492+
{start: 2, end: 4, size: 10, want: rehash(2, 3, id(1, 0), id(2, 1), id(1, 4))}, // aa bbb ee
493+
{start: 4, end: 6, size: 10, want: rehash(2, 3, id(1, 3), id(2, 0), id(1, 4))}, // dd aaa ee
494+
{start: 4, end: 7, size: 11, want: rehash(4, 6, // ccc=hash(ee,k)
495+
id(0, 6), id(0, 7), id(1, 2), id(2, 0), id(0, 10), id(1, 4))}, // g h cc aaa k ee
496+
{start: 4, end: 8, size: 11, want: rehash(1, 3, // ccc=hash(ee,k)
497+
id(2, 0), id(0, 10), id(1, 4))}, // aaa k ee
498+
{start: 8, end: 13, size: 15, want: rehash(2, 3,
499+
id(0, 12), id(0, 13), id(0, 14), id(2, 2), id(3, 0))}, // m n o ccc aaaa
500+
{start: 8, end: 14, size: 15, want: rehash(1, 2, // hh=hash(o)
501+
id(1, 6), id(0, 14), id(2, 2), id(3, 0))}, // gg, o, ccc, aaaa
502+
{start: 8, end: 14, size: 16, want: nodes(
503+
id(1, 6), id(1, 7), id(2, 2), id(3, 0))}, // gg hh ccc aaaa
504+
{start: 8, end: 15, size: 16, want: nodes(
505+
id(0, 14), id(0, 15), id(1, 6), id(2, 2), id(3, 0))}, // o p gg ccc aaaa
506+
// end = size
507+
{start: 1, end: 2, size: 2, want: nodes(id(0, 0))}, // a
508+
{start: 3, end: 4, size: 4, want: nodes(id(0, 2), id(1, 0))}, // c aa
509+
{start: 5, end: 6, size: 6, want: nodes(id(0, 4), id(2, 0))}, // e aaa
510+
{start: 2, end: 3, size: 3, want: nodes(id(1, 0))}, // aa
511+
{start: 6, end: 8, size: 8, want: nodes(id(1, 2), id(2, 0))}, // cc aaa
512+
{start: 4, end: 7, size: 7, want: nodes(id(2, 0))}, // aaa
513+
{start: 6, end: 7, size: 7, want: nodes(id(1, 2), id(2, 0))}, // cc aaa
514+
{start: 4, end: 8, size: 8, want: nodes(id(2, 0))}, // aaa
515+
{start: 7, end: 8, size: 8, want: nodes(id(0, 6), id(1, 2), id(2, 0))}, // g h cc aaa
516+
517+
// Same tree size.
518+
{start: 0, end: 1, size: 1, want: Nodes{IDs: []compact.NodeID{}}},
519+
{start: 0, end: 2, size: 2, want: Nodes{IDs: []compact.NodeID{}}},
520+
{start: 0, end: 3, size: 3, want: Nodes{IDs: []compact.NodeID{}}},
521+
{start: 0, end: 4, size: 4, want: Nodes{IDs: []compact.NodeID{}}},
522+
{start: 0, end: 5, size: 5, want: Nodes{IDs: []compact.NodeID{}}},
523+
{start: 0, end: 7, size: 7, want: Nodes{IDs: []compact.NodeID{}}},
524+
{start: 0, end: 8, size: 8, want: Nodes{IDs: []compact.NodeID{}}},
525+
526+
// Smaller trees within a bigger stored tree.
527+
// start = 0
528+
{start: 0, end: 2, size: 4, want: nodes(id(1, 1))}, // bb
529+
{start: 0, end: 3, size: 5, want: rehash(3, 4,
530+
id(0, 2), id(0, 3), id(1, 0), id(0, 4))}, // c d aa e
531+
{start: 0, end: 3, size: 6, want: rehash(3, 4,
532+
id(0, 2), id(0, 3), id(1, 0), id(1, 2))}, // c d aa cc
533+
{start: 0, end: 4, size: 6, want: rehash(0, 1, id(1, 2))}, // cc
534+
{start: 0, end: 1, size: 7, want: rehash(2, 4, // bbb=hash(cc,g)
535+
id(0, 1), id(1, 1), id(0, 6), id(1, 2))}, // b bb g cc
536+
// start > 0
537+
{start: 2, end: 4, size: 6, want: rehash(1, 2, // bbb=hash(cc)
538+
id(1, 0), id(1, 2))}, // aa, cc
539+
{start: 4, end: 7, size: 9, want: rehash(4, 5, // bbbb=hash(i)
540+
id(0, 6), id(0, 7), id(1, 2), id(2, 0), id(0, 8))}, // g h cc aaa i
541+
{start: 4, end: 7, size: 10, want: rehash(4, 5, // bbbb=hash(ee)
542+
id(0, 6), id(0, 7), id(1, 2), id(2, 0), id(1, 4))}, // g h cc aaa ee
543+
{start: 4, end: 8, size: 10, want: rehash(1, 2, //ccc=hash(ee)
544+
id(2, 0), id(1, 4))}, // aa ee
545+
{start: 2, end: 3, size: 9, want: rehash(3, 4, // bbbb=hash(i)
546+
id(0, 3), id(1, 0), id(2, 1), id(0, 8))}, // d aa bbb i
547+
// end = size
548+
{start: 4, end: 6, size: 6, want: nodes(id(2, 0))}, // aaa
549+
{start: 8, end: 9, size: 9, want: nodes(id(3, 0))}, // aaaa
550+
{start: 8, end: 10, size: 10, want: nodes(id(3, 0))}, // aaaa
551+
{start: 8, end: 12, size: 12, want: nodes(id(3, 0))}, // aaaa
552+
553+
// Some rehashes in the middle of the returned list.
554+
{start: 0, end: 10, size: 15, want: rehash(2, 4,
555+
id(1, 4), id(1, 5), id(0, 14), id(1, 6), id(3, 0))},
556+
{start: 16, end: 26, size: 31, want: rehash(2, 4,
557+
id(1, 12), id(1, 13), id(0, 30), id(1, 14), id(3, 2), id(4, 0))},
558+
{start: 0, end: 24, size: 31, want: rehash(1, 4,
559+
id(3, 2),
560+
id(0, 30), id(1, 14), id(2, 6),
561+
id(4, 0),
562+
)},
563+
{start: 32, end: 56, size: 63, want: rehash(1, 4,
564+
id(3, 6),
565+
id(0, 62), id(1, 30), id(2, 14),
566+
id(4, 2),
567+
id(5, 0),
568+
)},
569+
{start: 0, end: 81, size: 95, want: rehash(4, 7,
570+
id(0, 80), id(0, 81), id(1, 41), id(2, 21),
571+
id(0, 94), id(1, 46), id(2, 22),
572+
id(4, 4), id(6, 0),
573+
)},
574+
{start: 128, end: 209, size: 223, want: rehash(4, 7,
575+
id(0, 208), id(0, 209), id(1, 105), id(2, 53),
576+
id(0, 222), id(1, 110), id(2, 54),
577+
id(4, 12), id(6, 2),
578+
id(7, 0),
579+
)},
580+
} {
581+
t.Run(fmt.Sprintf("%d:%d:%d", tc.start, tc.end, tc.size), func(t *testing.T) {
582+
proof, err := SubtreeConsistency(tc.start, tc.end, tc.size)
583+
if tc.wantErr {
584+
if err == nil {
585+
t.Fatal("accepted bad params")
586+
}
587+
return
588+
} else if err != nil {
589+
t.Fatalf("Consistency: %v", err)
590+
}
591+
// Ignore the ephemeral node, it is tested separately.
592+
proof.ephem = compact.NodeID{}
593+
if diff := cmp.Diff(tc.want, proof, cmp.AllowUnexported(Nodes{})); diff != "" {
594+
t.Errorf("paths mismatch:\n%v", diff)
595+
}
596+
})
597+
}
598+
}
599+
415600
func TestInclusionSucceedsUpToTreeSize(t *testing.T) {
416601
const maxSize = uint64(555)
417602
for ts := uint64(1); ts <= maxSize; ts++ {
@@ -450,6 +635,22 @@ func TestConsistencySucceedsUpToTreeSize(t *testing.T) {
450635
}
451636
}
452637

638+
func TestSubtreeConsistencySucceedsUpToTreeSize(t *testing.T) {
639+
const maxSize = uint64(100)
640+
for s := uint64(1); s <= maxSize; s++ {
641+
for sbe := uint64(1); sbe <= s; sbe++ {
642+
for sbs := range sbe {
643+
if err := isSubtreeValid(sbs, sbe); err != nil {
644+
continue
645+
}
646+
if _, err := SubtreeConsistency(sbs, sbe, s); err != nil {
647+
t.Errorf("SubtreeConsistency(sbs:%d, sbe:%d, s:%d) = %v", sbs, sbe, s, err)
648+
}
649+
}
650+
}
651+
}
652+
}
653+
453654
func TestEphem(t *testing.T) {
454655
id := compact.NewNodeID
455656
for _, tc := range []struct {

0 commit comments

Comments
 (0)