diff --git a/CHANGELOG.md b/CHANGELOG.md index 1935ed2..78c96eb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ## HEAD +* Breaking change: consistency proofs from `size1 = 0` to `size2 != 0` now always fail + * Previously, this could succeed if the empty proof was provided * Bump Go version from 1.19 to 1.20 ## v0.0.2 diff --git a/proof/verify.go b/proof/verify.go index d42e1af..8226ad4 100644 --- a/proof/verify.go +++ b/proof/verify.go @@ -74,24 +74,32 @@ func RootFromInclusionProof(hasher merkle.LogHasher, index, size uint64, leafHas // VerifyConsistency checks that the passed-in consistency proof is valid // between the passed in tree sizes, with respect to the corresponding root -// hashes. Requires 0 <= size1 <= size2. +// hashes. Requires 0 < size1 <= size2. func VerifyConsistency(hasher merkle.LogHasher, size1, size2 uint64, proof [][]byte, root1, root2 []byte) error { + hash2, err := RootFromConsistencyProof(hasher, size1, size2, proof, root1) + if err != nil { + return err + } + return verifyMatch(hash2, root2) +} + +// RootFromConsistencyProof calculates the expected root hash for a tree of the +// given size2, provided a tree of size1 with root1, and a consistency proof. +// Requires 0 < size1 <= size2. +// Note that consistency proofs from a size1==0 cannot be computed. +func RootFromConsistencyProof(hasher merkle.LogHasher, size1, size2 uint64, proof [][]byte, root1 []byte) ([]byte, error) { switch { case size2 < size1: - return fmt.Errorf("size2 (%d) < size1 (%d)", size1, size2) + return nil, fmt.Errorf("size2 (%d) < size1 (%d)", size1, size2) case size1 == size2: if len(proof) > 0 { - return errors.New("size1=size2, but proof is not empty") + return nil, errors.New("size1=size2, but proof is not empty") } - return verifyMatch(root1, root2) + return root1, nil case size1 == 0: - // Any size greater than 0 is consistent with size 0. - if len(proof) > 0 { - return fmt.Errorf("expected empty proof, but got %d components", len(proof)) - } - return nil // Proof OK. + return nil, errors.New("consistency proof from empty tree is meaningless") case len(proof) == 0: - return errors.New("empty proof") + return nil, errors.New("empty proof") } inner, border := decompInclProof(size1-1, size2) @@ -104,7 +112,7 @@ func VerifyConsistency(hasher merkle.LogHasher, size1, size2 uint64, proof [][]b seed, start = root1, 0 } if got, want := len(proof), start+inner+border; got != want { - return fmt.Errorf("wrong proof size %d, want %d", got, want) + return nil, fmt.Errorf("wrong proof size %d, want %d", got, want) } proof = proof[start:] // Now len(proof) == inner+border, and proof is effectively a suffix of @@ -115,13 +123,13 @@ func VerifyConsistency(hasher merkle.LogHasher, size1, size2 uint64, proof [][]b hash1 := chainInnerRight(hasher, seed, proof[:inner], mask) hash1 = chainBorderRight(hasher, hash1, proof[inner:]) if err := verifyMatch(hash1, root1); err != nil { - return err + return nil, err } // Verify the second root. hash2 := chainInner(hasher, seed, proof[:inner], mask) hash2 = chainBorderRight(hasher, hash2, proof[inner:]) - return verifyMatch(hash2, root2) + return hash2, nil } // decompInclProof breaks down inclusion proof for a leaf at the specified diff --git a/proof/verify_test.go b/proof/verify_test.go index d62931b..86aafa7 100644 --- a/proof/verify_test.go +++ b/proof/verify_test.go @@ -344,7 +344,7 @@ func TestVerifyConsistency(t *testing.T) { {1, 1, root1, root2, proof1, true}, // Sizes that are always consistent. {0, 0, root1, root1, proof1, false}, - {0, 1, root1, root2, proof1, false}, + {0, 1, root1, root2, proof1, true}, {1, 1, root2, root2, proof1, false}, // Time travel to the past. {1, 0, root1, root2, proof1, true}, diff --git a/testonly/tree_fuzz_test.go b/testonly/tree_fuzz_test.go index 2dd1b29..6a4e1df 100644 --- a/testonly/tree_fuzz_test.go +++ b/testonly/tree_fuzz_test.go @@ -14,9 +14,9 @@ import ( // Compute and verify consistency proofs func FuzzConsistencyProofAndVerify(f *testing.F) { - for size := 0; size <= 8; size++ { - for end := 0; end <= size; end++ { - for begin := 0; begin <= end; begin++ { + for size := 1; size <= 8; size++ { + for end := 1; end <= size; end++ { + for begin := 1; begin <= end; begin++ { f.Add(uint64(size), uint64(begin), uint64(end)) } } @@ -30,6 +30,9 @@ func FuzzConsistencyProofAndVerify(f *testing.F) { if begin > end || end > size { return } + if begin == 0 && end > 0 { + return + } tree := newTree(genEntries(size)) p, err := tree.ConsistencyProof(begin, end) t.Logf("proof=%v", p)