From 68dd801dfe858f953558c2c82d93db0b64c35cf3 Mon Sep 17 00:00:00 2001 From: Philippe Boneff Date: Tue, 21 Jul 2026 13:55:28 +0000 Subject: [PATCH 1/5] Align subtrees with updated specs --- cmd/proofgen/main.go | 48 ++++++++++++++----- proof/proof.go | 30 +++++++----- proof/proof_test.go | 21 ++++---- proof/verify.go | 26 +++++++++- .../roots-do-not-match-and-sizes-are-one.json | 10 ++++ ...sizes-do-not-match-and-proof-is-empty.json | 10 ++++ ...ee-is-empty-roots-match-but-not-valid.json | 10 ++++ ...ty-roots-valid-but-proof-is-not-empty.json | 12 +++++ ...e-equal-one-roots-match-but-not-valid.json | 10 ++++ ...ne-roots-valid-but-proof-is-not-empty.json | 12 +++++ ...one-subtree-root-valid-proof-is-empty.json | 10 ++++ ...valid-tree-root-random-proof-is-empty.json | 10 ++++ ...-equal-zero-roots-match-but-not-valid.json | 10 ++++ ...ro-roots-valid-but-proof-is-not-empty.json | 12 +++++ ...ero-subtree-root-valid-proof-is-empty.json | 10 ++++ ...valid-tree-root-random-proof-is-empty.json | 10 ++++ ...pty-subtree-root-valid-proof-is-empty.json | 10 ++++ ...valid-tree-root-random-proof-is-empty.json | 10 ++++ testonly/reference_test.go | 9 +++- testonly/tree.go | 7 ++- testonly/tree_test.go | 9 ++-- testonly/vectors_test.go | 46 ++++++++---------- 22 files changed, 270 insertions(+), 72 deletions(-) create mode 100644 testdata/subtreeconsistency/additional/roots-do-not-match-and-sizes-are-one.json create mode 100644 testdata/subtreeconsistency/additional/sizes-do-not-match-and-proof-is-empty.json create mode 100644 testdata/subtreeconsistency/additional/subtree-is-empty-roots-match-but-not-valid.json create mode 100644 testdata/subtreeconsistency/additional/subtree-is-empty-roots-valid-but-proof-is-not-empty.json create mode 100644 testdata/subtreeconsistency/additional/subtree-is-empty-sizes-are-equal-one-roots-match-but-not-valid.json create mode 100644 testdata/subtreeconsistency/additional/subtree-is-empty-sizes-are-equal-one-roots-valid-but-proof-is-not-empty.json create mode 100644 testdata/subtreeconsistency/additional/subtree-is-empty-sizes-are-equal-one-subtree-root-valid-proof-is-empty.json create mode 100644 testdata/subtreeconsistency/additional/subtree-is-empty-sizes-are-equal-one-subtree-root-valid-tree-root-random-proof-is-empty.json create mode 100644 testdata/subtreeconsistency/additional/subtree-is-empty-sizes-are-equal-zero-roots-match-but-not-valid.json create mode 100644 testdata/subtreeconsistency/additional/subtree-is-empty-sizes-are-equal-zero-roots-valid-but-proof-is-not-empty.json create mode 100644 testdata/subtreeconsistency/additional/subtree-is-empty-sizes-are-equal-zero-subtree-root-valid-proof-is-empty.json create mode 100644 testdata/subtreeconsistency/additional/subtree-is-empty-sizes-are-equal-zero-subtree-root-valid-tree-root-random-proof-is-empty.json create mode 100644 testdata/subtreeconsistency/additional/subtree-is-empty-subtree-root-valid-proof-is-empty.json create mode 100644 testdata/subtreeconsistency/additional/subtree-is-empty-subtree-root-valid-tree-root-random-proof-is-empty.json diff --git a/cmd/proofgen/main.go b/cmd/proofgen/main.go index 360f0fc..5fefe62 100644 --- a/cmd/proofgen/main.go +++ b/cmd/proofgen/main.go @@ -744,18 +744,42 @@ func invalidSubtreeConsistencyProof(size1, size2 uint64, root1, root2 []byte, pr } func staticSubtreeConsistencyProbes(dir string) error { - for _, p := range staticConsistencyProbes() { - sp := subtreeConsistencyProbe{ - Start: 0, - End: p.Size1, - Size: p.Size2, - Root1: p.Root1, - Root2: p.Root2, - Proof: p.Proof, - Desc: p.Desc, - WantError: p.WantError, - } - if err := writeSubtreeConsistencyProbe(dir, sp); err != nil { + root1 := []byte("don't care 1") + root2 := []byte("don't care 2") + proof1 := [][]byte{} + proof2 := [][]byte{sha256EmptyTreeHash} + + for _, p := range []subtreeConsistencyProbe{ + {0, 0, 0, root1, root2, proof1, "sizes are equal (zero) but roots are not", true}, + {0, 1, 1, root1, root2, proof1, "sizes are equal (one) but roots are not", true}, + {0, 0, 1, root1, root2, proof1, "size1 is zero and does not equal size2", true}, + // Sizes that are always consistent. + {0, 1, 1, root2, root2, proof1, "sizes are equal (one) and proof is empty", false}, + // Empty subtree + {0, 0, 0, sha256EmptyTreeHash, sha256EmptyTreeHash, proof1, "subtree is empty sizes are equal (zero) subtree root valid proof is empty", false}, + {0, 0, 0, sha256EmptyTreeHash, root1, proof1, "subtree is empty sizes are equal (zero) subtree root valid tree root random proof is empty", false}, + {0, 0, 0, sha256EmptyTreeHash, sha256EmptyTreeHash, proof2, "subtree is empty sizes are equal (zero) roots valid but proof is not empty", true}, + {0, 0, 0, root1, root1, proof1, "subtree is empty sizes are equal (zero) roots match but not valid", true}, + {1, 1, 1, sha256EmptyTreeHash, sha256EmptyTreeHash, proof1, "subtree is empty sizes are equal (one) subtree root valid proof is empty", false}, + {1, 1, 1, sha256EmptyTreeHash, root1, proof1, "subtree is empty sizes are equal (one) subtree root valid tree root random proof is empty", false}, + {1, 1, 1, sha256EmptyTreeHash, sha256EmptyTreeHash, proof2, "subtree is empty sizes are equal (one) roots valid but proof is not empty", true}, + {1, 1, 1, root1, root1, proof1, "subtree is empty sizes are equal (one) roots match but not valid", true}, + {1, 1, 2, sha256EmptyTreeHash, sha256EmptyTreeHash, proof1, "subtree is empty subtree root valid proof is empty", false}, + {1, 1, 2, sha256EmptyTreeHash, sha256EmptyTreeHash, proof1, "subtree is empty subtree root valid tree root random proof is empty", false}, + {1, 1, 2, sha256EmptyTreeHash, sha256EmptyTreeHash, proof2, "subtree is empty roots valid but proof is not empty", true}, + {1, 1, 2, root1, root1, proof1, "subtree is empty roots match but not valid", true}, + // Time travel to the past. + {0, 1, 0, root1, root2, proof1, "size1 is greater than size2", true}, + {0, 2, 1, root1, root2, proof1, "size1 is greater than size2 again", true}, + // Empty proof. + {0, 1, 2, root1, root2, proof1, "sizes do not match and proof is empty", true}, + // Roots don't match. + {0, 1, 1, sha256EmptyTreeHash, root2, proof1, "roots do not match and sizes are one", true}, + // Sizes match but the proof is not empty. + {0, 0, 0, sha256EmptyTreeHash, sha256EmptyTreeHash, proof2, "sizes match but proof is not empty and sizes are zero", true}, + {0, 1, 1, sha256EmptyTreeHash, sha256EmptyTreeHash, proof2, "sizes match but proof is not empty and sizes are one", true}, + } { + if err := writeSubtreeConsistencyProbe(dir, p); err != nil { return err } } diff --git a/proof/proof.go b/proof/proof.go index e83cf0c..080fbd6 100644 --- a/proof/proof.go +++ b/proof/proof.go @@ -113,6 +113,9 @@ func subtreeConsistency(start, end, size uint64) (Nodes, error) { if start == 0 && end == size { return Nodes{IDs: []compact.NodeID{}}, nil } + if start == end { + return Nodes{IDs: []compact.NodeID{}}, nil + } // If end == size, prove inclusion of [start, end) into the tree. if end == size { @@ -273,24 +276,24 @@ type Subtree struct { Start, End uint64 } -// FindSubtrees returns one or two subtrees that efficiently cover [start, end). +// FindSubtrees returns two subtrees that efficiently cover [start, end). // -// If the provided [start, end) range is already a valid subtree, then that subtree is returned directly. -// Otherwise, this function continues by applying the "Selecting Two Subtrees" procedure -// from Section 4.5.1 of draft-ietf-plants-merkle-tree-certs. +// This function applies the "Selecting Two Subtrees" procedure from +// Section 4.5.1 of draft-ietf-plants-merkle-tree-certs. // // Note that: -// - If the provided [start, end) range is already a valid subtree, then it is returned as the only entry in the slice. -// - The 2nd subtree, if present, is adjacent to the first, and may not be a perfect subtree. +// - If the provided subtree has a size <= 1, then this function returns that subtree and an empty subtree. +// - If the provided [start, end) range is already a valid subtree, then it is still split into two smaller subtrees. +// - The 2nd subtree, if not empty, is adjacent to the first, and may not be a perfect subtree. // - The returned subtree(s) fully cover the [start, end) range. // - There are no "extra" entries covered past end, but there may be covered entries prior to start. // - The number of entries covered before start is always less than half the size of the first returned subtree. func FindSubtrees(start, end uint64) ([]Subtree, error) { - if start >= end { - return nil, fmt.Errorf("start %d must be strictly less than end %d", start, end) + if start > end { + return nil, fmt.Errorf("start %d must be less than or equal to end %d", start, end) } - if end-start == 1 || isSubtreeValid(start, end) == nil { - return []Subtree{{Start: start, End: end}}, nil + if end-start <= 1 { + return []Subtree{{Start: start, End: end}, {Start: end, End: end}}, nil } last := end - 1 // Find where start and last's tree paths diverge. @@ -313,12 +316,15 @@ func FindSubtrees(start, end uint64) ([]Subtree, error) { // - no extra node to the left of the subtree // - potentially extra nodes to the right of the subtree func isSubtreeValid(start, end uint64) error { - if start >= end { - return fmt.Errorf("start %d must be strictly less than end %d", start, end) + if start > end { + return fmt.Errorf("start %d must be less than or equal to end %d", start, end) } if start == 0 { return nil } + if start == end { + return nil + } l := end - start diff --git a/proof/proof_test.go b/proof/proof_test.go index e40b842..7c7dafd 100644 --- a/proof/proof_test.go +++ b/proof/proof_test.go @@ -457,8 +457,6 @@ func TestSubtreeConsistency(t *testing.T) { wantErr bool }{ // Errors. - {start: 0, end: 0, size: 0, wantErr: true}, // start = end = 0 - {start: 1, end: 1, size: 1, wantErr: true}, // start = end {start: 2, end: 1, size: 0, wantErr: true}, // start > end {start: 0, end: 5, size: 0, wantErr: true}, // end > size {start: 0, end: 9, size: 8, wantErr: true}, // end > size @@ -467,6 +465,7 @@ func TestSubtreeConsistency(t *testing.T) { // Small trees. // start = 0 + {start: 0, end: 0, size: 0, want: Nodes{IDs: []compact.NodeID{}}}, // start = end = 0 {start: 0, end: 1, size: 2, want: nodes(id(0, 1))}, // b {start: 0, end: 1, size: 4, want: nodes(id(0, 1), id(1, 1))}, // b bb {start: 0, end: 1, size: 6, want: rehash(2, 3, id(0, 1), id(1, 1), id(1, 2))}, // b bb cc @@ -485,12 +484,14 @@ func TestSubtreeConsistency(t *testing.T) { {start: 0, end: 7, size: 8, want: nodes( id(0, 6), id(0, 7), id(1, 2), id(2, 0))}, // g h cc aaa // start > 0 + {start: 1, end: 1, size: 1, want: Nodes{IDs: []compact.NodeID{}}}, // start = end {start: 1, end: 2, size: 3, want: rehash(1, 2, id(0, 0), id(0, 2))}, // a c {start: 1, end: 2, size: 5, want: rehash(2, 3, id(0, 0), id(1, 1), id(0, 4))}, // a bb e {start: 2, end: 4, size: 5, want: rehash(1, 2, id(1, 0), id(0, 4))}, // aa e {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 {start: 2, end: 4, size: 10, want: rehash(2, 3, id(1, 0), id(2, 1), id(1, 4))}, // aa bbb ee {start: 4, end: 6, size: 10, want: rehash(2, 3, id(1, 3), id(2, 0), id(1, 4))}, // dd aaa ee + {start: 4, end: 4, size: 10, want: Nodes{IDs: []compact.NodeID{}}}, // start = end {start: 4, end: 7, size: 11, want: rehash(4, 6, // ccc=hash(ee,k) id(0, 6), id(0, 7), id(1, 2), id(2, 0), id(0, 10), id(1, 4))}, // g h cc aaa k ee {start: 4, end: 8, size: 11, want: rehash(1, 3, // ccc=hash(ee,k) @@ -637,9 +638,9 @@ func TestConsistencySucceedsUpToTreeSize(t *testing.T) { func TestSubtreeConsistencySucceedsUpToTreeSize(t *testing.T) { const maxSize = uint64(100) - for s := uint64(1); s <= maxSize; s++ { - for sbe := uint64(1); sbe <= s; sbe++ { - for sbs := range sbe { + for s := range maxSize + 1 { + for sbe := range s + 1 { + for sbs := range sbe + 1 { if err := isSubtreeValid(sbs, sbe); err != nil { continue } @@ -829,16 +830,16 @@ func TestFindSubtrees(t *testing.T) { }{ // Already-valid subtrees are returned as-is. // Single entry subtrees: - {start: 0, end: 1, want: []Subtree{{Start: 0, End: 1}}}, - {start: 3, end: 4, want: []Subtree{{Start: 3, End: 4}}}, + {start: 0, end: 1, want: []Subtree{{Start: 0, End: 1}, {Start: 1, End: 1}}}, + {start: 3, end: 4, want: []Subtree{{Start: 3, End: 4}, {Start: 4, End: 4}}}, // Perfectly aligned subtrees: - {start: 4, end: 6, want: []Subtree{{Start: 4, End: 6}}}, - {start: 16, end: 32, want: []Subtree{{Start: 16, End: 32}}}, + {start: 4, end: 6, want: []Subtree{{Start: 4, End: 5}, {Start: 5, End: 6}}}, + {start: 16, end: 32, want: []Subtree{{Start: 16, End: 24}, {Start: 24, End: 32}}}, // Non-perfect trees are split into two: {start: 5, end: 13, want: []Subtree{{Start: 4, End: 8}, {Start: 8, End: 13}}}, {start: 7, end: 9, want: []Subtree{{Start: 7, End: 8}, {Start: 8, End: 9}}}, // Invalid inputs: - {start: 5, end: 5, wantErr: true}, + {start: 5, end: 5, want: []Subtree{{Start: 5, End: 5}, {Start: 5, End: 5}}}, {start: 6, end: 5, wantErr: true}, } { t.Run(fmt.Sprintf("%d:%d", tc.start, tc.end), func(t *testing.T) { diff --git a/proof/verify.go b/proof/verify.go index 0c03147..7aa9e84 100644 --- a/proof/verify.go +++ b/proof/verify.go @@ -107,10 +107,22 @@ func VerifyConsistency(hasher merkle.LogHasher, size1, size2 uint64, proof [][]b // VerifySubtreeConsistency checks that the passed-in subtree consistency proof // is valid between the passed in subtree indices and parent tree size, with // respect to the corresponding subtree root node hash. It Requires: -// - 0 <= start < end <= size. +// - 0 <= start <= end <= size. // - start to be a multiple of the smallest power of two greater than or equal to // (end - start) func VerifySubtreeConsistency(hasher merkle.LogHasher, start, end, size uint64, proof [][]byte, subRoot, parentRoot []byte) error { + if start == end { + if end > size { + return fmt.Errorf("size (%d) < end (%d)", size, end) + } + if len(proof) > 0 { + return errors.New("start=end, but proof is not empty") + } + if !bytes.Equal(subRoot, hasher.EmptyRoot()) { + return errors.New("start=end, but subRoot is not empty root") + } + return nil + } hash2, err := RootFromSubtreeConsistencyProof(hasher, start, end, size, proof, subRoot) if err != nil { return err @@ -144,7 +156,7 @@ func RootFromConsistencyProof(hasher merkle.LogHasher, size1, size2 uint64, proo // a consistency proof. // // It requires: -// - 0 <= start < end <= size. +// - 0 <= start <= end <= size. // - start to be a multiple of the smallest power of two greater than or equal to // (end - start) // @@ -156,6 +168,16 @@ func RootFromSubtreeConsistencyProof(hasher merkle.LogHasher, start, end, size u return nil, fmt.Errorf("subtree invalid: %v", err) case size < end: return nil, fmt.Errorf("size (%d) < end (%d)", size, end) + case start == end && size == 0: + if len(proof) > 0 { + return nil, errors.New("start=end, but proof is not empty") + } + if !bytes.Equal(subRoot, hasher.EmptyRoot()) { + return nil, errors.New("start=end, but subRoot is not empty root") + } + return hasher.EmptyRoot(), nil + case start == end: + return nil, errors.New("cannot reconstruct non-empty tree root from empty subtree") case start == 0 && size == end: if len(proof) > 0 { return nil, errors.New("start=0 and end=size, but proof is not empty") diff --git a/testdata/subtreeconsistency/additional/roots-do-not-match-and-sizes-are-one.json b/testdata/subtreeconsistency/additional/roots-do-not-match-and-sizes-are-one.json new file mode 100644 index 0000000..bae93de --- /dev/null +++ b/testdata/subtreeconsistency/additional/roots-do-not-match-and-sizes-are-one.json @@ -0,0 +1,10 @@ +{ + "start": 0, + "end": 1, + "size": 1, + "root1": "47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=", + "root2": "ZG9uJ3QgY2FyZSAy", + "proof": [], + "desc": "roots do not match and sizes are one", + "wantErr": true +} \ No newline at end of file diff --git a/testdata/subtreeconsistency/additional/sizes-do-not-match-and-proof-is-empty.json b/testdata/subtreeconsistency/additional/sizes-do-not-match-and-proof-is-empty.json new file mode 100644 index 0000000..c0a7077 --- /dev/null +++ b/testdata/subtreeconsistency/additional/sizes-do-not-match-and-proof-is-empty.json @@ -0,0 +1,10 @@ +{ + "start": 0, + "end": 1, + "size": 2, + "root1": "ZG9uJ3QgY2FyZSAx", + "root2": "ZG9uJ3QgY2FyZSAy", + "proof": [], + "desc": "sizes do not match and proof is empty", + "wantErr": true +} \ No newline at end of file diff --git a/testdata/subtreeconsistency/additional/subtree-is-empty-roots-match-but-not-valid.json b/testdata/subtreeconsistency/additional/subtree-is-empty-roots-match-but-not-valid.json new file mode 100644 index 0000000..e01d16b --- /dev/null +++ b/testdata/subtreeconsistency/additional/subtree-is-empty-roots-match-but-not-valid.json @@ -0,0 +1,10 @@ +{ + "start": 1, + "end": 1, + "size": 2, + "root1": "ZG9uJ3QgY2FyZSAx", + "root2": "ZG9uJ3QgY2FyZSAx", + "proof": [], + "desc": "subtree is empty roots match but not valid", + "wantErr": true +} \ No newline at end of file diff --git a/testdata/subtreeconsistency/additional/subtree-is-empty-roots-valid-but-proof-is-not-empty.json b/testdata/subtreeconsistency/additional/subtree-is-empty-roots-valid-but-proof-is-not-empty.json new file mode 100644 index 0000000..82e9f36 --- /dev/null +++ b/testdata/subtreeconsistency/additional/subtree-is-empty-roots-valid-but-proof-is-not-empty.json @@ -0,0 +1,12 @@ +{ + "start": 1, + "end": 1, + "size": 2, + "root1": "47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=", + "root2": "47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=", + "proof": [ + "47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=" + ], + "desc": "subtree is empty roots valid but proof is not empty", + "wantErr": true +} \ No newline at end of file diff --git a/testdata/subtreeconsistency/additional/subtree-is-empty-sizes-are-equal-one-roots-match-but-not-valid.json b/testdata/subtreeconsistency/additional/subtree-is-empty-sizes-are-equal-one-roots-match-but-not-valid.json new file mode 100644 index 0000000..c2c7e1a --- /dev/null +++ b/testdata/subtreeconsistency/additional/subtree-is-empty-sizes-are-equal-one-roots-match-but-not-valid.json @@ -0,0 +1,10 @@ +{ + "start": 1, + "end": 1, + "size": 1, + "root1": "ZG9uJ3QgY2FyZSAx", + "root2": "ZG9uJ3QgY2FyZSAx", + "proof": [], + "desc": "subtree is empty sizes are equal (one) roots match but not valid", + "wantErr": true +} \ No newline at end of file diff --git a/testdata/subtreeconsistency/additional/subtree-is-empty-sizes-are-equal-one-roots-valid-but-proof-is-not-empty.json b/testdata/subtreeconsistency/additional/subtree-is-empty-sizes-are-equal-one-roots-valid-but-proof-is-not-empty.json new file mode 100644 index 0000000..0ef5315 --- /dev/null +++ b/testdata/subtreeconsistency/additional/subtree-is-empty-sizes-are-equal-one-roots-valid-but-proof-is-not-empty.json @@ -0,0 +1,12 @@ +{ + "start": 1, + "end": 1, + "size": 1, + "root1": "47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=", + "root2": "47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=", + "proof": [ + "47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=" + ], + "desc": "subtree is empty sizes are equal (one) roots valid but proof is not empty", + "wantErr": true +} \ No newline at end of file diff --git a/testdata/subtreeconsistency/additional/subtree-is-empty-sizes-are-equal-one-subtree-root-valid-proof-is-empty.json b/testdata/subtreeconsistency/additional/subtree-is-empty-sizes-are-equal-one-subtree-root-valid-proof-is-empty.json new file mode 100644 index 0000000..808fe2d --- /dev/null +++ b/testdata/subtreeconsistency/additional/subtree-is-empty-sizes-are-equal-one-subtree-root-valid-proof-is-empty.json @@ -0,0 +1,10 @@ +{ + "start": 1, + "end": 1, + "size": 1, + "root1": "47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=", + "root2": "47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=", + "proof": [], + "desc": "subtree is empty sizes are equal (one) subtree root valid proof is empty", + "wantErr": false +} \ No newline at end of file diff --git a/testdata/subtreeconsistency/additional/subtree-is-empty-sizes-are-equal-one-subtree-root-valid-tree-root-random-proof-is-empty.json b/testdata/subtreeconsistency/additional/subtree-is-empty-sizes-are-equal-one-subtree-root-valid-tree-root-random-proof-is-empty.json new file mode 100644 index 0000000..814e588 --- /dev/null +++ b/testdata/subtreeconsistency/additional/subtree-is-empty-sizes-are-equal-one-subtree-root-valid-tree-root-random-proof-is-empty.json @@ -0,0 +1,10 @@ +{ + "start": 1, + "end": 1, + "size": 1, + "root1": "47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=", + "root2": "ZG9uJ3QgY2FyZSAx", + "proof": [], + "desc": "subtree is empty sizes are equal (one) subtree root valid tree root random proof is empty", + "wantErr": false +} \ No newline at end of file diff --git a/testdata/subtreeconsistency/additional/subtree-is-empty-sizes-are-equal-zero-roots-match-but-not-valid.json b/testdata/subtreeconsistency/additional/subtree-is-empty-sizes-are-equal-zero-roots-match-but-not-valid.json new file mode 100644 index 0000000..7474fb6 --- /dev/null +++ b/testdata/subtreeconsistency/additional/subtree-is-empty-sizes-are-equal-zero-roots-match-but-not-valid.json @@ -0,0 +1,10 @@ +{ + "start": 0, + "end": 0, + "size": 0, + "root1": "ZG9uJ3QgY2FyZSAx", + "root2": "ZG9uJ3QgY2FyZSAx", + "proof": [], + "desc": "subtree is empty sizes are equal (zero) roots match but not valid", + "wantErr": true +} \ No newline at end of file diff --git a/testdata/subtreeconsistency/additional/subtree-is-empty-sizes-are-equal-zero-roots-valid-but-proof-is-not-empty.json b/testdata/subtreeconsistency/additional/subtree-is-empty-sizes-are-equal-zero-roots-valid-but-proof-is-not-empty.json new file mode 100644 index 0000000..8a6e3d2 --- /dev/null +++ b/testdata/subtreeconsistency/additional/subtree-is-empty-sizes-are-equal-zero-roots-valid-but-proof-is-not-empty.json @@ -0,0 +1,12 @@ +{ + "start": 0, + "end": 0, + "size": 0, + "root1": "47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=", + "root2": "47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=", + "proof": [ + "47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=" + ], + "desc": "subtree is empty sizes are equal (zero) roots valid but proof is not empty", + "wantErr": true +} \ No newline at end of file diff --git a/testdata/subtreeconsistency/additional/subtree-is-empty-sizes-are-equal-zero-subtree-root-valid-proof-is-empty.json b/testdata/subtreeconsistency/additional/subtree-is-empty-sizes-are-equal-zero-subtree-root-valid-proof-is-empty.json new file mode 100644 index 0000000..f24be5a --- /dev/null +++ b/testdata/subtreeconsistency/additional/subtree-is-empty-sizes-are-equal-zero-subtree-root-valid-proof-is-empty.json @@ -0,0 +1,10 @@ +{ + "start": 0, + "end": 0, + "size": 0, + "root1": "47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=", + "root2": "47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=", + "proof": [], + "desc": "subtree is empty sizes are equal (zero) subtree root valid proof is empty", + "wantErr": false +} \ No newline at end of file diff --git a/testdata/subtreeconsistency/additional/subtree-is-empty-sizes-are-equal-zero-subtree-root-valid-tree-root-random-proof-is-empty.json b/testdata/subtreeconsistency/additional/subtree-is-empty-sizes-are-equal-zero-subtree-root-valid-tree-root-random-proof-is-empty.json new file mode 100644 index 0000000..a1d62c3 --- /dev/null +++ b/testdata/subtreeconsistency/additional/subtree-is-empty-sizes-are-equal-zero-subtree-root-valid-tree-root-random-proof-is-empty.json @@ -0,0 +1,10 @@ +{ + "start": 0, + "end": 0, + "size": 0, + "root1": "47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=", + "root2": "ZG9uJ3QgY2FyZSAx", + "proof": [], + "desc": "subtree is empty sizes are equal (zero) subtree root valid tree root random proof is empty", + "wantErr": false +} \ No newline at end of file diff --git a/testdata/subtreeconsistency/additional/subtree-is-empty-subtree-root-valid-proof-is-empty.json b/testdata/subtreeconsistency/additional/subtree-is-empty-subtree-root-valid-proof-is-empty.json new file mode 100644 index 0000000..96ad82e --- /dev/null +++ b/testdata/subtreeconsistency/additional/subtree-is-empty-subtree-root-valid-proof-is-empty.json @@ -0,0 +1,10 @@ +{ + "start": 1, + "end": 1, + "size": 2, + "root1": "47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=", + "root2": "47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=", + "proof": [], + "desc": "subtree is empty subtree root valid proof is empty", + "wantErr": false +} \ No newline at end of file diff --git a/testdata/subtreeconsistency/additional/subtree-is-empty-subtree-root-valid-tree-root-random-proof-is-empty.json b/testdata/subtreeconsistency/additional/subtree-is-empty-subtree-root-valid-tree-root-random-proof-is-empty.json new file mode 100644 index 0000000..3299052 --- /dev/null +++ b/testdata/subtreeconsistency/additional/subtree-is-empty-subtree-root-valid-tree-root-random-proof-is-empty.json @@ -0,0 +1,10 @@ +{ + "start": 1, + "end": 1, + "size": 2, + "root1": "47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=", + "root2": "47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=", + "proof": [], + "desc": "subtree is empty subtree root valid tree root random proof is empty", + "wantErr": false +} \ No newline at end of file diff --git a/testonly/reference_test.go b/testonly/reference_test.go index c1954d7..668d173 100644 --- a/testonly/reference_test.go +++ b/testonly/reference_test.go @@ -108,10 +108,15 @@ func refConsistencyProof(entries [][]byte, size2, size1 uint64, hasher merkle.Lo // 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 { + // Invalid inputs + if start > end { return nil } - if end == 0 || end > size { + if end > size { + return nil + } + // start == end is a valid input, and the proof is always empty + if start == end { return nil } // Consistency proof between a tree and itself is empty. diff --git a/testonly/tree.go b/testonly/tree.go index e7ac572..1d61b1a 100644 --- a/testonly/tree.go +++ b/testonly/tree.go @@ -165,12 +165,15 @@ func (t *Tree) getNodes(ids []compact.NodeID) [][]byte { // - no extra node to the left of the subtree // - potentially extra nodes to the right of the subtree func isSubtreeValid(start, end uint64) error { - if start >= end { - return fmt.Errorf("start %d must be strictly less than end %d", start, end) + if start > end { + return fmt.Errorf("start %d must be less than or equal to end %d", start, end) } if start == 0 { return nil } + if start == end { + return nil + } l := end - start diff --git a/testonly/tree_test.go b/testonly/tree_test.go index 0b74d43..3bfca0a 100644 --- a/testonly/tree_test.go +++ b/testonly/tree_test.go @@ -204,17 +204,14 @@ func TestSubtreeTreeConsistencyProof(t *testing.T) { 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)") } maxSize := uint64(len(entries)) - for end := uint64(1); end <= maxSize; end++ { - for size := end; size <= maxSize; size++ { - for start := range end { + for size := range maxSize + 1 { + for end := range size + 1 { + for start := range end + 1 { if err := isSubtreeValid(start, end); err != nil { continue } diff --git a/testonly/vectors_test.go b/testonly/vectors_test.go index e98ce6d..9db0d8f 100644 --- a/testonly/vectors_test.go +++ b/testonly/vectors_test.go @@ -62,8 +62,8 @@ func writeProofLine(t *testing.T, w io.Writer, prefix string, proof [][]byte) { func TestSubtreeHashVectors(t *testing.T) { tree := subtreeVectorTree() h := sha256.New() - for end := uint64(1); end <= subtreeVectorMax; end++ { - for start := range end { + for end := range subtreeVectorMax + 1 { + for start := range end + 1 { if err := isSubtreeValid(start, end); err != nil { continue } @@ -74,7 +74,7 @@ func TestSubtreeHashVectors(t *testing.T) { } } - const want = "94a95384a8c69acea9b50d035a58285b3a777cb7a724005faa5e1f1e1190007f" + const want = "b82806ad4265bb151c1119c0f4db437bb4d1a1f887b3a7fba1cd4ebf552e3e81" if got := fmt.Sprintf("%x", h.Sum(nil)); got != want { t.Errorf("subtree hash vector = %s, want %s", got, want) } @@ -83,8 +83,8 @@ func TestSubtreeHashVectors(t *testing.T) { func TestSubtreeInclusionProofVectors(t *testing.T) { tree := subtreeVectorTree() h := sha256.New() - for end := uint64(1); end <= subtreeVectorMax; end++ { - for start := range end { + for end := range subtreeVectorMax + 1 { + for start := range end + 1 { if err := isSubtreeValid(start, end); err != nil { continue } @@ -107,8 +107,8 @@ func TestSubtreeConsistencyProofVectors(t *testing.T) { tree := subtreeVectorTree() h := sha256.New() for n := range subtreeVectorMax + 1 { - for end := uint64(1); end <= n; end++ { - for start := range end { + for end := range n + 1 { + for start := range end + 1 { if err := isSubtreeValid(start, end); err != nil { continue } @@ -120,7 +120,7 @@ func TestSubtreeConsistencyProofVectors(t *testing.T) { } } } - const want = "c586ebbb73a5621baf2140095d87dde934e3b6503a562a1a5215b8209edd083d" + const want = "10fa99b37bf9bf9ffa26b412fbd98bd75363256d0b75d61bc4538b9c9c5a0a74" if got := fmt.Sprintf("%x", h.Sum(nil)); got != want { t.Errorf("subtree consistency proof vector = %s, want %s", got, want) } @@ -128,27 +128,21 @@ func TestSubtreeConsistencyProofVectors(t *testing.T) { func TestSubtreeCoveringVectors(t *testing.T) { h := sha256.New() - for end := uint64(1); end <= subtreeVectorMax; end++ { - for start := range end { - if err := isSubtreeValid(start, end); err == nil { - if _, err := fmt.Fprintf(h, "[%d, %d)\n", start, end); err != nil { - t.Fatalf("fmt.Fprintf: %v", err) - } - } else { - subtrees, err := proof.FindSubtrees(start, end) - if err != nil { - t.Fatalf("FindSubtrees(%d, %d): %v", start, end, err) - } - if l := len(subtrees); l != 2 { - t.Fatalf("FindSubtrees(%d, %d) returned unexpected number of subtrees: %d", start, end, l) - } - if _, err := fmt.Fprintf(h, "[%d, %d) [%d, %d)\n", subtrees[0].Start, subtrees[0].End, subtrees[1].Start, subtrees[1].End); err != nil { - t.Fatalf("fmt.Fprintf: %v", err) - } + for end := range subtreeVectorMax + 1 { + for start := range end + 1 { + subtrees, err := proof.FindSubtrees(start, end) + if err != nil { + t.Fatalf("FindSubtrees(%d, %d): %v", start, end, err) + } + if l := len(subtrees); l != 2 { + t.Fatalf("FindSubtrees(%d, %d) returned unexpected number of subtrees: %d", start, end, l) + } + if _, err := fmt.Fprintf(h, "[%d, %d) [%d, %d)\n", subtrees[0].Start, subtrees[0].End, subtrees[1].Start, subtrees[1].End); err != nil { + t.Fatalf("fmt.Fprintf: %v", err) } } } - const want = "e0aecb912a10c57d753b6ecc64db73217f9bc4ed10fcb4e9062be3b6fbe1ebfd" + const want = "7fd9c8b926e9d2b5cf831560e8ce295a5ef97ad5c5ede4ea0dea28a8c8fc8bb0" if got := fmt.Sprintf("%x", h.Sum(nil)); got != want { t.Errorf("subtree covering vector = %s, want %s", got, want) } From 3f3fe2d1a14bd615aeb2c224a4a2c1a963b312d1 Mon Sep 17 00:00:00 2001 From: Philippe Boneff Date: Thu, 23 Jul 2026 09:48:47 +0000 Subject: [PATCH 2/5] Add standalone invalidSubtreeConsistencyProof to proofgen Replace the wrapper around invalidConsistencyProof with a dedicated invalidSubtreeConsistencyProof function in cmd/proofgen/main.go. TAG=agy CONV=1f76bf92-7d8b-43a5-942a-a4bd88025626 --- cmd/proofgen/main.go | 54 ++++++++++++++----- ...roots-do-not-match-and-sizes-are-zero.json | 10 ---- .../size1-is-zero-and-size2-is-not-zero.json | 10 ---- 3 files changed, 40 insertions(+), 34 deletions(-) delete mode 100644 testdata/subtreeconsistency/additional/roots-do-not-match-and-sizes-are-zero.json delete mode 100644 testdata/subtreeconsistency/additional/size1-is-zero-and-size2-is-not-zero.json diff --git a/cmd/proofgen/main.go b/cmd/proofgen/main.go index 5fefe62..ae32fa6 100644 --- a/cmd/proofgen/main.go +++ b/cmd/proofgen/main.go @@ -715,21 +715,47 @@ func corruptedSubtreeConsistencyProbes(dir string, size1, size2 uint64, proof [] return nil } -func invalidSubtreeConsistencyProof(size1, size2 uint64, root1, root2 []byte, proof [][]byte) []subtreeConsistencyProbe { - cProbes := invalidConsistencyProof(size1, size2, root1, root2, proof) - ret := make([]subtreeConsistencyProbe, 0, len(cProbes)+1) - for _, p := range cProbes { - ret = append(ret, subtreeConsistencyProbe{ - Start: 0, - End: p.Size1, - Size: p.Size2, - Root1: p.Root1, - Root2: p.Root2, - Proof: p.Proof, - Desc: p.Desc, - WantError: p.WantError, - }) +func invalidSubtreeConsistencyProof(end, size uint64, root1, root2 []byte, proof [][]byte) []subtreeConsistencyProbe { + ln := len(proof) + ret := []subtreeConsistencyProbe{ + // Wrong end (size1). + {0, end - 1, size, root1, root2, proof, "size1 sub @1", true}, + {0, end + 1, size, root1, root2, proof, "size1 plus @1", true}, + {0, end ^ 2, size, root1, root2, proof, "size1 XOR @2", true}, + // Wrong tree size (size2). + {0, end, size * 2, root1, root2, proof, "size2 mul @2", true}, + {0, end, size / 2, root1, root2, proof, "size2 div @2", true}, + // Wrong root. + {0, end, size, []byte("WrongRoot"), root2, proof, "wrong root1", true}, + {0, end, size, root1, []byte("WrongRoot"), proof, "wrong root2", true}, + {0, end, size, root2, root1, proof, "swapped roots", true}, + // Empty proof. + {0, end, size, root1, root2, [][]byte{}, "empty proof", true}, + // Add garbage at the end. + {0, end, size, root1, root2, extend(proof, []byte{}), "trailing garbage", true}, + {0, end, size, root1, root2, extend(proof, root1), "trailing root1", true}, + {0, end, size, root1, root2, extend(proof, root2), "trailing root2", true}, + // Add garbage at the front. + {0, end, size, root1, root2, prepend(proof, []byte{}), "preceding garbage", true}, + {0, end, size, root1, root2, prepend(proof, root1), "preceding root1", true}, + {0, end, size, root1, root2, prepend(proof, root2), "preceding root2", true}, + {0, end, size, root1, root2, prepend(proof, proof[0]), "preceding proof @0", true}, + } + + // Remove a node from the end. + if ln > 0 { + ret = append(ret, subtreeConsistencyProbe{0, end, size, root1, root2, proof[:ln-1], "truncated proof", true}) } + + // Modify single bit in an element of the proof. + for i := range ln { + wrongProof := prepend(proof) // Copy the proof slice. + wrongProof[i] = append([]byte(nil), wrongProof[i]...) // But also the modified data. + wrongProof[i][0] ^= 16 // Flip the bit. + desc := fmt.Sprintf("modified proof@%d bit @4", i) + ret = append(ret, subtreeConsistencyProbe{0, end, size, root1, root2, wrongProof, desc, true}) + } + ret = append(ret, subtreeConsistencyProbe{ Start: 1, End: 15, diff --git a/testdata/subtreeconsistency/additional/roots-do-not-match-and-sizes-are-zero.json b/testdata/subtreeconsistency/additional/roots-do-not-match-and-sizes-are-zero.json deleted file mode 100644 index 55a1fcc..0000000 --- a/testdata/subtreeconsistency/additional/roots-do-not-match-and-sizes-are-zero.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "start": 0, - "end": 0, - "size": 0, - "root1": "47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=", - "root2": "ZG9uJ3QgY2FyZSAy", - "proof": [], - "desc": "roots do not match and sizes are zero", - "wantErr": true -} \ No newline at end of file diff --git a/testdata/subtreeconsistency/additional/size1-is-zero-and-size2-is-not-zero.json b/testdata/subtreeconsistency/additional/size1-is-zero-and-size2-is-not-zero.json deleted file mode 100644 index 84f69a5..0000000 --- a/testdata/subtreeconsistency/additional/size1-is-zero-and-size2-is-not-zero.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "start": 0, - "end": 0, - "size": 1, - "root1": "47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=", - "root2": "ZG9uJ3QgY2FyZSAy", - "proof": [], - "desc": "size1 is zero and size2 is not zero", - "wantErr": true -} \ No newline at end of file From 6ffb78f5ef103711892d7fbb7bf57b0d59e77a0b Mon Sep 17 00:00:00 2001 From: Philippe Boneff Date: Thu, 23 Jul 2026 10:42:41 +0000 Subject: [PATCH 3/5] move invalid subtree test and add more --- cmd/proofgen/main.go | 15 +++++---------- .../invalid-subtree-start-1-end-15-size-15.json | 10 ++++++++++ .../invalid-subtree-start-1-end-3-size-8.json | 10 ++++++++++ .../invalid-subtree-start-2-end-5-size-8.json | 10 ++++++++++ .../invalid-subtree-start-2-end-6-size-8.json | 10 ++++++++++ 5 files changed, 45 insertions(+), 10 deletions(-) create mode 100644 testdata/subtreeconsistency/additional/invalid-subtree-start-1-end-15-size-15.json create mode 100644 testdata/subtreeconsistency/additional/invalid-subtree-start-1-end-3-size-8.json create mode 100644 testdata/subtreeconsistency/additional/invalid-subtree-start-2-end-5-size-8.json create mode 100644 testdata/subtreeconsistency/additional/invalid-subtree-start-2-end-6-size-8.json diff --git a/cmd/proofgen/main.go b/cmd/proofgen/main.go index ae32fa6..50145c4 100644 --- a/cmd/proofgen/main.go +++ b/cmd/proofgen/main.go @@ -756,16 +756,6 @@ func invalidSubtreeConsistencyProof(end, size uint64, root1, root2 []byte, proof ret = append(ret, subtreeConsistencyProbe{0, end, size, root1, root2, wrongProof, desc, true}) } - ret = append(ret, subtreeConsistencyProbe{ - Start: 1, - End: 15, - Size: 15, - Root1: root1, - Root2: root2, - Proof: proof, - Desc: "invalid subtree", - WantError: true, - }) return ret } @@ -794,6 +784,11 @@ func staticSubtreeConsistencyProbes(dir string) error { {1, 1, 2, sha256EmptyTreeHash, sha256EmptyTreeHash, proof1, "subtree is empty subtree root valid tree root random proof is empty", false}, {1, 1, 2, sha256EmptyTreeHash, sha256EmptyTreeHash, proof2, "subtree is empty roots valid but proof is not empty", true}, {1, 1, 2, root1, root1, proof1, "subtree is empty roots match but not valid", true}, + // Invalid subtree boundaries (not a multiple of power of 2 >= end - start). + {1, 15, 15, root1, root2, proof1, "invalid subtree start 1 end 15 size 15", true}, + {1, 3, 8, root1, root2, proof1, "invalid subtree start 1 end 3 size 8", true}, + {2, 5, 8, root1, root2, proof1, "invalid subtree start 2 end 5 size 8", true}, + {2, 6, 8, root1, root2, proof1, "invalid subtree start 2 end 6 size 8", true}, // Time travel to the past. {0, 1, 0, root1, root2, proof1, "size1 is greater than size2", true}, {0, 2, 1, root1, root2, proof1, "size1 is greater than size2 again", true}, diff --git a/testdata/subtreeconsistency/additional/invalid-subtree-start-1-end-15-size-15.json b/testdata/subtreeconsistency/additional/invalid-subtree-start-1-end-15-size-15.json new file mode 100644 index 0000000..497d8d6 --- /dev/null +++ b/testdata/subtreeconsistency/additional/invalid-subtree-start-1-end-15-size-15.json @@ -0,0 +1,10 @@ +{ + "start": 1, + "end": 15, + "size": 15, + "root1": "ZG9uJ3QgY2FyZSAx", + "root2": "ZG9uJ3QgY2FyZSAy", + "proof": [], + "desc": "invalid subtree start 1 end 15 size 15", + "wantErr": true +} \ No newline at end of file diff --git a/testdata/subtreeconsistency/additional/invalid-subtree-start-1-end-3-size-8.json b/testdata/subtreeconsistency/additional/invalid-subtree-start-1-end-3-size-8.json new file mode 100644 index 0000000..f2c6447 --- /dev/null +++ b/testdata/subtreeconsistency/additional/invalid-subtree-start-1-end-3-size-8.json @@ -0,0 +1,10 @@ +{ + "start": 1, + "end": 3, + "size": 8, + "root1": "ZG9uJ3QgY2FyZSAx", + "root2": "ZG9uJ3QgY2FyZSAy", + "proof": [], + "desc": "invalid subtree start 1 end 3 size 8", + "wantErr": true +} \ No newline at end of file diff --git a/testdata/subtreeconsistency/additional/invalid-subtree-start-2-end-5-size-8.json b/testdata/subtreeconsistency/additional/invalid-subtree-start-2-end-5-size-8.json new file mode 100644 index 0000000..e98c642 --- /dev/null +++ b/testdata/subtreeconsistency/additional/invalid-subtree-start-2-end-5-size-8.json @@ -0,0 +1,10 @@ +{ + "start": 2, + "end": 5, + "size": 8, + "root1": "ZG9uJ3QgY2FyZSAx", + "root2": "ZG9uJ3QgY2FyZSAy", + "proof": [], + "desc": "invalid subtree start 2 end 5 size 8", + "wantErr": true +} \ No newline at end of file diff --git a/testdata/subtreeconsistency/additional/invalid-subtree-start-2-end-6-size-8.json b/testdata/subtreeconsistency/additional/invalid-subtree-start-2-end-6-size-8.json new file mode 100644 index 0000000..b6d6f6e --- /dev/null +++ b/testdata/subtreeconsistency/additional/invalid-subtree-start-2-end-6-size-8.json @@ -0,0 +1,10 @@ +{ + "start": 2, + "end": 6, + "size": 8, + "root1": "ZG9uJ3QgY2FyZSAx", + "root2": "ZG9uJ3QgY2FyZSAy", + "proof": [], + "desc": "invalid subtree start 2 end 6 size 8", + "wantErr": true +} \ No newline at end of file From eaf6ab92c21e64b8e7ebe4ed933cda93c928ce3f Mon Sep 17 00:00:00 2001 From: Philippe Boneff Date: Thu, 23 Jul 2026 10:44:49 +0000 Subject: [PATCH 4/5] Refactor FindSubtrees to return (Subtree, Subtree, error) directly Update FindSubtrees in proof/proof.go to return two Subtree structs by value instead of allocating a []Subtree slice. TAG=agy CONV=1f76bf92-7d8b-43a5-942a-a4bd88025626 --- proof/proof.go | 13 +++++-------- proof/proof_test.go | 28 ++++++++++++++++------------ testonly/vectors_test.go | 7 ++----- 3 files changed, 23 insertions(+), 25 deletions(-) diff --git a/proof/proof.go b/proof/proof.go index 080fbd6..71499de 100644 --- a/proof/proof.go +++ b/proof/proof.go @@ -285,15 +285,15 @@ type Subtree struct { // - If the provided subtree has a size <= 1, then this function returns that subtree and an empty subtree. // - If the provided [start, end) range is already a valid subtree, then it is still split into two smaller subtrees. // - The 2nd subtree, if not empty, is adjacent to the first, and may not be a perfect subtree. -// - The returned subtree(s) fully cover the [start, end) range. +// - The returned subtrees fully cover the [start, end) range. // - There are no "extra" entries covered past end, but there may be covered entries prior to start. // - The number of entries covered before start is always less than half the size of the first returned subtree. -func FindSubtrees(start, end uint64) ([]Subtree, error) { +func FindSubtrees(start, end uint64) (Subtree, Subtree, error) { if start > end { - return nil, fmt.Errorf("start %d must be less than or equal to end %d", start, end) + return Subtree{}, Subtree{}, fmt.Errorf("start %d must be less than or equal to end %d", start, end) } if end-start <= 1 { - return []Subtree{{Start: start, End: end}, {Start: end, End: end}}, nil + return Subtree{Start: start, End: end}, Subtree{Start: end, End: end}, nil } last := end - 1 // Find where start and last's tree paths diverge. @@ -304,10 +304,7 @@ func FindSubtrees(start, end uint64) ([]Subtree, error) { // Maximize the left endpoint. leftSplit := bits.Len64(^start & mask) leftStart := start & ^((uint64(1) << leftSplit) - 1) - return []Subtree{ - {Start: leftStart, End: mid}, - {Start: mid, End: end}, - }, nil + return Subtree{Start: leftStart, End: mid}, Subtree{Start: mid, End: end}, nil } // isSubtreeValid returns whether a subtree covers a valid range. diff --git a/proof/proof_test.go b/proof/proof_test.go index 7c7dafd..a5b1510 100644 --- a/proof/proof_test.go +++ b/proof/proof_test.go @@ -825,25 +825,26 @@ func inclusion(t *testing.T, index, size uint64) Nodes { func TestFindSubtrees(t *testing.T) { for _, tc := range []struct { start, end uint64 - want []Subtree + wantLeft Subtree + wantRight Subtree wantErr bool }{ - // Already-valid subtrees are returned as-is. // Single entry subtrees: - {start: 0, end: 1, want: []Subtree{{Start: 0, End: 1}, {Start: 1, End: 1}}}, - {start: 3, end: 4, want: []Subtree{{Start: 3, End: 4}, {Start: 4, End: 4}}}, + {start: 0, end: 1, wantLeft: Subtree{Start: 0, End: 1}, wantRight: Subtree{Start: 1, End: 1}}, + {start: 3, end: 4, wantLeft: Subtree{Start: 3, End: 4}, wantRight: Subtree{Start: 4, End: 4}}, // Perfectly aligned subtrees: - {start: 4, end: 6, want: []Subtree{{Start: 4, End: 5}, {Start: 5, End: 6}}}, - {start: 16, end: 32, want: []Subtree{{Start: 16, End: 24}, {Start: 24, End: 32}}}, + {start: 4, end: 6, wantLeft: Subtree{Start: 4, End: 5}, wantRight: Subtree{Start: 5, End: 6}}, + {start: 16, end: 32, wantLeft: Subtree{Start: 16, End: 24}, wantRight: Subtree{Start: 24, End: 32}}, // Non-perfect trees are split into two: - {start: 5, end: 13, want: []Subtree{{Start: 4, End: 8}, {Start: 8, End: 13}}}, - {start: 7, end: 9, want: []Subtree{{Start: 7, End: 8}, {Start: 8, End: 9}}}, + {start: 5, end: 13, wantLeft: Subtree{Start: 4, End: 8}, wantRight: Subtree{Start: 8, End: 13}}, + {start: 7, end: 9, wantLeft: Subtree{Start: 7, End: 8}, wantRight: Subtree{Start: 8, End: 9}}, + // Empty subtrees. + {start: 5, end: 5, wantLeft: Subtree{Start: 5, End: 5}, wantRight: Subtree{Start: 5, End: 5}}, // Invalid inputs: - {start: 5, end: 5, want: []Subtree{{Start: 5, End: 5}, {Start: 5, End: 5}}}, {start: 6, end: 5, wantErr: true}, } { t.Run(fmt.Sprintf("%d:%d", tc.start, tc.end), func(t *testing.T) { - got, err := FindSubtrees(tc.start, tc.end) + gotLeft, gotRight, err := FindSubtrees(tc.start, tc.end) if tc.wantErr { if err == nil { t.Fatal("expected error, got nil") @@ -853,8 +854,11 @@ func TestFindSubtrees(t *testing.T) { if err != nil { t.Fatalf("FindSubtrees: %v", err) } - if diff := cmp.Diff(tc.want, got); diff != "" { - t.Errorf("FindSubtrees mismatch (-want +got):\n%s", diff) + if diff := cmp.Diff(tc.wantLeft, gotLeft); diff != "" { + t.Errorf("FindSubtrees left mismatch (-want +got):\n%s", diff) + } + if diff := cmp.Diff(tc.wantRight, gotRight); diff != "" { + t.Errorf("FindSubtrees right mismatch (-want +got):\n%s", diff) } }) } diff --git a/testonly/vectors_test.go b/testonly/vectors_test.go index 9db0d8f..2bea041 100644 --- a/testonly/vectors_test.go +++ b/testonly/vectors_test.go @@ -130,14 +130,11 @@ func TestSubtreeCoveringVectors(t *testing.T) { h := sha256.New() for end := range subtreeVectorMax + 1 { for start := range end + 1 { - subtrees, err := proof.FindSubtrees(start, end) + left, right, err := proof.FindSubtrees(start, end) if err != nil { t.Fatalf("FindSubtrees(%d, %d): %v", start, end, err) } - if l := len(subtrees); l != 2 { - t.Fatalf("FindSubtrees(%d, %d) returned unexpected number of subtrees: %d", start, end, l) - } - if _, err := fmt.Fprintf(h, "[%d, %d) [%d, %d)\n", subtrees[0].Start, subtrees[0].End, subtrees[1].Start, subtrees[1].End); err != nil { + if _, err := fmt.Fprintf(h, "[%d, %d) [%d, %d)\n", left.Start, left.End, right.Start, right.End); err != nil { t.Fatalf("fmt.Fprintf: %v", err) } } From 05851929b03edca1ef6281a5444e52fcbc7ea77f Mon Sep 17 00:00:00 2001 From: Philippe Boneff Date: Thu, 23 Jul 2026 13:17:29 +0000 Subject: [PATCH 5/5] Refactor FindSubtrees to return (start, mid, end uint64, err error) --- proof/proof.go | 26 +++++++++++--------------- proof/proof_test.go | 30 +++++++++++++----------------- testonly/vectors_test.go | 4 ++-- 3 files changed, 26 insertions(+), 34 deletions(-) diff --git a/proof/proof.go b/proof/proof.go index 71499de..0ae658a 100644 --- a/proof/proof.go +++ b/proof/proof.go @@ -271,29 +271,25 @@ func reverse(ids []compact.NodeID) { } } -// Subtree represents a valid Merkle subtree [Start, End). -type Subtree struct { - Start, End uint64 -} - -// FindSubtrees returns two subtrees that efficiently cover [start, end). +// FindSubtrees returns three indices (start, mid, end) defining two adjacent +// subtrees [start, mid) and [mid, end) that efficiently cover the input range. // // This function applies the "Selecting Two Subtrees" procedure from // Section 4.5.1 of draft-ietf-plants-merkle-tree-certs. // // Note that: -// - If the provided subtree has a size <= 1, then this function returns that subtree and an empty subtree. +// - If the input range has a size <= 1, then this function returns [start, end) and an empty subtree [end, end). // - If the provided [start, end) range is already a valid subtree, then it is still split into two smaller subtrees. -// - The 2nd subtree, if not empty, is adjacent to the first, and may not be a perfect subtree. -// - The returned subtrees fully cover the [start, end) range. -// - There are no "extra" entries covered past end, but there may be covered entries prior to start. -// - The number of entries covered before start is always less than half the size of the first returned subtree. -func FindSubtrees(start, end uint64) (Subtree, Subtree, error) { +// - The [mid, end) range, if not empty, is adjacent to the first, and may not be a perfect subtree. +// - The returned subtrees ranges fully cover the input range. +// - There are no "extra" entries covered past end, but there may be covered entries prior to start (i.e. returned start <= input start). +// - The number of entries covered before the input start is always less than half the size of the first returned subtree. +func FindSubtrees(start, end uint64) (uint64, uint64, uint64, error) { if start > end { - return Subtree{}, Subtree{}, fmt.Errorf("start %d must be less than or equal to end %d", start, end) + return 0, 0, 0, fmt.Errorf("start %d must be less than or equal to end %d", start, end) } if end-start <= 1 { - return Subtree{Start: start, End: end}, Subtree{Start: end, End: end}, nil + return start, end, end, nil } last := end - 1 // Find where start and last's tree paths diverge. @@ -304,7 +300,7 @@ func FindSubtrees(start, end uint64) (Subtree, Subtree, error) { // Maximize the left endpoint. leftSplit := bits.Len64(^start & mask) leftStart := start & ^((uint64(1) << leftSplit) - 1) - return Subtree{Start: leftStart, End: mid}, Subtree{Start: mid, End: end}, nil + return leftStart, mid, end, nil } // isSubtreeValid returns whether a subtree covers a valid range. diff --git a/proof/proof_test.go b/proof/proof_test.go index a5b1510..fc3f4de 100644 --- a/proof/proof_test.go +++ b/proof/proof_test.go @@ -824,27 +824,26 @@ func inclusion(t *testing.T, index, size uint64) Nodes { func TestFindSubtrees(t *testing.T) { for _, tc := range []struct { - start, end uint64 - wantLeft Subtree - wantRight Subtree - wantErr bool + start, end uint64 + wantStart, wantMid, wantEnd uint64 + wantErr bool }{ // Single entry subtrees: - {start: 0, end: 1, wantLeft: Subtree{Start: 0, End: 1}, wantRight: Subtree{Start: 1, End: 1}}, - {start: 3, end: 4, wantLeft: Subtree{Start: 3, End: 4}, wantRight: Subtree{Start: 4, End: 4}}, + {start: 0, end: 1, wantStart: 0, wantMid: 1, wantEnd: 1}, + {start: 3, end: 4, wantStart: 3, wantMid: 4, wantEnd: 4}, // Perfectly aligned subtrees: - {start: 4, end: 6, wantLeft: Subtree{Start: 4, End: 5}, wantRight: Subtree{Start: 5, End: 6}}, - {start: 16, end: 32, wantLeft: Subtree{Start: 16, End: 24}, wantRight: Subtree{Start: 24, End: 32}}, + {start: 4, end: 6, wantStart: 4, wantMid: 5, wantEnd: 6}, + {start: 16, end: 32, wantStart: 16, wantMid: 24, wantEnd: 32}, // Non-perfect trees are split into two: - {start: 5, end: 13, wantLeft: Subtree{Start: 4, End: 8}, wantRight: Subtree{Start: 8, End: 13}}, - {start: 7, end: 9, wantLeft: Subtree{Start: 7, End: 8}, wantRight: Subtree{Start: 8, End: 9}}, + {start: 5, end: 13, wantStart: 4, wantMid: 8, wantEnd: 13}, + {start: 7, end: 9, wantStart: 7, wantMid: 8, wantEnd: 9}, // Empty subtrees. - {start: 5, end: 5, wantLeft: Subtree{Start: 5, End: 5}, wantRight: Subtree{Start: 5, End: 5}}, + {start: 5, end: 5, wantStart: 5, wantMid: 5, wantEnd: 5}, // Invalid inputs: {start: 6, end: 5, wantErr: true}, } { t.Run(fmt.Sprintf("%d:%d", tc.start, tc.end), func(t *testing.T) { - gotLeft, gotRight, err := FindSubtrees(tc.start, tc.end) + gotStart, gotMid, gotEnd, err := FindSubtrees(tc.start, tc.end) if tc.wantErr { if err == nil { t.Fatal("expected error, got nil") @@ -854,11 +853,8 @@ func TestFindSubtrees(t *testing.T) { if err != nil { t.Fatalf("FindSubtrees: %v", err) } - if diff := cmp.Diff(tc.wantLeft, gotLeft); diff != "" { - t.Errorf("FindSubtrees left mismatch (-want +got):\n%s", diff) - } - if diff := cmp.Diff(tc.wantRight, gotRight); diff != "" { - t.Errorf("FindSubtrees right mismatch (-want +got):\n%s", diff) + if gotStart != tc.wantStart || gotMid != tc.wantMid || gotEnd != tc.wantEnd { + t.Errorf("FindSubtrees(%d, %d) = (%d, %d, %d), want (%d, %d, %d)", tc.start, tc.end, gotStart, gotMid, gotEnd, tc.wantStart, tc.wantMid, tc.wantEnd) } }) } diff --git a/testonly/vectors_test.go b/testonly/vectors_test.go index 2bea041..8ac114a 100644 --- a/testonly/vectors_test.go +++ b/testonly/vectors_test.go @@ -130,11 +130,11 @@ func TestSubtreeCoveringVectors(t *testing.T) { h := sha256.New() for end := range subtreeVectorMax + 1 { for start := range end + 1 { - left, right, err := proof.FindSubtrees(start, end) + start, mid, end, err := proof.FindSubtrees(start, end) if err != nil { t.Fatalf("FindSubtrees(%d, %d): %v", start, end, err) } - if _, err := fmt.Fprintf(h, "[%d, %d) [%d, %d)\n", left.Start, left.End, right.Start, right.End); err != nil { + if _, err := fmt.Fprintf(h, "[%d, %d) [%d, %d)\n", start, mid, mid, end); err != nil { t.Fatalf("fmt.Fprintf: %v", err) } }