From accbc13e5454288e10eb69cac43a92808892c605 Mon Sep 17 00:00:00 2001 From: Philippe Boneff Date: Tue, 21 Jul 2026 13:55:28 +0000 Subject: [PATCH] Align subtrees with updated specs --- cmd/proofgen/main.go | 48 ++++++++++++++----- proof/proof.go | 18 ++++--- proof/proof_test.go | 27 ++++++----- proof/verify.go | 24 +++++++++- ...roots-do-not-match-and-sizes-are-zero.json | 10 ---- .../size1-is-zero-and-size2-is-not-zero.json | 10 ---- ...es-are-equal-(one)-and-proof-is-empty.json | 10 ---- ...zes-are-equal-(one)-but-roots-are-not.json | 10 ---- ...es-are-equal-(zero)-but-roots-are-not.json | 10 ---- ...e-is-empty-roots-match-but-not-valid.json} | 8 ++-- ...ty-roots-valid-but-proof-is-not-empty.json | 12 +++++ ...e-equal-one-roots-match-but-not-valid.json | 10 ++++ ...e-roots-valid-but-proof-is-not-empty.json} | 6 +-- ...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} | 2 +- ...o-roots-valid-but-proof-is-not-empty.json} | 4 +- ...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 ++++++++---------- 25 files changed, 211 insertions(+), 129 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 delete mode 100644 testdata/subtreeconsistency/additional/sizes-are-equal-(one)-and-proof-is-empty.json delete mode 100644 testdata/subtreeconsistency/additional/sizes-are-equal-(one)-but-roots-are-not.json delete mode 100644 testdata/subtreeconsistency/additional/sizes-are-equal-(zero)-but-roots-are-not.json rename testdata/subtreeconsistency/additional/{sizes-are-equal-zero-and-proof-is-empty.json => subtree-is-empty-roots-match-but-not-valid.json} (50%) 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 rename testdata/subtreeconsistency/additional/{consistency-check-on-empty-tree-size1-is-zero-is-useless.json => subtree-is-empty-sizes-are-equal-one-roots-valid-but-proof-is-not-empty.json} (66%) 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 rename testdata/subtreeconsistency/additional/{sizes-are-equal-(zero)-and-proof-is-empty.json => subtree-is-empty-sizes-are-equal-zero-roots-match-but-not-valid.json} (63%) rename testdata/subtreeconsistency/additional/{consistency-check-on-empty-tree-(size1-is-zero)-is-useless.json => subtree-is-empty-sizes-are-equal-zero-roots-valid-but-proof-is-not-empty.json} (69%) 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..39a609f 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 watch and proof is empty", true}, + // Roots don't match. + {0, 1, 1, sha256EmptyTreeHash, root2, proof1, "roots do not 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..40f79e3 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 { @@ -286,11 +289,11 @@ type Subtree struct { // - 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..5acb369 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) @@ -599,8 +600,8 @@ func TestSubtreeConsistency(t *testing.T) { func TestInclusionSucceedsUpToTreeSize(t *testing.T) { const maxSize = uint64(555) - for ts := uint64(1); ts <= maxSize; ts++ { - for i := ts; i < ts; i++ { + for ts := range maxSize + 1 { + for i := range ts { if _, err := Inclusion(i, ts); err != nil { t.Errorf("Inclusion(ts:%d, i:%d) = %v", ts, i, err) } @@ -608,7 +609,7 @@ func TestInclusionSucceedsUpToTreeSize(t *testing.T) { } } -func TestInclusionSubtreeSucceedsUpToTreeSize(t *testing.T) { +func TestSubtreeInclusionSucceedsUpToTreeSize(t *testing.T) { const maxSize = uint64(555) for sbe := uint64(1); sbe <= maxSize; sbe++ { for sbs := range sbe { @@ -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..3b5da0f 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,14 @@ 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 == 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-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 diff --git a/testdata/subtreeconsistency/additional/sizes-are-equal-(one)-and-proof-is-empty.json b/testdata/subtreeconsistency/additional/sizes-are-equal-(one)-and-proof-is-empty.json deleted file mode 100644 index 0218579..0000000 --- a/testdata/subtreeconsistency/additional/sizes-are-equal-(one)-and-proof-is-empty.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "start": 0, - "end": 1, - "size": 1, - "root1": "ZG9uJ3QgY2FyZSAy", - "root2": "ZG9uJ3QgY2FyZSAy", - "proof": [], - "desc": "sizes are equal (one) and proof is empty", - "wantErr": false -} \ No newline at end of file diff --git a/testdata/subtreeconsistency/additional/sizes-are-equal-(one)-but-roots-are-not.json b/testdata/subtreeconsistency/additional/sizes-are-equal-(one)-but-roots-are-not.json deleted file mode 100644 index 4320aab..0000000 --- a/testdata/subtreeconsistency/additional/sizes-are-equal-(one)-but-roots-are-not.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "start": 0, - "end": 1, - "size": 1, - "root1": "ZG9uJ3QgY2FyZSAx", - "root2": "ZG9uJ3QgY2FyZSAy", - "proof": [], - "desc": "sizes are equal (one) but roots are not", - "wantErr": true -} \ No newline at end of file diff --git a/testdata/subtreeconsistency/additional/sizes-are-equal-(zero)-but-roots-are-not.json b/testdata/subtreeconsistency/additional/sizes-are-equal-(zero)-but-roots-are-not.json deleted file mode 100644 index a9ae44f..0000000 --- a/testdata/subtreeconsistency/additional/sizes-are-equal-(zero)-but-roots-are-not.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "start": 0, - "end": 0, - "size": 0, - "root1": "ZG9uJ3QgY2FyZSAx", - "root2": "ZG9uJ3QgY2FyZSAy", - "proof": [], - "desc": "sizes are equal (zero) but roots are not", - "wantErr": true -} \ No newline at end of file diff --git a/testdata/subtreeconsistency/additional/sizes-are-equal-zero-and-proof-is-empty.json b/testdata/subtreeconsistency/additional/subtree-is-empty-roots-match-but-not-valid.json similarity index 50% rename from testdata/subtreeconsistency/additional/sizes-are-equal-zero-and-proof-is-empty.json rename to testdata/subtreeconsistency/additional/subtree-is-empty-roots-match-but-not-valid.json index d66f96d..e01d16b 100644 --- a/testdata/subtreeconsistency/additional/sizes-are-equal-zero-and-proof-is-empty.json +++ b/testdata/subtreeconsistency/additional/subtree-is-empty-roots-match-but-not-valid.json @@ -1,10 +1,10 @@ { - "start": 0, - "end": 0, - "size": 0, + "start": 1, + "end": 1, + "size": 2, "root1": "ZG9uJ3QgY2FyZSAx", "root2": "ZG9uJ3QgY2FyZSAx", "proof": [], - "desc": "sizes are equal (zero) and proof is empty", + "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/consistency-check-on-empty-tree-size1-is-zero-is-useless.json b/testdata/subtreeconsistency/additional/subtree-is-empty-sizes-are-equal-one-roots-valid-but-proof-is-not-empty.json similarity index 66% rename from testdata/subtreeconsistency/additional/consistency-check-on-empty-tree-size1-is-zero-is-useless.json rename to testdata/subtreeconsistency/additional/subtree-is-empty-sizes-are-equal-one-roots-valid-but-proof-is-not-empty.json index a181a06..0ef5315 100644 --- a/testdata/subtreeconsistency/additional/consistency-check-on-empty-tree-size1-is-zero-is-useless.json +++ b/testdata/subtreeconsistency/additional/subtree-is-empty-sizes-are-equal-one-roots-valid-but-proof-is-not-empty.json @@ -1,12 +1,12 @@ { - "start": 0, - "end": 0, + "start": 1, + "end": 1, "size": 1, "root1": "47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=", "root2": "47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=", "proof": [ "47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=" ], - "desc": "consistency check on empty tree (size1 is zero) is useless", + "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/sizes-are-equal-(zero)-and-proof-is-empty.json b/testdata/subtreeconsistency/additional/subtree-is-empty-sizes-are-equal-zero-roots-match-but-not-valid.json similarity index 63% rename from testdata/subtreeconsistency/additional/sizes-are-equal-(zero)-and-proof-is-empty.json rename to testdata/subtreeconsistency/additional/subtree-is-empty-sizes-are-equal-zero-roots-match-but-not-valid.json index d66f96d..7474fb6 100644 --- a/testdata/subtreeconsistency/additional/sizes-are-equal-(zero)-and-proof-is-empty.json +++ b/testdata/subtreeconsistency/additional/subtree-is-empty-sizes-are-equal-zero-roots-match-but-not-valid.json @@ -5,6 +5,6 @@ "root1": "ZG9uJ3QgY2FyZSAx", "root2": "ZG9uJ3QgY2FyZSAx", "proof": [], - "desc": "sizes are equal (zero) and proof is empty", + "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/consistency-check-on-empty-tree-(size1-is-zero)-is-useless.json b/testdata/subtreeconsistency/additional/subtree-is-empty-sizes-are-equal-zero-roots-valid-but-proof-is-not-empty.json similarity index 69% rename from testdata/subtreeconsistency/additional/consistency-check-on-empty-tree-(size1-is-zero)-is-useless.json rename to testdata/subtreeconsistency/additional/subtree-is-empty-sizes-are-equal-zero-roots-valid-but-proof-is-not-empty.json index a181a06..8a6e3d2 100644 --- a/testdata/subtreeconsistency/additional/consistency-check-on-empty-tree-(size1-is-zero)-is-useless.json +++ b/testdata/subtreeconsistency/additional/subtree-is-empty-sizes-are-equal-zero-roots-valid-but-proof-is-not-empty.json @@ -1,12 +1,12 @@ { "start": 0, "end": 0, - "size": 1, + "size": 0, "root1": "47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=", "root2": "47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=", "proof": [ "47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=" ], - "desc": "consistency check on empty tree (size1 is zero) is useless", + "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) }