Skip to content

Commit

Permalink
Support calculating root from consistency proof (#140)
Browse files Browse the repository at this point in the history
This is useful for other teams in the transparency space and was requested via the transparency-dev Slack channel. The new method is similar in essence to RootFromInclusionProof so fits in within the API.

As noted in the CHANGELOG, this change fixes a logical bug in the previous code that would have successfully verified an _empty_ proof from a tree size of 0 to any other tree size. In this change, trying to verify a consistency from a tree size of 0 to any other size than 0 will be considered an error, no matter what proof is provided.

Updated comment on VerifyConsistency to say size1 is required to be > 0. The function is only clearly defined in this case. It's now undefined where size1 is 0.
  • Loading branch information
mhutchinson authored Sep 18, 2024
1 parent 406fabc commit 022f84c
Show file tree
Hide file tree
Showing 4 changed files with 30 additions and 17 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
34 changes: 21 additions & 13 deletions proof/verify.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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
Expand All @@ -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
Expand Down
2 changes: 1 addition & 1 deletion proof/verify_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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},
Expand Down
9 changes: 6 additions & 3 deletions testonly/tree_fuzz_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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))
}
}
Expand All @@ -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)
Expand Down

0 comments on commit 022f84c

Please sign in to comment.