diff --git a/go.mod b/go.mod index 8a286efbc68..75398072a27 100644 --- a/go.mod +++ b/go.mod @@ -38,6 +38,7 @@ require ( go.opentelemetry.io/otel/trace v1.43.0 go.yaml.in/yaml/v3 v3.0.4 golang.org/x/crypto v0.52.0 + golang.org/x/mod v0.35.0 golang.org/x/net v0.55.0 golang.org/x/sync v0.20.0 golang.org/x/term v0.43.0 @@ -85,7 +86,6 @@ require ( go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 // indirect go.opentelemetry.io/otel/metric v1.43.0 // indirect go.opentelemetry.io/proto/otlp v1.7.1 // indirect - golang.org/x/mod v0.35.0 // indirect golang.org/x/sys v0.45.0 // indirect golang.org/x/tools v0.44.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217 // indirect diff --git a/tlog/helpers_test.go b/tlog/helpers_test.go new file mode 100644 index 00000000000..e62156e1820 --- /dev/null +++ b/tlog/helpers_test.go @@ -0,0 +1,54 @@ +package tlog + +import ( + "fmt" + "testing" + + xtlog "golang.org/x/mod/sumdb/tlog" +) + +// seqLeaves returns n distinct entries for round-trip tests. +func seqLeaves(n int) [][]byte { + entries := make([][]byte, n) + for i := range entries { + entries[i] = []byte{byte(i), byte(i >> 8), byte(i >> 16)} + } + return entries +} + +// leafHashes returns the RFC 6962 leaf hashes of the provided entries. +func leafHashes(entries [][]byte) []xtlog.Hash { + hs := make([]xtlog.Hash, len(entries)) + for i, e := range entries { + hs[i] = xtlog.RecordHash(e) + } + return hs +} + +// inmemHashReader is an in-memory tlog.HashReader indexed by stored hash index. +type inmemHashReader []xtlog.Hash + +func (m inmemHashReader) ReadHashes(indexes []int64) ([]xtlog.Hash, error) { + out := make([]xtlog.Hash, len(indexes)) + for i, x := range indexes { + if x < 0 || x >= int64(len(m)) { + return nil, fmt.Errorf("stored hash index %d out of range [0, %d)", x, len(m)) + } + out[i] = m[x] + } + return out, nil +} + +func buildHashReader(t *testing.T, entries [][]byte) inmemHashReader { + t.Helper() + + var m inmemHashReader + for n, e := range entries { + hashes, err := xtlog.StoredHashes(int64(n), e, m) + if err != nil { + t.Fatalf("StoredHashes(%d): %s", n, err) + } + m = append(m, hashes...) + } + return m +} diff --git a/tlog/subtree.go b/tlog/subtree.go new file mode 100644 index 00000000000..64058bfa098 --- /dev/null +++ b/tlog/subtree.go @@ -0,0 +1,277 @@ +package tlog + +import ( + "crypto/sha256" + "fmt" + "math/bits" + + xtlog "golang.org/x/mod/sumdb/tlog" +) + +// largestPowerOfTwoSmallerThan returns the largest power of two strictly less +// than n, for n > 1. n <= 1 results in a panic. +func largestPowerOfTwoSmallerThan(n int64) int64 { + if n <= 1 { + panic(fmt.Sprintf("n must be > 1, got %d", n)) + } + return int64(1) << (bits.Len64(uint64(n-1)) - 1) //nolint:gosec // G115: n > 1, so n-1 is positive. +} + +// SubtreeHash returns the RFC 6962 section 2.1 Merkle Tree Hash over leaves +// treated as an independent list. Note: callers must ensure the leaves +// correspond to a ValidSubtree range. +func SubtreeHash(leaves []xtlog.Hash) xtlog.Hash { + switch len(leaves) { + case 0: + // The hash of an empty list is the hash of an empty string. + return xtlog.Hash(sha256.Sum256(nil)) + case 1: + // The hash of a list with one entry is just the leaf hash. + return leaves[0] + } + + // Split the list into two subtree roots, the left being a "perfect" subtree + // and the right being the remainder which may or may not be perfect. + k := largestPowerOfTwoSmallerThan(int64(len(leaves))) + + // Hash the two subtree roots together as SHA-256(0x01 || left || right). + return xtlog.NodeHash(SubtreeHash(leaves[:k]), SubtreeHash(leaves[k:])) +} + +// ValidSubtree reports whether [start, end) is a valid subtree per the MTC +// draft section 4.1 Definition of a Subtree: 0 <= start < end and start is a +// multiple of BIT_CEIL(end - start). +func ValidSubtree(start, end int64) bool { + if start < 0 || start >= end { + // A subtree must have 0 <= start < end. + return false + } + // bitCeil is BIT_CEIL(end-start). A multiple of a power of two has its low + // bits zero, so start & (bitCeil-1) == 0 becomes our validity test. + bitCeil := uint64(1) << bits.Len64(uint64(end-start-1)) //nolint:gosec // G115: start < end, so end-start-1 is non-negative. + return uint64(start)&(bitCeil-1) == 0 +} + +// perfectSubtree reports whether [lo, hi) is an aligned perfect subtree +// (power-of-two size, start aligned to that size), and if so its level. +func perfectSubtree(lo, hi int64) (level int, ok bool) { + if lo < 0 || lo >= hi || hi < 0 { + panic(fmt.Sprintf("invalid range [%d, %d)", lo, hi)) + } + size := hi - lo + if bits.OnesCount64(uint64(size)) != 1 || lo&(size-1) != 0 { //nolint:gosec // G115: callers pass lo < hi, so size is positive. + return 0, false + } + return bits.TrailingZeros64(uint64(size)), true //nolint:gosec // G115: callers pass lo < hi, so size is positive. +} + +// foldRangeHash folds subtree roots, in the order perfectSubtreeIndexes lists +// them, into MTH(D[lo:hi)). It returns the hash and the unconsumed remainder. +func foldRangeHash(lo, hi int64, hashes []xtlog.Hash) (xtlog.Hash, []xtlog.Hash) { + _, ok := perfectSubtree(lo, hi) + if ok { + return hashes[0], hashes[1:] + } + k := largestPowerOfTwoSmallerThan(hi - lo) + left, rest := foldRangeHash(lo, lo+k, hashes) + right, rest := foldRangeHash(lo+k, hi, rest) + return xtlog.NodeHash(left, right), rest +} + +// perfectSubtreeIndexes appends, in left-to-right order, the stored hash index +// of each subtree in the maximal aligned perfect decomposition of [lo, hi). +func perfectSubtreeIndexes(lo, hi int64, indexes []int64) []int64 { + level, ok := perfectSubtree(lo, hi) + if ok { + return append(indexes, xtlog.StoredHashIndex(level, lo>>level)) + } + k := largestPowerOfTwoSmallerThan(hi - lo) + indexes = perfectSubtreeIndexes(lo, lo+k, indexes) + return perfectSubtreeIndexes(lo+k, hi, indexes) +} + +// rangeHash returns MTH(D[lo:hi)), the RFC 6962 section 2.1 Merkle Tree Hash +// over the leaves in [lo, hi) as an independent list, read through the provided +// reader. It decomposes [lo, hi) into its maximal aligned perfect subtrees and +// reads all of their roots in a single ReadHashes call before folding them +// together. +func rangeHash(lo, hi int64, reader xtlog.HashReader) (xtlog.Hash, error) { + indexes := perfectSubtreeIndexes(lo, hi, nil) + hashes, err := reader.ReadHashes(indexes) + if err != nil { + return xtlog.Hash{}, err + } + if len(hashes) != len(indexes) { + // Reader returned a slice shorter or larger than the requested indexes. + // Avoid panicking on the fold. + return xtlog.Hash{}, fmt.Errorf("ReadHashes returned %d hashes for %d indexes", len(hashes), len(indexes)) + } + h, _ := foldRangeHash(lo, hi, hashes) + return h, nil +} + +func appendRangeHash(lo, hi int64, reader xtlog.HashReader, proof []xtlog.Hash) ([]xtlog.Hash, error) { + h, err := rangeHash(lo, hi, reader) + if err != nil { + return nil, err + } + return append(proof, h), nil +} + +// subtreeSubProof implements SUBTREE_SUBPROOF(start, end, D_n, b) from the MTC +// draft section 4.4.1 Generating a Subtree Consistency Proof, detailed further +// in the draft's Appendix B.4. start and end are relative to the current +// subtree D_n of size n rooted at absolute offset base, and known is the +// draft's b flag. It reads stored hashes through the provided reader and +// returns proof with the hashes it emits appended. +func subtreeSubProof(start, end, base, n int64, known bool, reader xtlog.HashReader, proof []xtlog.Hash) ([]xtlog.Hash, error) { + if start == 0 && end == n { + // [start, end) now covers this whole node D_n, the SUBTREE_SUBPROOF + // base case. known decides whether the proof carries it. + if known { + // The verifier already has this node, so emit nothing. + return proof, nil + } + + // The verifier doesn't have it, so emit its hash MTH(D_n). + h, err := rangeHash(base, base+n, reader) + if err != nil { + return nil, err + } + return append(proof, h), nil + } + + // [start, end) covers only part of this node, so split at k. The switch + // routes by where the subtree falls (left child, right child, or straddle) + // and names the other child as the sibling the shared tail appends. + k := largestPowerOfTwoSmallerThan(n) + var err error + var siblingLo int64 + var siblingHi int64 + switch { + case end <= k: + // The subtree fits in the left child. Recurse there, with the right + // child [k, n) as the sibling. + proof, err = subtreeSubProof(start, end, base, k, known, reader, proof) + siblingLo = base + k + siblingHi = base + n + case k <= start: + // The subtree fits in the right child. Recurse there (shifting + // coordinates by k), with the left child [0, k) as the sibling. + proof, err = subtreeSubProof(start-k, end-k, base+k, n-k, known, reader, proof) + siblingLo = base + siblingHi = base + k + default: + // The subtree straddles the split (start < k < end), which a valid + // subtree only does when start == 0. Recurse on the right child's + // prefix [0, end-k), no longer a node the verifier knows (known = + // false), with the left child [0, k) as the sibling. + proof, err = subtreeSubProof(0, end-k, base+k, n-k, false, reader, proof) + siblingLo = base + siblingHi = base + k + } + if err != nil { + return nil, err + } + return appendRangeHash(siblingLo, siblingHi, reader, proof) +} + +// SubtreeConsistencyProof returns SUBTREE_PROOF(start, end, D_n) for the tree +// of size treeSize, reading stored hashes through the provided reader, per the +// MTC draft section 4.4.1 Generating a Subtree Consistency Proof, detailed +// further in the draft's Appendix B.4. [start, end) must be a valid subtree +// with end <= treeSize. +func SubtreeConsistencyProof(start, end, treeSize int64, reader xtlog.HashReader) ([]xtlog.Hash, error) { + if !ValidSubtree(start, end) || end > treeSize { + return nil, fmt.Errorf("[%d, %d) is not a valid subtree of a tree of size %d", start, end, treeSize) + } + return subtreeSubProof(start, end, 0, treeSize, true, reader, nil) +} + +// VerifySubtreeConsistency reports whether proof shows that the subtree [start, +// end), whose hash is nodeHash, sits at those positions in the tree of size n +// with root rootHash. It follows the procedure in MTC draft section 4.4.3, +// detailed further in the draft's Appendix B.5. +func VerifySubtreeConsistency(start, end, n int64, proof []xtlog.Hash, nodeHash, rootHash xtlog.Hash) bool { + if !ValidSubtree(start, end) || end > n { + return false + } + + // fn, sn, tn track the subtree's first leaf, its last leaf, and the tree's + // last leaf. Right-shifting a cursor climbs one level. + fn := start + sn := end - 1 + tn := n - 1 + + // Skip the levels that need no proof hash. The branch turns on whether the + // subtree's right edge meets the tree's right edge (sn == tn) or not. + if sn == tn { + // A flush subtree has no outside sibling to fold on the way up to + // nodeHash, so climb every level. + for fn != sn { + fn >>= 1 + sn >>= 1 + tn >>= 1 + } + } else { + // An interior subtree eventually meets an outside sibling, so climb + // only while sn is a right child. + for fn != sn && sn&1 == 1 { + fn >>= 1 + sn >>= 1 + tn >>= 1 + } + } + + // fr and sr climb together from a shared seed: fr rebuilds the subtree + // hash, sr the tree root. + var fr xtlog.Hash + var sr xtlog.Hash + var rest []xtlog.Hash + if fn == sn { + // A single node: the seed is its hash, nodeHash. + fr = nodeHash + sr = nodeHash + rest = proof + } else { + // The subtree is larger, so the seed is proof[0], the largest perfect + // subtree flush with its right edge. + if len(proof) == 0 { + return false + } + fr = proof[0] + sr = proof[0] + rest = proof[1:] + } + + for _, c := range rest { + if tn == 0 { + // The proof has more hashes than the tree has levels. + return false + } + if sn&1 == 1 || sn == tn { + if fn < sn { + // fr only folds while fn < sn. Freezing it at fn == sn is what + // makes the final fr == nodeHash check meaningful. + fr = xtlog.NodeHash(c, fr) + } + sr = xtlog.NodeHash(c, sr) + // At the ragged right edge (sn == tn) the just-merged node is + // shorter than its left sibling, so skip its empty levels here, + // consuming no proof hash, until sn is odd again. + for sn&1 == 0 { + fn >>= 1 + sn >>= 1 + tn >>= 1 + } + } else { + // c is the node's right sibling, outside the subtree, so it extends + // sr toward the root. + sr = xtlog.NodeHash(sr, c) + } + fn >>= 1 + sn >>= 1 + tn >>= 1 + } + return tn == 0 && fr == nodeHash && sr == rootHash +} diff --git a/tlog/subtree_test.go b/tlog/subtree_test.go new file mode 100644 index 00000000000..f3b89b86f76 --- /dev/null +++ b/tlog/subtree_test.go @@ -0,0 +1,468 @@ +package tlog + +import ( + "crypto/sha256" + "encoding/hex" + "errors" + "fmt" + "math" + "slices" + "testing" + + xtlog "golang.org/x/mod/sumdb/tlog" +) + +func TestValidSubtree(t *testing.T) { + cases := []struct { + name string + start int64 + end int64 + expect bool + }{ + // Valid + {"Single leaf", 0, 1, true}, + {"Start 0 aligns to any size", 0, 14, true}, + {"Aligned size-2 block", 2, 4, true}, + {"Single leaf at an odd offset", 3, 4, true}, + {"Aligned size-4 block", 4, 8, true}, + {"Aligned size-4 block, start a higher multiple of size", 8, 12, true}, + {"Non-power-of-two size, start aligned to BIT_CEIL", 8, 13, true}, + {"Start 0 aligns to the 2^63 ceiling", 0, math.MaxInt64, true}, + // Invalid + {"Misaligned start", 1, 3, false}, + {"Misaligned start, higher offset", 7, 9, false}, + {"Empty", 4, 4, false}, + {"Inverted", 5, 4, false}, + {"Nonzero start can't align to the 2^63 ceiling", 1, math.MaxInt64, false}, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + got := ValidSubtree(tc.start, tc.end) + if got != tc.expect { + t.Errorf("ValidSubtree(%d, %d) = %v, want %v", tc.start, tc.end, got, tc.expect) + } + }) + } +} + +// TestSubtreeHashVectors tests SubtreeHash against the published RFC 6962 +// reference roots for sizes 0-8. +func TestSubtreeHashVectors(t *testing.T) { + entryHexes := []string{ + "", + "00", + "10", + "2021", + "3031", + "40414243", + "5051525354555657", + "606162636465666768696a6b6c6d6e6f", + } + entries := make([][]byte, len(entryHexes)) + for i, h := range entryHexes { + var err error + entries[i], err = hex.DecodeString(h) + if err != nil { + t.Fatalf("decoding entry %q: %s", h, err) + } + } + leaves := leafHashes(entries) + expect := []string{ + "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "6e340b9cffb37a989ca544e6bb780a2c78901d3fb33738768511a30617afa01d", + "fac54203e7cc696cf0dfcb42c92a1d9dbaf70ad9e621f4bd8d98662f00e3c125", + "aeb6bcfe274b70a14fb067a5e5578264db0fa9b51af5e0ba159158f329e06e77", + "d37ee418976dd95753c1c73862b9398fa2a2cf9b4ff0fdfe8b30cd95209614b7", + "4e3bbb1f7b478dcfe71fb631631519a3bca12c9aefca1612bfce4c13a86264d4", + "76e67dadbcdf1e10e1b74ddc608abd2f98dfb16fbce75277b5232a127f2087ef", + "ddb89be403809e325750d3d263cd78929c2942b7942a34b77e122c9594a74c8c", + "5dc9da79a70659a9ad559cb701ded9a2ab9d823aad2f4960cfe370eff4604328", + } + for size := 0; size <= 8; size++ { + got := SubtreeHash(leaves[:size]) + if hex.EncodeToString(got[:]) != expect[size] { + t.Errorf("SubtreeHash(size %d) = %x, want %s", size, got, expect[size]) + } + } +} + +// TestSubtreeHashAppendixVector pins SubtreeHash to the accumulated digest in +// the MTC draft appendix C.1 Subtree Hashes for every valid subtree up to size +// 130, which the draft's reference implementation also reproduces. +func TestSubtreeHashAppendixVector(t *testing.T) { + want := "94a95384a8c69acea9b50d035a58285b3a777cb7a724005faa5e1f1e1190007f" + entries := make([][]byte, 130) + for i := range entries { + entries[i] = []byte{byte(i)} + } + leaves := leafHashes(entries) + + h := sha256.New() + for end := int64(1); end <= 130; end++ { + for start := int64(0); start < end; start++ { + if !ValidSubtree(start, end) { + continue + } + subtree := SubtreeHash(leaves[start:end]) + fmt.Fprintf(h, "[%d, %d) %s\n", start, end, hex.EncodeToString(subtree[:])) + } + } + got := hex.EncodeToString(h.Sum(nil)) + if got != want { + t.Errorf("subtree hash accumulator:\n got %s\n want %s", got, want) + } +} + +// TestTreeHashMatchesOracle checks SubtreeHash against x/mod/sumdb/tlog's +// TreeHash. It also validates the in-memory HashReader the proof tests rely on. +func TestTreeHashMatchesOracle(t *testing.T) { + for n := 1; n <= 32; n++ { + entries := seqLeaves(n) + got, err := xtlog.TreeHash(int64(n), buildHashReader(t, entries)) + if err != nil { + t.Fatalf("TreeHash(%d): %s", n, err) + } + want := SubtreeHash(leafHashes(entries)) + if got != want { + t.Errorf("TreeHash(%d) = %x, want %x", n, got, want) + } + } +} + +// TestSubtreeProofExamples covers the generate and verify round trip for the +// two worked examples in the MTC draft, which are small enough to be +// human-readable and have published expected proofs. +func TestSubtreeProofExamples(t *testing.T) { + leaves := leafHashes(seqLeaves(14)) + reader := buildHashReader(t, seqLeaves(14)) + root := SubtreeHash(leaves) + mth := func(start, end int64) xtlog.Hash { + return SubtreeHash(leaves[start:end]) + } + + cases := []struct { + start int64 + end int64 + expect []xtlog.Hash + }{ + {4, 8, []xtlog.Hash{mth(0, 4), mth(8, 14)}}, + {8, 13, []xtlog.Hash{mth(12, 13), mth(13, 14), mth(8, 12), mth(0, 8)}}, + } + for _, tc := range cases { + proof, err := SubtreeConsistencyProof(tc.start, tc.end, 14, reader) + if err != nil { + t.Fatalf("SubtreeConsistencyProof(%d, %d, 14): %s", tc.start, tc.end, err) + } + if !slices.Equal(proof, tc.expect) { + t.Errorf("SubtreeConsistencyProof(%d, %d, 14) = %x, want %x", tc.start, tc.end, proof, tc.expect) + } + if !VerifySubtreeConsistency(tc.start, tc.end, 14, proof, mth(tc.start, tc.end), root) { + t.Errorf("VerifySubtreeConsistency(%d, %d, 14) rejected a valid proof", tc.start, tc.end) + } + } +} + +// TestSubtreeConsistencyProofAppendixVector pins SubtreeConsistencyProof to the +// accumulated digest in MTC draft appendix C.3 Subtree Consistency Proofs, +// covering every valid subtree of every tree up to size 130. It also runs each +// generated proof through VerifySubtreeConsistency, pinning the verifier's +// accept path across the full range, including the start > 0 proofs that +// x/mod/sumdb/tlog's CheckTree (prefix-only) cannot oracle. +func TestSubtreeConsistencyProofAppendixVector(t *testing.T) { + want := "c586ebbb73a5621baf2140095d87dde934e3b6503a562a1a5215b8209edd083d" + entries := make([][]byte, 130) + for i := range entries { + entries[i] = []byte{byte(i)} + } + leaves := leafHashes(entries) + reader := buildHashReader(t, entries) + + h := sha256.New() + for n := int64(0); n <= 130; n++ { + root := SubtreeHash(leaves[:n]) + for end := int64(1); end <= n; end++ { + for start := int64(0); start < end; start++ { + if !ValidSubtree(start, end) { + continue + } + proof, err := SubtreeConsistencyProof(start, end, n, reader) + if err != nil { + t.Fatalf("SubtreeConsistencyProof(%d, %d, %d): %s", start, end, n, err) + } + node := SubtreeHash(leaves[start:end]) + if !VerifySubtreeConsistency(start, end, n, proof, node, root) { + t.Errorf("VerifySubtreeConsistency(%d, %d, %d) rejected a valid proof", start, end, n) + } + fmt.Fprintf(h, "[%d, %d) %d", start, end, n) + for _, p := range proof { + fmt.Fprintf(h, " %s", hex.EncodeToString(p[:])) + } + h.Write([]byte{'\n'}) + } + } + } + got := hex.EncodeToString(h.Sum(nil)) + if got != want { + t.Errorf("subtree consistency proof accumulator:\n got %s\n want %s", got, want) + } +} + +func TestSubtreeConsistencyProofRejectsBadInput(t *testing.T) { + reader := buildHashReader(t, seqLeaves(14)) + cases := []struct { + name string + start int64 + end int64 + size int64 + }{ + {"Misaligned subtree", 1, 3, 14}, + {"End past tree size", 0, 5, 4}, + {"Empty interval", 4, 4, 14}, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + _, err := SubtreeConsistencyProof(tc.start, tc.end, tc.size, reader) + if err == nil { + t.Errorf("SubtreeConsistencyProof(%s) = nil error, want error", tc.name) + } + }) + } +} + +// failingHashReader fails every read. +type failingHashReader struct{} + +func (failingHashReader) ReadHashes([]int64) ([]xtlog.Hash, error) { + return nil, errors.New("read failed") +} + +func TestSubtreeConsistencyProofPropagatesReadError(t *testing.T) { + _, err := SubtreeConsistencyProof(4, 8, 14, failingHashReader{}) + if err == nil { + t.Error("SubtreeConsistencyProof with a failing reader = nil error, want error") + } +} + +// shortHashReader returns one fewer hash than was requested. +type shortHashReader struct { + inner xtlog.HashReader +} + +func (s shortHashReader) ReadHashes(indexes []int64) ([]xtlog.Hash, error) { + hashes, err := s.inner.ReadHashes(indexes) + if err != nil || len(hashes) == 0 { + return hashes, err + } + return hashes[:len(hashes)-1], nil +} + +func TestSubtreeConsistencyProofShortReader(t *testing.T) { + reader := shortHashReader{inner: buildHashReader(t, seqLeaves(7))} + _, err := SubtreeConsistencyProof(0, 4, 7, reader) + if err == nil { + t.Error("SubtreeConsistencyProof with a short HashReader = nil error, want error") + } +} + +// countingHashReader counts ReadHashes calls, to check read batching. +type countingHashReader struct { + inner xtlog.HashReader + calls int +} + +func (c *countingHashReader) ReadHashes(indexes []int64) ([]xtlog.Hash, error) { + c.calls++ + return c.inner.ReadHashes(indexes) +} + +// TestSubtreeConsistencyProofBatchesReads checks that each emitted proof hash +// costs at most one ReadHashes call. +func TestSubtreeConsistencyProofBatchesReads(t *testing.T) { + reader := &countingHashReader{inner: buildHashReader(t, seqLeaves(14))} + + // SubtreeConsistencyProof(4, 8, 14) emits two proof hashes: MTH(0, 4), a + // perfect sibling, and MTH(8, 14), a ragged sibling that requires two + // stored hashes, [8,12) + [12,14), to compute. + proof, err := SubtreeConsistencyProof(4, 8, 14, reader) + if err != nil { + t.Fatalf("SubtreeConsistencyProof(4, 8, 14): %s", err) + } + + // Batching should fold each emitted hash's reads into one ReadHashes, so + // the call count stays at or below len(proof). Without it, MTH(8, 14)'s two + // stored hashes would cost an extra call. + if reader.calls > len(proof) { + t.Errorf("ReadHashes calls = %d, want at most %d (one per emitted hash)", reader.calls, len(proof)) + } +} + +func TestVerifySubtreeConsistencyRejectsBadInput(t *testing.T) { + leaves := leafHashes(seqLeaves(14)) + reader := buildHashReader(t, seqLeaves(14)) + root := SubtreeHash(leaves) + node := SubtreeHash(leaves[8:13]) + proof, err := SubtreeConsistencyProof(8, 13, 14, reader) + if err != nil { + t.Fatalf("SubtreeConsistencyProof(8, 13, 14): %s", err) + } + + cases := []struct { + name string + start int64 + end int64 + n int64 + proof []xtlog.Hash + }{ + {"Misaligned subtree", 1, 3, 14, proof}, + {"End past tree size", 8, 13, 12, proof}, + {"Empty proof where one is required", 8, 13, 14, nil}, + {"Over-long proof", 8, 13, 14, append(slices.Clone(proof), proof...)}, + {"Corrupted proof", 8, 13, 14, append(slices.Clone(proof), node)}, + {"Too short proof", 8, 13, 14, proof[:len(proof)-1]}, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + if VerifySubtreeConsistency(tc.start, tc.end, tc.n, tc.proof, node, root) { + t.Errorf("VerifySubtreeConsistency accepted inconsistent input: %s", tc.name) + } + }) + } +} + +// TestVerifySubtreeConsistencyMatchesXtlogCheckTree independently verifies +// VerifySubtreeConsistency against x/mod/sumdb/tlog's CheckTree, which +// implements the same algorithm but only for prefix subtrees [0, end). +func TestVerifySubtreeConsistencyMatchesXtlogCheckTree(t *testing.T) { + agree := func(t *testing.T, end, n int64, proof []xtlog.Hash, subRoot, root xtlog.Hash, label string) bool { + t.Helper() + + ours := VerifySubtreeConsistency(0, end, n, proof, subRoot, root) + theirs := xtlog.CheckTree(proof, n, root, end, subRoot) == nil + if ours != theirs { + t.Errorf("(0, %d, %d) %s: VerifySubtreeConsistency=%v, CheckTree=%v", end, n, label, ours, theirs) + } + return ours + } + + for n := int64(2); n <= 33; n++ { + entries := seqLeaves(int(n)) + reader := buildHashReader(t, entries) + leaves := leafHashes(entries) + root := SubtreeHash(leaves) + for end := int64(1); end < n; end++ { + subRoot := SubtreeHash(leaves[:end]) + proof, err := SubtreeConsistencyProof(0, end, n, reader) + if err != nil { + t.Fatalf("SubtreeConsistencyProof(0, %d, %d): %s", end, n, err) + } + + if !agree(t, end, n, proof, subRoot, root, "valid") { + t.Errorf("(0, %d, %d) both verifiers rejected a valid proof", end, n) + } + + for i := range proof { + bad := slices.Clone(proof) + bad[i][0] ^= 0xff + agree(t, end, n, bad, subRoot, root, fmt.Sprintf("corrupt proof[%d]", i)) + } + badSub := subRoot + badSub[0] ^= 0xff + agree(t, end, n, proof, badSub, root, "corrupt subtree root") + badRoot := root + badRoot[0] ^= 0xff + agree(t, end, n, proof, subRoot, badRoot, "corrupt tree root") + } + } +} + +func TestVerifySubtreeConsistencyRejectsMismatchedProof(t *testing.T) { + leaves := leafHashes(seqLeaves(14)) + reader := buildHashReader(t, seqLeaves(14)) + root := SubtreeHash(leaves) + mth := func(start, end int64) xtlog.Hash { + return SubtreeHash(leaves[start:end]) + } + + proof, err := SubtreeConsistencyProof(8, 13, 14, reader) + if err != nil { + t.Fatalf("SubtreeConsistencyProof(8, 13, 14): %s", err) + } + if !VerifySubtreeConsistency(8, 13, 14, proof, mth(8, 13), root) { + t.Fatal("valid proof for [8, 13) of size-14 tree was rejected") + } + + otherEntries := make([][]byte, 14) + for i := range otherEntries { + otherEntries[i] = []byte{0xa1, byte(i)} + } + rootAt13 := SubtreeHash(leafHashes(seqLeaves(13))) + otherRoot := SubtreeHash(leafHashes(otherEntries)) + + cases := []struct { + name string + start int64 + end int64 + n int64 + node xtlog.Hash + treeRoot xtlog.Hash + }{ + {"Incorrect subtree coordinates", 4, 8, 14, mth(4, 8), root}, + {"Incorrect tree size (smaller)", 8, 13, 13, mth(8, 13), rootAt13}, + {"Incorrect tree root", 8, 13, 14, mth(8, 13), otherRoot}, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + if VerifySubtreeConsistency(tc.start, tc.end, tc.n, proof, tc.node, tc.treeRoot) { + t.Errorf("incorrectly verified the [8,13)/size-14 proof against %s", tc.name) + } + }) + } +} + +// TestSubtreeRoundTrip covers the generate and verify round trip and checks +// that the verifier rejects tampering. +func TestSubtreeRoundTrip(t *testing.T) { + for n := int64(1); n <= 48; n++ { + entries := seqLeaves(int(n)) + leaves := leafHashes(entries) + reader := buildHashReader(t, entries) + root := SubtreeHash(leaves) + + for start := int64(0); start < n; start++ { + for end := start + 1; end <= n; end++ { + if !ValidSubtree(start, end) { + continue + } + node := SubtreeHash(leaves[start:end]) + proof, err := SubtreeConsistencyProof(start, end, n, reader) + if err != nil { + t.Fatalf("SubtreeConsistencyProof(%d, %d, %d): %s", start, end, n, err) + } + if !VerifySubtreeConsistency(start, end, n, proof, node, root) { + t.Errorf("(%d, %d, %d) rejected the valid proof", start, end, n) + } + + // Flipping a byte in any proof hash must result in a rejection. + for i := range proof { + bad := slices.Clone(proof) + bad[i][0] ^= 0xff + if VerifySubtreeConsistency(start, end, n, bad, node, root) { + t.Errorf("(%d, %d, %d) accepted a proof with hash %d corrupted", start, end, n, i) + } + } + // Flipping a byte in the node hash must result in a rejection. + badNode := node + badNode[0] ^= 0xff + if VerifySubtreeConsistency(start, end, n, proof, badNode, root) { + t.Errorf("(%d, %d, %d) accepted a corrupted node hash", start, end, n) + } + // Flipping a byte in the root hash must result in a rejection. + badRoot := root + badRoot[0] ^= 0xff + if VerifySubtreeConsistency(start, end, n, proof, node, badRoot) { + t.Errorf("(%d, %d, %d) accepted a corrupted root hash", start, end, n) + } + } + } + } +} diff --git a/vendor/golang.org/x/mod/sumdb/tlog/tlog.go b/vendor/golang.org/x/mod/sumdb/tlog/tlog.go new file mode 100644 index 00000000000..480b5eff5af --- /dev/null +++ b/vendor/golang.org/x/mod/sumdb/tlog/tlog.go @@ -0,0 +1,605 @@ +// Copyright 2019 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Package tlog implements a tamper-evident log +// used in the Go module go.sum database server. +// +// This package follows the design of Certificate Transparency (RFC 6962) +// and its proofs are compatible with that system. +// See TestCertificateTransparency. +package tlog + +import ( + "crypto/sha256" + "encoding/base64" + "errors" + "fmt" + "math/bits" +) + +// A Hash is a hash identifying a log record or tree root. +type Hash [HashSize]byte + +// HashSize is the size of a Hash in bytes. +const HashSize = 32 + +// String returns a base64 representation of the hash for printing. +func (h Hash) String() string { + return base64.StdEncoding.EncodeToString(h[:]) +} + +// MarshalJSON marshals the hash as a JSON string containing the base64-encoded hash. +func (h Hash) MarshalJSON() ([]byte, error) { + return []byte(`"` + h.String() + `"`), nil +} + +// UnmarshalJSON unmarshals a hash from JSON string containing the a base64-encoded hash. +func (h *Hash) UnmarshalJSON(data []byte) error { + if len(data) != 1+44+1 || data[0] != '"' || data[len(data)-2] != '=' || data[len(data)-1] != '"' { + return errors.New("cannot decode hash") + } + + // As of Go 1.12, base64.StdEncoding.Decode insists on + // slicing into target[33:] even when it only writes 32 bytes. + // Since we already checked that the hash ends in = above, + // we can use base64.RawStdEncoding with the = removed; + // RawStdEncoding does not exhibit the same bug. + // We decode into a temporary to avoid writing anything to *h + // unless the entire input is well-formed. + var tmp Hash + n, err := base64.RawStdEncoding.Decode(tmp[:], data[1:len(data)-2]) + if err != nil || n != HashSize { + return errors.New("cannot decode hash") + } + *h = tmp + return nil +} + +// ParseHash parses the base64-encoded string form of a hash. +func ParseHash(s string) (Hash, error) { + data, err := base64.StdEncoding.DecodeString(s) + if err != nil || len(data) != HashSize { + return Hash{}, fmt.Errorf("malformed hash") + } + var h Hash + copy(h[:], data) + return h, nil +} + +// maxpow2 returns k, the maximum power of 2 smaller than n, +// as well as l = log₂ k (so k = 1< 0; l-- { + n = 2*n + 1 + } + + // Level 0's n'th hash is written at n+n/2+n/4+... (eventually n/2ⁱ hits zero). + i := int64(0) + for ; n > 0; n >>= 1 { + i += n + } + + return i + int64(level) +} + +// SplitStoredHashIndex is the inverse of [StoredHashIndex]. +// That is, SplitStoredHashIndex(StoredHashIndex(level, n)) == level, n. +func SplitStoredHashIndex(index int64) (level int, n int64) { + // Determine level 0 record before index. + // StoredHashIndex(0, n) < 2*n, + // so the n we want is in [index/2, index/2+log₂(index)]. + n = index / 2 + indexN := StoredHashIndex(0, n) + if indexN > index { + panic("bad math") + } + for { + // Each new record n adds 1 + trailingZeros(n) hashes. + x := indexN + 1 + int64(bits.TrailingZeros64(uint64(n+1))) + if x > index { + break + } + n++ + indexN = x + } + // The hash we want was committed with record n, + // meaning it is one of (0, n), (1, n/2), (2, n/4), ... + level = int(index - indexN) + return level, n >> uint(level) +} + +// StoredHashCount returns the number of stored hashes +// that are expected for a tree with n records. +func StoredHashCount(n int64) int64 { + if n == 0 { + return 0 + } + // The tree will have the hashes up to the last leaf hash. + numHash := StoredHashIndex(0, n-1) + 1 + // And it will have any hashes for subtrees completed by that leaf. + for i := uint64(n - 1); i&1 != 0; i >>= 1 { + numHash++ + } + return numHash +} + +// StoredHashes returns the hashes that must be stored when writing +// record n with the given data. The hashes should be stored starting +// at StoredHashIndex(0, n). The result will have at most 1 + log₂ n hashes, +// but it will average just under two per call for a sequence of calls for n=1..k. +// +// StoredHashes may read up to log n earlier hashes from r +// in order to compute hashes for completed subtrees. +func StoredHashes(n int64, data []byte, r HashReader) ([]Hash, error) { + return StoredHashesForRecordHash(n, RecordHash(data), r) +} + +// StoredHashesForRecordHash is like [StoredHashes] but takes +// as its second argument RecordHash(data) instead of data itself. +func StoredHashesForRecordHash(n int64, h Hash, r HashReader) ([]Hash, error) { + // Start with the record hash. + hashes := []Hash{h} + + // Build list of indexes needed for hashes for completed subtrees. + // Each trailing 1 bit in the binary representation of n completes a subtree + // and consumes a hash from an adjacent subtree. + m := int(bits.TrailingZeros64(uint64(n + 1))) + indexes := make([]int64, m) + for i := range m { + // We arrange indexes in sorted order. + // Note that n>>i is always odd. + indexes[m-1-i] = StoredHashIndex(i, n>>uint(i)-1) + } + + // Fetch hashes. + old, err := r.ReadHashes(indexes) + if err != nil { + return nil, err + } + if len(old) != len(indexes) { + return nil, fmt.Errorf("tlog: ReadHashes(%d indexes) = %d hashes", len(indexes), len(old)) + } + + // Build new hashes. + for i := range m { + h = NodeHash(old[m-1-i], h) + hashes = append(hashes, h) + } + return hashes, nil +} + +// A HashReader can read hashes for nodes in the log's tree structure. +type HashReader interface { + // ReadHashes returns the hashes with the given stored hash indexes + // (see StoredHashIndex and SplitStoredHashIndex). + // ReadHashes must return a slice of hashes the same length as indexes, + // or else it must return a non-nil error. + // ReadHashes may run faster if indexes is sorted in increasing order. + ReadHashes(indexes []int64) ([]Hash, error) +} + +// A HashReaderFunc is a function implementing [HashReader]. +type HashReaderFunc func([]int64) ([]Hash, error) + +func (f HashReaderFunc) ReadHashes(indexes []int64) ([]Hash, error) { + return f(indexes) +} + +// emptyHash is the hash of the empty tree, per RFC 6962, Section 2.1. +// It is the hash of the empty string. +var emptyHash = Hash{ + 0xe3, 0xb0, 0xc4, 0x42, 0x98, 0xfc, 0x1c, 0x14, + 0x9a, 0xfb, 0xf4, 0xc8, 0x99, 0x6f, 0xb9, 0x24, + 0x27, 0xae, 0x41, 0xe4, 0x64, 0x9b, 0x93, 0x4c, + 0xa4, 0x95, 0x99, 0x1b, 0x78, 0x52, 0xb8, 0x55, +} + +// TreeHash computes the hash for the root of the tree with n records, +// using the HashReader to obtain previously stored hashes +// (those returned by StoredHashes during the writes of those n records). +// TreeHash makes a single call to ReadHash requesting at most 1 + log₂ n hashes. +func TreeHash(n int64, r HashReader) (Hash, error) { + if n == 0 { + return emptyHash, nil + } + indexes := subTreeIndex(0, n, nil) + hashes, err := r.ReadHashes(indexes) + if err != nil { + return Hash{}, err + } + if len(hashes) != len(indexes) { + return Hash{}, fmt.Errorf("tlog: ReadHashes(%d indexes) = %d hashes", len(indexes), len(hashes)) + } + hash, hashes := subTreeHash(0, n, hashes) + if len(hashes) != 0 { + panic("tlog: bad index math in TreeHash") + } + return hash, nil +} + +// subTreeIndex returns the storage indexes needed to compute +// the hash for the subtree containing records [lo, hi), +// appending them to need and returning the result. +// See https://tools.ietf.org/html/rfc6962#section-2.1 +func subTreeIndex(lo, hi int64, need []int64) []int64 { + // See subTreeHash below for commentary. + for lo < hi { + k, level := maxpow2(hi - lo + 1) + if lo&(k-1) != 0 { + panic("tlog: bad math in subTreeIndex") + } + need = append(need, StoredHashIndex(level, lo>>uint(level))) + lo += k + } + return need +} + +// subTreeHash computes the hash for the subtree containing records [lo, hi), +// assuming that hashes are the hashes corresponding to the indexes +// returned by subTreeIndex(lo, hi). +// It returns any leftover hashes. +func subTreeHash(lo, hi int64, hashes []Hash) (Hash, []Hash) { + // Repeatedly partition the tree into a left side with 2^level nodes, + // for as large a level as possible, and a right side with the fringe. + // The left hash is stored directly and can be read from storage. + // The right side needs further computation. + numTree := 0 + for lo < hi { + k, _ := maxpow2(hi - lo + 1) + if lo&(k-1) != 0 || lo >= hi { + panic("tlog: bad math in subTreeHash") + } + numTree++ + lo += k + } + + if len(hashes) < numTree { + panic("tlog: bad index math in subTreeHash") + } + + // Reconstruct hash. + h := hashes[numTree-1] + for i := numTree - 2; i >= 0; i-- { + h = NodeHash(hashes[i], h) + } + return h, hashes[numTree:] +} + +// A RecordProof is a verifiable proof that a particular log root contains a particular record. +// RFC 6962 calls this a “Merkle audit path.” +type RecordProof []Hash + +// ProveRecord returns the proof that the tree of size t contains the record with index n. +func ProveRecord(t, n int64, r HashReader) (RecordProof, error) { + if t < 0 || n < 0 || n >= t { + return nil, fmt.Errorf("tlog: invalid inputs in ProveRecord") + } + indexes := leafProofIndex(0, t, n, nil) + if len(indexes) == 0 { + return RecordProof{}, nil + } + hashes, err := r.ReadHashes(indexes) + if err != nil { + return nil, err + } + if len(hashes) != len(indexes) { + return nil, fmt.Errorf("tlog: ReadHashes(%d indexes) = %d hashes", len(indexes), len(hashes)) + } + + p, hashes := leafProof(0, t, n, hashes) + if len(hashes) != 0 { + panic("tlog: bad index math in ProveRecord") + } + return p, nil +} + +// leafProofIndex builds the list of indexes needed to construct the proof +// that leaf n is contained in the subtree with leaves [lo, hi). +// It appends those indexes to need and returns the result. +// See https://tools.ietf.org/html/rfc6962#section-2.1.1 +func leafProofIndex(lo, hi, n int64, need []int64) []int64 { + // See leafProof below for commentary. + if !(lo <= n && n < hi) { + panic("tlog: bad math in leafProofIndex") + } + if lo+1 == hi { + return need + } + if k, _ := maxpow2(hi - lo); n < lo+k { + need = leafProofIndex(lo, lo+k, n, need) + need = subTreeIndex(lo+k, hi, need) + } else { + need = subTreeIndex(lo, lo+k, need) + need = leafProofIndex(lo+k, hi, n, need) + } + return need +} + +// leafProof constructs the proof that leaf n is contained in the subtree with leaves [lo, hi). +// It returns any leftover hashes as well. +// See https://tools.ietf.org/html/rfc6962#section-2.1.1 +func leafProof(lo, hi, n int64, hashes []Hash) (RecordProof, []Hash) { + // We must have lo <= n < hi or else the code here has a bug. + if !(lo <= n && n < hi) { + panic("tlog: bad math in leafProof") + } + + if lo+1 == hi { // n == lo + // Reached the leaf node. + // The verifier knows what the leaf hash is, so we don't need to send it. + return RecordProof{}, hashes + } + + // Walk down the tree toward n. + // Record the hash of the path not taken (needed for verifying the proof). + var p RecordProof + var th Hash + if k, _ := maxpow2(hi - lo); n < lo+k { + // n is on left side + p, hashes = leafProof(lo, lo+k, n, hashes) + th, hashes = subTreeHash(lo+k, hi, hashes) + } else { + // n is on right side + th, hashes = subTreeHash(lo, lo+k, hashes) + p, hashes = leafProof(lo+k, hi, n, hashes) + } + return append(p, th), hashes +} + +var errProofFailed = errors.New("invalid transparency proof") + +// CheckRecord verifies that p is a valid proof that the tree of size t +// with hash th has an n'th record with hash h. +func CheckRecord(p RecordProof, t int64, th Hash, n int64, h Hash) error { + if t < 0 || n < 0 || n >= t { + return fmt.Errorf("tlog: invalid inputs in CheckRecord") + } + th2, err := runRecordProof(p, 0, t, n, h) + if err != nil { + return err + } + if th2 == th { + return nil + } + return errProofFailed +} + +// runRecordProof runs the proof p that leaf n is contained in the subtree with leaves [lo, hi). +// Running the proof means constructing and returning the implied hash of that +// subtree. +func runRecordProof(p RecordProof, lo, hi, n int64, leafHash Hash) (Hash, error) { + // We must have lo <= n < hi or else the code here has a bug. + if !(lo <= n && n < hi) { + panic("tlog: bad math in runRecordProof") + } + + if lo+1 == hi { // m == lo + // Reached the leaf node. + // The proof must not have any unnecessary hashes. + if len(p) != 0 { + return Hash{}, errProofFailed + } + return leafHash, nil + } + + if len(p) == 0 { + return Hash{}, errProofFailed + } + + k, _ := maxpow2(hi - lo) + if n < lo+k { + th, err := runRecordProof(p[:len(p)-1], lo, lo+k, n, leafHash) + if err != nil { + return Hash{}, err + } + return NodeHash(th, p[len(p)-1]), nil + } else { + th, err := runRecordProof(p[:len(p)-1], lo+k, hi, n, leafHash) + if err != nil { + return Hash{}, err + } + return NodeHash(p[len(p)-1], th), nil + } +} + +// A TreeProof is a verifiable proof that a particular log tree contains +// as a prefix all records present in an earlier tree. +// RFC 6962 calls this a “Merkle consistency proof.” +type TreeProof []Hash + +// ProveTree returns the proof that the tree of size t contains +// as a prefix all the records from the tree of smaller size n. +func ProveTree(t, n int64, h HashReader) (TreeProof, error) { + if t < 1 || n < 1 || n > t { + return nil, fmt.Errorf("tlog: invalid inputs in ProveTree") + } + indexes := treeProofIndex(0, t, n, nil) + if len(indexes) == 0 { + return TreeProof{}, nil + } + hashes, err := h.ReadHashes(indexes) + if err != nil { + return nil, err + } + if len(hashes) != len(indexes) { + return nil, fmt.Errorf("tlog: ReadHashes(%d indexes) = %d hashes", len(indexes), len(hashes)) + } + + p, hashes := treeProof(0, t, n, hashes) + if len(hashes) != 0 { + panic("tlog: bad index math in ProveTree") + } + return p, nil +} + +// treeProofIndex builds the list of indexes needed to construct +// the sub-proof related to the subtree containing records [lo, hi). +// See https://tools.ietf.org/html/rfc6962#section-2.1.2. +func treeProofIndex(lo, hi, n int64, need []int64) []int64 { + // See treeProof below for commentary. + if !(lo < n && n <= hi) { + panic("tlog: bad math in treeProofIndex") + } + + if n == hi { + if lo == 0 { + return need + } + return subTreeIndex(lo, hi, need) + } + + if k, _ := maxpow2(hi - lo); n <= lo+k { + need = treeProofIndex(lo, lo+k, n, need) + need = subTreeIndex(lo+k, hi, need) + } else { + need = subTreeIndex(lo, lo+k, need) + need = treeProofIndex(lo+k, hi, n, need) + } + return need +} + +// treeProof constructs the sub-proof related to the subtree containing records [lo, hi). +// It returns any leftover hashes as well. +// See https://tools.ietf.org/html/rfc6962#section-2.1.2. +func treeProof(lo, hi, n int64, hashes []Hash) (TreeProof, []Hash) { + // We must have lo < n <= hi or else the code here has a bug. + if !(lo < n && n <= hi) { + panic("tlog: bad math in treeProof") + } + + // Reached common ground. + if n == hi { + if lo == 0 { + // This subtree corresponds exactly to the old tree. + // The verifier knows that hash, so we don't need to send it. + return TreeProof{}, hashes + } + th, hashes := subTreeHash(lo, hi, hashes) + return TreeProof{th}, hashes + } + + // Interior node for the proof. + // Decide whether to walk down the left or right side. + var p TreeProof + var th Hash + if k, _ := maxpow2(hi - lo); n <= lo+k { + // m is on left side + p, hashes = treeProof(lo, lo+k, n, hashes) + th, hashes = subTreeHash(lo+k, hi, hashes) + } else { + // m is on right side + th, hashes = subTreeHash(lo, lo+k, hashes) + p, hashes = treeProof(lo+k, hi, n, hashes) + } + return append(p, th), hashes +} + +// CheckTree verifies that p is a valid proof that the tree of size t with hash th +// contains as a prefix the tree of size n with hash h. +func CheckTree(p TreeProof, t int64, th Hash, n int64, h Hash) error { + if t < 1 || n < 1 || n > t { + return fmt.Errorf("tlog: invalid inputs in CheckTree") + } + h2, th2, err := runTreeProof(p, 0, t, n, h) + if err != nil { + return err + } + if th2 == th && h2 == h { + return nil + } + return errProofFailed +} + +// runTreeProof runs the sub-proof p related to the subtree containing records [lo, hi), +// where old is the hash of the old tree with n records. +// Running the proof means constructing and returning the implied hashes of that +// subtree in both the old and new tree. +func runTreeProof(p TreeProof, lo, hi, n int64, old Hash) (Hash, Hash, error) { + // We must have lo < n <= hi or else the code here has a bug. + if !(lo < n && n <= hi) { + panic("tlog: bad math in runTreeProof") + } + + // Reached common ground. + if n == hi { + if lo == 0 { + if len(p) != 0 { + return Hash{}, Hash{}, errProofFailed + } + return old, old, nil + } + if len(p) != 1 { + return Hash{}, Hash{}, errProofFailed + } + return p[0], p[0], nil + } + + if len(p) == 0 { + return Hash{}, Hash{}, errProofFailed + } + + // Interior node for the proof. + k, _ := maxpow2(hi - lo) + if n <= lo+k { + oh, th, err := runTreeProof(p[:len(p)-1], lo, lo+k, n, old) + if err != nil { + return Hash{}, Hash{}, err + } + return oh, NodeHash(th, p[len(p)-1]), nil + } else { + oh, th, err := runTreeProof(p[:len(p)-1], lo+k, hi, n, old) + if err != nil { + return Hash{}, Hash{}, err + } + return NodeHash(p[len(p)-1], oh), NodeHash(p[len(p)-1], th), nil + } +} diff --git a/vendor/modules.txt b/vendor/modules.txt index 095d6d1cb2d..b6fc8b97952 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -397,6 +397,7 @@ golang.org/x/crypto/ocsp # golang.org/x/mod v0.35.0 ## explicit; go 1.25.0 golang.org/x/mod/semver +golang.org/x/mod/sumdb/tlog # golang.org/x/net v0.55.0 ## explicit; go 1.25.0 golang.org/x/net/bpf