diff --git a/go.mod b/go.mod index 278d1cb2..5fbfef44 100644 --- a/go.mod +++ b/go.mod @@ -13,7 +13,7 @@ require ( github.com/aws/aws-sdk-go-v2/feature/s3/transfermanager v0.3.12 github.com/aws/aws-sdk-go-v2/service/s3 v1.107.1 github.com/aws/smithy-go v1.27.7 - github.com/cbergoon/merkletree v0.2.0 + github.com/cbergoon/merkletree v0.5.0 github.com/cilium/ebpf v0.22.0 github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2 github.com/containernetworking/cni v1.3.0 diff --git a/go.sum b/go.sum index d11c0b44..b76af1fe 100644 --- a/go.sum +++ b/go.sum @@ -146,6 +146,8 @@ github.com/brianvoe/gofakeit/v6 v6.28.0 h1:Xib46XXuQfmlLS2EXRuJpqcw8St6qSZz75OUo github.com/brianvoe/gofakeit/v6 v6.28.0/go.mod h1:Xj58BMSnFqcn/fAQeSK+/PLtC5kSb7FJIq4JyGa8vEs= github.com/cbergoon/merkletree v0.2.0 h1:Bttqr3OuoiZEo4ed1L7fTasHka9II+BF9fhBfbNEEoQ= github.com/cbergoon/merkletree v0.2.0/go.mod h1:5c15eckUgiucMGDOCanvalj/yJnD+KAZj1qyJtRW5aM= +github.com/cbergoon/merkletree v0.5.0 h1:570ckfhpRbI6SK1vZddMymVXtKQBnaSKyU49icyIExQ= +github.com/cbergoon/merkletree v0.5.0/go.mod h1:O9XkOuoRsCjlxpEqOEyXeFNe8YxGCnRwYRDt+1waRsc= github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM= diff --git a/vendor/github.com/cbergoon/merkletree/.travis.yml b/vendor/github.com/cbergoon/merkletree/.travis.yml deleted file mode 100644 index 4f2ee4d9..00000000 --- a/vendor/github.com/cbergoon/merkletree/.travis.yml +++ /dev/null @@ -1 +0,0 @@ -language: go diff --git a/vendor/github.com/cbergoon/merkletree/README.md b/vendor/github.com/cbergoon/merkletree/README.md index 14ab1087..0c47d0fd 100644 --- a/vendor/github.com/cbergoon/merkletree/README.md +++ b/vendor/github.com/cbergoon/merkletree/README.md @@ -1,9 +1,8 @@

Merkle Tree in Golang

-Build -Report -Docs -Version +Build +Docs +Version

An implementation of a Merkle Tree written in Go. A Merkle Tree is a hash tree that provides an efficient way to verify @@ -18,7 +17,248 @@ nlog2(n) steps in the worst case. #### Documentation -See the docs [here](https://godoc.org/github.com/cbergoon/merkletree). +See the docs [here](https://pkg.go.dev/github.com/cbergoon/merkletree). + +#### Constructions + +The tree can be built three ways. All of them are supported; they produce different roots +and are not interchangeable, so pick one when the tree is created. + +| Construction | Built with | Notes | +| --- | --- | --- | +| Bitcoin-style (default) | `NewTree`, `NewTreeWithHashStrategy` | Pairs siblings in order, duplicates the last node on an odd count | +| Sorted siblings | `NewTreeWithHashStrategySorted`, `WithSortedSiblings()` | Orders each pair before hashing, matching OpenZeppelin `MerkleProof` | +| RFC 6962 | `WithRFC6962()` | Prefixed leaf and interior hashes, splits instead of padding | + +The default follows Bitcoin deliberately, so the roots line up with Bitcoin-style trees. +That construction carries two well-known properties: + +**Duplicated odd nodes.** A level holding an odd number of nodes duplicates its last node +so it can be paired, which means a tree built from an odd number of leaves has the same +root as a tree that spells the duplicate out — this is CVE-2012-2459: + +``` +[A, B, C] and [A, B, C, C] -> same root +[A] and [A, A] -> same root +``` + +**No separation between leaf and interior hashes.** Both are computed the same way, so an +interior digest can be handed back as a leaf. A two-leaf tree whose leaves are the two +subtree hashes of a four-leaf tree reproduces the original root exactly, and the forged +tree verifies against itself. + +Sorted mode shares both, and additionally discards leaf order: `[A B C D]`, `[B A C D]` +and `[D C B A]` all produce the same root, because each pair is ordered before it is +hashed. Only regrouping which leaves are paired changes the root. Check +`tree.Sorted()` if you depend on the root committing to order. + +##### RFC 6962 + +`WithRFC6962()` builds the tree specified by +[RFC 6962 section 2.1](https://datatracker.ietf.org/doc/html/rfc6962#section-2.1), which +closes both of the above: + +```go +tree, err := merkletree.NewTreeWithOptions(list, merkletree.WithRFC6962()) +``` + +Leaf hashes are computed as `H(0x00 ‖ digest)` and interior hashes as `H(0x01 ‖ left ‖ right)`, +so a forged leaf would need a genuine collision between two differently prefixed inputs +rather than a rearrangement. Odd node counts are split at the largest power of two below +their length instead of being padded, so every distinct leaf sequence gets a distinct +root and `[A, B, C]` no longer collides with `[A, B, C, C]`. + +Two things to know. RFC 6962 hashes raw leaf data, whereas this tree hashes whatever +`CalculateHash` returns — roots match a Certificate Transparency log only if your +`CalculateHash` returns the leaf bytes themselves rather than a digest of them. The +structural guarantees hold either way. And a single-leaf tree is its own root, so its +audit path is legitimately empty. + +`WithRFC6962()` cannot be combined with `WithSortedSiblings()`; RFC 6962 specifies its own +sibling ordering, and asking for both returns an error. + +#### Parallel construction + +`WithParallelism(n)` builds the tree across up to `n` goroutines, or GOMAXPROCS of them +when `n < 1`. It is off by default. + +```go +tree, err := merkletree.NewTreeWithOptions(list, merkletree.WithParallelism(0)) +``` + +**`Content.CalculateHash` is called concurrently when this is set**, and your +implementation must be safe for that. Nothing else in the package calls it concurrently +and the tree cannot check whether yours is safe, so this requirement is the entire cost +of the option and the reason it is not the default. + +The root is unaffected — every hash lands in a slot fixed by its position, so a tree +built in parallel is byte for byte the tree built serially. + +Whether it pays depends almost entirely on what `CalculateHash` costs. Measured on an +M5 Max, best of 3: + +| Content | Leaves | Serial | Parallel | | +| --- | --- | --- | --- | --- | +| short string | 256 | 26.5µs | 43.7µs | **0.61×** — slower | +| short string | 4,096 | 476µs | 333µs | 1.43× | +| short string | 65,536 | 6.40ms | 2.14ms | 2.99× | +| 4KB blob | 4,096 | 5.21ms | 723µs | **7.21×** | + +Content hashing is always spread across the goroutine budget, since its cost belongs to +you and cannot be guessed here — which is why the option wins big on expensive leaves at +any size, and can lose on a small tree of cheap ones. Interior hashing is our own work +and known to be small, so it stays serial until the tree is large enough to be worth +splitting. Measure before adopting it for small trees. + +The option covers every construction, including `WithRFC6962`: the unbalanced interior +an RFC tree builds forks across goroutines the same way, so an RFC build of 65,536 short +leaves drops from 9.98ms serial to 2.01ms in parallel. + +Parallelism is not recorded when a tree is serialized, since it has no bearing on the +root; a tree read back rebuilds serially unless built again with the option. + +#### Serving proofs + +`GetMerklePath` locates content by scanning every leaf; build with `WithLeafIndex` to +make that one hash and a map probe, or address leaves by position with +`GetMerklePathByIndex`. When proofs are served at rate, the two slices each proof +returns become the bottleneck — the garbage collector turns into the shared resource +every goroutine queues on. The `Append` forms generate the same proofs into buffers you +own, so a server that reuses them allocates nothing per proof: + +```go +var path [][]byte +var index []int64 +for i := range list { + path, index, err = t.AppendMerklePathByIndex(path[:0], index[:0], i) + // path holds the sibling hashes, index which side each sits on +} +``` + +The appended hashes are the tree's own, not copies: treat them as read-only, and copy +any proof that must outlive the next reuse of its buffer. + +The difference is not subtle. At 65,536 leaves a proof appends in ~17ns against ~76ns +returning fresh slices, and under 18 goroutines serving from one shared tree the append +form runs at 2.9ns per proof — 28× the slice-returning form, because with no allocation +there is nothing shared left to queue on. The comparison tables below carry the +cross-library context. + +#### Serialization + +A tree can be written out and read back. What gets written is not the node graph but the +content the tree is rebuilt from: the ordered leaf content, the name of the hash strategy, +the sibling sort flag, and the Merkle root. Everything else is derived, and the recorded +root makes decoding self-checking — a payload that has been altered, or that is decoded +with the wrong hash strategy, fails rather than producing a tree that quietly verifies +against nothing. + +Register your content type and the standard codecs work directly: + +```go +merkletree.RegisterContent(TestContent{}) // needs MarshalBinary/UnmarshalBinary + +err := gob.NewEncoder(&buf).Encode(tree) + +var decoded merkletree.MerkleTree +err = gob.NewDecoder(&buf).Decode(&decoded) +``` + +`MarshalBinary`, `UnmarshalBinary`, `MarshalJSON`, and `UnmarshalJSON` are all available, +so anything built on `encoding.BinaryMarshaler` or `json.Marshaler` works too. + +To avoid the package-level registry entirely — in a library, or for content that already +has an encoding of its own — supply the content codec directly: + +```go +data, err := tree.MarshalWith(func(c merkletree.Content) ([]byte, error) { + return []byte(c.(TestContent).x), nil +}) + +decoded, err := merkletree.UnmarshalWith(data, func(b []byte) (merkletree.Content, error) { + return TestContent{x: string(b)}, nil +}) +``` + +Hash strategies are recorded by name, since a function cannot be serialized. Everything in +the standard library is registered for you; anything else is one call: + +```go +merkletree.RegisterHashStrategy("keccak256", sha3.NewLegacyKeccak256) +tree, err := merkletree.NewTreeWithHashStrategy(list, sha3.NewLegacyKeccak256) +``` + +Note that a tree with reference cycles cannot be handed to a reflection-based codec +directly — `Node` points back at its `Tree` and its `Parent`. Before these marshalers +existed, `gob.Encode(tree)` did not return an error, it crashed the process with a stack +overflow. The marshalers above are the supported path. + +#### Comparison + +Measured against the other Merkle tree libraries in the Go ecosystem — `txaty/go-merkletree`, +`wealdtech/go-merkletree`, `onrik/gomerkle`, `xsleonard/go-merkle` and `jvsteiner/merkle` — +with SHA-256 and the same leaf data throughout. The benchmarks, the methodology and the +caveats are in [`benchmarks/ANALYSIS.md`](benchmarks/ANALYSIS.md); the numbers below are +from an Apple M5 Max on Go 1.26, so treat the ratios as the durable part. + +`txaty/go-merkletree` gets its own column because it is the closest competitor — the only +other library here with parallel construction, and the one whose published comparisons +prompted this exercise. Where it is also the best of the field, the two columns agree. + +| | merkletree | txaty | best of the rest | rank | +|---|---|---|---|---| +| Construction, serial (65,536 leaves) | 7.67 ms | 9.67 ms | onrik 7.61 ms | 2 / 6 | +| Construction, parallel (65,536 leaves) | **3.08 ms** | 6.47 ms | txaty 6.47 ms | **1 / 2** | +| Parallel speedup, 16 KiB leaves | **12.8×** | 11.9× | txaty 11.9× | **1 / 2** | +| Allocations building 65,536 leaves | 131,140 | 328,230 | onrik 131,090 | 2 / 6 | +| Memory building 65,536 leaves | 18.4 MB | 24.8 MB | wealdtech 11.5 MB | 4 / 6 | +| Single proof (65,536 leaves) | **16.7 ns** | 128 ns | txaty 128 ns | **1 / 6** | +| Full proof set (4,096 leaves) | **25.4 ns/proof** | 158 ns/proof | txaty 158 ns/proof | **1 / 6** | +| Verify a proof (65,536 leaves) | **934 ns** | 1,212 ns | onrik 1,108 ns | **1 / 4** | +| Concurrent proofs, 18 goroutines | **2.9 ns/op** | 165 ns/op | jvsteiner 153 ns/op | **1 / 6** | +| Concurrent scaling, 1 → 18 goroutines | **16.4×** | 1.14× | wealdtech 13.1× | **1 / 6** | +| Proof size on the wire, depth 16 | 640 B | 516 B | txaty 516 B | 2 / 2 | + +The proof rows are `AppendMerklePathByIndex` serving into reused buffers, which +allocates nothing per proof; the slice-returning `GetMerklePathByIndex` runs at 76 ns +and the `WithLeafIndex` lookup at 135 ns, still ahead of or level with everything else. +Without any of the three, locating content is a scan and a full proof set is quadratic. +Serial construction is a near-tie because every implementation is waiting on SHA-256 — +at 16 KiB leaves all six land within 3% of each other, and parallelism is the only thing +that still separates them. + +| | merkletree | txaty | wealdtech | onrik | xsleonard | jvsteiner | +|---|---|---|---|---|---|---| +| Proof generation | ✅ | ✅ | ✅ | ✅ | ❌ | ✅ | +| Proof by leaf position 1 | ✅ | ✅ | ❌ | ✅ | ❌ | ✅ | +| Allocation-free proofs 2 | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | +| Verify without the tree 3 | ✅ | ✅ | ✅ | ❌ | ❌ | ✅ | +| Parallel construction | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ | +| Pluggable hash | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | +| Sorted siblings (OpenZeppelin) | ✅ | ✅ | ❌ | ❌ | ✅ | ❌ | +| RFC 6962 | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | +| Serialization | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | +| Runtime dependencies | none | x/sync | x/crypto | none | none | protobuf | +| Odd leaf count | duplicate 4 | duplicate | pad to 2ⁿ | promote | promote 5 | promote | + +1 Addressing a leaf by position rather than by value, which avoids hashing the +query. txaty offers this only through the precomputed `Proofs` slice in `ModeProofGen`. +It matters more than it looks: at 16 KiB leaves, by position is 95× faster than by value. +2 `AppendMerklePath` and `AppendMerklePathByIndex` write into slices the +caller supplies and keeps, so a proof server reusing its buffers generates proofs without +allocating at all. No other library here exposes the proof buffers; every one of them +returns freshly allocated structures. It is what makes the concurrency row possible — a +proof that allocates nothing shares nothing, so it scales with cores instead of +contending on the garbage collector. +3 A package-level verify taking a proof, a root and the data. onrik's is a +method, so it needs a `Tree` value it does not otherwise use. +4 Or split, under `WithRFC6962`. 5 Or duplicate, with `DoubleOddNodes`. + +That last row decides interoperability, not speed. These libraries agree on the root for +any power-of-two leaf count and split into three groups otherwise, so a proof from one +group will not verify against a root from another. merkletree's default agrees with txaty +and with xsleonard's `DoubleOddNodes`; its `WithRFC6962` mode is checked against the +Certificate Transparency reference vectors in [`oracle/`](oracle/). #### Install ``` @@ -32,6 +272,7 @@ package main import ( "crypto/sha256" + "errors" "log" "github.com/cbergoon/merkletree" @@ -54,7 +295,11 @@ func (t TestContent) CalculateHash() ([]byte, error) { //Equals tests for equality of two Contents func (t TestContent) Equals(other merkletree.Content) (bool, error) { - return t.x == other.(TestContent).x, nil + otherTC, ok := other.(TestContent) + if !ok { + return false, errors.New("value is not of type TestContent") + } + return t.x == otherTC.x, nil } func main() { @@ -99,5 +344,59 @@ func main() { ![merkletree](merkle_tree.png) +#### Testing + +```bash +go test -race ./... # unit, property, golden, and regression tests +scripts/fuzz.sh # every fuzz target, 30s each +scripts/bench.sh # benchmarks, 6 runs each +``` + +The suite checks roots against a reference implementation written directly from each +construction's definition, and pins golden roots and golden serialized payloads that +were produced outside this repository. A change to the hashing or the wire format has +to be made in several independent places before it can pass unnoticed; regenerating a +golden payload to make a test pass is a compatibility break, not a fix. + +##### Fuzzing + +A normal test run only replays each target's seed corpus. `scripts/fuzz.sh` does the +actual fuzzing, one target at a time, discovering targets from the source so a newly +added `FuzzXxx` is picked up automatically: + +```bash +scripts/fuzz.sh # every target, 30s each +scripts/fuzz.sh FuzzUnmarshalBinary # just one +FUZZTIME=5m scripts/fuzz.sh # longer budget per target +``` + +| Target | What it drives | +| --- | --- | +| `FuzzTreeInvariants` | Tree construction, verification, and proof replay across every construction | +| `FuzzUnmarshalBinary` | The binary decoder: no faults, decoded trees verify, payloads are canonical | +| `FuzzUnmarshalJSON` | The JSON decoder | +| `FuzzPayloadCorruption` | That an altered payload can never decode to a different Merkle root | + +CI fuzzes on every pull request for 60s per target, and on every merge to `master` for +180s per target. Both share a corpus cached between runs, so the search accumulates +across builds rather than restarting each time. A failing input is written to +`testdata/fuzz//` and uploaded as a build artifact; committing that file turns +the finding into a permanent regression test. + +##### Benchmarks + +`scripts/bench.sh` covers construction, verification, proof generation, rebuilds, and +all three codecs, across each construction and a range of leaf counts. Timings are +noisy, so it runs each benchmark six times by default and can hand the results to +benchstat: + +```bash +scripts/bench.sh # everything +scripts/bench.sh Verify # only benchmarks matching /Verify/ +scripts/bench.sh -o before.txt # save a baseline +scripts/bench.sh -o after.txt -b before.txt # ...and compare against it +scripts/bench.sh -s # smoke: one iteration, just checks they run +``` + #### License This project is licensed under the MIT License. diff --git a/vendor/github.com/cbergoon/merkletree/doc.go b/vendor/github.com/cbergoon/merkletree/doc.go index 5da0be47..e4338a89 100644 --- a/vendor/github.com/cbergoon/merkletree/doc.go +++ b/vendor/github.com/cbergoon/merkletree/doc.go @@ -1,7 +1,8 @@ // Copyright 2017 Cameron Bergoon // Licensed under the MIT License, see LICENCE file for details. -/*Package merkletree implements a Merkle Tree capable of storing arbitrary content. +/* +Package merkletree implements a Merkle Tree capable of storing arbitrary content. A Merkle Tree is a hash tree that provides an efficient way to verify the contents of a set data are present and untampered with. At its core, a Merkle Tree is @@ -17,14 +18,121 @@ Creating a new merkletree requires that the type that the tree will be construct from implements the Content interface. type Content interface { - CalculateHash() []byte - Equals(other Content) bool + CalculateHash() ([]byte, error) + Equals(other Content) (bool, error) } A slice of the Content items should be created and then passed to the NewTree method. - t, err := merkle.NewTree(list) + t, err := merkletree.NewTree(list) t represents the Merkle Tree and can be verified and manipulated with the API methods -described below.*/ +described below. + +# Constructions + +By default the tree is built the way Bitcoin builds one: sibling hashes are concatenated +in the order given, and a level holding an odd number of nodes duplicates its last node +so it can be paired. NewTreeWithHashStrategySorted additionally orders each pair of +siblings before hashing, matching the OpenZeppelin MerkleProof convention. + +Both of those constructions inherit two well known properties. The last node duplication +means [A B C] and [A B C C] hash to the same root, which is CVE-2012-2459. And because +leaf and interior hashes are computed the same way, an interior digest can be presented +as a leaf: a two leaf tree whose leaves are the two subtree hashes of a four leaf tree +reproduces the original root, and verifies. + +NewTreeWithOptions with WithRFC6962 builds the tree specified by RFC 6962 section 2.1 +instead, which closes both. Leaf and interior hashes carry distinct one byte prefixes, +and node lists are split at the largest power of two below their length rather than +padded, so every distinct leaf sequence has a distinct root. + + t, err := merkletree.NewTreeWithOptions(list, merkletree.WithRFC6962()) + +The constructions produce different roots and are not interchangeable, so pick one when +the tree is created. See WithRFC6962 for the details, including how it relates to +Certificate Transparency. + +# Parallel construction + +WithParallelism builds a tree across several goroutines. It is off by default, because +it calls Content.CalculateHash concurrently and only the caller knows whether their +implementation is safe for that. + + t, err := merkletree.NewTreeWithOptions(list, merkletree.WithParallelism(0)) + +The root is unaffected: results are written to slots fixed by position, so a tree built +in parallel is byte for byte the tree built serially. What it is worth depends on what +CalculateHash costs, and is large when content is expensive to hash and negative on a +small tree of cheap content. See WithParallelism. + +# Proof lookup + +GetMerklePath and VerifyContent locate their content by scanning every leaf, which makes +a single proof cost O(n) in the leaf count even though the walk it performs afterwards is +only O(log n), and a full set of proofs O(n²). Two options avoid the scan. + +WithLeafIndex records a lookup table from leaf hash to leaf position at construction, so +a lookup becomes one hash and one map probe whatever the leaf count: + + t, err := merkletree.NewTreeWithOptions(list, merkletree.WithLeafIndex()) + +It costs memory proportional to the leaf count and changes how content is located, from +Content.Equals to a comparison of hashes. The Content interface already requires the two +to agree, so any implementation honoring that contract gets the same leaf either way. See +WithLeafIndex. + +GetMerklePathByIndex takes the position in Leafs directly, needs no option and no extra +memory, and is the better choice when the caller already tracks which item is which. + +AppendMerklePath and AppendMerklePathByIndex produce the same proofs into caller +supplied slices, so a proof server reusing its buffers generates proofs without +allocating at all: + + path, index, err = t.AppendMerklePathByIndex(path[:0], index[:0], i) + +# Verifying a proof without the tree + +VerifyProof checks an audit path against a root and needs no tree, which is the operation +a verifier performs when it holds a root from a source it trusts and a proof from one it +does not: + + ok, err := merkletree.VerifyProof(content, path, index, root, merkletree.WithRFC6962()) + +The options must describe the construction the proof was produced under. A verifier that +has no Content implementation can use VerifyProofWithDigest instead, and one that does +hold the tree can call the MerkleTree.VerifyProof method and skip restating the options. + +This is a different question from MerkleTree.VerifyContent, which walks a tree it already +has and recomputes every hash on the path from its own stored nodes. VerifyContent is the +stronger check where the tree is available; VerifyProof is the only one available where +it is not. See VerifyProof for what a verified proof does and does not establish, and why +WithRFC6962 matters more for proofs from untrusted sources. + +# Serialization + +A tree holds reference cycles - a Node points back at its Tree and at its Parent - so it +cannot be handed to a reflection-based codec directly. Use the marshalers instead, which +write the content the tree is rebuilt from rather than the node graph: the ordered leaf +content, the name of the hash strategy, the construction the tree was built with, and the +Merkle root. The recorded root is checked against the rebuilt tree on decode, so a payload +that has been tampered with, truncated, or decoded with the wrong hash strategy is +rejected. + +Register the content type and encoding/gob, encoding/json, and anything else built on +encoding.BinaryMarshaler or json.Marshaler will work: + + merkletree.RegisterContent(MyContent{}) + err := gob.NewEncoder(&buf).Encode(t) + +Content types registered this way must implement encoding.BinaryMarshaler, and a pointer +to them must implement encoding.BinaryUnmarshaler. + +To serialize without touching a package-level registry, supply the content codec directly +with MarshalWith and UnmarshalWith. + +Hash strategies are recorded by name, because a function value cannot be serialized. The +standard library strategies are registered automatically; register anything else, such as +keccak256 or blake2b, with RegisterHashStrategy before marshaling a tree that uses it. +*/ package merkletree diff --git a/vendor/github.com/cbergoon/merkletree/merkle_tree.go b/vendor/github.com/cbergoon/merkletree/merkle_tree.go index ae2e2769..435def49 100644 --- a/vendor/github.com/cbergoon/merkletree/merkle_tree.go +++ b/vendor/github.com/cbergoon/merkletree/merkle_tree.go @@ -9,26 +9,167 @@ import ( "errors" "fmt" "hash" + "math/bits" + "runtime" + "strings" + "sync" ) -//Content represents the data that is stored and verified by the tree. A type that -//implements this interface can be used as an item in the tree. +var ( + // ErrNoContent is returned when a tree is built from an empty list of content. + ErrNoContent = errors.New("error: cannot construct tree with no content") + + // ErrNilContent is returned when a list of content holds a nil entry. + ErrNilContent = errors.New("error: cannot construct tree with nil content") + + // ErrContentNotFound is returned by GetMerklePath when the requested content is + // not held by any leaf of the tree. Test for it with errors.Is. + ErrContentNotFound = errors.New("error: content not found in tree") + + // ErrMalformedTree is returned by the verification methods when the tree they + // are asked to walk is not one a constructor could have produced: a zero value + // MerkleTree, or a node graph whose interior nodes are missing a child or a tree + // back-pointer. Node exposes its fields, so a caller can assemble such a graph by + // hand or by editing a decoded tree; verification reports it rather than faulting. + ErrMalformedTree = errors.New("error: tree is empty or malformed") +) + +// Content represents the data that is stored and verified by the tree. A type that +// implements this interface can be used as an item in the tree. +// +// Implementations are expected to keep Equals and CalculateHash consistent: two +// items that report equal should hash equal, and two items that hash equal should +// report equal. Lookups by content return the first matching leaf, so a type that +// breaks this correspondence can be located by one method and hashed by the other. type Content interface { CalculateHash() ([]byte, error) Equals(other Content) (bool, error) } -//MerkleTree is the container for the tree. It holds a pointer to the root of the tree, -//a list of pointers to the leaf nodes, and the merkle root. +// MerkleTree is the container for the tree. It holds a pointer to the root of the tree, +// a list of pointers to the leaf nodes, and the merkle root. +// +// Note that Node points back at its Tree and at its Parent, so the tree contains +// reference cycles and cannot be handed directly to a reflection-based codec. Use the +// marshalers in serialize.go, which encode the content the tree is rebuilt from rather +// than the node graph itself. type MerkleTree struct { - Root *Node - merkleRoot []byte - Leafs []*Node - hashStrategy func() hash.Hash + Root *Node + merkleRoot []byte + Leafs []*Node + // hashStrategyName is the name hashStrategy is registered under, when it is + // known. It is only ever set by the unmarshalers, to preserve the name a payload + // was written with even when the strategy was supplied via WithHashStrategy + // rather than resolved through the registry. When it is empty the name is looked + // up from hashStrategy at marshal time, so registering a strategy after building + // a tree with it still works. + hashStrategyName string + hashStrategy func() hash.Hash + sort bool + rfc6962 bool + // parallelism is the goroutine budget for building this tree, or zero to build + // serially. Unlike sort and rfc6962 it does not affect the root, so it is a + // property of how a tree is built rather than of the tree itself, and it is + // deliberately absent from the serialized form. + parallelism int + // wantLeafIndex records that WithLeafIndex was asked for, so that a rebuild + // regenerates the index rather than silently dropping it. Like parallelism it + // does not affect the root and is absent from the serialized form. + wantLeafIndex bool + // leafIndex maps a leaf hash to the lowest index in Leafs holding it, or is nil + // when the tree was built without WithLeafIndex. It is written only while a tree + // is being built or rebuilt and is read only afterwards, so proof serving needs + // no synchronization. + leafIndex map[string]int + // scratchPool recycles the hasher and replay buffer one proof verification + // needs, or is nil, in which case they are created per call. Only + // defaultProofConfig carries one: its strategy is fixed forever, so a recycled + // hasher can never be of the wrong kind. Trees deliberately have none, because + // hashStrategy is a mutable field within this package and a pool filled before + // a swap would keep hashing with the old strategy. Held by pointer so that + // copying a MerkleTree, as the value-receiver marshalers do, copies a reference + // rather than the pool's internals. + scratchPool *sync.Pool +} + +// Prefixes that separate the two kinds of hash an RFC 6962 tree computes, so that no +// interior digest can ever be mistaken for a leaf digest. +const ( + rfc6962LeafPrefix = 0x00 + rfc6962InteriorPrefix = 0x01 +) + +// The prefixes as slices, so writing one does not allocate on every node. +var ( + rfc6962LeafPrefixBytes = []byte{rfc6962LeafPrefix} + rfc6962InteriorPrefixBytes = []byte{rfc6962InteriorPrefix} +) + +// hashLeaf produces the hash recorded on the leaf holding c. +// +// In the default construction this is whatever Content.CalculateHash returned. Under +// RFC 6962 that digest is hashed again behind a leaf prefix, which is what stops an +// interior digest being presented as a leaf. +func (m *MerkleTree) hashLeaf(c Content) ([]byte, error) { + digest, err := c.CalculateHash() + if err != nil { + return nil, err + } + if !m.rfc6962 { + return digest, nil + } + + return m.appendLeafDigest(m.hashStrategy(), nil, digest) +} + +// appendLeafDigest appends the RFC 6962 leaf hash of digest to dst, reusing h. +// +// The hasher must not be shared across goroutines. Construction keeps its hasher local +// to the call, and VerifyTree creates one for the walk it then runs to completion on a +// single goroutine, so concurrent reads of a built tree remain safe. +func (m *MerkleTree) appendLeafDigest(h hash.Hash, dst, digest []byte) ([]byte, error) { + h.Reset() + if _, err := h.Write(rfc6962LeafPrefixBytes); err != nil { + return nil, err + } + if _, err := h.Write(digest); err != nil { + return nil, err + } + + return h.Sum(dst), nil +} + +// hashInterior produces the hash recorded on the interior node above left and right. +func (m *MerkleTree) hashInterior(left, right []byte) ([]byte, error) { + return m.appendInteriorHash(m.hashStrategy(), nil, left, right) +} + +// appendInteriorHash appends the interior hash of left and right to dst, reusing h. +// +// The pair is written to the hasher in two calls rather than being concatenated into a +// scratch slice first. The bytes fed to the hash are identical either way, so roots are +// unaffected, but the concatenation is one allocation per interior node. +func (m *MerkleTree) appendInteriorHash(h hash.Hash, dst, left, right []byte) ([]byte, error) { + h.Reset() + if m.rfc6962 { + if _, err := h.Write(rfc6962InteriorPrefixBytes); err != nil { + return nil, err + } + } else { + left, right = sortPair(m.sort, left, right) + } + if _, err := h.Write(left); err != nil { + return nil, err + } + if _, err := h.Write(right); err != nil { + return nil, err + } + + return h.Sum(dst), nil } -//Node represents a node, root, or leaf in the tree. It stores pointers to its immediate -//relationships, a hash, the content stored if it is a leaf, and other metadata. +// Node represents a node, root, or leaf in the tree. It stores pointers to its immediate +// relationships, a hash, the content stored if it is a leaf, and other metadata. type Node struct { Tree *MerkleTree Parent *Node @@ -40,49 +181,181 @@ type Node struct { C Content } -//verifyNode walks down the tree until hitting a leaf, calculating the hash at each level -//and returning the resulting hash of Node n. -func (n *Node) verifyNode() ([]byte, error) { +// sortAppend concatenates a and b, optionally ordering the pair by big-endian +// integer value first so the result matches the OpenZeppelin MerkleProof convention. +// https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/utils/cryptography/MerkleProof.sol +// +// The result is always a freshly allocated slice. Appending directly onto a or b +// would write into their spare capacity, and Content.CalculateHash may legally +// return a slice with cap > len that the caller still owns. The concatenated bytes +// are identical either way, so Merkle roots are unaffected. +func sortAppend(sort bool, a, b []byte) []byte { + a, b = sortPair(sort, a, b) + + out := make([]byte, 0, len(a)+len(b)) + out = append(out, a...) + return append(out, b...) +} + +// sortPair returns a and b in the order they should be hashed: unchanged by default, +// or ordered by big-endian integer value when sort is set. It is the ordering half of +// sortAppend, split out so the hashing path can order a pair without concatenating it. +func sortPair(sort bool, a, b []byte) ([]byte, []byte) { + if sort && compareBigEndian(a, b) != -1 { + return b, a + } + + return a, b +} + +// compareBigEndian compares a and b as big-endian unsigned integers, returning -1, 0 +// or 1 the way big.Int.Cmp does. +// +// This reproduces big.Int comparison exactly rather than approximating it with +// bytes.Compare. The two agree only for equal-length inputs: read as integers, +// {0x01, 0x00} is larger than {0xff}, while lexicographically it is smaller. Digests +// from a single hash strategy are all the same width, but Content.CalculateHash is +// free to return any width, and the ordering chosen here decides the Merkle root. +// +// Going through big.Int would cost two heap allocations per interior node, which on a +// sorted tree is the largest single source of allocation during construction. +func compareBigEndian(a, b []byte) int { + // Leading zero bytes do not change the value, so {0x00, 0x01} and {0x01} are + // equal here just as they are to big.Int. + for len(a) > 0 && a[0] == 0 { + a = a[1:] + } + for len(b) > 0 && b[0] == 0 { + b = b[1:] + } + if len(a) != len(b) { + if len(a) < len(b) { + return -1 + } + + return 1 + } + + return bytes.Compare(a, b) +} + +// verifyNode walks down the tree until hitting a leaf, recalculating the hash at each +// level from the content beneath it. It appends the recalculated hash of Node n to dst +// and returns the extended buffer along with whether n and everything below it matched +// the hash each node has recorded. The recalculated hash is the tail of the returned +// buffer, from the length dst had on entry. +// +// Recalculating alone only proves the content still hashes to the root; it says nothing +// about the hashes cached on the nodes in between. Reporting both lets VerifyTree catch +// a tree whose interior hashes have been edited as well as one whose content has. +// +// The hasher and the buffer are threaded through the whole walk rather than being +// created per node. A fresh hasher and a fresh digest for every node made allocation, +// not hashing, the dominant cost of verifying: the node count is linear in the leaves, +// so a tree of any size paid two allocations per node. Both are owned by the caller for +// the duration of the walk, which is single goroutine, so nothing here is shared. +func (n *Node) verifyNode(h hash.Hash, dst []byte) ([]byte, bool, error) { + // Hashing anything at all needs the tree's strategy, and an interior node needs + // both children. Neither can be missing in a tree a constructor built, but the + // fields are exported, so check rather than fault on a graph assembled by hand. + if n.Tree == nil { + return nil, false, fmt.Errorf("%w: node is not attached to a tree", ErrMalformedTree) + } if n.leaf { - return n.C.CalculateHash() + off := len(dst) + digest, err := n.C.CalculateHash() + if err != nil { + return nil, false, err + } + // Only RFC 6962 hashes the leaf digest again; the default construction + // records what CalculateHash returned. This mirrors hashLeaf, but appends + // into the shared buffer rather than allocating. + if n.Tree.rfc6962 { + if dst, err = n.Tree.appendLeafDigest(h, dst, digest); err != nil { + return nil, false, err + } + } else { + dst = append(dst, digest...) + } + + return dst, bytes.Equal(dst[off:], n.Hash), nil + } + if n.Left == nil || n.Right == nil { + return nil, false, fmt.Errorf("%w: interior node is missing a child", ErrMalformedTree) } - rightBytes, err := n.Right.verifyNode() + + off := len(dst) + dst, rightMatched, err := n.Right.verifyNode(h, dst) if err != nil { - return nil, err + return nil, false, err } + mid := len(dst) - leftBytes, err := n.Left.verifyNode() + dst, leftMatched, err := n.Left.verifyNode(h, dst) if err != nil { - return nil, err + return nil, false, err } + end := len(dst) - h := n.Tree.hashStrategy() - if _, err := h.Write(append(leftBytes, rightBytes...)); err != nil { - return nil, err + // The children's digests are read into the hasher before Sum appends, so a growth + // of dst here cannot invalidate them. + dst, err = n.Tree.appendInteriorHash(h, dst, dst[mid:end], dst[off:mid]) + if err != nil { + return nil, false, err } - return h.Sum(nil), nil + // The children's digests are dead once this node's is computed, so slide it down + // over them. The buffer then holds only the path currently being walked, which + // keeps it at the depth of the tree rather than the size of it. + dst = dst[:off+copy(dst[off:], dst[end:])] + + return dst, leftMatched && rightMatched && bytes.Equal(dst[off:], n.Hash), nil } -//calculateNodeHash is a helper function that calculates the hash of the node. -func (n *Node) calculateNodeHash() ([]byte, error) { - if n.leaf { - return n.C.CalculateHash() +// calculateNodeHash is a helper function that calculates the hash of the node. +// +// The argument is retained so existing callers keep compiling. The tree's own sort +// setting is what gets applied, and every call site has always passed exactly that. +func (n *Node) calculateNodeHash(_ bool) ([]byte, error) { + if n.Tree == nil { + return nil, fmt.Errorf("%w: node is not attached to a tree", ErrMalformedTree) } - h := n.Tree.hashStrategy() - if _, err := h.Write(append(n.Left.Hash, n.Right.Hash...)); err != nil { - return nil, err + return n.appendCalculatedHash(n.Tree.hashStrategy(), nil) +} + +// appendCalculatedHash appends the hash calculateNodeHash would return to dst, +// reusing h instead of creating a hasher and a digest per node. It is what lets +// VerifyContent walk a whole path with one hasher and one buffer, the same way +// verifyNode does; a fresh pair per node made allocation the dominant cost of both. +func (n *Node) appendCalculatedHash(h hash.Hash, dst []byte) ([]byte, error) { + if n.Tree == nil { + return nil, fmt.Errorf("%w: node is not attached to a tree", ErrMalformedTree) + } + if n.leaf { + digest, err := n.C.CalculateHash() + if err != nil { + return nil, err + } + if n.Tree.rfc6962 { + return n.Tree.appendLeafDigest(h, dst, digest) + } + + return append(dst, digest...), nil + } + if n.Left == nil || n.Right == nil { + return nil, fmt.Errorf("%w: interior node is missing a child", ErrMalformedTree) } - return h.Sum(nil), nil + return n.Tree.appendInteriorHash(h, dst, n.Left.Hash, n.Right.Hash) } -//NewTree creates a new Merkle Tree using the content cs. +// NewTree creates a new Merkle Tree using the content cs. func NewTree(cs []Content) (*MerkleTree, error) { var defaultHashStrategy = sha256.New t := &MerkleTree{ hashStrategy: defaultHashStrategy, + sort: false, } root, leafs, err := buildWithContent(cs, t) if err != nil { @@ -94,12 +367,13 @@ func NewTree(cs []Content) (*MerkleTree, error) { return t, nil } -//NewTreeWithHashStrategy creates a new Merkle Tree using the content cs using the provided hash -//strategy. Note that the hash type used in the type that implements the Content interface must -//match the hash type profided to the tree. +// NewTreeWithHashStrategy creates a new Merkle Tree using the content cs using the provided hash +// strategy. Note that the hash type used in the type that implements the Content interface must +// match the hash type provided to the tree. func NewTreeWithHashStrategy(cs []Content, hashStrategy func() hash.Hash) (*MerkleTree, error) { t := &MerkleTree{ hashStrategy: hashStrategy, + sort: false, } root, leafs, err := buildWithContent(cs, t) if err != nil { @@ -111,67 +385,606 @@ func NewTreeWithHashStrategy(cs []Content, hashStrategy func() hash.Hash) (*Merk return t, nil } -// GetMerklePath: Get Merkle path and indexes(left leaf or right leaf) -func (m *MerkleTree) GetMerklePath(content Content) ([][]byte, []int64, error) { - for _, current := range m.Leafs { - ok, err := current.C.Equals(content) +// NewTreeWithHashStrategySorted just like NewTreeWithHashStrategy +// but sorts the siblings before hashing, mostly to follow the OpenZepplin Merkle implementation +// https://github.com/OpenZeppelin/openzeppelin-contracts-ethereum-package/blob/master/contracts/cryptography/MerkleProof.sol +func NewTreeWithHashStrategySorted(cs []Content, hashStrategy func() hash.Hash, sort bool) (*MerkleTree, error) { + t := &MerkleTree{ + hashStrategy: hashStrategy, + sort: sort, + } + root, leafs, err := buildWithContent(cs, t) + if err != nil { + return nil, err + } + t.Root = root + t.Leafs = leafs + t.merkleRoot = root.Hash + return t, nil +} + +// TreeOption configures a tree built by NewTreeWithOptions. +type TreeOption func(*MerkleTree) + +// WithHasher sets the hash strategy used for interior nodes, and for the leaf prefix +// under WithRFC6962. It defaults to sha256.New. +// +// The hash a Content implementation returns from CalculateHash must be produced by a +// compatible algorithm; the tree does not and cannot check this. +func WithHasher(strategy func() hash.Hash) TreeOption { + return func(m *MerkleTree) { + m.hashStrategy = strategy + } +} + +// WithSortedSiblings orders each pair of siblings by their big-endian integer value +// before hashing them, matching the OpenZeppelin MerkleProof convention. It is the +// option form of NewTreeWithHashStrategySorted. +// +// Sorting makes the root independent of the order content was supplied in; see +// MerkleTree.Sorted. It cannot be combined with WithRFC6962, which specifies its own +// unsorted ordering. +func WithSortedSiblings() TreeOption { + return func(m *MerkleTree) { + m.sort = true + } +} + +// WithRFC6962 builds the tree the way RFC 6962 section 2.1 specifies, which closes two +// weaknesses in the default Bitcoin-style construction: +// +// Leaf and interior hashes are computed behind distinct one byte prefixes, 0x00 and +// 0x01. Without them an interior digest can be handed back as a leaf: a two leaf tree +// whose leaves are the two subtree hashes of a four leaf tree reproduces the original +// root exactly, and the forged tree verifies. With them a forgery would need a genuine +// collision between two differently prefixed inputs. +// +// Odd node counts are split rather than duplicated. The default construction pairs the +// last node with itself, so [A B C] and [A B C C] hash alike, which is CVE-2012-2459. +// RFC 6962 splits each node list at the largest power of two below its length instead, +// so every distinct leaf sequence has a distinct root. +// +// Note that RFC 6962 hashes raw leaf data, whereas this tree hashes whatever +// Content.CalculateHash returns. Roots therefore match a Certificate Transparency log +// only if CalculateHash returns the leaf bytes themselves rather than a digest of +// them. The structural guarantees above hold either way. +// +// Trees built this way do not share roots with trees built any other way, so this is a +// choice to make when a tree is created rather than one to change later. +func WithRFC6962() TreeOption { + return func(m *MerkleTree) { + m.rfc6962 = true + } +} + +// WithParallelism builds the tree using up to n goroutines, or GOMAXPROCS of them when +// n is less than one. It is off by default. +// +// Content.CalculateHash is called concurrently from several goroutines when this is +// set, and implementations must be safe for that. Nothing else in this package calls +// it concurrently and the tree cannot check whether yours is safe, so this requirement +// is the whole cost of the option and the reason it is not the default. +// +// The root is unaffected. Every hash is written to a slot fixed by its position, so the +// order the goroutines finish in cannot change the result: a tree built with this option +// is byte for byte the tree built without it. +// +// How much it buys depends almost entirely on what CalculateHash costs. Hashing the +// interior of the tree parallelizes too, but that work is small and fixed, so it is left +// serial below a threshold where the coordination would cost more than it saves. Content +// hashing is neither small nor knowable in advance, so it is always spread across the +// goroutine budget when this option is set, whatever the leaf count. +// +// That last point cuts both ways, and it is worth measuring rather than assuming. Where +// content is expensive to hash this option is worth several times the serial build, and +// the advantage grows with the cost. Where content is cheap the leaves finish so quickly +// that handing them out costs more than it saves on a small tree: a few thousand leaves +// of a short string is roughly the break-even, and below it a parallel build can be +// slower than a serial one. +// +// Parallelism is not recorded when a tree is serialized, because it has no bearing on +// the root. A tree read back with UnmarshalBinary or UnmarshalJSON rebuilds serially +// unless it is built again with this option. +func WithParallelism(n int) TreeOption { + return func(m *MerkleTree) { + if n < 1 { + n = runtime.GOMAXPROCS(0) + } + m.parallelism = n + } +} + +// WithLeafIndex builds a lookup table from leaf hash to leaf position, so that +// GetMerklePath and VerifyContent find their content by hashing it once instead of +// scanning every leaf. It is off by default. +// +// Without it, locating content is a linear scan calling Content.Equals on each leaf, +// which makes a single proof O(n) and a full set of proofs O(n²). At a hundred thousand +// leaves that is around 98µs per proof and roughly ten seconds for the whole set. With +// it, a lookup is one hash and one map probe whatever the leaf count. +// +// The index costs memory proportional to the leaf count, on the order of the digest +// width plus map overhead per leaf, which is why it is opt in rather than automatic. It +// costs no extra hashing to build: every leaf hash it records was already computed and +// stored during construction. +// +// It changes how content is located, which is the reason to think before reaching for +// it. Without the index a leaf is found by Content.Equals; with it a leaf is found by +// comparing the hash of the query against the recorded leaf hashes. The Content +// interface already requires the two to agree - items that are equal must hash equal, +// and items that hash equal must be equal - so for any implementation that honors that +// contract both routes return the same leaf, including the rule that the earliest +// matching leaf wins. For an implementation that does not, the two disagree, and only +// the scan reflects Equals. +// +// One consequence is worth stating plainly, because it shows up in error handling: an +// indexed lookup hashes the content it is given and never calls Equals, where a scan +// calls Equals on each leaf and never hashes what it was given. A query whose +// CalculateHash returns an error therefore fails only on the indexed path, and content +// whose Equals returns an error surfaces that only on the scanning path. +// +// The root is unaffected, and neither is anything a proof is checked against. Like +// WithParallelism the index is absent from the serialized form, so a tree read back +// with UnmarshalBinary or UnmarshalJSON has no index unless it is asked for again; +// RebuildTree and RebuildTreeWith do regenerate it. +func WithLeafIndex() TreeOption { + return func(m *MerkleTree) { + m.wantLeafIndex = true + } +} + +// NewTreeWithOptions creates a new Merkle Tree using the content cs, configured by the +// given options. With no options it is equivalent to NewTree. +func NewTreeWithOptions(cs []Content, opts ...TreeOption) (*MerkleTree, error) { + // Resolved through the same function the proof verifier uses, so that a tree and a + // proof checked against it cannot disagree about what an option meant. + t, err := configFromOptions(opts) + if err != nil { + return nil, err + } + + root, leafs, err := buildWithContent(cs, t) + if err != nil { + return nil, err + } + t.Root = root + t.Leafs = leafs + t.merkleRoot = root.Hash + t.buildLeafIndex() + + return t, nil +} + +// buildLeafIndex regenerates leafIndex from the current leaves, or clears it when the +// tree was not asked for one. It is called after every build, so a rebuilt tree neither +// keeps a stale index nor silently loses the one it was asked for. +// +// The padding leaf that an odd content count produces carries the same hash as the leaf +// it copies. Keeping the lowest index for a hash means the copy never displaces the +// original, which is the same "earliest leaf wins" rule the scan follows. +func (m *MerkleTree) buildLeafIndex() { + if !m.wantLeafIndex { + m.leafIndex = nil + + return + } + + // The keys are substrings of one backing string holding every leaf hash, rather + // than a string(l.Hash) conversion per leaf. The conversion copies, so building + // the index that way cost one allocation per leaf - the single largest source of + // allocation in an indexed build. Every key is retained by the map anyway, so + // sharing one backing keeps the same bytes alive in one object instead of n. + total := 0 + for _, l := range m.Leafs { + total += len(l.Hash) + } + var b strings.Builder + b.Grow(total) + for _, l := range m.Leafs { + b.Write(l.Hash) + } + keys := b.String() + + idx := make(map[string]int, len(m.Leafs)) + off := 0 + for i, l := range m.Leafs { + k := keys[off : off+len(l.Hash)] + off += len(l.Hash) + if _, seen := idx[k]; !seen { + idx[k] = i + } + } + m.leafIndex = idx +} + +// findLeaf returns the position in Leafs of the first leaf holding content, or -1 if no +// leaf holds it. It goes through the index when the tree has one and falls back to the +// scan over Content.Equals when it does not. +func (m *MerkleTree) findLeaf(content Content) (int, error) { + if m.leafIndex != nil { + // The recorded leaf hashes are what hashLeaf produces, prefix and all under + // RFC 6962, so the query has to go through the same function to be comparable. + digest, err := m.hashLeaf(content) if err != nil { - return nil, nil, err + return -1, err } + // Indexing a map with string(byteSlice) does not allocate the string. + if i, ok := m.leafIndex[string(digest)]; ok { + return i, nil + } + + return -1, nil + } + for i, l := range m.Leafs { + ok, err := l.C.Equals(content) + if err != nil { + return -1, err + } if ok { - currentParent := current.Parent - var merklePath [][]byte - var index []int64 - for currentParent != nil { - if bytes.Equal(currentParent.Left.Hash, current.Hash) { - merklePath = append(merklePath, currentParent.Right.Hash) - index = append(index, 1) // right leaf - } else { - merklePath = append(merklePath, currentParent.Left.Hash) - index = append(index, 0) // left leaf + return i, nil + } + } + + return -1, nil +} + +// pathFromLeaf walks from a leaf up to the root, collecting the sibling hash at each +// level and which side it sits on. +func (m *MerkleTree) pathFromLeaf(current *Node) ([][]byte, []int64) { + // The walk takes one step per level, so the depth of the tree is how many entries + // the two slices end up holding. bits.Len of the highest leaf position is that + // depth exactly, for a power-of-two count as much as a padded or split one, so + // the slices come out exact-fit; this is the hottest allocation site in the + // package, and a spare entry per proof is measurable at proof-serving rates. + depth := bits.Len(uint(len(m.Leafs) - 1)) + + return m.appendPathFromLeaf(make([][]byte, 0, depth), make([]int64, 0, depth), current) +} + +// appendPathFromLeaf is pathFromLeaf appending into caller supplied slices. It is the +// walk both the slice-returning and the appending proof methods share. +func (m *MerkleTree) appendPathFromLeaf(merklePath [][]byte, index []int64, current *Node) ([][]byte, []int64) { + for currentParent := current.Parent; currentParent != nil; currentParent = current.Parent { + // Whether current sits on the left or the right is a structural question, so + // compare node identity rather than hashes. Comparing hashes gives the same + // answer only while Content.Equals agrees with Content.CalculateHash: an + // implementation where two distinct items hash alike would see a right-hand + // node reported as a left-hand one, producing a correct path with a wrong + // index. + if currentParent.Left == current { + merklePath = append(merklePath, currentParent.Right.Hash) + index = append(index, 1) // right leaf + } else { + merklePath = append(merklePath, currentParent.Left.Hash) + index = append(index, 0) // left leaf + } + current = currentParent + } + + return merklePath, index +} + +// GetMerklePath returns the sibling hashes on the path from the leaf holding content +// up to the root, together with an index describing which side each sibling sits on: +// 1 when the sibling is the right hand node, 0 when it is the left hand node. +// +// Content is located by Content.Equals and the first matching leaf wins, so a value +// stored in more than one leaf yields the proof for the earliest of them. If no leaf +// holds the content, the returned error wraps ErrContentNotFound. +// +// Locating the content is a scan over every leaf, which makes this O(n) in the leaf +// count while the walk it performs afterwards is only O(log n). Build the tree with +// WithLeafIndex to replace the scan with a single hash and map probe, or use +// GetMerklePathByIndex when the position is already known. AppendMerklePath produces +// the same proof into caller supplied slices, for callers generating proofs at a rate +// where the two returned slices are a cost worth removing. +func (m *MerkleTree) GetMerklePath(content Content) ([][]byte, []int64, error) { + i, err := m.findLeaf(content) + if err != nil { + return nil, nil, err + } + if i < 0 { + return nil, nil, ErrContentNotFound + } + + merklePath, index := m.pathFromLeaf(m.Leafs[i]) + + return merklePath, index, nil +} + +// GetMerklePathByIndex returns the same audit path as GetMerklePath for the leaf at +// position i in Leafs, without locating it by content first. It costs one step per +// level of the tree whatever the leaf count, where GetMerklePath has to find the leaf +// before it can walk. +// +// The index is a position in Leafs, which is the order the content was supplied in. +// Note that a tree built from an odd number of items holds one more leaf than it was +// given, the last being a copy of the one before it; that copy is addressable here and +// yields a valid path, though its content is not distinct. +// +// Returns ErrContentNotFound if i is outside the range of Leafs. +func (m *MerkleTree) GetMerklePathByIndex(i int) ([][]byte, []int64, error) { + if i < 0 || i >= len(m.Leafs) { + return nil, nil, fmt.Errorf("%w: no leaf at index %d, the tree has %d", ErrContentNotFound, i, len(m.Leafs)) + } + + merklePath, index := m.pathFromLeaf(m.Leafs[i]) + + return merklePath, index, nil +} + +// AppendMerklePath appends the audit path for the leaf holding content to path, and +// the side each sibling sits on to index, returning the extended slices. It is +// GetMerklePath in the style of strconv.AppendInt: the proof produced is identical, +// but where the slices land is the caller's decision. Passing slices with capacity to +// spare - most simply the previous proof's, resliced to length zero - makes proof +// generation allocate nothing, which is worth having where proofs are served at rate: +// the per-proof slice allocations are otherwise the ceiling on concurrent throughput, +// with the garbage collector as the shared resource every goroutine queues on. +// +// var path [][]byte +// var index []int64 +// for _, c := range contents { +// path, index, err = tree.AppendMerklePath(path[:0], index[:0], c) +// ... +// } +// +// The appended hashes are the tree's own, not copies; treat them as read only, and +// note that reusing the path buffer overwrites the positions of the previous proof, +// so a proof that must outlive the next call has to be copied out. +// +// Content is located exactly as GetMerklePath locates it, including the leaf index +// when the tree has one and the first-match rule. If no leaf holds the content, the +// returned error wraps ErrContentNotFound and the slices are returned unchanged. +func (m *MerkleTree) AppendMerklePath(path [][]byte, index []int64, content Content) ([][]byte, []int64, error) { + i, err := m.findLeaf(content) + if err != nil { + return path, index, err + } + if i < 0 { + return path, index, ErrContentNotFound + } + + path, index = m.appendPathFromLeaf(path, index, m.Leafs[i]) + + return path, index, nil +} + +// AppendMerklePathByIndex appends the audit path for the leaf at position i in Leafs +// to path, and the side each sibling sits on to index, returning the extended slices. +// It is to GetMerklePathByIndex what AppendMerklePath is to GetMerklePath, and the +// notes there - identical proofs, reused buffers making generation allocation free, +// appended hashes being the tree's own - apply unchanged. +// +// Returns ErrContentNotFound and the slices unchanged if i is outside the range of +// Leafs. +func (m *MerkleTree) AppendMerklePathByIndex(path [][]byte, index []int64, i int) ([][]byte, []int64, error) { + if i < 0 || i >= len(m.Leafs) { + return path, index, fmt.Errorf("%w: no leaf at index %d, the tree has %d", ErrContentNotFound, i, len(m.Leafs)) + } + + path, index = m.appendPathFromLeaf(path, index, m.Leafs[i]) + + return path, index, nil +} + +// buildWithContent is a helper function that for a given set of Contents, generates a +// corresponding tree and returns the root node, a list of leaf nodes, and a possible error. +// Returns ErrNoContent if cs is empty and ErrNilContent if any entry is nil. +// Chunking limits for a parallel build. Content hashing costs whatever the caller's +// CalculateHash costs, which is unknowable here, so it is spread as thinly as the +// goroutine budget allows. Interior hashing is a fixed, small amount of work per node, +// so a level is only worth splitting up once it is large enough that coordinating the +// split costs less than doing the work in one goroutine. +const ( + minLeafChunk = 1 + minInteriorChunk = 64 + parallelInteriorMinNodes = 1024 +) + +// buildWorkers is the goroutine budget for a build, where one means build serially. +func (m *MerkleTree) buildWorkers() int { + if m.parallelism > 1 { + return m.parallelism + } + + return 1 +} + +// newHashers creates n hashers up front, on the calling goroutine. A parallel build +// hands one to each worker rather than calling hashStrategy from the workers, so a +// caller supplied strategy is never invoked concurrently even though the build is. +func (m *MerkleTree) newHashers(n int) []hash.Hash { + hashers := make([]hash.Hash, n) + for i := range hashers { + hashers[i] = m.hashStrategy() + } + + return hashers +} + +// runParallel splits [0, n) into contiguous chunks across at most workers goroutines and +// calls fn for every index. fn receives the index of the worker running it, so it can +// reach for per-worker state such as that worker's hasher. +// +// The error returned is the one from the lowest failing index rather than from whichever +// goroutine happened to report first, so the same input always fails the same way. +func runParallel(n, workers, minChunk int, fn func(worker, i int) error) error { + chunk := (n + workers - 1) / workers + if chunk < minChunk { + chunk = minChunk + } + + var ( + wg sync.WaitGroup + mu sync.Mutex + firstErr error + errIndex = -1 + panicked any + ) + + worker := 0 + for start := 0; start < n; start += chunk { + end := start + chunk + if end > n { + end = n + } + + wg.Add(1) + go func(w, s, e int) { + defer wg.Done() + // A panic out of Content.CalculateHash would kill the process from here, + // where a serial build would have delivered it to the caller. Carry it + // back and re-raise it on the calling goroutine instead. + defer func() { + if r := recover(); r != nil { + mu.Lock() + if panicked == nil { + panicked = r + } + mu.Unlock() + } + }() + + for i := s; i < e; i++ { + if err := fn(w, i); err != nil { + mu.Lock() + if errIndex == -1 || i < errIndex { + firstErr, errIndex = err, i + } + mu.Unlock() + + return } - current = currentParent - currentParent = currentParent.Parent } - return merklePath, index, nil - } + }(worker, start, end) + worker++ } - return nil, nil, nil + wg.Wait() + + if panicked != nil { + panic(panicked) + } + + return firstErr } -//buildWithContent is a helper function that for a given set of Contents, generates a -//corresponding tree and returns the root node, a list of leaf nodes, and a possible error. -//Returns an error if cs contains no Contents. +// Building allocates the nodes of each level as a single slab rather than one at a +// time, and appends the digests of a level into a single buffer rather than calling +// Sum(nil) per node. The tree that comes out is the same one either way - the same +// Node values, the same Parent, Left, Right and Tree pointers, the same content held +// on the leaves - but the node count is linear in the leaves, so allocating per node +// makes allocation, not hashing, the dominant cost of construction. +// +// Nodes are handed out as pointers into a slab that is sized exactly and never +// appended to, so no reallocation can invalidate a pointer already taken from it. func buildWithContent(cs []Content, t *MerkleTree) (*Node, []*Node, error) { if len(cs) == 0 { - return nil, nil, errors.New("error: cannot construct tree with no content") + return nil, nil, ErrNoContent + } + + // One hasher serves the whole build. It never leaves this call tree, so a tree + // under construction shares no hash state with anything verifying concurrently. + h := t.hashStrategy() + + // A default-construction tree pads an odd leaf count with a duplicate; an + // RFC 6962 tree splits instead and so holds exactly what the caller supplied. + leafCount := len(cs) + if !t.rfc6962 && leafCount%2 == 1 { + leafCount++ + } + slab := make([]Node, leafCount) + leafs := make([]*Node, 0, leafCount) + + // Only RFC 6962 hashes the leaf digest again, so only it needs somewhere to put + // the result. The default construction records what CalculateHash returned. + var ( + leafBuf []byte + leafSize int + ) + if t.rfc6962 { + leafSize = h.Size() + leafBuf = make([]byte, len(cs)*leafSize) } - var leafs []*Node - for _, c := range cs { + + // hashLeafAt fills in leaf i using the given hasher. Every leaf writes only to its + // own slab entry and its own region of leafBuf, so running this across goroutines + // needs no coordination and cannot depend on the order they run in. + hashLeafAt := func(lh hash.Hash, i int) error { + c := cs[i] + // A nil entry would panic on the call below, so reject it as an error the + // caller can handle rather than a fault. + if c == nil { + return fmt.Errorf("%w: index %d", ErrNilContent, i) + } hash, err := c.CalculateHash() if err != nil { + return err + } + if t.rfc6962 { + off := i * leafSize + if hash, err = t.appendLeafDigest(lh, leafBuf[off:off:off+leafSize], hash); err != nil { + return err + } + } + + n := &slab[i] + n.Hash = hash + n.C = c + n.leaf = true + n.Tree = t + + return nil + } + + workers := t.buildWorkers() + if workers > 1 { + // Content hashing is spread across the budget whatever the leaf count, since + // how expensive it is belongs to the caller and cannot be guessed here. + var hashers []hash.Hash + if t.rfc6962 { + hashers = t.newHashers(workers) + } else { + hashers = make([]hash.Hash, workers) + } + if err := runParallel(len(cs), workers, minLeafChunk, func(w, i int) error { + return hashLeafAt(hashers[w], i) + }); err != nil { return nil, nil, err } + } else { + for i := range cs { + if err := hashLeafAt(h, i); err != nil { + return nil, nil, err + } + } + } - leafs = append(leafs, &Node{ - Hash: hash, - C: c, - leaf: true, - Tree: t, - }) - } - if len(leafs)%2 == 1 { - duplicate := &Node{ - Hash: leafs[len(leafs)-1].Hash, - C: leafs[len(leafs)-1].C, - leaf: true, - dup: true, - Tree: t, + for i := range cs { + leafs = append(leafs, &slab[i]) + } + if t.rfc6962 { + // RFC 6962 splits the leaves rather than padding them, so there is no + // duplicate to append and Leafs holds exactly what the caller supplied. + root, err := buildRFC6962(leafs, t, h) + if err != nil { + return nil, nil, err } - leafs = append(leafs, duplicate) + + return root, leafs, nil } - root, err := buildIntermediate(leafs, t) + if len(cs)%2 == 1 { + last := leafs[len(leafs)-1] + n := &slab[len(cs)] + n.Hash = last.Hash + n.C = last.C + n.leaf = true + n.dup = true + n.Tree = t + leafs = append(leafs, n) + } + root, err := buildIntermediate(leafs, t, h) if err != nil { return nil, nil, err } @@ -179,46 +992,271 @@ func buildWithContent(cs []Content, t *MerkleTree) (*Node, []*Node, error) { return root, leafs, nil } -//buildIntermediate is a helper function that for a given list of leaf nodes, constructs -//the intermediate and root levels of the tree. Returns the resulting root node of the tree. -func buildIntermediate(nl []*Node, t *MerkleTree) (*Node, error) { - var nodes []*Node - for i := 0; i < len(nl); i += 2 { - h := t.hashStrategy() - var left, right int = i, i + 1 - if i+1 == len(nl) { - right = i - } - chash := append(nl[left].Hash, nl[right].Hash...) - if _, err := h.Write(chash); err != nil { - return nil, err - } - n := &Node{ - Left: nl[left], - Right: nl[right], - Hash: h.Sum(nil), - Tree: t, +// buildRFC6962 assembles the tree described by RFC 6962 section 2.1. The node list is +// split at the largest power of two below its length, so a count that is not a power +// of two yields an unbalanced tree instead of a duplicated node. A single node is its +// own root, which is why a one item RFC 6962 tree has a root equal to its only leaf +// hash and an empty audit path. +// +// https://datatracker.ietf.org/doc/html/rfc6962#section-2.1 +func buildRFC6962(nl []*Node, t *MerkleTree, h hash.Hash) (*Node, error) { + // Splitting rather than padding means the interior nodes of a tree over n leaves + // number exactly n-1, whatever shape the splits produce, so one slab covers the + // whole recursion. More than that: the subtree over any k consecutive leaves + // owns exactly k-1 of them, so every subtree's slab and digest region is known + // before anything is built - which is what lets the recursion fork. + b := &rfc6962Builder{ + t: t, + size: h.Size(), + slab: make([]Node, len(nl)-1), + buf: make([]byte, (len(nl)-1)*h.Size()), + } + + // The same policy buildIntermediate applies: interior work is small and fixed + // per node, so the tree only earns the goroutines once it has enough of them. + if workers := t.buildWorkers(); workers > 1 && len(nl) >= parallelInteriorMinNodes { + return b.buildParallel(nl, 0, t.newHashers(workers)) + } + + return b.build(nl, 0, h) +} + +// rfc6962Builder carries the slab and the digest buffer down the recursion, so that +// assembling the tree costs two allocations rather than one per node. +// +// Regions are addressed by position rather than handed out from a cursor: the subtree +// over nl owns slab entries and digest slots [base, base+len(nl)-1), its left child +// the first k-1 of them, its right child the next len(nl)-k-1, and the node joining +// them the last one. Sibling regions are disjoint by construction, which is what lets +// buildParallel assemble them on separate goroutines with no coordination at all. +type rfc6962Builder struct { + t *MerkleTree + size int + slab []Node + buf []byte +} + +// build assembles the subtree over nl into region [base, base+len(nl)-1) of the slab +// and digest buffer, reusing h, and returns the subtree's root. +func (b *rfc6962Builder) build(nl []*Node, base int, h hash.Hash) (*Node, error) { + if len(nl) == 1 { + return nl[0], nil + } + + k := largestPowerOfTwoBelow(len(nl)) + left, err := b.build(nl[:k], base, h) + if err != nil { + return nil, err + } + right, err := b.build(nl[k:], base+k-1, h) + if err != nil { + return nil, err + } + + return b.join(left, right, base+len(nl)-2, h) +} + +// join builds the node above left and right in slab slot i. +func (b *rfc6962Builder) join(left, right *Node, i int, h hash.Hash) (*Node, error) { + off := i * b.size + // Cap the slice at its own digest so that appending to a node's Hash cannot + // reach into the digest stored after it. + nodeHash, err := b.t.appendInteriorHash(h, b.buf[off:off:off+b.size], left.Hash, right.Hash) + if err != nil { + return nil, err + } + + n := &b.slab[i] + n.Left = left + n.Right = right + n.Hash = nodeHash + n.Tree = b.t + left.Parent = n + right.Parent = n + + return n, nil +} + +// buildParallel is build forking across goroutines. hashers is the goroutine budget: +// each fork splits it between the two subtrees in proportion to their size, and a +// subtree whose share is down to one hasher, or which is too small to be worth the +// coordination, is built serially with the first hasher of its share. The shares stay +// disjoint the way the slab regions do, and the whole budget is created up front on +// the calling goroutine, so a caller supplied strategy is never invoked concurrently +// even though the build is. +func (b *rfc6962Builder) buildParallel(nl []*Node, base int, hashers []hash.Hash) (*Node, error) { + if len(hashers) == 1 || len(nl) < parallelInteriorMinNodes { + return b.build(nl, base, hashers[0]) + } + + k := largestPowerOfTwoBelow(len(nl)) + share := len(hashers) * k / len(nl) + if share < 1 { + share = 1 + } + if share > len(hashers)-1 { + share = len(hashers) - 1 + } + + var ( + wg sync.WaitGroup + right *Node + rightErr error + panicked any + ) + wg.Add(1) + go func() { + defer wg.Done() + // A panic out of a caller supplied hasher would kill the process from here, + // where a serial build would have delivered it to the caller. Carry it back + // and re-raise it on the calling goroutine instead. + defer func() { + if r := recover(); r != nil { + panicked = r + } + }() + right, rightErr = b.buildParallel(nl[k:], base+k-1, hashers[share:]) + }() + + left, leftErr := b.buildParallel(nl[:k], base, hashers[:share]) + wg.Wait() + + if panicked != nil { + panic(panicked) + } + // The serial build reaches the left subtree's error first; deliver errors in + // the same order however the goroutines happened to finish, so the same input + // always fails the same way. + if leftErr != nil { + return nil, leftErr + } + if rightErr != nil { + return nil, rightErr + } + + return b.join(left, right, base+len(nl)-2, hashers[0]) +} + +// largestPowerOfTwoBelow returns the largest power of two strictly less than n, which +// is the split point RFC 6962 uses. n is always greater than one here. +func largestPowerOfTwoBelow(n int) int { + return 1 << (bits.Len(uint(n-1)) - 1) +} + +// buildIntermediate is a helper function that for a given list of leaf nodes, constructs +// the intermediate and root levels of the tree. Returns the resulting root node of the tree. +func buildIntermediate(nl []*Node, t *MerkleTree, h hash.Hash) (*Node, error) { + workers := t.buildWorkers() + + // The worker hashers are created once and reused for every level large enough to + // build in parallel, rather than a fresh set per level. Created lazily, so a + // serial build or a small tree never pays for them. + var hashers []hash.Hash + + for len(nl) > 1 { + // Each pass consumes a level and produces the one above it, so the node + // count is known before any of them are built. + count := (len(nl) + 1) / 2 + size := h.Size() + slab := make([]Node, count) + next := make([]*Node, count) + buf := make([]byte, count*size) + + // buildNode assembles the node above pair p. Pair p reads only nodes 2p and + // 2p+1 and writes only its own slab entry, digest region and next slot, so + // pairs are independent of each other and of the order they are built in. + buildNode := func(nh hash.Hash, p int) error { + left, right := p*2, p*2+1 + if right == len(nl) { + right = left + } + + off := p * size + // Cap the slice at its own digest so that appending to a node's Hash + // cannot reach into the digest stored after it. + nodeHash, err := t.appendInteriorHash(nh, buf[off:off:off+size], nl[left].Hash, nl[right].Hash) + if err != nil { + return err + } + + n := &slab[p] + n.Left = nl[left] + n.Right = nl[right] + n.Hash = nodeHash + n.Tree = t + nl[left].Parent = n + nl[right].Parent = n + next[p] = n + + return nil } - nodes = append(nodes, n) - nl[left].Parent = n - nl[right].Parent = n - if len(nl) == 2 { - return n, nil + + // Interior work is small and fixed per node, so a level only earns the + // coordination once there are enough nodes in it. + if workers > 1 && count >= parallelInteriorMinNodes { + if hashers == nil { + hashers = t.newHashers(workers) + } + if err := runParallel(count, workers, minInteriorChunk, func(w, p int) error { + return buildNode(hashers[w], p) + }); err != nil { + return nil, err + } + } else { + for p := 0; p < count; p++ { + if err := buildNode(h, p); err != nil { + return nil, err + } + } } + nl = next } - return buildIntermediate(nodes, t) + + return nl[0], nil } -//MerkleRoot returns the unverified Merkle Root (hash of the root node) of the tree. +// MerkleRoot returns the unverified Merkle Root (hash of the root node) of the tree. +// +// The returned slice is the tree's own, not a copy; treat it as read only. func (m *MerkleTree) MerkleRoot() []byte { return m.merkleRoot } -//RebuildTree is a helper function that will rebuild the tree reusing only the content that -//it holds in the leaves. +// Sorted reports whether the tree orders each pair of siblings before hashing them, +// as chosen by NewTreeWithHashStrategySorted. +// +// Sorting siblings makes the root independent of the order the content was supplied +// in: [A B C D] and [B A C D] and [D C B A] all produce the same root, because each +// pair is ordered before it is hashed and the pairing itself is all that survives. +// Only regrouping which items are paired together changes the root. Callers that +// depend on the root committing to leaf order should check this is false. +func (m *MerkleTree) Sorted() bool { + return m.sort +} + +// RFC6962 reports whether the tree was built with WithRFC6962, meaning leaf and +// interior hashes carry distinct prefixes and odd node counts are split rather than +// duplicated. +func (m *MerkleTree) RFC6962() bool { + return m.rfc6962 +} + +// RebuildTree is a helper function that will rebuild the tree reusing only the content that +// it holds in the leaves. func (m *MerkleTree) RebuildTree() error { - var cs []Content + // Sized to the leaf count up front; at most one entry, the padding copy, goes + // unused. + cs := make([]Content, 0, len(m.Leafs)) for _, c := range m.Leafs { + // Leafs holds the padding copy that buildWithContent appends when the + // content count is odd. Feeding it back in would promote that copy to + // real content, so the tree would lose track of which leaf is padding + // and report one more item than the caller supplied. The root is + // unaffected either way; skipping it keeps Leafs and the dup marker + // accurate across repeated rebuilds. + if c.dup { + continue + } cs = append(cs, c.C) } root, leafs, err := buildWithContent(cs, m) @@ -228,12 +1266,13 @@ func (m *MerkleTree) RebuildTree() error { m.Root = root m.Leafs = leafs m.merkleRoot = root.Hash + m.buildLeafIndex() return nil } -//RebuildTreeWith replaces the content of the tree and does a complete rebuild; while the root of -//the tree will be replaced the MerkleTree completely survives this operation. Returns an error if the -//list of content cs contains no entries. +// RebuildTreeWith replaces the content of the tree and does a complete rebuild; while the root of +// the tree will be replaced the MerkleTree completely survives this operation. Returns an error if the +// list of content cs contains no entries. func (m *MerkleTree) RebuildTreeWith(cs []Content) error { root, leafs, err := buildWithContent(cs, m) if err != nil { @@ -242,73 +1281,110 @@ func (m *MerkleTree) RebuildTreeWith(cs []Content) error { m.Root = root m.Leafs = leafs m.merkleRoot = root.Hash + m.buildLeafIndex() return nil } -//VerifyTree verify tree validates the hashes at each level of the tree and returns true if the -//resulting hash at the root of the tree matches the resulting root hash; returns false otherwise. +// VerifyTree verify tree validates the hashes at each level of the tree and returns true if the +// resulting hash at the root of the tree matches the resulting root hash; returns false otherwise. func (m *MerkleTree) VerifyTree() (bool, error) { - calculatedMerkleRoot, err := m.Root.verifyNode() + // A zero value MerkleTree has no root to walk. Report it rather than faulting, + // so that a caller handed a tree from elsewhere can tell "never built" apart + // from "built and does not verify". + if m.Root == nil { + return false, fmt.Errorf("%w: tree has no root", ErrMalformedTree) + } + // One hasher and one buffer serve the whole walk. The buffer holds only the path + // being walked, so the depth of the tree is enough for it; append covers the case + // of a Content.CalculateHash that returns a digest wider than the tree's own. + h := m.hashStrategy() + scratch := make([]byte, 0, (bits.Len(uint(len(m.Leafs)))+2)*h.Size()) + + calculatedMerkleRoot, matched, err := m.Root.verifyNode(h, scratch) if err != nil { return false, err } - - if bytes.Compare(m.merkleRoot, calculatedMerkleRoot) == 0 { - return true, nil + if !matched { + return false, nil } - return false, nil + + return bytes.Equal(m.merkleRoot, calculatedMerkleRoot), nil } -//VerifyContent indicates whether a given content is in the tree and the hashes are valid for that content. -//Returns true if the expected Merkle Root is equivalent to the Merkle root calculated on the critical path -//for a given content. Returns true if valid and false otherwise. +// VerifyContent indicates whether a given content is in the tree and the hashes are valid for that content. +// Returns true if the expected Merkle Root is equivalent to the Merkle root calculated on the critical path +// for a given content. Returns true if valid and false otherwise. func (m *MerkleTree) VerifyContent(content Content) (bool, error) { - for _, l := range m.Leafs { - ok, err := l.C.Equals(content) + // Locating the content carries the same O(n) scan as GetMerklePath unless the + // tree was built with WithLeafIndex. + i, err := m.findLeaf(content) + if err != nil { + return false, err + } + if i < 0 { + return false, nil + } + + // One hasher and one buffer serve the whole climb, the way verifyNode threads + // them through its walk. Recomputing a level takes three digests - each child + // from what sits beneath it, then the parent from the pair - and creating a + // hasher and a digest for each made allocation, not hashing, the dominant cost + // here. The buffer never holds more than one level, so three digests is its + // whole working set; append covers a Content.CalculateHash wider than the + // tree's own digest. + h := m.hashStrategy() + scratch := make([]byte, 0, 4*h.Size()) + + for currentParent := m.Leafs[i].Parent; currentParent != nil; currentParent = currentParent.Parent { + if currentParent.Left == nil || currentParent.Right == nil { + return false, fmt.Errorf("%w: interior node is missing a child", ErrMalformedTree) + } + scratch = scratch[:0] + scratch, err = currentParent.Right.appendCalculatedHash(h, scratch) if err != nil { return false, err } + mid := len(scratch) - if ok { - currentParent := l.Parent - for currentParent != nil { - h := m.hashStrategy() - rightBytes, err := currentParent.Right.calculateNodeHash() - if err != nil { - return false, err - } - - leftBytes, err := currentParent.Left.calculateNodeHash() - if err != nil { - return false, err - } + scratch, err = currentParent.Left.appendCalculatedHash(h, scratch) + if err != nil { + return false, err + } + end := len(scratch) - if _, err := h.Write(append(leftBytes, rightBytes...)); err != nil { - return false, err - } - if bytes.Compare(h.Sum(nil), currentParent.Hash) != 0 { - return false, nil - } - currentParent = currentParent.Parent - } - return true, nil + // The children's digests are read into the hasher before Sum appends, so a + // growth of scratch here cannot invalidate them. + scratch, err = m.appendInteriorHash(h, scratch, scratch[mid:end], scratch[:mid]) + if err != nil { + return false, err + } + if !bytes.Equal(scratch[end:], currentParent.Hash) { + return false, nil } } - return false, nil + // The walk above recomputes and checks every hash on the path from the leaf up to + // and including the root node. Confirm that node really is the root this tree + // advertises, otherwise a tampered merkleRoot would go unnoticed. + if m.Root == nil { + return false, fmt.Errorf("%w: tree has no root", ErrMalformedTree) + } + + return bytes.Equal(m.Root.Hash, m.merkleRoot), nil } -//String returns a string representation of the node. +// String returns a string representation of the node. func (n *Node) String() string { return fmt.Sprintf("%t %t %v %s", n.leaf, n.dup, n.Hash, n.C) } -//String returns a string representation of the tree. Only leaf nodes are included -//in the output. +// String returns a string representation of the tree. Only leaf nodes are included +// in the output. func (m *MerkleTree) String() string { - s := "" + // One builder rather than repeated string concatenation, which re-copies the + // whole prefix on every leaf and turns printing a large tree quadratic. + var sb strings.Builder for _, l := range m.Leafs { - s += fmt.Sprint(l) - s += "\n" + fmt.Fprintln(&sb, l) } - return s + return sb.String() } diff --git a/vendor/github.com/cbergoon/merkletree/proof.go b/vendor/github.com/cbergoon/merkletree/proof.go new file mode 100644 index 00000000..28c0b522 --- /dev/null +++ b/vendor/github.com/cbergoon/merkletree/proof.go @@ -0,0 +1,230 @@ +// Copyright 2017 Cameron Bergoon +// Licensed under the MIT License, see LICENCE file for details. + +package merkletree + +import ( + "bytes" + "crypto/sha256" + "errors" + "fmt" + "hash" + "sync" +) + +// ErrMalformedProof is returned when a proof cannot be walked at all: a path and an +// index of different lengths, or a side marker that is neither 0 nor 1. It reports a +// proof that is the wrong shape rather than one that is the right shape and does not +// verify, which is an ordinary false. Test for it with errors.Is. +var ErrMalformedProof = errors.New("error: proof is malformed") + +// proofScratch is the working state one proof verification needs: a hasher and the +// buffer the replay slides its digests through. Pooled as a pair, so the option-less +// verification path allocates nothing of its own. +type proofScratch struct { + h hash.Hash + buf []byte +} + +// defaultProofConfig is the configuration an option-less VerifyProof runs under: the +// same value configFromOptions produces for an empty option list, allocated once +// rather than per verification. It is shared across goroutines and must never be +// mutated; the verification path only ever reads it. +// +// Being private and immutable is also what makes it the one place a scratch pool is +// safe; see the field's comment on MerkleTree. +var defaultProofConfig = &MerkleTree{ + hashStrategy: sha256.New, + scratchPool: &sync.Pool{New: func() any { + return &proofScratch{h: sha256.New(), buf: make([]byte, 0, 2*sha256.Size)} + }}, +} + +// VerifyProof reports whether content, the audit path and the side index returned by +// GetMerklePath together reproduce root. +// +// This is the operation a verifier performs when it holds a root obtained from somewhere +// it trusts, and a proof obtained from somewhere it does not. It needs no tree: the +// whole point is that the party checking the proof does not have to hold, rebuild, or +// have ever seen the tree the proof came from. MerkleTree.VerifyContent answers a +// different question, on a tree it already has. +// +// The options configure the construction the proof was produced under, and they must +// match the tree that produced it or verification fails. Only the options that affect +// hashing are meaningful here: WithHasher, WithSortedSiblings and WithRFC6962. The +// others describe how a tree is built rather than how its hashes are computed, and are +// accepted and ignored so that the same option list can be shared with the constructor. +// A tree's own settings are readable through MerkleTree.Sorted and MerkleTree.RFC6962. +// +// Returns false when the proof simply does not reproduce root, and an error when the +// proof is malformed or hashing the content fails. +// +// # What a verified proof does and does not establish +// +// It establishes that content sits in some tree whose root is root. It says nothing +// about whether root is the root a verifier should be trusting; that has to arrive +// through a channel the verifier already trusts, and this function cannot check it. +// +// Under the default construction it establishes less than it appears to. Leaf and +// interior hashes are computed the same way, so nothing distinguishes a leaf digest from +// an interior one, and a path can be presented that treats an interior node as though it +// were a leaf. Building the tree with WithRFC6962 closes this: the two kinds of hash +// carry distinct prefixes, so a forgery of that shape would need a genuine collision. +// The distinction matters far more here than it does for VerifyContent, which walks a +// real tree and so can only be shown nodes that are really in it. Prefer WithRFC6962 for +// proofs that arrive from somewhere untrusted. +func VerifyProof(content Content, path [][]byte, index []int64, root []byte, opts ...TreeOption) (bool, error) { + if content == nil { + return false, ErrNilContent + } + digest, err := content.CalculateHash() + if err != nil { + return false, err + } + + return VerifyProofWithDigest(digest, path, index, root, opts...) +} + +// VerifyProofWithDigest is VerifyProof for a verifier that holds the leaf digest rather +// than the content itself, and so has no reason to implement Content. +// +// The digest is the value Content.CalculateHash returns for the leaf, which is the leaf +// hash the tree records under the default construction. Under WithRFC6962 the tree +// records the prefixed hash of that digest instead, and this function applies the prefix +// itself, so the argument is the same value either way: what CalculateHash returned, not +// what the tree stored. +func VerifyProofWithDigest(digest []byte, path [][]byte, index []int64, root []byte, opts ...TreeOption) (bool, error) { + if len(path) != len(index) { + return false, fmt.Errorf("%w: path has %d entries and the index has %d", ErrMalformedProof, len(path), len(index)) + } + + // With no options there is nothing to resolve, so the shared default carries the + // configuration instead of a fresh one allocated per call. rootFromProof only + // reads it, which is what makes sharing it across goroutines safe. + cfg := defaultProofConfig + if len(opts) > 0 { + var err error + if cfg, err = configFromOptions(opts); err != nil { + return false, err + } + } + + return cfg.proofReproducesRoot(digest, path, index, root) +} + +// proofReproducesRoot replays a proof from the leaf upwards and reports whether it +// arrives at root. +// +// The hashing goes through the same appendLeafDigest and appendInteriorHash the +// constructor uses, so a proof is checked with the code that built the tree rather than +// with a second implementation of the same rules that could drift from it. What keeps +// that from being circular is the oracle module, which checks the construction itself +// against the RFC 6962 reference implementation. +// +// Merkle roots are public values, so the ordinary comparison at the end is +// appropriate; there is no secret here whose length or content a timing difference +// could leak. +func (m *MerkleTree) proofReproducesRoot(digest []byte, path [][]byte, index []int64, root []byte) (bool, error) { + // One hasher and one buffer serve the whole replay, recycled through the pool + // when the configuration carries one. Each level appends its hash and then + // slides it down over the previous one, so the buffer stays the width of a + // single digest however tall the tree is. Doing the comparison here rather than + // returning the computed root is what lets the buffer go back to the pool. + var ( + scratch *proofScratch + h hash.Hash + buf []byte + ) + if m.scratchPool != nil { + scratch = m.scratchPool.Get().(*proofScratch) + h, buf = scratch.h, scratch.buf[:0] + } else { + h = m.hashStrategy() + buf = make([]byte, 0, 2*h.Size()) + } + defer func() { + if scratch != nil { + scratch.buf = buf + m.scratchPool.Put(scratch) + } + }() + + var err error + if m.rfc6962 { + if buf, err = m.appendLeafDigest(h, buf, digest); err != nil { + return false, err + } + } else { + buf = append(buf, digest...) + } + + for k := range path { + // GetMerklePath records 1 when the sibling is the right hand node, which is + // to say when the node being carried up is the left hand one. + var left, right []byte + switch index[k] { + case 1: + left, right = buf, path[k] + case 0: + left, right = path[k], buf + default: + return false, fmt.Errorf("%w: index entry %d is %d, expected 0 or 1", ErrMalformedProof, k, index[k]) + } + + off := len(buf) + if buf, err = m.appendInteriorHash(h, buf, left, right); err != nil { + return false, err + } + buf = buf[:copy(buf, buf[off:])] + } + + return bytes.Equal(buf, root), nil +} + +// configFromOptions applies opts to a MerkleTree that carries configuration and nothing +// else, and reports the same conflicts the constructor does. +// +// The verification path and the construction path resolve options through this one +// function, so a tree and a proof checked against it can never disagree about what an +// option meant. +func configFromOptions(opts []TreeOption) (*MerkleTree, error) { + m := &MerkleTree{ + hashStrategy: sha256.New, + } + for _, opt := range opts { + if opt == nil { + continue + } + opt(m) + } + if m.hashStrategy == nil { + return nil, errors.New("error: hash strategy cannot be nil") + } + if m.rfc6962 && m.sort { + return nil, errors.New("error: WithRFC6962 and WithSortedSiblings cannot be combined; RFC 6962 specifies its own sibling ordering") + } + + return m, nil +} + +// VerifyProof reports whether the given proof reproduces this tree's root, using this +// tree's construction settings. +// +// It is the convenience form of the package level VerifyProof for a caller that does +// hold the tree, and saves restating the tree's options. It still verifies the proof it +// is given rather than looking anything up, so it will reject a proof for content this +// tree holds if that proof was generated somewhere else under different settings. +func (m *MerkleTree) VerifyProof(content Content, path [][]byte, index []int64) (bool, error) { + if content == nil { + return false, ErrNilContent + } + if len(path) != len(index) { + return false, fmt.Errorf("%w: path has %d entries and the index has %d", ErrMalformedProof, len(path), len(index)) + } + digest, err := content.CalculateHash() + if err != nil { + return false, err + } + + return m.proofReproducesRoot(digest, path, index, m.merkleRoot) +} diff --git a/vendor/github.com/cbergoon/merkletree/serialize.go b/vendor/github.com/cbergoon/merkletree/serialize.go new file mode 100644 index 00000000..d1cdba2a --- /dev/null +++ b/vendor/github.com/cbergoon/merkletree/serialize.go @@ -0,0 +1,913 @@ +// Copyright 2017 Cameron Bergoon +// Licensed under the MIT License, see LICENCE file for details. + +package merkletree + +import ( + "bytes" + "crypto/md5" + "crypto/sha1" + "crypto/sha256" + "crypto/sha512" + "encoding" + "encoding/binary" + "encoding/json" + "errors" + "fmt" + "hash" + "io" + "reflect" + "slices" + "sync" +) + +// A serialized tree records only what is needed to regenerate it: the ordered leaf +// content, the name of the hash strategy, the sibling sort flag, and the Merkle root +// the encoder observed. Every other part of the tree - the intermediate nodes, the +// parent and tree back-pointers, the leaf and padding markers - is derived from those +// inputs by buildWithContent, so storing it would be redundant. +// +// Persisting the derived structure is not merely wasteful, it is impossible with the +// standard codecs: Node points at its Tree and its Parent while the tree points back +// down at its Root and Leafs, and those cycles send encoding/gob and encoding/json +// into unbounded recursion rather than an error. Recording the seed sidesteps the +// cycle instead of working around it, and the recorded root turns decoding into an +// integrity check for free - a corrupted payload, a mismatched hash strategy, or a +// non-deterministic content encoder all surface as a root mismatch. +const ( + // serializationMagic prefixes every binary blob so a truncated or foreign + // payload is rejected before any length is trusted. + serializationMagic = "MTREE" + // serializationVersion is the wire format version written by this package. + // Version 2 added the RFC 6962 flag after the sort flag. + serializationVersion = 2 +) + +var ( + // ErrUnsupportedVersion is returned when decoding a tree written by a newer + // format version than this build understands. + ErrUnsupportedVersion = errors.New("merkletree: unsupported serialization version") + // ErrCorruptData is returned when a serialized tree is malformed, truncated, + // or not a merkletree payload at all. + ErrCorruptData = errors.New("merkletree: corrupt serialized tree") + // ErrRootMismatch is returned when the tree rebuilt from a payload does not + // hash to the Merkle root recorded in that payload. + ErrRootMismatch = errors.New("merkletree: rebuilt Merkle root does not match the encoded root") + // ErrNoHashStrategy is returned when a hash strategy cannot be named at encode + // time or resolved at decode time. + ErrNoHashStrategy = errors.New("merkletree: unknown hash strategy") + // ErrNoContentType is returned when a content type cannot be named at encode + // time or resolved at decode time. + ErrNoContentType = errors.New("merkletree: unregistered content type") +) + +// hashStrategyRegistry maps names to hash strategies and back. A function value +// cannot be serialized, so a tree records the name of its strategy and the decoder +// looks the name up here. The reverse index is keyed by the strategy's code pointer, +// which is stable for both plain functions and closure literals. +var hashStrategyRegistry = struct { + sync.RWMutex + byName map[string]func() hash.Hash + byFunc map[uintptr]string +}{ + byName: map[string]func() hash.Hash{}, + byFunc: map[uintptr]string{}, +} + +func init() { + RegisterHashStrategy("sha256", sha256.New) + RegisterHashStrategy("sha224", sha256.New224) + RegisterHashStrategy("sha512", sha512.New) + RegisterHashStrategy("sha384", sha512.New384) + RegisterHashStrategy("sha512_224", sha512.New512_224) + RegisterHashStrategy("sha512_256", sha512.New512_256) + RegisterHashStrategy("sha1", sha1.New) + RegisterHashStrategy("md5", md5.New) +} + +// RegisterHashStrategy makes a hash strategy serializable under the given name. The +// hash strategies in the standard library are registered automatically; anything else +// - keccak256, blake2b, or a strategy of your own - must be registered before a tree +// using it can be marshaled or unmarshaled. +// +// merkletree.RegisterHashStrategy("keccak256", sha3.NewLegacyKeccak256) +// t, err := merkletree.NewTreeWithHashStrategy(list, sha3.NewLegacyKeccak256) +// +// Registration is idempotent: registering the same name and strategy again is a no-op. +// Registering a different strategy under a name already in use panics, because doing +// so would silently change the meaning of previously written payloads. Registering a +// second name for an already registered strategy is allowed, but the first name wins +// when encoding. +// +// RegisterHashStrategy is safe for concurrent use, though the natural place to call +// it is package initialization. +func RegisterHashStrategy(name string, strategy func() hash.Hash) { + if name == "" { + panic("merkletree: cannot register a hash strategy under an empty name") + } + if strategy == nil { + panic("merkletree: cannot register a nil hash strategy") + } + + ptr := reflect.ValueOf(strategy).Pointer() + + hashStrategyRegistry.Lock() + defer hashStrategyRegistry.Unlock() + + if existing, ok := hashStrategyRegistry.byName[name]; ok { + if reflect.ValueOf(existing).Pointer() == ptr { + return + } + panic(fmt.Sprintf("merkletree: hash strategy %q is already registered to a different function", name)) + } + hashStrategyRegistry.byName[name] = strategy + if _, ok := hashStrategyRegistry.byFunc[ptr]; !ok { + hashStrategyRegistry.byFunc[ptr] = name + } +} + +// HashStrategyNames returns the sorted names of every registered hash strategy. +func HashStrategyNames() []string { + hashStrategyRegistry.RLock() + defer hashStrategyRegistry.RUnlock() + + names := make([]string, 0, len(hashStrategyRegistry.byName)) + for name := range hashStrategyRegistry.byName { + names = append(names, name) + } + slices.Sort(names) + return names +} + +// lookupHashStrategy resolves a registered name to its strategy. +func lookupHashStrategy(name string) (func() hash.Hash, bool) { + hashStrategyRegistry.RLock() + defer hashStrategyRegistry.RUnlock() + + strategy, ok := hashStrategyRegistry.byName[name] + return strategy, ok +} + +// lookupHashStrategyName resolves a strategy back to the name it was registered under. +func lookupHashStrategyName(strategy func() hash.Hash) (string, bool) { + if strategy == nil { + return "", false + } + + hashStrategyRegistry.RLock() + defer hashStrategyRegistry.RUnlock() + + name, ok := hashStrategyRegistry.byFunc[reflect.ValueOf(strategy).Pointer()] + return name, ok +} + +// contentRegistry maps names to content types and back, mirroring the role gob.Register +// plays for interface values. Content is defined by the application, so the tree cannot +// know how to rebuild a concrete type from bytes without being told. +var contentRegistry = struct { + sync.RWMutex + byName map[string]reflect.Type + byType map[reflect.Type]string +}{ + byName: map[string]reflect.Type{}, + byType: map[reflect.Type]string{}, +} + +// RegisterContent makes a Content type serializable by the registry-backed marshalers +// (MarshalBinary, MarshalJSON, and anything built on them such as encoding/gob), under +// a name derived from its package path and type name. +// +// The value passed is a template, not data: pass a zero value of the type exactly as it +// is stored in the tree. Registering TestContent{} round-trips leaves back to +// TestContent values; registering &TestContent{} round-trips them back to pointers. +// +// The type must marshal itself. c must implement encoding.BinaryMarshaler and a pointer +// to it must implement encoding.BinaryUnmarshaler, which is the usual pairing of a value +// receiver for MarshalBinary and a pointer receiver for UnmarshalBinary. RegisterContent +// panics if either is missing, or if the type is unnamed. +// +// Callers who would rather not use a package-level registry can skip registration +// entirely and use MarshalWith and UnmarshalWith instead. +func RegisterContent(c Content) { + rt := reflect.TypeOf(c) + if rt == nil { + panic("merkletree: cannot register nil content") + } + name := contentTypeName(rt) + if name == "" { + panic(fmt.Sprintf("merkletree: cannot register unnamed content type %s", rt)) + } + RegisterContentName(name, c) +} + +// RegisterContentName is RegisterContent with an explicit name. Use it to pin a stable +// name into the wire format so that renaming or moving the Go type does not invalidate +// payloads already written. +func RegisterContentName(name string, c Content) { + if name == "" { + panic("merkletree: cannot register content under an empty name") + } + rt := reflect.TypeOf(c) + if rt == nil { + panic("merkletree: cannot register nil content") + } + if _, ok := c.(encoding.BinaryMarshaler); !ok { + panic(fmt.Sprintf("merkletree: content type %s does not implement encoding.BinaryMarshaler", rt)) + } + // A pointer to the underlying type must be able to load itself from bytes. When a + // pointer type is registered that is rt itself; when a value type is registered it + // is the address of a fresh value, which is what the decoder will unmarshal into. + ptrType := rt + if ptrType.Kind() != reflect.Pointer { + ptrType = reflect.PointerTo(rt) + } + if !ptrType.Implements(reflect.TypeOf((*encoding.BinaryUnmarshaler)(nil)).Elem()) { + panic(fmt.Sprintf("merkletree: %s does not implement encoding.BinaryUnmarshaler", ptrType)) + } + + contentRegistry.Lock() + defer contentRegistry.Unlock() + + if existing, ok := contentRegistry.byName[name]; ok { + if existing == rt { + return + } + panic(fmt.Sprintf("merkletree: content name %q is already registered to %s", name, existing)) + } + contentRegistry.byName[name] = rt + if _, ok := contentRegistry.byType[rt]; !ok { + contentRegistry.byType[rt] = name + } +} + +// contentTypeName derives a stable, unambiguous name for a content type. Pointer depth +// is preserved so that T and *T round-trip back to what was registered. +func contentTypeName(rt reflect.Type) string { + prefix := "" + for rt.Kind() == reflect.Pointer { + prefix += "*" + rt = rt.Elem() + } + if rt.Name() == "" { + return "" + } + if rt.PkgPath() == "" { + return prefix + rt.Name() + } + return prefix + rt.PkgPath() + "." + rt.Name() +} + +// contentTypeCache remembers the last content type resolved against a registry. A +// tree overwhelmingly holds one concrete type, so with the cache the registry lock +// and lookup are paid once per encode or decode rather than once per leaf. The zero +// value is an empty cache; it is single-use state for one loop, never shared. +type contentTypeCache struct { + name string + rt reflect.Type +} + +// marshalRegisteredContent encodes a single content item along with the name needed to +// rebuild its concrete type. +func marshalRegisteredContent(c Content, cache *contentTypeCache) (string, []byte, error) { + rt := reflect.TypeOf(c) + if rt == nil { + return "", nil, fmt.Errorf("%w: tree contains nil content", ErrNoContentType) + } + + name := cache.name + if rt != cache.rt || cache.name == "" { + contentRegistry.RLock() + var ok bool + name, ok = contentRegistry.byType[rt] + contentRegistry.RUnlock() + + if !ok { + return "", nil, fmt.Errorf("%w: %s; call merkletree.RegisterContent to make it serializable, or use MarshalWith", ErrNoContentType, rt) + } + cache.name, cache.rt = name, rt + } + + bm, ok := c.(encoding.BinaryMarshaler) + if !ok { + return "", nil, fmt.Errorf("%w: %s does not implement encoding.BinaryMarshaler", ErrNoContentType, rt) + } + payload, err := bm.MarshalBinary() + if err != nil { + return "", nil, fmt.Errorf("merkletree: marshaling content of type %s: %w", rt, err) + } + return name, payload, nil +} + +// unmarshalRegisteredContent rebuilds a concrete content value from its recorded name +// and payload. The cache carries the last resolved name across the records of one +// decode; the binary decoder interns repeated names, so the comparison is usually a +// pointer match. +func unmarshalRegisteredContent(name string, payload []byte, cache *contentTypeCache) (Content, error) { + if name == "" { + return nil, fmt.Errorf("%w: payload carries no content type names, which means it was written by MarshalWith; decode it with UnmarshalWith", ErrNoContentType) + } + + rt := cache.rt + if name != cache.name || rt == nil { + contentRegistry.RLock() + var ok bool + rt, ok = contentRegistry.byName[name] + contentRegistry.RUnlock() + + if !ok { + return nil, fmt.Errorf("%w: %q; call merkletree.RegisterContent for that type before unmarshaling", ErrNoContentType, name) + } + cache.name, cache.rt = name, rt + } + + isPointer := rt.Kind() == reflect.Pointer + elem := rt + if isPointer { + elem = rt.Elem() + } + + pv := reflect.New(elem) + bu, ok := pv.Interface().(encoding.BinaryUnmarshaler) + if !ok { + return nil, fmt.Errorf("%w: %s does not implement encoding.BinaryUnmarshaler", ErrNoContentType, pv.Type()) + } + // UnmarshalBinary implementations are permitted to retain the slice they are + // handed, so payload has to be one no one else holds. Both decoders already + // produce exactly that: the binary reader carves each record its own capped + // region of a decode-wide arena, and encoding/json decodes base64 into a fresh + // slice. The treeData carrying them is transient and is dropped once the tree is + // built, so there is nothing left to alias. Copying again here duplicated every + // content payload on the way in, which on a large tree is one wasted allocation + // per leaf. The UnmarshalWith path has always handed record.Payload straight to + // the caller's decoder on the same reasoning. + if err := bu.UnmarshalBinary(payload); err != nil { + return nil, fmt.Errorf("merkletree: unmarshaling content of type %q: %w", name, err) + } + + var v any = pv.Elem().Interface() + if isPointer { + v = pv.Interface() + } + c, ok := v.(Content) + if !ok { + return nil, fmt.Errorf("%w: %q does not implement merkletree.Content", ErrNoContentType, name) + } + return c, nil +} + +// ContentMarshalFunc encodes a single content item to bytes. It must be deterministic: +// two content items that are Equals must encode identically, or a decoded tree will not +// reproduce the root it was encoded with. +type ContentMarshalFunc func(Content) ([]byte, error) + +// ContentUnmarshalFunc decodes a single content item produced by a ContentMarshalFunc. +type ContentUnmarshalFunc func([]byte) (Content, error) + +// MarshalOption adjusts how a tree is encoded. +type MarshalOption func(*marshalConfig) + +type marshalConfig struct { + hashStrategyName string +} + +// WithHashStrategyName records the given name for the tree's hash strategy instead of +// looking the strategy up in the package registry. Pair it with WithHashStrategy at +// decode time to serialize trees without touching global state at all. +func WithHashStrategyName(name string) MarshalOption { + return func(c *marshalConfig) { c.hashStrategyName = name } +} + +// UnmarshalOption adjusts how a tree is decoded. +type UnmarshalOption func(*unmarshalConfig) + +type unmarshalConfig struct { + hashStrategy func() hash.Hash +} + +// WithHashStrategy rebuilds the tree with the given hash strategy instead of resolving +// the name recorded in the payload through the package registry. The recorded name is +// still preserved, so re-encoding the decoded tree reproduces the original name. +func WithHashStrategy(strategy func() hash.Hash) UnmarshalOption { + return func(c *unmarshalConfig) { c.hashStrategy = strategy } +} + +// treeData is the on-the-wire form of a tree: the seed it can be rebuilt from. +type treeData struct { + Version int `json:"version"` + HashStrategy string `json:"hashStrategy"` + Sort bool `json:"sort"` + RFC6962 bool `json:"rfc6962,omitempty"` + MerkleRoot []byte `json:"merkleRoot"` + Contents []contentRecord `json:"contents"` +} + +// contentRecord is one leaf's content. Type is empty for payloads written by +// MarshalWith, where the caller's own decoder supplies the concrete type. +type contentRecord struct { + Type string `json:"type,omitempty"` + Payload []byte `json:"payload"` +} + +// snapshot captures the tree as the seed it can be rebuilt from. enc may be nil, in +// which case content is encoded through the package registry. +// +// The marshalers all take a value receiver, following the example of time.Time. A +// pointer receiver would leave a tree that is held by value rather than by pointer - +// a struct field, say - falling back to reflection inside encoding/json and +// encoding/gob, which is precisely the unbounded recursion these methods exist to +// prevent. Marshaling only reads, so the copy costs nothing that matters. +func (m MerkleTree) snapshot(enc ContentMarshalFunc, opts ...MarshalOption) (*treeData, error) { + if m.Root == nil || len(m.Leafs) == 0 { + return nil, errors.New("merkletree: cannot marshal an empty tree") + } + + var cfg marshalConfig + for _, opt := range opts { + opt(&cfg) + } + + name := cfg.hashStrategyName + if name == "" { + name = m.hashStrategyName + } + if name == "" { + var ok bool + if name, ok = lookupHashStrategyName(m.hashStrategy); !ok { + return nil, fmt.Errorf("%w: the tree's hash strategy has no registered name; call merkletree.RegisterHashStrategy for it, or pass merkletree.WithHashStrategyName", ErrNoHashStrategy) + } + } + + td := &treeData{ + Version: serializationVersion, + HashStrategy: name, + Sort: m.sort, + RFC6962: m.rfc6962, + MerkleRoot: bytes.Clone(m.merkleRoot), + // Sized to the leaf count up front; at most one entry, the padding copy, goes + // unused, where growing by append reallocates log n times on a large tree. + Contents: make([]contentRecord, 0, len(m.Leafs)), + } + + var cache contentTypeCache + for _, l := range m.Leafs { + // Skip the padding copy buildWithContent appends for an odd content count. + // It is regenerated on rebuild; encoding it would promote it to real content + // and the decoded tree would report one more item than was put in. + if l.dup { + continue + } + if enc != nil { + payload, err := enc(l.C) + if err != nil { + return nil, fmt.Errorf("merkletree: marshaling content: %w", err) + } + td.Contents = append(td.Contents, contentRecord{Payload: payload}) + continue + } + typeName, payload, err := marshalRegisteredContent(l.C, &cache) + if err != nil { + return nil, err + } + td.Contents = append(td.Contents, contentRecord{Type: typeName, Payload: payload}) + } + + if len(td.Contents) == 0 { + return nil, errors.New("merkletree: cannot marshal a tree with no content") + } + return td, nil +} + +// tree rebuilds a tree from its seed and verifies that it hashes back to the recorded +// root. dec may be nil, in which case content is decoded through the package registry. +func (td *treeData) tree(dec ContentUnmarshalFunc, opts ...UnmarshalOption) (*MerkleTree, error) { + if td.Version != serializationVersion { + return nil, fmt.Errorf("%w: got %d, this build writes and reads %d", ErrUnsupportedVersion, td.Version, serializationVersion) + } + + var cfg unmarshalConfig + for _, opt := range opts { + opt(&cfg) + } + + strategy := cfg.hashStrategy + if strategy == nil { + var ok bool + if strategy, ok = lookupHashStrategy(td.HashStrategy); !ok { + return nil, fmt.Errorf("%w: %q; call merkletree.RegisterHashStrategy for it before unmarshaling, or pass merkletree.WithHashStrategy", ErrNoHashStrategy, td.HashStrategy) + } + } + + if len(td.Contents) == 0 { + return nil, errors.New("merkletree: serialized tree contains no content") + } + + cs := make([]Content, 0, len(td.Contents)) + var cache contentTypeCache + for i, record := range td.Contents { + var ( + c Content + err error + ) + if dec != nil { + if c, err = dec(record.Payload); err != nil { + return nil, fmt.Errorf("merkletree: unmarshaling content at index %d: %w", i, err) + } + } else if c, err = unmarshalRegisteredContent(record.Type, record.Payload, &cache); err != nil { + return nil, err + } + if c == nil { + return nil, fmt.Errorf("merkletree: content at index %d decoded to nil", i) + } + cs = append(cs, c) + } + + if td.Sort && td.RFC6962 { + return nil, fmt.Errorf("%w: both the sort and RFC 6962 flags are set, which no tree can be built with", ErrCorruptData) + } + + t := &MerkleTree{ + hashStrategy: strategy, + hashStrategyName: td.HashStrategy, + sort: td.Sort, + rfc6962: td.RFC6962, + } + root, leafs, err := buildWithContent(cs, t) + if err != nil { + return nil, err + } + // The recorded root makes decoding self-checking. Content that decoded to + // something other than what was encoded, a hash strategy that does not match the + // one the payload was written with, and bit rot anywhere in the payload all land + // here rather than producing a tree that looks fine and verifies against nothing. + if !bytes.Equal(root.Hash, td.MerkleRoot) { + return nil, fmt.Errorf("%w: rebuilt %x, encoded %x", ErrRootMismatch, root.Hash, td.MerkleRoot) + } + + t.Root = root + t.Leafs = leafs + t.merkleRoot = root.Hash + return t, nil +} + +// MarshalWith encodes the tree, using enc to encode each content item. It requires no +// package-level registration, which makes it the right choice for libraries and for +// content types that already have an encoding of their own. +// +// Only the leaf content, the hash strategy name, the sibling sort flag, and the Merkle +// root are written; the tree structure is rebuilt on decode. Pair this with +// UnmarshalWith and a decoder that is the exact inverse of enc. +func (m MerkleTree) MarshalWith(enc ContentMarshalFunc, opts ...MarshalOption) ([]byte, error) { + if enc == nil { + return nil, errors.New("merkletree: MarshalWith requires a content marshal function") + } + td, err := m.snapshot(enc, opts...) + if err != nil { + return nil, err + } + return td.marshalBinary(), nil +} + +// UnmarshalWith decodes a tree written by MarshalWith, using dec to decode each content +// item. The rebuilt tree is verified against the Merkle root recorded in data, so a +// successful return means the tree hashes exactly as it did when it was encoded. +func UnmarshalWith(data []byte, dec ContentUnmarshalFunc, opts ...UnmarshalOption) (*MerkleTree, error) { + if dec == nil { + return nil, errors.New("merkletree: UnmarshalWith requires a content unmarshal function") + } + td, err := unmarshalTreeData(data) + if err != nil { + return nil, err + } + return td.tree(dec, opts...) +} + +// MarshalBinary encodes the tree using the package content registry, implementing +// encoding.BinaryMarshaler. This is what makes a tree work with encoding/gob and any +// other codec that honors BinaryMarshaler: +// +// merkletree.RegisterContent(MyContent{}) +// err := gob.NewEncoder(&buf).Encode(tree) +// +// Every content type in the tree must have been registered with RegisterContent, and +// the tree's hash strategy must have been registered with RegisterHashStrategy. Use +// MarshalWith to avoid the registries. +func (m MerkleTree) MarshalBinary() ([]byte, error) { + td, err := m.snapshot(nil) + if err != nil { + return nil, err + } + return td.marshalBinary(), nil +} + +// UnmarshalBinary rebuilds the tree from a payload written by MarshalBinary, +// implementing encoding.BinaryUnmarshaler. The receiver is left untouched if decoding +// fails for any reason, including the rebuilt root failing to match the recorded one. +func (m *MerkleTree) UnmarshalBinary(data []byte) error { + td, err := unmarshalTreeData(data) + if err != nil { + return err + } + t, err := td.tree(nil) + if err != nil { + return err + } + *m = *t + m.adoptNodes() + return nil +} + +// MarshalJSON encodes the tree using the package content registry, implementing +// json.Marshaler. Byte fields are base64 encoded by encoding/json as usual. +// +// Without this method json.Marshal would recurse forever on the tree's parent and tree +// back-pointers. Content is encoded through encoding.BinaryMarshaler, the same as +// MarshalBinary, so the JSON form carries opaque content payloads rather than the +// content's own JSON shape. Use MarshalWith when you need control over that. +func (m MerkleTree) MarshalJSON() ([]byte, error) { + td, err := m.snapshot(nil) + if err != nil { + return nil, err + } + return json.Marshal(td) +} + +// UnmarshalJSON rebuilds the tree from a payload written by MarshalJSON, implementing +// json.Unmarshaler. As with UnmarshalBinary the receiver is left untouched on failure. +func (m *MerkleTree) UnmarshalJSON(data []byte) error { + var td treeData + if err := json.Unmarshal(data, &td); err != nil { + return fmt.Errorf("%w: %w", ErrCorruptData, err) + } + t, err := td.tree(nil) + if err != nil { + return err + } + *m = *t + m.adoptNodes() + return nil +} + +// adoptNodes re-points every node's Tree back-pointer at m. The unmarshalers build into +// a scratch tree so that a failure leaves the receiver alone, which leaves the nodes +// referring to that scratch tree once its fields are copied over. +// +// Walking up from the leaves reaches every node, and stopping as soon as an ancestor +// has already been adopted keeps the whole pass linear. +func (m *MerkleTree) adoptNodes() { + for _, l := range m.Leafs { + for n := l; n != nil && n.Tree != m; n = n.Parent { + n.Tree = m + } + } +} + +// marshalBinary writes the seed in a compact, deterministic, self-describing format: +// +// magic "MTREE" +// version uvarint +// strategy uvarint length + bytes +// sort one byte, 0 or 1 +// rfc6962 one byte, 0 or 1 +// merkleRoot uvarint length + bytes +// count uvarint +// type uvarint length + bytes (repeated count times) +// payload uvarint length + bytes +// +// Encoding the same tree twice always produces identical bytes, so payloads can be +// compared or content-addressed directly. +func (td *treeData) marshalBinary() []byte { + // The wire size is exactly computable before writing a byte, so the buffer is + // grown to it once. Without this the buffer doubles its way up, which on a large + // tree re-copies the payload log n times and leaves the final allocation up to + // twice the size it needs to be. + size := len(serializationMagic) + + uvarintLen(uint64(td.Version)) + + uvarintLen(uint64(len(td.HashStrategy))) + len(td.HashStrategy) + + 2 + + uvarintLen(uint64(len(td.MerkleRoot))) + len(td.MerkleRoot) + + uvarintLen(uint64(len(td.Contents))) + for _, record := range td.Contents { + size += uvarintLen(uint64(len(record.Type))) + len(record.Type) + + uvarintLen(uint64(len(record.Payload))) + len(record.Payload) + } + + var buf bytes.Buffer + buf.Grow(size) + buf.WriteString(serializationMagic) + writeUvarint(&buf, uint64(td.Version)) + writeBytes(&buf, []byte(td.HashStrategy)) + if td.Sort { + buf.WriteByte(1) + } else { + buf.WriteByte(0) + } + if td.RFC6962 { + buf.WriteByte(1) + } else { + buf.WriteByte(0) + } + writeBytes(&buf, td.MerkleRoot) + writeUvarint(&buf, uint64(len(td.Contents))) + for _, record := range td.Contents { + writeBytes(&buf, []byte(record.Type)) + writeBytes(&buf, record.Payload) + } + return buf.Bytes() +} + +// unmarshalTreeData parses the format written by marshalBinary. Every length is checked +// against the bytes actually remaining before it is used to allocate, so a corrupt or +// hostile payload fails rather than exhausting memory. +func unmarshalTreeData(data []byte) (*treeData, error) { + if len(data) < len(serializationMagic) || string(data[:len(serializationMagic)]) != serializationMagic { + return nil, fmt.Errorf("%w: missing %q header", ErrCorruptData, serializationMagic) + } + r := &binaryReader{data: data[len(serializationMagic):]} + + version, err := r.uvarint() + if err != nil { + return nil, fmt.Errorf("%w: reading version: %w", ErrCorruptData, err) + } + if version != serializationVersion { + return nil, fmt.Errorf("%w: got %d, this build writes and reads %d", ErrUnsupportedVersion, version, serializationVersion) + } + + td := &treeData{Version: int(version)} + + strategy, err := r.view() + if err != nil { + return nil, fmt.Errorf("%w: reading hash strategy: %w", ErrCorruptData, err) + } + td.HashStrategy = string(strategy) + + sortFlag, err := r.readByte() + if err != nil { + return nil, fmt.Errorf("%w: reading sort flag: %w", ErrCorruptData, err) + } + if sortFlag > 1 { + return nil, fmt.Errorf("%w: sort flag is %d, expected 0 or 1", ErrCorruptData, sortFlag) + } + td.Sort = sortFlag == 1 + + rfcFlag, err := r.readByte() + if err != nil { + return nil, fmt.Errorf("%w: reading RFC 6962 flag: %w", ErrCorruptData, err) + } + if rfcFlag > 1 { + return nil, fmt.Errorf("%w: RFC 6962 flag is %d, expected 0 or 1", ErrCorruptData, rfcFlag) + } + td.RFC6962 = rfcFlag == 1 + + // The root is compared once while the tree is rebuilt and then dropped with the + // treeData carrying it, so viewing it in place is safe; nothing retains it. + if td.MerkleRoot, err = r.view(); err != nil { + return nil, fmt.Errorf("%w: reading Merkle root: %w", ErrCorruptData, err) + } + + count, err := r.uvarint() + if err != nil { + return nil, fmt.Errorf("%w: reading content count: %w", ErrCorruptData, err) + } + // Each record costs at least two length bytes, so a count larger than the bytes + // remaining cannot be honest. This bounds the allocations below. + if count > uint64(r.remaining()) { + return nil, fmt.Errorf("%w: content count %d exceeds the %d bytes remaining", ErrCorruptData, count, r.remaining()) + } + + td.Contents = make([]contentRecord, 0, count) + // Every record carries its type name, and in practice a tree holds one content + // type, so the same name is repeated for every leaf. Interning them means one + // string for the whole payload rather than one per record. Indexing a map with + // string(byteSlice) does not allocate, so a repeat name costs only the lookup. + interned := make(map[string]string, 1) + // One arena serves every payload. UnmarshalBinary implementations are permitted + // to retain the slice they are handed, so a payload must not alias the caller's + // data - but copying each into its own allocation cost one per record. Carving + // copies out of a single arena keeps the no-aliasing guarantee at one allocation + // for the whole decode. Each carve is capped at its own length, so appending to + // one payload cannot reach the record stored after it, and the contents all live + // and die with the tree they decode into, so the shared backing pins nothing + // that was not already pinned. The remaining bytes bound the payload bytes, so + // the arena never grows and every carve stays inside the one allocation. + arena := make([]byte, 0, r.remaining()) + for i := uint64(0); i < count; i++ { + typeName, err := r.view() + if err != nil { + return nil, fmt.Errorf("%w: reading content type at index %d: %w", ErrCorruptData, i, err) + } + name, ok := interned[string(typeName)] + if !ok { + name = string(typeName) + interned[name] = name + } + view, err := r.view() + if err != nil { + return nil, fmt.Errorf("%w: reading content payload at index %d: %w", ErrCorruptData, i, err) + } + off := len(arena) + arena = append(arena, view...) + td.Contents = append(td.Contents, contentRecord{Type: name, Payload: arena[off:len(arena):len(arena)]}) + } + + if r.remaining() != 0 { + return nil, fmt.Errorf("%w: %d trailing bytes after the last content record", ErrCorruptData, r.remaining()) + } + return td, nil +} + +// binaryReader reads the length-prefixed pieces of the wire format. It is a cursor +// over the payload slice rather than a bytes.Reader: every read is a bounds check and +// a reslice with no interface calls, and a field that is only inspected rather than +// kept - a type name about to be interned, the recorded root - can be viewed in place +// instead of copied out. +type binaryReader struct { + data []byte + off int +} + +// remaining reports how many bytes are left to read. +func (br *binaryReader) remaining() int { + return len(br.data) - br.off +} + +func (br *binaryReader) readByte() (byte, error) { + if br.off >= len(br.data) { + return 0, io.EOF + } + b := br.data[br.off] + br.off++ + return b, nil +} + +// view returns the next length-prefixed field as a subslice of the payload, without +// copying. The result aliases the caller's input; anything that outlives the decode +// must copy what it keeps. +func (br *binaryReader) view() ([]byte, error) { + n, err := br.uvarint() + if err != nil { + return nil, err + } + if n > uint64(br.remaining()) { + return nil, fmt.Errorf("length %d exceeds the %d bytes remaining", n, br.remaining()) + } + b := br.data[br.off : br.off+int(n) : br.off+int(n)] + br.off += int(n) + return b, nil +} + +// uvarint reads one uvarint, rejecting any encoding that is not the shortest one for +// the value it carries. +// +// binary.ReadUvarint accepts padded forms - 0x80 0x00 decodes to zero just as 0x00 +// does - which would make the wire format malleable: a payload could be rewritten +// into different bytes that decode to exactly the same tree. Since marshalBinary is +// documented as deterministic so that payloads can be compared or content addressed, +// the decoder has to hold up the other half of that guarantee. +func (br *binaryReader) uvarint() (uint64, error) { + var x uint64 + var s uint + for i := 0; ; i++ { + b, err := br.readByte() + if err != nil { + if i > 0 && errors.Is(err, io.EOF) { + err = io.ErrUnexpectedEOF + } + return 0, err + } + if i == binary.MaxVarintLen64-1 && b > 1 { + return 0, errors.New("uvarint overflows a 64 bit value") + } + if b < 0x80 { + // The final byte holds the most significant group. A zero group means + // the same value had a shorter encoding, so this one is not canonical. + if i > 0 && b == 0 { + return 0, errors.New("uvarint is not minimally encoded") + } + + return x | uint64(b)<= 0x80 { + v >>= 7 + n++ + } + return n +} + +func writeUvarint(buf *bytes.Buffer, v uint64) { + var scratch [binary.MaxVarintLen64]byte + buf.Write(scratch[:binary.PutUvarint(scratch[:], v)]) +} + +func writeBytes(buf *bytes.Buffer, b []byte) { + writeUvarint(buf, uint64(len(b))) + buf.Write(b) +} diff --git a/vendor/modules.txt b/vendor/modules.txt index a049ce69..470ca25e 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -181,8 +181,8 @@ github.com/aws/smithy-go/waiter # github.com/beorn7/perks v1.0.1 ## explicit; go 1.11 github.com/beorn7/perks/quantile -# github.com/cbergoon/merkletree v0.2.0 -## explicit; go 1.12 +# github.com/cbergoon/merkletree v0.5.0 +## explicit; go 1.21 github.com/cbergoon/merkletree # github.com/cenkalti/backoff/v4 v4.3.0 ## explicit; go 1.18