Skip to content

Commit 76c956a

Browse files
authored
Add FindSubtrees (transparency-dev#250)
Add a FindSubtrees func which implements an extended version of the MTC algorithm for determining the one or two subtrees which efficiently cover a range of entries.
1 parent 600c703 commit 76c956a

3 files changed

Lines changed: 107 additions & 0 deletions

File tree

proof/proof.go

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -268,6 +268,45 @@ func reverse(ids []compact.NodeID) {
268268
}
269269
}
270270

271+
// Subtree represents a valid Merkle subtree [Start, End).
272+
type Subtree struct {
273+
Start, End uint64
274+
}
275+
276+
// FindSubtrees returns one or two subtrees that efficiently cover [start, end).
277+
//
278+
// If the provided [start, end) range is already a valid subtree, then that subtree is returned directly.
279+
// Otherwise, this function continues by applying the "Selecting Two Subtrees" procedure
280+
// from Section 4.5.1 of draft-ietf-plants-merkle-tree-certs.
281+
//
282+
// Note that:
283+
// - If the provided [start, end) range is already a valid subtree, then it is returned as the only entry in the slice.
284+
// - The 2nd subtree, if present, is adjacent to the first, and may not be a perfect subtree.
285+
// - The returned subtree(s) fully cover the [start, end) range.
286+
// - There are no "extra" entries covered past end, but there may be covered entries prior to start.
287+
// - The number of entries covered before start is always less than half the size of the first returned subtree.
288+
func FindSubtrees(start, end uint64) ([]Subtree, error) {
289+
if start >= end {
290+
return nil, fmt.Errorf("start %d must be strictly less than end %d", start, end)
291+
}
292+
if end-start == 1 || isSubtreeValid(start, end) == nil {
293+
return []Subtree{{Start: start, End: end}}, nil
294+
}
295+
last := end - 1
296+
// Find where start and last's tree paths diverge.
297+
split := bits.Len64(start^last) - 1
298+
mask := (uint64(1) << split) - 1
299+
mid := last & ^mask
300+
301+
// Maximize the left endpoint.
302+
leftSplit := bits.Len64(^start & mask)
303+
leftStart := start & ^((uint64(1) << leftSplit) - 1)
304+
return []Subtree{
305+
{Start: leftStart, End: mid},
306+
{Start: mid, End: end},
307+
}, nil
308+
}
309+
271310
// isSubtreeValid returns whether a subtree covers a valid range.
272311
// A subtree is valid if there exist a parent tree node to:
273312
// - all the subtree nodes

proof/proof_test.go

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -820,3 +820,41 @@ func inclusion(t *testing.T, index, size uint64) Nodes {
820820
}
821821
return n
822822
}
823+
824+
func TestFindSubtrees(t *testing.T) {
825+
for _, tc := range []struct {
826+
start, end uint64
827+
want []Subtree
828+
wantErr bool
829+
}{
830+
// Already-valid subtrees are returned as-is.
831+
// Single entry subtrees:
832+
{start: 0, end: 1, want: []Subtree{{Start: 0, End: 1}}},
833+
{start: 3, end: 4, want: []Subtree{{Start: 3, End: 4}}},
834+
// Perfectly aligned subtrees:
835+
{start: 4, end: 6, want: []Subtree{{Start: 4, End: 6}}},
836+
{start: 16, end: 32, want: []Subtree{{Start: 16, End: 32}}},
837+
// Non-perfect trees are split into two:
838+
{start: 5, end: 13, want: []Subtree{{Start: 4, End: 8}, {Start: 8, End: 13}}},
839+
{start: 7, end: 9, want: []Subtree{{Start: 7, End: 8}, {Start: 8, End: 9}}},
840+
// Invalid inputs:
841+
{start: 5, end: 5, wantErr: true},
842+
{start: 6, end: 5, wantErr: true},
843+
} {
844+
t.Run(fmt.Sprintf("%d:%d", tc.start, tc.end), func(t *testing.T) {
845+
got, err := FindSubtrees(tc.start, tc.end)
846+
if tc.wantErr {
847+
if err == nil {
848+
t.Fatal("expected error, got nil")
849+
}
850+
return
851+
}
852+
if err != nil {
853+
t.Fatalf("FindSubtrees: %v", err)
854+
}
855+
if diff := cmp.Diff(tc.want, got); diff != "" {
856+
t.Errorf("FindSubtrees mismatch (-want +got):\n%s", diff)
857+
}
858+
})
859+
}
860+
}

testonly/vectors_test.go

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ import (
1919
"fmt"
2020
"io"
2121
"testing"
22+
23+
"github.com/transparency-dev/merkle/proof"
2224
)
2325

2426
// These tests reproduce the accumulated test vectors from the "Subtree Test
@@ -123,3 +125,31 @@ func TestSubtreeConsistencyProofVectors(t *testing.T) {
123125
t.Errorf("subtree consistency proof vector = %s, want %s", got, want)
124126
}
125127
}
128+
129+
func TestSubtreeCoveringVectors(t *testing.T) {
130+
h := sha256.New()
131+
for end := uint64(1); end <= subtreeVectorMax; end++ {
132+
for start := range end {
133+
if err := isSubtreeValid(start, end); err == nil {
134+
if _, err := fmt.Fprintf(h, "[%d, %d)\n", start, end); err != nil {
135+
t.Fatalf("fmt.Fprintf: %v", err)
136+
}
137+
} else {
138+
subtrees, err := proof.FindSubtrees(start, end)
139+
if err != nil {
140+
t.Fatalf("FindSubtrees(%d, %d): %v", start, end, err)
141+
}
142+
if l := len(subtrees); l != 2 {
143+
t.Fatalf("FindSubtrees(%d, %d) returned unexpected number of subtrees: %d", start, end, l)
144+
}
145+
if _, err := fmt.Fprintf(h, "[%d, %d) [%d, %d)\n", subtrees[0].Start, subtrees[0].End, subtrees[1].Start, subtrees[1].End); err != nil {
146+
t.Fatalf("fmt.Fprintf: %v", err)
147+
}
148+
}
149+
}
150+
}
151+
const want = "e0aecb912a10c57d753b6ecc64db73217f9bc4ed10fcb4e9062be3b6fbe1ebfd"
152+
if got := fmt.Sprintf("%x", h.Sum(nil)); got != want {
153+
t.Errorf("subtree covering vector = %s, want %s", got, want)
154+
}
155+
}

0 commit comments

Comments
 (0)