From 048536df25cc20f570c2749ad60b60f6279ddb5e Mon Sep 17 00:00:00 2001 From: Amaury Chamayou Date: Mon, 24 Aug 2026 15:01:17 +0100 Subject: [PATCH] Make tree deserialisation exception-safe Own partially reconstructed nodes until the complete tree has been built, then transfer the final root and leaf metadata. Add a malformed flushed-tree regression that exercises cleanup after partial reconstruction. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- merklecpp.h | 32 ++++++++++++++++++++------------ test/unit_tests.cpp | 10 ++++++++++ 2 files changed, 30 insertions(+), 12 deletions(-) diff --git a/merklecpp.h b/merklecpp.h index ca02e9c..86c9626 100644 --- a/merklecpp.h +++ b/merklecpp.h @@ -1589,17 +1589,19 @@ namespace merkle throw std::runtime_error("not enough bytes"); } - num_flushed = deserialised_num_flushed; - leaf_nodes.reserve(num_leaf_nodes); + std::vector deserialised_leaf_nodes; + deserialised_leaf_nodes.reserve(num_leaf_nodes); + std::vector> level; + level.reserve(num_leaf_nodes); for (size_t i = 0; i < num_leaf_nodes; i++) { - Node* n = Node::make(Hash(bytes, position)); - leaf_nodes.push_back(n); + auto n = std::unique_ptr(Node::make(Hash(bytes, position))); + deserialised_leaf_nodes.push_back(n.get()); + level.push_back(std::move(n)); } - std::vector level = leaf_nodes; - std::vector next_level; - size_t it = num_flushed; + std::vector> next_level; + size_t it = deserialised_num_flushed; uint8_t level_no = 0; while (it != 0 || level.size() > 1) { @@ -1608,11 +1610,11 @@ namespace merkle { Hash h(bytes, position); MERKLECPP_TRACE(MERKLECPP_TOUT << "+";); - auto n = Node::make(h); + auto n = std::unique_ptr(Node::make(std::move(h))); n->height = level_no + 1; n->size = Node::full_size(n->height); assert(n->invariant()); - level.insert(level.begin(), n); + level.insert(level.begin(), std::move(n)); } MERKLECPP_TRACE( @@ -1625,11 +1627,15 @@ namespace merkle { if (i + 1 >= level.size()) { - next_level.push_back(level.at(i)); + next_level.push_back(std::move(level.at(i))); } else { - next_level.push_back(Node::make(level.at(i), level.at(i + 1))); + auto parent = std::unique_ptr( + Node::make(level.at(i).get(), level.at(i + 1).get())); + level.at(i).release(); + level.at(i + 1).release(); + next_level.push_back(std::move(parent)); } } @@ -1644,9 +1650,11 @@ namespace merkle if (level.size() == 1) { - _root = level.at(0); + _root = level.at(0).release(); assert(_root->invariant()); } + leaf_nodes = std::move(deserialised_leaf_nodes); + num_flushed = deserialised_num_flushed; } /// @brief Operator to serialise the tree diff --git a/test/unit_tests.cpp b/test/unit_tests.cpp index 975f376..5acd3b4 100644 --- a/test/unit_tests.cpp +++ b/test/unit_tests.cpp @@ -343,6 +343,16 @@ TEST_CASE("TreeT rejects invalid serialised leaf data") (void)merkle::Tree(truncated_extra_hash), "not enough bytes", std::runtime_error); + + std::vector truncated_flushed_hashes; + merkle::serialise_uint64_t(1, truncated_flushed_hashes); + merkle::serialise_uint64_t(3, truncated_flushed_hashes); + truncated_flushed_hashes.resize( + truncated_flushed_hashes.size() + 2 * merkle::Hash::size_bytes); + REQUIRE_THROWS_WITH_AS( + (void)merkle::Tree(truncated_flushed_hashes), + "not enough bytes", + std::runtime_error); } TEST_CASE("TreeT deserialises flushed counts beyond signed shift width")