blob: 2f781a489cbc75b1ecc2a89fbc8e84df2989f981 [file]
// Copyright 2025 The BoringSSL Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#include "merkle_tree.h"
#include <cassert>
#include <cstdint>
#include <limits>
#include <gmock/gmock.h>
#include <gtest/gtest.h>
BSSL_NAMESPACE_BEGIN
namespace {
// Returns a subtree consistency proof for `subtree` in the first `n` elements
// of `tree`. This is currently implemented recursively, matching the
// specification. If we ever need to expose it, we can implement it more
// efficiently.
std::vector<uint8_t> SubtreeConsistencyProof(const MerkleTree &mt,
Subtree subtree,
const Subtree &tree,
bool known_hash = true) {
BSSL_CHECK(subtree.IsValid());
BSSL_CHECK(tree.IsValid());
BSSL_CHECK(tree.Contains(subtree));
if (subtree == tree) {
if (known_hash) {
return {};
}
TreeHash h = mt.SubtreeHash(subtree);
return std::vector(h.begin(), h.end());
}
uint64_t k = tree.Split();
Subtree subproof_tree, mth_tree;
if (subtree.end <= k) {
subproof_tree = tree.Left();
mth_tree = tree.Right();
} else if (subtree.start >= k) {
mth_tree = tree.Left();
subproof_tree = tree.Right();
} else {
subtree.start = k;
mth_tree = tree.Left();
subproof_tree = tree.Right();
known_hash = false;
}
std::vector<uint8_t> subproof =
SubtreeConsistencyProof(mt, subtree, subproof_tree, known_hash);
TreeHash mth = mt.SubtreeHash(mth_tree);
subproof.insert(subproof.end(), mth.begin(), mth.end());
return subproof;
}
std::vector<std::vector<uint8_t>> MakeTestEntries(std::string_view label,
size_t n) {
std::vector<std::vector<uint8_t>> entries;
entries.reserve(n);
for (size_t i = 0; i < n; i++) {
std::vector<uint8_t> entry(label.begin(), label.end());
for (size_t j = 0; j < 8; j++) {
entry.push_back(static_cast<uint8_t>(i >> (j * 8)));
}
entries.push_back(std::move(entry));
}
return entries;
}
std::vector<uint8_t> ConcatProof(const std::vector<TreeHash> &proof) {
std::vector<uint8_t> out;
for (const auto &p : proof) {
out.insert(out.end(), p.begin(), p.end());
}
return out;
}
TEST(MerkleTreeTest, SubtreeIsValid) {
// An empty subtree is invalid.
EXPECT_FALSE((Subtree{0, 0}.IsValid()));
// But if the end is before start, it's invalid.
EXPECT_FALSE((Subtree{1, 0}.IsValid()));
// A subtree of the maximum expressible size is valid.
EXPECT_TRUE((Subtree{0, std::numeric_limits<uint64_t>::max()}.IsValid()));
// Subtrees don't have to start at 0.
EXPECT_TRUE((Subtree{4, 8}.IsValid()));
// But if they don't start at 0, there's a limit to how big they can be.
EXPECT_FALSE((Subtree{4, 9}.IsValid()));
// Subtrees can have a ragged right edge.
EXPECT_TRUE((Subtree{4, 6}.IsValid()));
EXPECT_TRUE((Subtree{0, 6}.IsValid()));
}
TEST(MerkleTreeTest, SubtreeSplit) {
// Empty subtree.
EXPECT_EQ((Subtree{24601, 24601}).Split(), 24601ul);
// Single-item subtree.
EXPECT_EQ((Subtree{1336, 1337}).Split(), 1337ul);
// Two items in subtree.
EXPECT_EQ((Subtree{42, 44}).Split(), 43ul);
// Subtree size is 1 less than a power of 2.
EXPECT_EQ((Subtree{0, 31}).Split(), 16ul);
// Subtree size is a power of 2.
EXPECT_EQ((Subtree{64, 128}).Split(), 96ul);
/// Subtree size is 1 more than a power of 2.
EXPECT_EQ((Subtree{0, 257}).Split(), 256ul);
static const uint64_t u64_max = std::numeric_limits<uint64_t>::max();
// Maximum size tree.
EXPECT_EQ((Subtree{0, u64_max}).Split(), 1ull << 63);
// Small tree, with end at maximum value.
EXPECT_EQ((Subtree{u64_max - 3, u64_max}).Split(), u64_max - 1);
}
TEST(MerkleTreeTest, VerifySubtreeInclusionProof) {
auto entries = MakeTestEntries("label", 847);
MerkleTreeInMemory tree(entries);
uint64_t index = 0;
auto node_hash = tree.SubtreeHash({index, index + 1});
Subtree subtree{0, 16};
auto proof = tree.SubtreeInclusionProof(index, subtree);
auto root_hash = EvaluateMerkleSubtreeConsistencyProof(
subtree.end, {index, index + 1}, proof, node_hash);
ASSERT_TRUE(root_hash.has_value());
EXPECT_EQ(root_hash, tree.SubtreeHash(subtree));
// Check again with EvaluateMerkleSubtreeInclusionProof
root_hash =
EvaluateMerkleSubtreeInclusionProof(proof, index, node_hash, subtree);
ASSERT_TRUE(root_hash.has_value());
EXPECT_EQ(root_hash, tree.SubtreeHash(subtree));
// Build and verify a proof from a subtree with start != 0
index = 845;
node_hash = tree.SubtreeHash({index, index + 1});
subtree = {840, 847};
proof = tree.SubtreeInclusionProof(index, subtree);
root_hash = EvaluateMerkleSubtreeConsistencyProof(
subtree.Size(), {index - subtree.start, index - subtree.start + 1}, proof,
node_hash);
ASSERT_TRUE(root_hash.has_value());
EXPECT_EQ(root_hash, tree.SubtreeHash(subtree));
// Check again with EvaluateMerkleSubtreeInclusionProof
root_hash =
EvaluateMerkleSubtreeInclusionProof(proof, index, node_hash, subtree);
ASSERT_TRUE(root_hash.has_value());
EXPECT_EQ(root_hash, tree.SubtreeHash(subtree));
}
TEST(MerkleTreeTest, SubtreeInclusionProofInvalidArgs) {
auto entries = MakeTestEntries("label", 847);
MerkleTreeInMemory tree(entries);
uint64_t index = 845;
auto node_hash = tree.SubtreeHash({index, index + 1});
Subtree subtree = {840, 847};
auto proof = tree.SubtreeInclusionProof(index, subtree);
// If the wrong node hash is passed in, the function will still compute a
// root hash, but it won't match the expected value.
auto wrong_node_hash = tree.SubtreeHash({index + 1, index + 2});
auto root_hash = EvaluateMerkleSubtreeInclusionProof(
proof, index, wrong_node_hash, subtree);
ASSERT_TRUE(root_hash.has_value());
EXPECT_NE(root_hash, tree.SubtreeHash(subtree));
// If the subtree isn't valid, the function will fail.
ASSERT_FALSE(
EvaluateMerkleSubtreeInclusionProof(proof, index, node_hash, {840, 849}));
// If the index isn't contained within the subtree, the function will fail.
ASSERT_FALSE(
EvaluateMerkleSubtreeInclusionProof(proof, 848, node_hash, {840, 847}));
}
// Test that the computed consistency proofs match the examples given in RFC
// 9162 section 2.1.5.
TEST(MerkleTreeTest, SubtreeConsistencyProofRFC9162) {
auto entries = MakeTestEntries("label", 7);
MerkleTreeInMemory tree(entries);
// The example from section 2.1.5 has a final tree with 7 leaves.
Subtree final_tree{0, 7};
// The examples refer to letters representing the hashes of various subtrees
// within that tree. a and e aren't used in any of the examples.
auto b = tree.SubtreeHash({1, 2});
auto c = tree.SubtreeHash({2, 3});
auto d = tree.SubtreeHash({3, 4});
auto f = tree.SubtreeHash({5, 6});
auto g = tree.SubtreeHash({0, 2});
auto h = tree.SubtreeHash({2, 4});
auto i = tree.SubtreeHash({4, 6});
auto j = tree.SubtreeHash({6, 7});
auto k = tree.SubtreeHash({0, 4});
auto l = tree.SubtreeHash({4, 7});
// Inclusion proofs:
// Section 2.1.5: "The inclusion proof for `d0` is `[b, h, l]`."
auto d0_proof = tree.SubtreeInclusionProof(0, final_tree);
EXPECT_EQ(d0_proof, ConcatProof({b, h, l}));
// Section 2.1.5: "The inclusion proof for `d3` is `[c, g, l]`."
auto d3_proof = tree.SubtreeInclusionProof(3, final_tree);
EXPECT_EQ(d3_proof, ConcatProof({c, g, l}));
// Section 2.1.5: "The inclusion proof for `d4` is `[f, j, k]`."
auto d4_proof = tree.SubtreeInclusionProof(4, final_tree);
EXPECT_EQ(d4_proof, ConcatProof({f, j, k}));
// Section 2.1.5: "The inclusion proof for `d6` is `[i, k]`."
auto d6_proof = tree.SubtreeInclusionProof(6, final_tree);
EXPECT_EQ(d6_proof, ConcatProof({i, k}));
// Consistency proofs:
// The consistency proofs refer to the lettered hashes above, as well as some
// hashes of the tree as it was incrementally built.
Subtree hash0_subtree = {0, 3};
Subtree hash1_subtree = {0, 4};
auto hash1 = tree.SubtreeHash(hash1_subtree);
ASSERT_EQ(hash1, k);
Subtree hash2_subtree = {0, 6};
// "The consistency proof between hash0 and hash is [c, d, g, l]."
auto hash0_proof = SubtreeConsistencyProof(tree, hash0_subtree, final_tree);
EXPECT_EQ(hash0_proof, ConcatProof({c, d, g, l}));
// "The consistency proof beween hash1 and hash is [l]."
auto hash1_proof = SubtreeConsistencyProof(tree, hash1_subtree, final_tree);
EXPECT_EQ(hash1_proof, ConcatProof({l}));
// "The consistency proof between hash2 and hash is [i, j, k]."
auto hash2_proof = SubtreeConsistencyProof(tree, hash2_subtree, final_tree);
EXPECT_EQ(hash2_proof, ConcatProof({i, j, k}));
}
TEST(MerkleTreeTest, ValidProofsTest) {
uint64_t n = 4, start = 0, end = 3;
Subtree full_tree{0, n};
auto entries = MakeTestEntries("label", n);
MerkleTreeInMemory tree(entries);
auto tree_hash = tree.SubtreeHash(full_tree);
Subtree subtree{start, end};
ASSERT_TRUE(subtree.IsValid());
auto subtree_hash = tree.SubtreeHash(subtree);
auto proof = SubtreeConsistencyProof(tree, subtree, full_tree);
auto computed_hash =
EvaluateMerkleSubtreeConsistencyProof(n, subtree, proof, subtree_hash);
EXPECT_EQ(computed_hash, tree_hash);
}
TEST(MerkleTreeTest, ValidProofs) {
// As of the time of writing this test, a run was performed with limit=257 and
// the test passed. This value is set to 129 to balance how much of the space
// to explore with test execution time. In particular, in an unoptimized
// build, limit=257 takes about 4 seconds.
for (bool incremental : {false, true}) {
SCOPED_TRACE(incremental);
uint64_t limit = 129;
auto entries = MakeTestEntries("label", limit);
MerkleTreeInMemory tree;
if (incremental) {
for (const auto &entry : entries) {
tree.Append(entry);
}
} else {
tree = MerkleTreeInMemory(entries);
}
// Exhaustively test subtree consistency proofs.
for (uint64_t n = 1; n < limit; n++) {
Subtree full_tree{0, n};
auto tree_hash = tree.SubtreeHash(full_tree);
for (uint64_t end = 0; end <= n; end++) {
for (uint64_t start = 0; start < end; start++) {
Subtree subtree{start, end};
if (!subtree.IsValid()) {
continue;
}
SCOPED_TRACE(testing::Message() << "Tree n=" << n << ", start: "
<< start << ", end: " << end);
auto subtree_hash = tree.SubtreeHash(subtree);
auto proof = SubtreeConsistencyProof(tree, subtree, full_tree);
auto computed_hash = EvaluateMerkleSubtreeConsistencyProof(
n, subtree, proof, subtree_hash);
EXPECT_EQ(computed_hash, tree_hash);
}
}
}
// Exhaustively test subtree inclusion proofs.
for (uint64_t end = 0; end <= limit; end++) {
for (uint64_t start = 0; start < end; start++) {
Subtree subtree{start, end};
if (!subtree.IsValid()) {
continue;
}
for (uint64_t index = start; index < end; index++) {
SCOPED_TRACE(testing::Message() << "index: " << index << ", start: "
<< start << ", end: " << end);
auto subtree_hash = tree.SubtreeHash(subtree);
auto entry_hash = tree.SubtreeHash({index, index + 1});
auto proof = tree.SubtreeInclusionProof(index, subtree);
auto computed_hash = EvaluateMerkleSubtreeInclusionProof(
proof, index, entry_hash, subtree);
EXPECT_EQ(computed_hash, subtree_hash);
// A subtree inclusion proof is a special case of a consistency proof.
auto proof2 =
SubtreeConsistencyProof(tree, {index, index + 1}, subtree);
EXPECT_EQ(proof, proof2);
}
}
}
}
}
class MerkleTreeLarge : public MerkleTree {
public:
// Constructs a Merkle Tree over 2^64 - 1 copies of `entry`.
explicit MerkleTreeLarge(Span<const uint8_t> entry) {
HashLeaf(entry, levels_[0]);
for (size_t i = 1; i < levels_.size(); i++) {
HashNode(levels_[i - 1], levels_[i - 1], levels_[i]);
}
}
uint64_t Size() const override {
return std::numeric_limits<uint64_t>::max();
}
TreeHash GetNode(size_t level, uint64_t index) const override {
BSSL_CHECK(level < 64);
return levels_[level];
}
private:
std::array<TreeHash, 64> levels_;
};
TEST(MerkleTreeTest, VeryLargeProofs) {
MerkleTreeLarge tree(StringAsBytes("entry"));
Subtree fullest_tree = {0, std::numeric_limits<uint64_t>::max()};
auto root_hash = tree.SubtreeHash(fullest_tree);
Subtree test_subtrees[] = {
fullest_tree,
{0, 1},
{0, 1ull << 63},
{1ull << 63, std::numeric_limits<uint64_t>::max()},
{std::numeric_limits<uint64_t>::max() - 1,
std::numeric_limits<uint64_t>::max()},
};
for (auto subtree : test_subtrees) {
SCOPED_TRACE(testing::Message() << "Subtree start: " << subtree.start
<< ", end: " << subtree.end);
auto proof = SubtreeConsistencyProof(tree, subtree, fullest_tree);
auto computed_root_hash = EvaluateMerkleSubtreeConsistencyProof(
fullest_tree.end, subtree, proof, tree.SubtreeHash(subtree));
EXPECT_EQ(computed_root_hash, root_hash);
}
}
} // namespace
BSSL_NAMESPACE_END