Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 63 additions & 0 deletions testonly/reference_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,69 @@ func refConsistencyProof(entries [][]byte, size2, size1 uint64, hasher merkle.Lo
refRootHash(entries[:split], hasher))
}

// refSubtreeConsistencyProof returns the subtree consistency proof for the
// subtree [start, end) in a Merkle tree with the given entries and size.
// This is a reference implementation based on the recursive algorithm from
// the RFC to be used for cross-checking only.
func refSubtreeConsistencyProof(start, end uint64, entries [][]byte, known bool, hasher merkle.LogHasher) [][]byte {
size := uint64(len(entries))
if start >= end {
return nil
}
if end == 0 || end > size {
return nil
}
// Consistency proof between a tree and itself is empty.
if start == 0 && end == size {
// Record the hash of this subtree if it's not the root for which the proof
// was originally requested (which happens when [start, end) is a full subtree).
if !known {
return [][]byte{refRootHash(entries[:size], hasher)}
}
return nil
}

// At this point: end < size.
split := downToPowerOfTwo(size)
switch {
// The subtree is on the left of split. Prove that the subtree is consistent
// with the subtree on the left of split, and record the root of the right
// subtree.
case end <= split:
return append(
refSubtreeConsistencyProof(start, end, entries[:split], known, hasher),
refRootHash(entries[split:], hasher))
// The subtree is on the right of split. Prove that the subtree is consistent
// with the subtree on the right of split, and record the root of the left
// subtree.
case split <= start:
return append(
refSubtreeConsistencyProof(start-split, end-split, entries[split:], known, hasher),
refRootHash(entries[:split], hasher))
// Otherwise, split is between start and end.
// This means that start is 0.
// Prove that the subtree is consistent with the subtree on right of split,
// and record the root of the left subtree.
Comment on lines +144 to +147

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

There is a minor grammatical typo in the comment: on right of split should be on the right of split.

Suggested change
// Otherwise, split is between start and end.
// This means that start is 0.
// Prove that the subtree is consistent with the subtree on right of split,
// and record the root of the left subtree.
// Otherwise, split is between start and end.
// This means that start is 0.
// Prove that the subtree is consistent with the subtree on the right of split,
// and record the root of the left subtree.

//
// Proof that start is 0:
// With C = bitCeil(len([start, end))):
// - By definition, end - start <= C.
// - Since the subtree is valid, start is a multiple of C (start = k * C).
// - In this case, start < split < end <= start + C and
// so k * C < split < (k+1) * C
// - Since split and C are both powers of 2:
// - If split < C, then if k >= 1, split < C <= start, contradicting
// start < split.
// - If split >= C, split must be a multiple of C, but no multiple of
// C lies strictly between k * C and (k + 1) * C.
// - Thus, k must be 0, meaning start is 0.
default:
return append(
refSubtreeConsistencyProof(0, end-split, entries[split:], false, hasher),
refRootHash(entries[:split], hasher))
}
}

// downToPowerOfTwo returns the largest power of two smaller than x.
func downToPowerOfTwo(x uint64) uint64 {
if x < 2 {
Expand Down
12 changes: 12 additions & 0 deletions testonly/tree.go
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,18 @@ func (t *Tree) ConsistencyProof(size1, size2 uint64) ([][]byte, error) {
return nodes.Rehash(t.getNodes(nodes.IDs), t.hasher.HashChildren)
}

// SubtreeConsistencyProof returns the subtree consistency proof between the
// [start, end) subtree and a parent tree of size |size|.
// It requires end <= Size(), and size <= Size(). May panic otherwise.
// May return an error if the subtree boundaries are not valid.
func (t *Tree) SubtreeConsistencyProof(start, end, size uint64) ([][]byte, error) {
nodes, err := proof.SubtreeConsistency(start, end, size)
if err != nil {
return nil, err
}
return nodes.Rehash(t.getNodes(nodes.IDs), t.hasher.HashChildren)
}

func (t *Tree) getNodes(ids []compact.NodeID) [][]byte {
hashes := make([][]byte, len(ids))
for i, id := range ids {
Expand Down
37 changes: 37 additions & 0 deletions testonly/tree_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,43 @@ func TestTreeConsistencyProofFuzz(t *testing.T) {
}
}

func TestSubtreeTreeConsistencyProof(t *testing.T) {
entries := LeafInputs()
mt := newTree(entries)
validateTree(t, mt, 8)

if _, err := mt.SubtreeConsistencyProof(0, 6, 3); err == nil {
t.Error("SubtreeConsistencyProof(0, 6, 3) succeeded unexpectedly (size < end)")
}
if _, err := mt.SubtreeConsistencyProof(3, 3, 8); err == nil {
t.Error("SubtreeConsistencyProof(3, 3, 8) succeeded unexpectedly (start >= end)")
}
if _, err := mt.SubtreeConsistencyProof(1, 3, 8); err == nil {
t.Error("SubtreeConsistencyProof(1, 3, 8) succeeded unexpectedly (invalid subtree)")
}
Comment on lines +204 to +212

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

It would be beneficial to also test that SubtreeConsistencyProof correctly returns an error when provided with an invalid subtree range (e.g., where start is not a multiple of bitCeil(end - start)), in addition to testing the end > size error case.

	if _, err := mt.SubtreeConsistencyProof(0, 6, 3); err == nil {
		t.Error("SubtreeConsistencyProof(0, 6, 3) succeeded unexpectedly")
	}
	if _, err := mt.SubtreeConsistencyProof(1, 3, 4); err == nil {
		t.Error("SubtreeConsistencyProof(1, 3, 4) succeeded unexpectedly")
	}

Comment on lines +204 to +212

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

To ensure comprehensive test coverage of the new SubtreeConsistencyProof API, we should also explicitly test other invalid subtree boundary conditions, such as when start >= end or when start is not properly aligned.

	if _, err := mt.SubtreeConsistencyProof(0, 6, 3); err == nil {
		t.Error("SubtreeConsistencyProof(0, 6, 3) succeeded unexpectedly")
	}
	if _, err := mt.SubtreeConsistencyProof(3, 3, 8); err == nil {
		t.Error("SubtreeConsistencyProof(3, 3, 8) succeeded unexpectedly (start >= end)")
	}
	if _, err := mt.SubtreeConsistencyProof(1, 3, 8); err == nil {
		t.Error("SubtreeConsistencyProof(1, 3, 8) succeeded unexpectedly (invalid alignment)")
	}


maxSize := uint64(len(entries))
for end := uint64(1); end <= maxSize; end++ {
for size := end; size <= maxSize; size++ {
for start := range end {
if err := isSubtreeValid(start, end); err != nil {
continue
}
t.Run(fmt.Sprintf("%d:%d:%d", start, end, size), func(t *testing.T) {
got, err := mt.SubtreeConsistencyProof(start, end, size)
if err != nil {
t.Fatalf("SubtreeConsistencyProof: %v", err)
}
want := refSubtreeConsistencyProof(start, end, entries[:size], true, mt.hasher)
if diff := cmp.Diff(got, want, cmpopts.EquateEmpty()); diff != "" {
t.Errorf("SubtreeConsistencyProof: diff (-got +want)\n%s", diff)
}
})
}
}
}
}

func TestTreeAppend(t *testing.T) {
entries := genEntries(256)
mt1 := newTree(entries)
Expand Down
Loading