From 54e88ed62507cf85fea4d865580e42157767382c Mon Sep 17 00:00:00 2001 From: greg7mdp Date: Mon, 25 Mar 2024 09:57:05 -0400 Subject: [PATCH 01/26] Fix misspelled function name, and incorrect name `clz_power_2` --- .../include/eosio/chain/incremental_merkle.hpp | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/libraries/chain/include/eosio/chain/incremental_merkle.hpp b/libraries/chain/include/eosio/chain/incremental_merkle.hpp index 1f157d20f7..d57f4686e1 100644 --- a/libraries/chain/include/eosio/chain/incremental_merkle.hpp +++ b/libraries/chain/include/eosio/chain/incremental_merkle.hpp @@ -3,7 +3,7 @@ #include #include -namespace eosio { namespace chain { +namespace eosio::chain { namespace detail { @@ -27,15 +27,15 @@ constexpr uint64_t next_power_of_2(uint64_t value) { } /** - * Given a power-of-2 (assumed correct) return the number of leading zeros + * Given a power-of-2 (assumed correct) return the number of trailing zeros * - * This is a classic count-leading-zeros in parallel without the necessary + * This is a classic count-trailing-zeros in parallel without the necessary * math to make it safe for anything that is not already a power-of-2 * * @param value - and integral power-of-2 - * @return the number of leading zeros + * @return the number of trailing zeros */ -constexpr int clz_power_2(uint64_t value) { +constexpr int ctz_power_2(uint64_t value) { int lz = 64; if (value) lz--; @@ -56,12 +56,12 @@ constexpr int clz_power_2(uint64_t value) { * @param node_count - the number of nodes in the implied tree * @return the max depth of the minimal tree that stores them */ -constexpr int calcluate_max_depth(uint64_t node_count) { +constexpr int calculate_max_depth(uint64_t node_count) { if (node_count == 0) { return 0; } auto implied_count = next_power_of_2(node_count); - return clz_power_2(implied_count) + 1; + return ctz_power_2(implied_count) + 1; } template @@ -166,7 +166,7 @@ class incremental_merkle_impl { */ const DigestType& append(const DigestType& digest) { bool partial = false; - auto max_depth = detail::calcluate_max_depth(_node_count + 1); + auto max_depth = detail::calculate_max_depth(_node_count + 1); auto current_depth = max_depth - 1; auto index = _node_count; auto top = digest; @@ -253,7 +253,7 @@ typedef incremental_merkle_impl incremental_le typedef incremental_merkle_impl shared_incremental_legacy_merkle_tree; typedef incremental_merkle_impl incremental_merkle_tree; -} } /// eosio::chain +} /// eosio::chain FC_REFLECT( eosio::chain::incremental_legacy_merkle_tree, (_active_nodes)(_node_count) ); FC_REFLECT( eosio::chain::incremental_merkle_tree, (_active_nodes)(_node_count) ); From cce8e5c642f609fa51ee18dc46dd5e02f0e71da2 Mon Sep 17 00:00:00 2001 From: greg7mdp Date: Mon, 25 Mar 2024 10:58:59 -0400 Subject: [PATCH 02/26] File reorg to separate `legacy` and `optimized` merkle implementation --- libraries/chain/CMakeLists.txt | 1 - .../eosio/chain/block_header_state.hpp | 1 - .../eosio/chain/block_header_state_legacy.hpp | 2 +- .../chain/include/eosio/chain/block_state.hpp | 1 + .../eosio/chain/global_property_object.hpp | 1 - .../eosio/chain/incremental_merkle.hpp | 249 +---------------- .../eosio/chain/incremental_merkle_legacy.hpp | 259 ++++++++++++++++++ .../chain/include/eosio/chain/merkle.hpp | 29 +- .../include/eosio/chain/merkle_legacy.hpp | 74 +++++ .../include/eosio/chain/snapshot_detail.hpp | 2 +- libraries/chain/merkle.cpp | 69 ----- unittests/merkle_tree_tests.cpp | 1 + 12 files changed, 344 insertions(+), 345 deletions(-) create mode 100644 libraries/chain/include/eosio/chain/incremental_merkle_legacy.hpp create mode 100644 libraries/chain/include/eosio/chain/merkle_legacy.hpp delete mode 100644 libraries/chain/merkle.cpp diff --git a/libraries/chain/CMakeLists.txt b/libraries/chain/CMakeLists.txt index 786a0972ff..d9d9acf431 100644 --- a/libraries/chain/CMakeLists.txt +++ b/libraries/chain/CMakeLists.txt @@ -90,7 +90,6 @@ target_include_directories(eosio_rapidjson INTERFACE ../rapidjson/include) ## SORT .cpp by most likely to change / break compile add_library( eosio_chain - merkle.cpp name.cpp transaction.cpp block.cpp diff --git a/libraries/chain/include/eosio/chain/block_header_state.hpp b/libraries/chain/include/eosio/chain/block_header_state.hpp index 29d5b47ee6..02291c3547 100644 --- a/libraries/chain/include/eosio/chain/block_header_state.hpp +++ b/libraries/chain/include/eosio/chain/block_header_state.hpp @@ -1,7 +1,6 @@ #pragma once #include #include -#include #include #include #include diff --git a/libraries/chain/include/eosio/chain/block_header_state_legacy.hpp b/libraries/chain/include/eosio/chain/block_header_state_legacy.hpp index e1cedcadcd..002a1f4b58 100644 --- a/libraries/chain/include/eosio/chain/block_header_state_legacy.hpp +++ b/libraries/chain/include/eosio/chain/block_header_state_legacy.hpp @@ -1,6 +1,6 @@ #pragma once #include -#include +#include #include #include #include diff --git a/libraries/chain/include/eosio/chain/block_state.hpp b/libraries/chain/include/eosio/chain/block_state.hpp index a933e08632..659af5733c 100644 --- a/libraries/chain/include/eosio/chain/block_state.hpp +++ b/libraries/chain/include/eosio/chain/block_state.hpp @@ -4,6 +4,7 @@ #include #include #include +#include namespace eosio::chain { diff --git a/libraries/chain/include/eosio/chain/global_property_object.hpp b/libraries/chain/include/eosio/chain/global_property_object.hpp index 51130d228a..8709cf9667 100644 --- a/libraries/chain/include/eosio/chain/global_property_object.hpp +++ b/libraries/chain/include/eosio/chain/global_property_object.hpp @@ -9,7 +9,6 @@ #include #include #include -#include #include #include #include "multi_index_includes.hpp" diff --git a/libraries/chain/include/eosio/chain/incremental_merkle.hpp b/libraries/chain/include/eosio/chain/incremental_merkle.hpp index d57f4686e1..d78f9af713 100644 --- a/libraries/chain/include/eosio/chain/incremental_merkle.hpp +++ b/libraries/chain/include/eosio/chain/incremental_merkle.hpp @@ -3,257 +3,14 @@ #include #include -namespace eosio::chain { - -namespace detail { - -/** - * given an unsigned integral number return the smallest - * power-of-2 which is greater than or equal to the given number - * - * @param value - an unsigned integral - * @return - the minimum power-of-2 which is >= value - */ -constexpr uint64_t next_power_of_2(uint64_t value) { - value -= 1; - value |= value >> 1; - value |= value >> 2; - value |= value >> 4; - value |= value >> 8; - value |= value >> 16; - value |= value >> 32; - value += 1; - return value; -} - -/** - * Given a power-of-2 (assumed correct) return the number of trailing zeros - * - * This is a classic count-trailing-zeros in parallel without the necessary - * math to make it safe for anything that is not already a power-of-2 - * - * @param value - and integral power-of-2 - * @return the number of trailing zeros - */ -constexpr int ctz_power_2(uint64_t value) { - int lz = 64; - - if (value) lz--; - if (value & 0x00000000FFFFFFFFULL) lz -= 32; - if (value & 0x0000FFFF0000FFFFULL) lz -= 16; - if (value & 0x00FF00FF00FF00FFULL) lz -= 8; - if (value & 0x0F0F0F0F0F0F0F0FULL) lz -= 4; - if (value & 0x3333333333333333ULL) lz -= 2; - if (value & 0x5555555555555555ULL) lz -= 1; - - return lz; -} - -/** - * Given a number of nodes return the depth required to store them - * in a fully balanced binary tree. - * - * @param node_count - the number of nodes in the implied tree - * @return the max depth of the minimal tree that stores them - */ -constexpr int calculate_max_depth(uint64_t node_count) { - if (node_count == 0) { - return 0; - } - auto implied_count = next_power_of_2(node_count); - return ctz_power_2(implied_count) + 1; -} +#include // temporary - remove when incremental_merkle implemented here -template -inline void move_nodes(ContainerA& to, const ContainerB& from) { - to.clear(); - to.insert(to.begin(), from.begin(), from.end()); -} -template -inline void move_nodes(Container& to, Container&& from) { - to = std::forward(from); -} +namespace eosio::chain { +namespace detail { } /// detail -/** - * A balanced merkle tree built in such that the set of leaf nodes can be - * appended to without triggering the reconstruction of inner nodes that - * represent a complete subset of previous nodes. - * - * to achieve this new nodes can either imply an set of future nodes - * that achieve a balanced tree OR realize one of these future nodes. - * - * Once a sub-tree contains only realized nodes its sub-root will never - * change. This allows proofs based on this merkle to be very stable - * after some time has past only needing to update or add a single - * value to maintain validity. - * - * @param canonical if true use the merkle make_canonical_pair which sets the left/right bits of the hash - */ -template class Container = vector, typename ...Args> -class incremental_merkle_impl { - public: - incremental_merkle_impl() = default; - incremental_merkle_impl( const incremental_merkle_impl& ) = default; - incremental_merkle_impl( incremental_merkle_impl&& ) = default; - incremental_merkle_impl& operator= (const incremental_merkle_impl& ) = default; - incremental_merkle_impl& operator= ( incremental_merkle_impl&& ) = default; - - template, incremental_merkle_impl>::value, int> = 0> - incremental_merkle_impl( Allocator&& alloc ):_active_nodes(forward(alloc)){} - - /* - template class OtherContainer, typename ...OtherArgs> - incremental_merkle_impl( incremental_merkle_impl&& other ) - :_node_count(other._node_count) - ,_active_nodes(other._active_nodes.begin(), other.active_nodes.end()) - {} - - incremental_merkle_impl( incremental_merkle_impl&& other ) - :_node_count(other._node_count) - ,_active_nodes(std::forward(other._active_nodes)) - {} - */ - - /** - * Add a node to the incremental tree and recalculate the _active_nodes so they - * are prepared for the next append. - * - * The algorithm for this is to start at the new node and retreat through the tree - * for any node that is the concatenation of a fully-realized node and a partially - * realized node we must record the value of the fully-realized node in the new - * _active_nodes so that the next append can fetch it. Fully realized nodes and - * Fully implied nodes do not have an effect on the _active_nodes. - * - * For convention _AND_ to allow appends when the _node_count is a power-of-2, the - * current root of the incremental tree is always appended to the end of the new - * _active_nodes. - * - * In practice, this can be done iteratively by recording any "left" value that - * is to be combined with an implied node. - * - * If the appended node is a "left" node in its pair, it will immediately push itself - * into the new active nodes list. - * - * If the new node is a "right" node it will begin collapsing upward in the tree, - * reading and discarding the "left" node data from the old active nodes list, until - * it becomes a "left" node. It must then push the "top" of its current collapsed - * sub-tree into the new active nodes list. - * - * Once any value has been added to the new active nodes, all remaining "left" nodes - * should be present in the order they are needed in the previous active nodes as an - * artifact of the previous append. As they are read from the old active nodes, they - * will need to be copied in to the new active nodes list as they are still needed - * for future appends. - * - * As a result, if an append collapses the entire tree while always being the "right" - * node, the new list of active nodes will be empty and by definition the tree contains - * a power-of-2 number of nodes. - * - * Regardless of the contents of the new active nodes list, the top "collapsed" value - * is appended. If this tree is _not_ a power-of-2 number of nodes, this node will - * not be used in the next append but still serves as a conventional place to access - * the root of the current tree. If this _is_ a power-of-2 number of nodes, this node - * will be needed during then collapse phase of the next append so, it serves double - * duty as a legitimate active node and the conventional storage location of the root. - * - * - * @param digest - the node to add - * @return - the new root - */ - const DigestType& append(const DigestType& digest) { - bool partial = false; - auto max_depth = detail::calculate_max_depth(_node_count + 1); - auto current_depth = max_depth - 1; - auto index = _node_count; - auto top = digest; - auto active_iter = _active_nodes.begin(); - auto updated_active_nodes = vector(); - updated_active_nodes.reserve(max_depth); - - while (current_depth > 0) { - if (!(index & 0x1)) { - // we are collapsing from a "left" value and an implied "right" creating a partial node - - // we only need to append this node if it is fully-realized and by definition - // if we have encountered a partial node during collapse this cannot be - // fully-realized - if (!partial) { - updated_active_nodes.emplace_back(top); - } - - // calculate the partially realized node value by implying the "right" value is identical - // to the "left" value - if constexpr (canonical) { - top = DigestType::hash(make_canonical_pair(top, top)); - } else { - top = DigestType::hash(std::make_pair(std::cref(top), std::cref(top))); - } - partial = true; - } else { - // we are collapsing from a "right" value and an fully-realized "left" - - // pull a "left" value from the previous active nodes - const auto& left_value = *active_iter; - ++active_iter; - - // if the "right" value is a partial node we will need to copy the "left" as future appends still need it - // otherwise, it can be dropped from the set of active nodes as we are collapsing a fully-realized node - if (partial) { - updated_active_nodes.emplace_back(left_value); - } - - // calculate the node - if constexpr (canonical) { - top = DigestType::hash(make_canonical_pair(left_value, top)); - } else { - top = DigestType::hash(std::make_pair(std::cref(left_value), std::cref(top))); - } - } - - // move up a level in the tree - --current_depth; - index = index >> 1; - } - - // append the top of the collapsed tree (aka the root of the merkle) - updated_active_nodes.emplace_back(top); - - // store the new active_nodes - detail::move_nodes(_active_nodes, std::move(updated_active_nodes)); - - // update the node count - ++_node_count; - - return _active_nodes.back(); - - } - - /** - * return the current root of the incremental merkle - */ - DigestType get_root() const { - if (_node_count > 0) { - return _active_nodes.back(); - } else { - return DigestType(); - } - } - - private: - friend struct fc::reflector; - uint64_t _node_count = 0; - Container _active_nodes; -}; - -typedef incremental_merkle_impl incremental_legacy_merkle_tree; -typedef incremental_merkle_impl shared_incremental_legacy_merkle_tree; -typedef incremental_merkle_impl incremental_merkle_tree; } /// eosio::chain - -FC_REFLECT( eosio::chain::incremental_legacy_merkle_tree, (_active_nodes)(_node_count) ); -FC_REFLECT( eosio::chain::incremental_merkle_tree, (_active_nodes)(_node_count) ); diff --git a/libraries/chain/include/eosio/chain/incremental_merkle_legacy.hpp b/libraries/chain/include/eosio/chain/incremental_merkle_legacy.hpp new file mode 100644 index 0000000000..17c7497da1 --- /dev/null +++ b/libraries/chain/include/eosio/chain/incremental_merkle_legacy.hpp @@ -0,0 +1,259 @@ +#pragma once +#include +#include +#include + +namespace eosio::chain { + +namespace detail { + +/** + * given an unsigned integral number return the smallest + * power-of-2 which is greater than or equal to the given number + * + * @param value - an unsigned integral + * @return - the minimum power-of-2 which is >= value + */ +constexpr uint64_t next_power_of_2(uint64_t value) { + value -= 1; + value |= value >> 1; + value |= value >> 2; + value |= value >> 4; + value |= value >> 8; + value |= value >> 16; + value |= value >> 32; + value += 1; + return value; +} + +/** + * Given a power-of-2 (assumed correct) return the number of trailing zeros + * + * This is a classic count-trailing-zeros in parallel without the necessary + * math to make it safe for anything that is not already a power-of-2 + * + * @param value - and integral power-of-2 + * @return the number of trailing zeros + */ +constexpr int ctz_power_2(uint64_t value) { + int lz = 64; + + if (value) lz--; + if (value & 0x00000000FFFFFFFFULL) lz -= 32; + if (value & 0x0000FFFF0000FFFFULL) lz -= 16; + if (value & 0x00FF00FF00FF00FFULL) lz -= 8; + if (value & 0x0F0F0F0F0F0F0F0FULL) lz -= 4; + if (value & 0x3333333333333333ULL) lz -= 2; + if (value & 0x5555555555555555ULL) lz -= 1; + + return lz; +} + +/** + * Given a number of nodes return the depth required to store them + * in a fully balanced binary tree. + * + * @param node_count - the number of nodes in the implied tree + * @return the max depth of the minimal tree that stores them + */ +constexpr int calculate_max_depth(uint64_t node_count) { + if (node_count == 0) { + return 0; + } + auto implied_count = next_power_of_2(node_count); + return ctz_power_2(implied_count) + 1; +} + +template +inline void move_nodes(ContainerA& to, const ContainerB& from) { + to.clear(); + to.insert(to.begin(), from.begin(), from.end()); +} + +template +inline void move_nodes(Container& to, Container&& from) { + to = std::forward(from); +} + + +} /// detail + +/** + * A balanced merkle tree built in such that the set of leaf nodes can be + * appended to without triggering the reconstruction of inner nodes that + * represent a complete subset of previous nodes. + * + * to achieve this new nodes can either imply an set of future nodes + * that achieve a balanced tree OR realize one of these future nodes. + * + * Once a sub-tree contains only realized nodes its sub-root will never + * change. This allows proofs based on this merkle to be very stable + * after some time has past only needing to update or add a single + * value to maintain validity. + * + * @param canonical if true use the merkle make_canonical_pair which sets the left/right bits of the hash + */ +template class Container = vector, typename ...Args> +class incremental_merkle_impl { + public: + incremental_merkle_impl() = default; + incremental_merkle_impl( const incremental_merkle_impl& ) = default; + incremental_merkle_impl( incremental_merkle_impl&& ) = default; + incremental_merkle_impl& operator= (const incremental_merkle_impl& ) = default; + incremental_merkle_impl& operator= ( incremental_merkle_impl&& ) = default; + + template, incremental_merkle_impl>::value, int> = 0> + incremental_merkle_impl( Allocator&& alloc ):_active_nodes(forward(alloc)){} + + /* + template class OtherContainer, typename ...OtherArgs> + incremental_merkle_impl( incremental_merkle_impl&& other ) + :_node_count(other._node_count) + ,_active_nodes(other._active_nodes.begin(), other.active_nodes.end()) + {} + + incremental_merkle_impl( incremental_merkle_impl&& other ) + :_node_count(other._node_count) + ,_active_nodes(std::forward(other._active_nodes)) + {} + */ + + /** + * Add a node to the incremental tree and recalculate the _active_nodes so they + * are prepared for the next append. + * + * The algorithm for this is to start at the new node and retreat through the tree + * for any node that is the concatenation of a fully-realized node and a partially + * realized node we must record the value of the fully-realized node in the new + * _active_nodes so that the next append can fetch it. Fully realized nodes and + * Fully implied nodes do not have an effect on the _active_nodes. + * + * For convention _AND_ to allow appends when the _node_count is a power-of-2, the + * current root of the incremental tree is always appended to the end of the new + * _active_nodes. + * + * In practice, this can be done iteratively by recording any "left" value that + * is to be combined with an implied node. + * + * If the appended node is a "left" node in its pair, it will immediately push itself + * into the new active nodes list. + * + * If the new node is a "right" node it will begin collapsing upward in the tree, + * reading and discarding the "left" node data from the old active nodes list, until + * it becomes a "left" node. It must then push the "top" of its current collapsed + * sub-tree into the new active nodes list. + * + * Once any value has been added to the new active nodes, all remaining "left" nodes + * should be present in the order they are needed in the previous active nodes as an + * artifact of the previous append. As they are read from the old active nodes, they + * will need to be copied in to the new active nodes list as they are still needed + * for future appends. + * + * As a result, if an append collapses the entire tree while always being the "right" + * node, the new list of active nodes will be empty and by definition the tree contains + * a power-of-2 number of nodes. + * + * Regardless of the contents of the new active nodes list, the top "collapsed" value + * is appended. If this tree is _not_ a power-of-2 number of nodes, this node will + * not be used in the next append but still serves as a conventional place to access + * the root of the current tree. If this _is_ a power-of-2 number of nodes, this node + * will be needed during then collapse phase of the next append so, it serves double + * duty as a legitimate active node and the conventional storage location of the root. + * + * + * @param digest - the node to add + * @return - the new root + */ + const DigestType& append(const DigestType& digest) { + bool partial = false; + auto max_depth = detail::calculate_max_depth(_node_count + 1); + auto current_depth = max_depth - 1; + auto index = _node_count; + auto top = digest; + auto active_iter = _active_nodes.begin(); + auto updated_active_nodes = vector(); + updated_active_nodes.reserve(max_depth); + + while (current_depth > 0) { + if (!(index & 0x1)) { + // we are collapsing from a "left" value and an implied "right" creating a partial node + + // we only need to append this node if it is fully-realized and by definition + // if we have encountered a partial node during collapse this cannot be + // fully-realized + if (!partial) { + updated_active_nodes.emplace_back(top); + } + + // calculate the partially realized node value by implying the "right" value is identical + // to the "left" value + if constexpr (canonical) { + top = DigestType::hash(make_canonical_pair(top, top)); + } else { + top = DigestType::hash(std::make_pair(std::cref(top), std::cref(top))); + } + partial = true; + } else { + // we are collapsing from a "right" value and an fully-realized "left" + + // pull a "left" value from the previous active nodes + const auto& left_value = *active_iter; + ++active_iter; + + // if the "right" value is a partial node we will need to copy the "left" as future appends still need it + // otherwise, it can be dropped from the set of active nodes as we are collapsing a fully-realized node + if (partial) { + updated_active_nodes.emplace_back(left_value); + } + + // calculate the node + if constexpr (canonical) { + top = DigestType::hash(make_canonical_pair(left_value, top)); + } else { + top = DigestType::hash(std::make_pair(std::cref(left_value), std::cref(top))); + } + } + + // move up a level in the tree + --current_depth; + index = index >> 1; + } + + // append the top of the collapsed tree (aka the root of the merkle) + updated_active_nodes.emplace_back(top); + + // store the new active_nodes + detail::move_nodes(_active_nodes, std::move(updated_active_nodes)); + + // update the node count + ++_node_count; + + return _active_nodes.back(); + + } + + /** + * return the current root of the incremental merkle + */ + DigestType get_root() const { + if (_node_count > 0) { + return _active_nodes.back(); + } else { + return DigestType(); + } + } + + private: + friend struct fc::reflector; + uint64_t _node_count = 0; + Container _active_nodes; +}; + +typedef incremental_merkle_impl incremental_legacy_merkle_tree; +typedef incremental_merkle_impl shared_incremental_legacy_merkle_tree; +typedef incremental_merkle_impl incremental_merkle_tree; + +} /// eosio::chain + +FC_REFLECT( eosio::chain::incremental_legacy_merkle_tree, (_active_nodes)(_node_count) ); +FC_REFLECT( eosio::chain::incremental_merkle_tree, (_active_nodes)(_node_count) ); diff --git a/libraries/chain/include/eosio/chain/merkle.hpp b/libraries/chain/include/eosio/chain/merkle.hpp index f7cbc0ae68..a0643333d5 100644 --- a/libraries/chain/include/eosio/chain/merkle.hpp +++ b/libraries/chain/include/eosio/chain/merkle.hpp @@ -1,29 +1,8 @@ #pragma once #include +#include +#include // temporary - remove when calculate_merkle implemented here -namespace eosio { namespace chain { +namespace eosio::chain { - digest_type make_canonical_left(const digest_type& val); - digest_type make_canonical_right(const digest_type& val); - - bool is_canonical_left(const digest_type& val); - bool is_canonical_right(const digest_type& val); - - - inline auto make_canonical_pair(const digest_type& l, const digest_type& r) { - return make_pair(make_canonical_left(l), make_canonical_right(r)); - }; - - /** - * Calculates the merkle root of a set of digests, if ids is odd it will duplicate the last id. - * Uses make_canonical_pair which before hashing sets the first bit of the previous hashes - * to 0 or 1 to indicate the side it is on. - */ - digest_type legacy_merkle( deque ids ); - - /** - * Calculates the merkle root of a set of digests. Does not manipulate the digests. - */ - digest_type calculate_merkle( deque ids ); - -} } /// eosio::chain +} /// eosio::chain diff --git a/libraries/chain/include/eosio/chain/merkle_legacy.hpp b/libraries/chain/include/eosio/chain/merkle_legacy.hpp new file mode 100644 index 0000000000..3aaa130e57 --- /dev/null +++ b/libraries/chain/include/eosio/chain/merkle_legacy.hpp @@ -0,0 +1,74 @@ +#pragma once +#include +#include + +namespace eosio::chain { + + inline digest_type make_canonical_left(const digest_type& val) { + digest_type canonical_l = val; + canonical_l._hash[0] &= 0xFFFFFFFFFFFFFF7FULL; + return canonical_l; + } + + inline digest_type make_canonical_right(const digest_type& val) { + digest_type canonical_r = val; + canonical_r._hash[0] |= 0x0000000000000080ULL; + return canonical_r; + } + + inline bool is_canonical_left(const digest_type& val) { + return (val._hash[0] & 0x0000000000000080ULL) == 0; + } + + inline bool is_canonical_right(const digest_type& val) { + return (val._hash[0] & 0x0000000000000080ULL) != 0; + } + + + inline auto make_canonical_pair(const digest_type& l, const digest_type& r) { + return make_pair(make_canonical_left(l), make_canonical_right(r)); + }; + + /** + * Calculates the merkle root of a set of digests, if ids is odd it will duplicate the last id. + * Uses make_canonical_pair which before hashing sets the first bit of the previous hashes + * to 0 or 1 to indicate the side it is on. + */ + inline digest_type legacy_merkle( deque ids ) { + if( 0 == ids.size() ) { return digest_type(); } + + while( ids.size() > 1 ) { + if( ids.size() % 2 ) + ids.push_back(ids.back()); + + for (size_t i = 0; i < ids.size() / 2; i++) { + ids[i] = digest_type::hash(make_canonical_pair(ids[2 * i], ids[(2 * i) + 1])); + } + + ids.resize(ids.size() / 2); + } + + return ids.front(); + } + + /** + * Calculates the merkle root of a set of digests. Does not manipulate the digests. + */ + inline digest_type calculate_merkle( deque ids ) { + if( 0 == ids.size() ) { return digest_type(); } + + while( ids.size() > 1 ) { + if( ids.size() % 2 ) + ids.push_back(ids.back()); + + for (size_t i = 0; i < ids.size() / 2; ++i) { + ids[i] = digest_type::hash(std::make_pair(std::cref(ids[2 * i]), std::cref(ids[(2 * i) + 1]))); + } + + ids.resize(ids.size() / 2); + } + + return ids.front(); + } + +} /// eosio::chain diff --git a/libraries/chain/include/eosio/chain/snapshot_detail.hpp b/libraries/chain/include/eosio/chain/snapshot_detail.hpp index 23c37fc149..691bb48175 100644 --- a/libraries/chain/include/eosio/chain/snapshot_detail.hpp +++ b/libraries/chain/include/eosio/chain/snapshot_detail.hpp @@ -1,6 +1,6 @@ #pragma once #include -#include +#include #include #include #include diff --git a/libraries/chain/merkle.cpp b/libraries/chain/merkle.cpp deleted file mode 100644 index de3468e7f7..0000000000 --- a/libraries/chain/merkle.cpp +++ /dev/null @@ -1,69 +0,0 @@ -#include -#include - -namespace eosio { namespace chain { - -/** - * in order to keep proofs concise, before hashing we set the first bit - * of the previous hashes to 0 or 1 to indicate the side it is on - * - * this relieves our proofs from having to indicate left vs right contactenation - * as the node values will imply it - */ - -digest_type make_canonical_left(const digest_type& val) { - digest_type canonical_l = val; - canonical_l._hash[0] &= 0xFFFFFFFFFFFFFF7FULL; - return canonical_l; -} - -digest_type make_canonical_right(const digest_type& val) { - digest_type canonical_r = val; - canonical_r._hash[0] |= 0x0000000000000080ULL; - return canonical_r; -} - -bool is_canonical_left(const digest_type& val) { - return (val._hash[0] & 0x0000000000000080ULL) == 0; -} - -bool is_canonical_right(const digest_type& val) { - return (val._hash[0] & 0x0000000000000080ULL) != 0; -} - - -digest_type legacy_merkle(deque ids) { - if( 0 == ids.size() ) { return digest_type(); } - - while( ids.size() > 1 ) { - if( ids.size() % 2 ) - ids.push_back(ids.back()); - - for (size_t i = 0; i < ids.size() / 2; i++) { - ids[i] = digest_type::hash(make_canonical_pair(ids[2 * i], ids[(2 * i) + 1])); - } - - ids.resize(ids.size() / 2); - } - - return ids.front(); -} - -digest_type calculate_merkle( deque ids ) { - if( 0 == ids.size() ) { return digest_type(); } - - while( ids.size() > 1 ) { - if( ids.size() % 2 ) - ids.push_back(ids.back()); - - for (size_t i = 0; i < ids.size() / 2; ++i) { - ids[i] = digest_type::hash(std::make_pair(std::cref(ids[2 * i]), std::cref(ids[(2 * i) + 1]))); - } - - ids.resize(ids.size() / 2); - } - - return ids.front(); -} - -} } // eosio::chain diff --git a/unittests/merkle_tree_tests.cpp b/unittests/merkle_tree_tests.cpp index db3fef8fa4..08afaa6aca 100644 --- a/unittests/merkle_tree_tests.cpp +++ b/unittests/merkle_tree_tests.cpp @@ -1,4 +1,5 @@ #include +#include #include #include #include From c775d7a39ec83115ffa2430249335906f4b98a06 Mon Sep 17 00:00:00 2001 From: greg7mdp Date: Mon, 25 Mar 2024 11:25:30 -0400 Subject: [PATCH 03/26] Make `merkle` names consistent and deprecate `canonical` --- libraries/chain/controller.cpp | 8 +- .../eosio/chain/block_header_state_legacy.hpp | 2 +- .../eosio/chain/incremental_merkle_legacy.hpp | 11 +- .../include/eosio/chain/merkle_legacy.hpp | 73 +++++----- .../include/eosio/chain/snapshot_detail.hpp | 4 +- unittests/block_tests.cpp | 4 +- unittests/forked_tests.cpp | 2 +- unittests/merkle_tree_tests.cpp | 129 +++++++++--------- unittests/protocol_feature_tests.cpp | 2 +- 9 files changed, 119 insertions(+), 116 deletions(-) diff --git a/libraries/chain/controller.cpp b/libraries/chain/controller.cpp index 851c20ae54..fc6e9723ca 100644 --- a/libraries/chain/controller.cpp +++ b/libraries/chain/controller.cpp @@ -668,13 +668,13 @@ struct building_block { auto [transaction_mroot, action_mroot] = std::visit( overloaded{[&](digests_t& trx_receipts) { // calculate the two merkle roots in separate threads auto trx_merkle_fut = - post_async_task(ioc, [&]() { return legacy_merkle(std::move(trx_receipts)); }); + post_async_task(ioc, [&]() { return calculate_merkle_legacy(std::move(trx_receipts)); }); auto action_merkle_fut = - post_async_task(ioc, [&]() { return legacy_merkle(std::move(action_receipts)); }); + post_async_task(ioc, [&]() { return calculate_merkle_legacy(std::move(action_receipts)); }); return std::make_pair(trx_merkle_fut.get(), action_merkle_fut.get()); }, [&](const checksum256_type& trx_checksum) { - return std::make_pair(trx_checksum, legacy_merkle(std::move(action_receipts))); + return std::make_pair(trx_checksum, calculate_merkle_legacy(std::move(action_receipts))); }}, trx_mroot_or_receipt_digests()); @@ -3858,7 +3858,7 @@ struct controller_impl { if (if_active) { return calculate_merkle( std::move(digests) ); } else { - return legacy_merkle( std::move(digests) ); + return calculate_merkle_legacy( std::move(digests) ); } } diff --git a/libraries/chain/include/eosio/chain/block_header_state_legacy.hpp b/libraries/chain/include/eosio/chain/block_header_state_legacy.hpp index 002a1f4b58..2ee36fd2b7 100644 --- a/libraries/chain/include/eosio/chain/block_header_state_legacy.hpp +++ b/libraries/chain/include/eosio/chain/block_header_state_legacy.hpp @@ -38,7 +38,7 @@ namespace detail { uint32_t dpos_proposed_irreversible_blocknum = 0; uint32_t dpos_irreversible_blocknum = 0; producer_authority_schedule active_schedule; - incremental_legacy_merkle_tree blockroot_merkle; + incremental_merkle_tree_legacy blockroot_merkle; flat_map producer_to_last_produced; flat_map producer_to_last_implied_irb; block_signing_authority valid_block_signing_authority; diff --git a/libraries/chain/include/eosio/chain/incremental_merkle_legacy.hpp b/libraries/chain/include/eosio/chain/incremental_merkle_legacy.hpp index 17c7497da1..d451dd8d49 100644 --- a/libraries/chain/include/eosio/chain/incremental_merkle_legacy.hpp +++ b/libraries/chain/include/eosio/chain/incremental_merkle_legacy.hpp @@ -188,7 +188,7 @@ class incremental_merkle_impl { // calculate the partially realized node value by implying the "right" value is identical // to the "left" value if constexpr (canonical) { - top = DigestType::hash(make_canonical_pair(top, top)); + top = DigestType::hash(detail::make_legacy_digest_pair(top, top)); } else { top = DigestType::hash(std::make_pair(std::cref(top), std::cref(top))); } @@ -208,7 +208,7 @@ class incremental_merkle_impl { // calculate the node if constexpr (canonical) { - top = DigestType::hash(make_canonical_pair(left_value, top)); + top = DigestType::hash(detail::make_legacy_digest_pair(left_value, top)); } else { top = DigestType::hash(std::make_pair(std::cref(left_value), std::cref(top))); } @@ -249,11 +249,10 @@ class incremental_merkle_impl { Container _active_nodes; }; -typedef incremental_merkle_impl incremental_legacy_merkle_tree; -typedef incremental_merkle_impl shared_incremental_legacy_merkle_tree; -typedef incremental_merkle_impl incremental_merkle_tree; +typedef incremental_merkle_impl incremental_merkle_tree_legacy; +typedef incremental_merkle_impl incremental_merkle_tree; } /// eosio::chain -FC_REFLECT( eosio::chain::incremental_legacy_merkle_tree, (_active_nodes)(_node_count) ); +FC_REFLECT( eosio::chain::incremental_merkle_tree_legacy, (_active_nodes)(_node_count) ); FC_REFLECT( eosio::chain::incremental_merkle_tree, (_active_nodes)(_node_count) ); diff --git a/libraries/chain/include/eosio/chain/merkle_legacy.hpp b/libraries/chain/include/eosio/chain/merkle_legacy.hpp index 3aaa130e57..6566875bdc 100644 --- a/libraries/chain/include/eosio/chain/merkle_legacy.hpp +++ b/libraries/chain/include/eosio/chain/merkle_legacy.hpp @@ -4,71 +4,74 @@ namespace eosio::chain { - inline digest_type make_canonical_left(const digest_type& val) { +namespace detail { + + inline digest_type make_legacy_left_digest(const digest_type& val) { digest_type canonical_l = val; canonical_l._hash[0] &= 0xFFFFFFFFFFFFFF7FULL; return canonical_l; } - inline digest_type make_canonical_right(const digest_type& val) { + inline digest_type make_legacy_right_digest(const digest_type& val) { digest_type canonical_r = val; canonical_r._hash[0] |= 0x0000000000000080ULL; return canonical_r; } - inline bool is_canonical_left(const digest_type& val) { + inline bool is_legacy_left_digest(const digest_type& val) { return (val._hash[0] & 0x0000000000000080ULL) == 0; } - inline bool is_canonical_right(const digest_type& val) { + inline bool is_legacy_right_digest(const digest_type& val) { return (val._hash[0] & 0x0000000000000080ULL) != 0; } - - inline auto make_canonical_pair(const digest_type& l, const digest_type& r) { - return make_pair(make_canonical_left(l), make_canonical_right(r)); + inline auto make_legacy_digest_pair(const digest_type& l, const digest_type& r) { + return make_pair(make_legacy_left_digest(l), make_legacy_right_digest(r)); }; - /** - * Calculates the merkle root of a set of digests, if ids is odd it will duplicate the last id. - * Uses make_canonical_pair which before hashing sets the first bit of the previous hashes - * to 0 or 1 to indicate the side it is on. - */ - inline digest_type legacy_merkle( deque ids ) { - if( 0 == ids.size() ) { return digest_type(); } +} - while( ids.size() > 1 ) { - if( ids.size() % 2 ) - ids.push_back(ids.back()); +/** + * Calculates the merkle root of a set of digests, if ids is odd it will duplicate the last id. + * Uses make_canonical_pair which before hashing sets the first bit of the previous hashes + * to 0 or 1 to indicate the side it is on. + */ +inline digest_type calculate_merkle_legacy( deque ids ) { + if( 0 == ids.size() ) { return digest_type(); } - for (size_t i = 0; i < ids.size() / 2; i++) { - ids[i] = digest_type::hash(make_canonical_pair(ids[2 * i], ids[(2 * i) + 1])); - } + while( ids.size() > 1 ) { + if( ids.size() % 2 ) + ids.push_back(ids.back()); - ids.resize(ids.size() / 2); + for (size_t i = 0; i < ids.size() / 2; i++) { + ids[i] = digest_type::hash(detail::make_legacy_digest_pair(ids[2 * i], ids[(2 * i) + 1])); } - return ids.front(); + ids.resize(ids.size() / 2); } - /** - * Calculates the merkle root of a set of digests. Does not manipulate the digests. - */ - inline digest_type calculate_merkle( deque ids ) { - if( 0 == ids.size() ) { return digest_type(); } + return ids.front(); +} - while( ids.size() > 1 ) { - if( ids.size() % 2 ) - ids.push_back(ids.back()); +/** + * Calculates the merkle root of a set of digests. Does not manipulate the digests. + */ +inline digest_type calculate_merkle( deque ids ) { + if( 0 == ids.size() ) { return digest_type(); } - for (size_t i = 0; i < ids.size() / 2; ++i) { - ids[i] = digest_type::hash(std::make_pair(std::cref(ids[2 * i]), std::cref(ids[(2 * i) + 1]))); - } + while( ids.size() > 1 ) { + if( ids.size() % 2 ) + ids.push_back(ids.back()); - ids.resize(ids.size() / 2); + for (size_t i = 0; i < ids.size() / 2; ++i) { + ids[i] = digest_type::hash(std::make_pair(std::cref(ids[2 * i]), std::cref(ids[(2 * i) + 1]))); } - return ids.front(); + ids.resize(ids.size() / 2); } + return ids.front(); +} + } /// eosio::chain diff --git a/libraries/chain/include/eosio/chain/snapshot_detail.hpp b/libraries/chain/include/eosio/chain/snapshot_detail.hpp index 691bb48175..6bf0a8b7a9 100644 --- a/libraries/chain/include/eosio/chain/snapshot_detail.hpp +++ b/libraries/chain/include/eosio/chain/snapshot_detail.hpp @@ -29,7 +29,7 @@ namespace eosio::chain::snapshot_detail { uint32_t dpos_proposed_irreversible_blocknum = 0; uint32_t dpos_irreversible_blocknum = 0; legacy::producer_schedule_type active_schedule; - incremental_legacy_merkle_tree blockroot_merkle; + incremental_merkle_tree_legacy blockroot_merkle; flat_map producer_to_last_produced; flat_map producer_to_last_implied_irb; public_key_type block_signing_key; @@ -57,7 +57,7 @@ namespace eosio::chain::snapshot_detail { uint32_t dpos_proposed_irreversible_blocknum = 0; uint32_t dpos_irreversible_blocknum = 0; producer_authority_schedule active_schedule; - incremental_legacy_merkle_tree blockroot_merkle; + incremental_merkle_tree_legacy blockroot_merkle; flat_map producer_to_last_produced; flat_map producer_to_last_implied_irb; block_signing_authority valid_block_signing_authority; diff --git a/unittests/block_tests.cpp b/unittests/block_tests.cpp index 8b1a23eb37..aa9733a15c 100644 --- a/unittests/block_tests.cpp +++ b/unittests/block_tests.cpp @@ -35,7 +35,7 @@ BOOST_AUTO_TEST_CASE(block_with_invalid_tx_test) const auto& trxs = copy_b->transactions; for( const auto& a : trxs ) trx_digests.emplace_back( a.digest() ); - copy_b->transaction_mroot = legacy_merkle( std::move(trx_digests) ); + copy_b->transaction_mroot = calculate_merkle_legacy( std::move(trx_digests) ); // Re-sign the block auto header_bmroot = digest_type::hash( std::make_pair( copy_b->digest(), main.control->head_block_state_legacy()->blockroot_merkle.get_root() ) ); @@ -115,7 +115,7 @@ std::pair corrupt_trx_in_block(validating_te const auto& trxs = copy_b->transactions; for( const auto& a : trxs ) trx_digests.emplace_back( a.digest() ); - copy_b->transaction_mroot = legacy_merkle( std::move(trx_digests) ); + copy_b->transaction_mroot = calculate_merkle_legacy( std::move(trx_digests) ); // Re-sign the block auto header_bmroot = digest_type::hash( std::make_pair( copy_b->digest(), main.control->head_block_state_legacy()->blockroot_merkle.get_root() ) ); diff --git a/unittests/forked_tests.cpp b/unittests/forked_tests.cpp index 28e4a46143..487677a36c 100644 --- a/unittests/forked_tests.cpp +++ b/unittests/forked_tests.cpp @@ -30,7 +30,7 @@ BOOST_AUTO_TEST_CASE( irrblock ) try { struct fork_tracker { vector blocks; - incremental_legacy_merkle_tree block_merkle; + incremental_merkle_tree_legacy block_merkle; }; BOOST_AUTO_TEST_CASE( fork_with_bad_block ) try { diff --git a/unittests/merkle_tree_tests.cpp b/unittests/merkle_tree_tests.cpp index 08afaa6aca..dc84892d7f 100644 --- a/unittests/merkle_tree_tests.cpp +++ b/unittests/merkle_tree_tests.cpp @@ -5,21 +5,22 @@ #include using namespace eosio::chain; +using eosio::chain::detail::make_legacy_digest_pair; BOOST_AUTO_TEST_SUITE(merkle_tree_tests) BOOST_AUTO_TEST_CASE(basic_append_and_root_check_canonical) { - incremental_legacy_merkle_tree tree; + incremental_merkle_tree_legacy tree; BOOST_CHECK_EQUAL(tree.get_root(), fc::sha256()); auto node1 = fc::sha256::hash("Node1"); tree.append(node1); BOOST_CHECK_EQUAL(tree.get_root().str(), node1.str()); - BOOST_CHECK_EQUAL(legacy_merkle({node1}).str(), node1.str()); + BOOST_CHECK_EQUAL(calculate_merkle_legacy({node1}).str(), node1.str()); } BOOST_AUTO_TEST_CASE(multiple_appends_canonical) { - incremental_legacy_merkle_tree tree; + incremental_merkle_tree_legacy tree; auto node1 = fc::sha256::hash("Node1"); auto node2 = fc::sha256::hash("Node2"); auto node3 = fc::sha256::hash("Node3"); @@ -32,119 +33,119 @@ BOOST_AUTO_TEST_CASE(multiple_appends_canonical) { tree.append(node1); BOOST_CHECK_EQUAL(tree.get_root().str(), node1.str()); - BOOST_CHECK_EQUAL(legacy_merkle({node1}).str(), node1.str()); + BOOST_CHECK_EQUAL(calculate_merkle_legacy({node1}).str(), node1.str()); tree.append(node2); - BOOST_CHECK_EQUAL(tree.get_root().str(), fc::sha256::hash(make_canonical_pair(node1, node2)).str()); - BOOST_CHECK_EQUAL(legacy_merkle({node1, node2}).str(), fc::sha256::hash(make_canonical_pair(node1, node2)).str()); + BOOST_CHECK_EQUAL(tree.get_root().str(), fc::sha256::hash(make_legacy_digest_pair(node1, node2)).str()); + BOOST_CHECK_EQUAL(calculate_merkle_legacy({node1, node2}).str(), fc::sha256::hash(make_legacy_digest_pair(node1, node2)).str()); tree.append(node3); - BOOST_CHECK_EQUAL(tree.get_root().str(), fc::sha256::hash(make_canonical_pair( - fc::sha256::hash(make_canonical_pair(node1, node2)), - fc::sha256::hash(make_canonical_pair(node3, node3)))).str()); - BOOST_CHECK_EQUAL(legacy_merkle({node1, node2, node3}).str(), fc::sha256::hash(make_canonical_pair( - fc::sha256::hash(make_canonical_pair(node1, node2)), - fc::sha256::hash(make_canonical_pair(node3, node3)))).str()); + BOOST_CHECK_EQUAL(tree.get_root().str(), fc::sha256::hash(make_legacy_digest_pair( + fc::sha256::hash(make_legacy_digest_pair(node1, node2)), + fc::sha256::hash(make_legacy_digest_pair(node3, node3)))).str()); + BOOST_CHECK_EQUAL(calculate_merkle_legacy({node1, node2, node3}).str(), fc::sha256::hash(make_legacy_digest_pair( + fc::sha256::hash(make_legacy_digest_pair(node1, node2)), + fc::sha256::hash(make_legacy_digest_pair(node3, node3)))).str()); tree.append(node4); - auto calculated_root = fc::sha256::hash(make_canonical_pair( - fc::sha256::hash(make_canonical_pair(node1, node2)), - fc::sha256::hash(make_canonical_pair(node3, node4)))); + auto calculated_root = fc::sha256::hash(make_legacy_digest_pair( + fc::sha256::hash(make_legacy_digest_pair(node1, node2)), + fc::sha256::hash(make_legacy_digest_pair(node3, node4)))); BOOST_CHECK_EQUAL(tree.get_root().str(), calculated_root.str()); - BOOST_CHECK_EQUAL(legacy_merkle({node1, node2, node3, node4}).str(), calculated_root.str()); + BOOST_CHECK_EQUAL(calculate_merkle_legacy({node1, node2, node3, node4}).str(), calculated_root.str()); tree.append(node5); calculated_root = fc::sha256::hash( - make_canonical_pair( - fc::sha256::hash(make_canonical_pair( - fc::sha256::hash(make_canonical_pair(node1, node2)), - fc::sha256::hash(make_canonical_pair(node3, node4)) + make_legacy_digest_pair( + fc::sha256::hash(make_legacy_digest_pair( + fc::sha256::hash(make_legacy_digest_pair(node1, node2)), + fc::sha256::hash(make_legacy_digest_pair(node3, node4)) )), - fc::sha256::hash(make_canonical_pair( - fc::sha256::hash(make_canonical_pair(node5, node5)), - fc::sha256::hash(make_canonical_pair(node5, node5)) + fc::sha256::hash(make_legacy_digest_pair( + fc::sha256::hash(make_legacy_digest_pair(node5, node5)), + fc::sha256::hash(make_legacy_digest_pair(node5, node5)) )) ) ); BOOST_CHECK_EQUAL(tree.get_root().str(), calculated_root.str()); - BOOST_CHECK_EQUAL(legacy_merkle({node1, node2, node3, node4, node5}).str(), calculated_root.str()); + BOOST_CHECK_EQUAL(calculate_merkle_legacy({node1, node2, node3, node4, node5}).str(), calculated_root.str()); tree.append(node6); calculated_root = fc::sha256::hash( - make_canonical_pair( - fc::sha256::hash(make_canonical_pair( - fc::sha256::hash(make_canonical_pair(node1, node2)), - fc::sha256::hash(make_canonical_pair(node3, node4)) + make_legacy_digest_pair( + fc::sha256::hash(make_legacy_digest_pair( + fc::sha256::hash(make_legacy_digest_pair(node1, node2)), + fc::sha256::hash(make_legacy_digest_pair(node3, node4)) )), - fc::sha256::hash(make_canonical_pair( - fc::sha256::hash(make_canonical_pair(node5, node6)), - fc::sha256::hash(make_canonical_pair(node5, node6)) + fc::sha256::hash(make_legacy_digest_pair( + fc::sha256::hash(make_legacy_digest_pair(node5, node6)), + fc::sha256::hash(make_legacy_digest_pair(node5, node6)) )) ) ); BOOST_CHECK_EQUAL(tree.get_root().str(), calculated_root.str()); - BOOST_CHECK_EQUAL(legacy_merkle({node1, node2, node3, node4, node5, node6}).str(), calculated_root.str()); + BOOST_CHECK_EQUAL(calculate_merkle_legacy({node1, node2, node3, node4, node5, node6}).str(), calculated_root.str()); tree.append(node7); calculated_root = fc::sha256::hash( - make_canonical_pair( - fc::sha256::hash(make_canonical_pair( - fc::sha256::hash(make_canonical_pair(node1, node2)), - fc::sha256::hash(make_canonical_pair(node3, node4)) + make_legacy_digest_pair( + fc::sha256::hash(make_legacy_digest_pair( + fc::sha256::hash(make_legacy_digest_pair(node1, node2)), + fc::sha256::hash(make_legacy_digest_pair(node3, node4)) )), - fc::sha256::hash(make_canonical_pair( - fc::sha256::hash(make_canonical_pair(node5, node6)), - fc::sha256::hash(make_canonical_pair(node7, node7)) + fc::sha256::hash(make_legacy_digest_pair( + fc::sha256::hash(make_legacy_digest_pair(node5, node6)), + fc::sha256::hash(make_legacy_digest_pair(node7, node7)) )) ) ); BOOST_CHECK_EQUAL(tree.get_root().str(), calculated_root.str()); - BOOST_CHECK_EQUAL(legacy_merkle({node1, node2, node3, node4, node5, node6, node7}).str(), calculated_root.str()); + BOOST_CHECK_EQUAL(calculate_merkle_legacy({node1, node2, node3, node4, node5, node6, node7}).str(), calculated_root.str()); tree.append(node8); calculated_root = fc::sha256::hash( - make_canonical_pair( - fc::sha256::hash(make_canonical_pair( - fc::sha256::hash(make_canonical_pair(node1, node2)), - fc::sha256::hash(make_canonical_pair(node3, node4)) + make_legacy_digest_pair( + fc::sha256::hash(make_legacy_digest_pair( + fc::sha256::hash(make_legacy_digest_pair(node1, node2)), + fc::sha256::hash(make_legacy_digest_pair(node3, node4)) )), - fc::sha256::hash(make_canonical_pair( - fc::sha256::hash(make_canonical_pair(node5, node6)), - fc::sha256::hash(make_canonical_pair(node7, node8)) + fc::sha256::hash(make_legacy_digest_pair( + fc::sha256::hash(make_legacy_digest_pair(node5, node6)), + fc::sha256::hash(make_legacy_digest_pair(node7, node8)) )) ) ); BOOST_CHECK_EQUAL(tree.get_root().str(), calculated_root.str()); - BOOST_CHECK_EQUAL(legacy_merkle({node1, node2, node3, node4, node5, node6, node7, node8}).str(), calculated_root.str()); + BOOST_CHECK_EQUAL(calculate_merkle_legacy({node1, node2, node3, node4, node5, node6, node7, node8}).str(), calculated_root.str()); tree.append(node9); - calculated_root = fc::sha256::hash(make_canonical_pair( + calculated_root = fc::sha256::hash(make_legacy_digest_pair( fc::sha256::hash( - make_canonical_pair( - fc::sha256::hash(make_canonical_pair( - fc::sha256::hash(make_canonical_pair(node1, node2)), - fc::sha256::hash(make_canonical_pair(node3, node4)) + make_legacy_digest_pair( + fc::sha256::hash(make_legacy_digest_pair( + fc::sha256::hash(make_legacy_digest_pair(node1, node2)), + fc::sha256::hash(make_legacy_digest_pair(node3, node4)) )), - fc::sha256::hash(make_canonical_pair( - fc::sha256::hash(make_canonical_pair(node5, node6)), - fc::sha256::hash(make_canonical_pair(node7, node8)) + fc::sha256::hash(make_legacy_digest_pair( + fc::sha256::hash(make_legacy_digest_pair(node5, node6)), + fc::sha256::hash(make_legacy_digest_pair(node7, node8)) )) ) ), fc::sha256::hash( - make_canonical_pair( - fc::sha256::hash(make_canonical_pair( - fc::sha256::hash(make_canonical_pair(node9, node9)), - fc::sha256::hash(make_canonical_pair(node9, node9)) + make_legacy_digest_pair( + fc::sha256::hash(make_legacy_digest_pair( + fc::sha256::hash(make_legacy_digest_pair(node9, node9)), + fc::sha256::hash(make_legacy_digest_pair(node9, node9)) )), - fc::sha256::hash(make_canonical_pair( - fc::sha256::hash(make_canonical_pair(node9, node9)), - fc::sha256::hash(make_canonical_pair(node9, node9)) + fc::sha256::hash(make_legacy_digest_pair( + fc::sha256::hash(make_legacy_digest_pair(node9, node9)), + fc::sha256::hash(make_legacy_digest_pair(node9, node9)) )) ) ) )); BOOST_CHECK_EQUAL(tree.get_root().str(), calculated_root.str()); - BOOST_CHECK_EQUAL(legacy_merkle({node1, node2, node3, node4, node5, node6, node7, node8, node9}).str(), calculated_root.str()); + BOOST_CHECK_EQUAL(calculate_merkle_legacy({node1, node2, node3, node4, node5, node6, node7, node8, node9}).str(), calculated_root.str()); } BOOST_AUTO_TEST_CASE(basic_append_and_root_check) { diff --git a/unittests/protocol_feature_tests.cpp b/unittests/protocol_feature_tests.cpp index a0b2e96c40..f589745ec3 100644 --- a/unittests/protocol_feature_tests.cpp +++ b/unittests/protocol_feature_tests.cpp @@ -2330,7 +2330,7 @@ BOOST_AUTO_TEST_CASE( block_validation_after_stage_1_test ) { try { const auto& trxs = copy_b->transactions; for( const auto& a : trxs ) trx_digests.emplace_back( a.digest() ); - copy_b->transaction_mroot = legacy_merkle( std::move(trx_digests) ); + copy_b->transaction_mroot = calculate_merkle_legacy( std::move(trx_digests) ); // Re-sign the block auto header_bmroot = digest_type::hash( std::make_pair( copy_b->digest(), tester1.control->head_block_state_legacy()->blockroot_merkle.get_root() ) ); From f7364c5a67bdaa2742a7e977789af8c064d939f2 Mon Sep 17 00:00:00 2001 From: greg7mdp Date: Mon, 25 Mar 2024 11:32:01 -0400 Subject: [PATCH 04/26] Move `merkle` types into their correct headers. --- .../eosio/chain/incremental_merkle.hpp | 3 +++ .../eosio/chain/incremental_merkle_legacy.hpp | 4 +--- .../chain/include/eosio/chain/merkle.hpp | 22 ++++++++++++++++++- .../include/eosio/chain/merkle_legacy.hpp | 20 ----------------- 4 files changed, 25 insertions(+), 24 deletions(-) diff --git a/libraries/chain/include/eosio/chain/incremental_merkle.hpp b/libraries/chain/include/eosio/chain/incremental_merkle.hpp index d78f9af713..4d7880b126 100644 --- a/libraries/chain/include/eosio/chain/incremental_merkle.hpp +++ b/libraries/chain/include/eosio/chain/incremental_merkle.hpp @@ -12,5 +12,8 @@ namespace detail { } /// detail +typedef incremental_merkle_impl incremental_merkle_tree; } /// eosio::chain + +FC_REFLECT( eosio::chain::incremental_merkle_tree, (_active_nodes)(_node_count) ); diff --git a/libraries/chain/include/eosio/chain/incremental_merkle_legacy.hpp b/libraries/chain/include/eosio/chain/incremental_merkle_legacy.hpp index d451dd8d49..598642fb31 100644 --- a/libraries/chain/include/eosio/chain/incremental_merkle_legacy.hpp +++ b/libraries/chain/include/eosio/chain/incremental_merkle_legacy.hpp @@ -249,10 +249,8 @@ class incremental_merkle_impl { Container _active_nodes; }; -typedef incremental_merkle_impl incremental_merkle_tree_legacy; -typedef incremental_merkle_impl incremental_merkle_tree; +typedef incremental_merkle_impl incremental_merkle_tree_legacy; } /// eosio::chain FC_REFLECT( eosio::chain::incremental_merkle_tree_legacy, (_active_nodes)(_node_count) ); -FC_REFLECT( eosio::chain::incremental_merkle_tree, (_active_nodes)(_node_count) ); diff --git a/libraries/chain/include/eosio/chain/merkle.hpp b/libraries/chain/include/eosio/chain/merkle.hpp index a0643333d5..7f17a2f3a5 100644 --- a/libraries/chain/include/eosio/chain/merkle.hpp +++ b/libraries/chain/include/eosio/chain/merkle.hpp @@ -1,8 +1,28 @@ #pragma once #include #include -#include // temporary - remove when calculate_merkle implemented here namespace eosio::chain { +/** + * Calculates the merkle root of a set of digests. Does not manipulate the digests. + */ +inline digest_type calculate_merkle( deque ids ) { + if( 0 == ids.size() ) { return digest_type(); } + + while( ids.size() > 1 ) { + if( ids.size() % 2 ) + ids.push_back(ids.back()); + + for (size_t i = 0; i < ids.size() / 2; ++i) { + ids[i] = digest_type::hash(std::make_pair(std::cref(ids[2 * i]), std::cref(ids[(2 * i) + 1]))); + } + + ids.resize(ids.size() / 2); + } + + return ids.front(); +} + + } /// eosio::chain diff --git a/libraries/chain/include/eosio/chain/merkle_legacy.hpp b/libraries/chain/include/eosio/chain/merkle_legacy.hpp index 6566875bdc..35d4508aed 100644 --- a/libraries/chain/include/eosio/chain/merkle_legacy.hpp +++ b/libraries/chain/include/eosio/chain/merkle_legacy.hpp @@ -54,24 +54,4 @@ inline digest_type calculate_merkle_legacy( deque ids ) { return ids.front(); } -/** - * Calculates the merkle root of a set of digests. Does not manipulate the digests. - */ -inline digest_type calculate_merkle( deque ids ) { - if( 0 == ids.size() ) { return digest_type(); } - - while( ids.size() > 1 ) { - if( ids.size() % 2 ) - ids.push_back(ids.back()); - - for (size_t i = 0; i < ids.size() / 2; ++i) { - ids[i] = digest_type::hash(std::make_pair(std::cref(ids[2 * i]), std::cref(ids[(2 * i) + 1]))); - } - - ids.resize(ids.size() / 2); - } - - return ids.front(); -} - } /// eosio::chain From fd028856770c03fe04c375db8b4d2758e475a5b9 Mon Sep 17 00:00:00 2001 From: greg7mdp Date: Thu, 28 Mar 2024 19:34:26 -0400 Subject: [PATCH 05/26] Initial new merkle tree implementation. --- .../eosio/chain/incremental_merkle.hpp | 71 +++++++- .../chain/include/eosio/chain/merkle.hpp | 88 +++++++++- unittests/merkle_tree_tests.cpp | 157 ++++++------------ 3 files changed, 209 insertions(+), 107 deletions(-) diff --git a/libraries/chain/include/eosio/chain/incremental_merkle.hpp b/libraries/chain/include/eosio/chain/incremental_merkle.hpp index 27d7402616..df45cc7502 100644 --- a/libraries/chain/include/eosio/chain/incremental_merkle.hpp +++ b/libraries/chain/include/eosio/chain/incremental_merkle.hpp @@ -9,12 +9,75 @@ namespace eosio::chain { -namespace detail { +// typedef incremental_merkle_impl incremental_merkle_tree; -} /// detail +class incremental_merkle_tree { +public: + void append(const digest_type& digest) { + char c; + assert(trees.size() == std::popcount(mask)); + _append(digest, trees.end(), 0, &c); + assert(trees.size() == std::popcount(mask)); + } -typedef incremental_merkle_impl incremental_merkle_tree; + digest_type get_root() const { + if (!mask) + return {}; + assert(!trees.empty()); + return _get_root(0); + }; + + int64_t max_stack_depth = 0; + +private: + friend struct fc::reflector; + using vec_it = std::vector::iterator; + + bool is_bit_set(size_t idx) const { return !!(mask & (1ull << idx)); } + void set_bit(size_t idx) { mask |= (1ull << idx); } + void clear_bit(size_t idx) { mask &= ~(1ull << idx); } + + digest_type _get_root(size_t idx) const { + if (idx + 1 == trees.size()) + return trees[idx]; + return detail::hash_combine(trees[idx], _get_root(idx + 1)); + } + + // slot points to the current insertion point. *(slot-1) is the digest for the first bit set >= idx + void _append(const digest_type& digest, vec_it slot, size_t idx, const char *p) { + char c; + max_stack_depth = std::max(max_stack_depth, p - &c); + if (is_bit_set(idx)) { + assert(!trees.empty()); + if (!is_bit_set(idx+1)) { + // next location is empty, replace its tree with new combination, same number of slots and one bits + *(slot-1) = detail::hash_combine(*(slot-1), digest); + clear_bit(idx); + set_bit(idx+1); + } else { + assert(trees.size() >= 2); + clear_bit(idx); + clear_bit(idx+1); + digest_type d = detail::hash_combine(*(slot-2), detail::hash_combine(*(slot-1), digest)); + trees.erase(slot-2, slot); + _append(d, slot-2, idx+2, p); + } + } else { + trees.insert(slot, digest); + set_bit(idx); + } + } + + + uint64_t mask = 0; // bits set signify tree present in trees vector. + // least signif. bit set maps to smallest tree present. + std::vector trees; // digests representing power of 2 trees, smallest tree last + // to minimize digest copying when appending. + // invariant: `trees.size() == std::popcount(mask)` + + static inline constexpr size_t num_bits = sizeof(mask) * 8; +}; } /// eosio::chain -FC_REFLECT( eosio::chain::incremental_merkle_tree, (_active_nodes)(_node_count) ); +FC_REFLECT( eosio::chain::incremental_merkle_tree, (mask)(trees) ); diff --git a/libraries/chain/include/eosio/chain/merkle.hpp b/libraries/chain/include/eosio/chain/merkle.hpp index 7f17a2f3a5..b1dea1cc40 100644 --- a/libraries/chain/include/eosio/chain/merkle.hpp +++ b/libraries/chain/include/eosio/chain/merkle.hpp @@ -1,14 +1,17 @@ #pragma once #include #include +#include namespace eosio::chain { +#if 0 /** * Calculates the merkle root of a set of digests. Does not manipulate the digests. */ inline digest_type calculate_merkle( deque ids ) { - if( 0 == ids.size() ) { return digest_type(); } + if (ids.empty()) + return {}; while( ids.size() > 1 ) { if( ids.size() % 2 ) @@ -23,6 +26,89 @@ inline digest_type calculate_merkle( deque ids ) { return ids.front(); } +#endif + +namespace detail { + +inline digest_type hash_combine(const digest_type& a, const digest_type& b) { + return digest_type::hash(std::make_pair(std::cref(a), std::cref(b))); +} + +// does not overwrite passed sequence +// ---------------------------------- +template +requires std::is_same_v::value_type, std::decay_t> +inline digest_type calculate_merkle_pow2(const It& start, const It& end) { + auto size = end - start; + assert(size >= 2); + assert(std::bit_floor(static_cast(size)) == size); + + if (size == 2) + return hash_combine(start[0], start[1]); + else { + auto mid = start + size / 2; + return hash_combine(calculate_merkle_pow2(start, mid), calculate_merkle_pow2(mid, end)); + } +} + + +template +requires std::is_same_v::value_type, digest_type> +inline digest_type calculate_merkle(const It& start, const It& end) { + assert(end >= start); + auto size = end - start; + if (size <= 1) + return (size == 0) ? digest_type{} : *start; + + auto midpoint = std::bit_floor(static_cast(size)); + if (size == midpoint) + return calculate_merkle_pow2(start, end); + + auto mid = start + midpoint; + return hash_combine(calculate_merkle_pow2(start, mid), calculate_merkle(mid, end)); +} + +// overwrites passed sequence +// -------------------------- +template +requires std::is_same_v::value_type, digest_type> +inline digest_type calculate_merkle_2(const It& start, const It& end) { + assert(end >= start); + auto size = end - start; + if (size <= 1) + return (size == 0) ? digest_type{} : *start; + + auto midpoint = std::bit_floor(static_cast(size)); + + // accumulate the first 2**N digests into start[0] + // ----------------------------------------------- + { + auto remaining = midpoint; + while (remaining > 1) { + for (size_t i = 0; i < remaining / 2; ++i) + start[i] = hash_combine(start[2 * i], start[(2 * i) + 1]); + remaining /= 2; + } + } + + if (midpoint == size) + return start[0]; + + // accumulate the rest of the digests into start[1], recursion limited to + // log2(size) so stack overflow is not a concern. + // ---------------------------------------------------------------------- + start[1] = calculate_merkle_2(start + midpoint, end); + + // combine and return + // ------------------ + return hash_combine(start[0], std::cref(start[1])); +} + +} + +inline digest_type calculate_merkle(const deque& ids) { + return detail::calculate_merkle(ids.cbegin(), ids.cend()); +} } /// eosio::chain diff --git a/unittests/merkle_tree_tests.cpp b/unittests/merkle_tree_tests.cpp index dc84892d7f..3c8d8e6519 100644 --- a/unittests/merkle_tree_tests.cpp +++ b/unittests/merkle_tree_tests.cpp @@ -1,15 +1,24 @@ #include #include -#include #include #include using namespace eosio::chain; using eosio::chain::detail::make_legacy_digest_pair; +std::vector create_test_digests(size_t n) { + std::vector v; + v.reserve(n); + for (size_t i=0; i digests { node1, node2, node3, node4, node5, node6, node7, node8, node9 }; + auto first = digests.cbegin(); + tree.append(node1); - BOOST_CHECK_EQUAL(tree.get_root().str(), node1.str()); - BOOST_CHECK_EQUAL(calculate_merkle({node1}).str(), node1.str()); + BOOST_CHECK_EQUAL(tree.get_root(), node1); + BOOST_CHECK_EQUAL(calculate_merkle({node1}), node1); tree.append(node2); - BOOST_CHECK_EQUAL(tree.get_root().str(), fc::sha256::hash(std::make_pair(node1, node2)).str()); - BOOST_CHECK_EQUAL(calculate_merkle({node1, node2}).str(), fc::sha256::hash(std::make_pair(node1, node2)).str()); + BOOST_CHECK_EQUAL(tree.get_root(), hash(node1, node2)); + BOOST_CHECK_EQUAL(calculate_merkle({first, first + 2}), hash(node1, node2)); tree.append(node3); - BOOST_CHECK_EQUAL(tree.get_root().str(), fc::sha256::hash(std::make_pair( - fc::sha256::hash(std::make_pair(node1, node2)), - fc::sha256::hash(std::make_pair(node3, node3)))).str()); - BOOST_CHECK_EQUAL(calculate_merkle({node1, node2, node3}).str(), fc::sha256::hash(std::make_pair( - fc::sha256::hash(std::make_pair(node1, node2)), - fc::sha256::hash(std::make_pair(node3, node3)))).str()); + auto calculated_root = hash(hash(node1, node2), node3); + BOOST_CHECK_EQUAL(tree.get_root(), calculated_root); + BOOST_CHECK_EQUAL(calculate_merkle({first, first + 3}), calculated_root); tree.append(node4); - auto calculated_root = fc::sha256::hash(std::make_pair( - fc::sha256::hash(std::make_pair(node1, node2)), - fc::sha256::hash(std::make_pair(node3, node4)))); - BOOST_CHECK_EQUAL(tree.get_root().str(), calculated_root.str()); - BOOST_CHECK_EQUAL(calculate_merkle({node1, node2, node3, node4}).str(), calculated_root.str()); + auto first_four_tree = hash(hash(node1, node2), hash(node3, node4)); + calculated_root = first_four_tree; + BOOST_CHECK_EQUAL(tree.get_root(), calculated_root); + BOOST_CHECK_EQUAL(calculate_merkle({first, first + 4}), calculated_root); tree.append(node5); - calculated_root = fc::sha256::hash( - std::make_pair( - fc::sha256::hash(std::make_pair( - fc::sha256::hash(std::make_pair(node1, node2)), - fc::sha256::hash(std::make_pair(node3, node4)) - )), - fc::sha256::hash(std::make_pair( - fc::sha256::hash(std::make_pair(node5, node5)), - fc::sha256::hash(std::make_pair(node5, node5)) - )) - ) - ); - BOOST_CHECK_EQUAL(tree.get_root().str(), calculated_root.str()); - BOOST_CHECK_EQUAL(calculate_merkle({node1, node2, node3, node4, node5}).str(), calculated_root.str()); + calculated_root = hash(first_four_tree, node5); + BOOST_CHECK_EQUAL(tree.get_root(), calculated_root); + BOOST_CHECK_EQUAL(calculate_merkle({first, first + 5}), calculated_root); tree.append(node6); - calculated_root = fc::sha256::hash( - std::make_pair( - fc::sha256::hash(std::make_pair( - fc::sha256::hash(std::make_pair(node1, node2)), - fc::sha256::hash(std::make_pair(node3, node4)) - )), - fc::sha256::hash(std::make_pair( - fc::sha256::hash(std::make_pair(node5, node6)), - fc::sha256::hash(std::make_pair(node5, node6)) - )) - ) - ); - BOOST_CHECK_EQUAL(tree.get_root().str(), calculated_root.str()); - BOOST_CHECK_EQUAL(calculate_merkle({node1, node2, node3, node4, node5, node6}).str(), calculated_root.str()); + calculated_root = hash(first_four_tree, hash(node5, node6)); + BOOST_CHECK_EQUAL(tree.get_root(), calculated_root); + BOOST_CHECK_EQUAL(calculate_merkle({first, first + 6}), calculated_root); tree.append(node7); - calculated_root = fc::sha256::hash( - std::make_pair( - fc::sha256::hash(std::make_pair( - fc::sha256::hash(std::make_pair(node1, node2)), - fc::sha256::hash(std::make_pair(node3, node4)) - )), - fc::sha256::hash(std::make_pair( - fc::sha256::hash(std::make_pair(node5, node6)), - fc::sha256::hash(std::make_pair(node7, node7)) - )) - ) - ); - BOOST_CHECK_EQUAL(tree.get_root().str(), calculated_root.str()); - BOOST_CHECK_EQUAL(calculate_merkle({node1, node2, node3, node4, node5, node6, node7}).str(), calculated_root.str()); + calculated_root = hash(first_four_tree, hash(hash(node5, node6), node7)); + BOOST_CHECK_EQUAL(tree.get_root(), calculated_root); + BOOST_CHECK_EQUAL(calculate_merkle({first, first + 7}), calculated_root); tree.append(node8); - calculated_root = fc::sha256::hash( - std::make_pair( - fc::sha256::hash(std::make_pair( - fc::sha256::hash(std::make_pair(node1, node2)), - fc::sha256::hash(std::make_pair(node3, node4)) - )), - fc::sha256::hash(std::make_pair( - fc::sha256::hash(std::make_pair(node5, node6)), - fc::sha256::hash(std::make_pair(node7, node8)) - )) - ) - ); - BOOST_CHECK_EQUAL(tree.get_root().str(), calculated_root.str()); - BOOST_CHECK_EQUAL(calculate_merkle({node1, node2, node3, node4, node5, node6, node7, node8}).str(), calculated_root.str()); + auto next_four_tree = hash(hash(node5, node6), hash(node7, node8)); + calculated_root = hash(first_four_tree, next_four_tree); + BOOST_CHECK_EQUAL(tree.get_root(), calculated_root); + BOOST_CHECK_EQUAL(calculate_merkle({first, first + 8}), calculated_root); tree.append(node9); - calculated_root = fc::sha256::hash(std::make_pair( - fc::sha256::hash( - std::make_pair( - fc::sha256::hash(std::make_pair( - fc::sha256::hash(std::make_pair(node1, node2)), - fc::sha256::hash(std::make_pair(node3, node4)) - )), - fc::sha256::hash(std::make_pair( - fc::sha256::hash(std::make_pair(node5, node6)), - fc::sha256::hash(std::make_pair(node7, node8)) - )) - ) - ), - fc::sha256::hash( - std::make_pair( - fc::sha256::hash(std::make_pair( - fc::sha256::hash(std::make_pair(node9, node9)), - fc::sha256::hash(std::make_pair(node9, node9)) - )), - fc::sha256::hash(std::make_pair( - fc::sha256::hash(std::make_pair(node9, node9)), - fc::sha256::hash(std::make_pair(node9, node9)) - )) - ) - ) )); - BOOST_CHECK_EQUAL(tree.get_root().str(), calculated_root.str()); - BOOST_CHECK_EQUAL(calculate_merkle({node1, node2, node3, node4, node5, node6, node7, node8, node9}).str(), calculated_root.str()); + calculated_root = hash(hash(first_four_tree, next_four_tree), node9); + BOOST_CHECK_EQUAL(tree.get_root(), calculated_root); + BOOST_CHECK_EQUAL(calculate_merkle({first, first + 9}), calculated_root); +} + +BOOST_AUTO_TEST_CASE(consistency_over_large_range) { + constexpr size_t num_digests = 1001ull; + + std::vector digests = create_test_digests(num_digests); + for (size_t i=1; i Date: Thu, 28 Mar 2024 19:41:32 -0400 Subject: [PATCH 06/26] Add stack depth check. For 10 levels of recursion, we used ~1540 bytes of stack. This translates to 5KB of stack space (32 levels of recursion) for appending 4 billion digests (the max. possible number of blocks). Since the default Ubuntu stack for a process is 10MB, and 2MB for a thread, this means that, even for a thread, we'd use at most 5/2000, or 0.25% of the stack available for any append(). If the stack is already 99.75% used before `incremental_merkle_tree::append()` is called, we have a problem elsewhere. --- unittests/merkle_tree_tests.cpp | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/unittests/merkle_tree_tests.cpp b/unittests/merkle_tree_tests.cpp index 3c8d8e6519..f74ffb0ed5 100644 --- a/unittests/merkle_tree_tests.cpp +++ b/unittests/merkle_tree_tests.cpp @@ -240,4 +240,19 @@ BOOST_AUTO_TEST_CASE(consistency_over_large_range) { } } +BOOST_AUTO_TEST_CASE(stack_check) { + constexpr size_t num_digests = 1024ull; + + std::vector digests = create_test_digests(num_digests); + incremental_merkle_tree tree; + for (size_t j=0; j Date: Thu, 28 Mar 2024 20:06:28 -0400 Subject: [PATCH 07/26] Add alternative to `std::popcount` for cdt which does not support C++20 yet. --- .../eosio/chain/incremental_merkle.hpp | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/libraries/chain/include/eosio/chain/incremental_merkle.hpp b/libraries/chain/include/eosio/chain/incremental_merkle.hpp index df45cc7502..7fd7a7ce77 100644 --- a/libraries/chain/include/eosio/chain/incremental_merkle.hpp +++ b/libraries/chain/include/eosio/chain/incremental_merkle.hpp @@ -4,20 +4,23 @@ #include #include -#include // temporary - remove when incremental_merkle implemented here - - namespace eosio::chain { -// typedef incremental_merkle_impl incremental_merkle_tree; +namespace detail { +#if __cplusplus >= 202002L + inline int popcount(uint64_t x) noexcept { return std::popcount(x); } +#else + inline int popcount(uint64_t x) noexcept { return __builtin_popcountll(x); } +#endif +} class incremental_merkle_tree { public: void append(const digest_type& digest) { char c; - assert(trees.size() == std::popcount(mask)); + assert(trees.size() == detail::popcount(mask)); _append(digest, trees.end(), 0, &c); - assert(trees.size() == std::popcount(mask)); + assert(trees.size() == detail::popcount(mask)); } digest_type get_root() const { @@ -68,14 +71,11 @@ class incremental_merkle_tree { } } - uint64_t mask = 0; // bits set signify tree present in trees vector. // least signif. bit set maps to smallest tree present. std::vector trees; // digests representing power of 2 trees, smallest tree last // to minimize digest copying when appending. - // invariant: `trees.size() == std::popcount(mask)` - - static inline constexpr size_t num_bits = sizeof(mask) * 8; + // invariant: `trees.size() == detail::popcount(mask)` }; } /// eosio::chain From a09fd7dba4bcc90acf00a193756be313969fd0c6 Mon Sep 17 00:00:00 2001 From: greg7mdp Date: Thu, 28 Mar 2024 20:08:58 -0400 Subject: [PATCH 08/26] Remove destructive version of `calculate_merkle`. --- .../chain/include/eosio/chain/merkle.hpp | 59 ------------------- 1 file changed, 59 deletions(-) diff --git a/libraries/chain/include/eosio/chain/merkle.hpp b/libraries/chain/include/eosio/chain/merkle.hpp index b1dea1cc40..e286b7cfae 100644 --- a/libraries/chain/include/eosio/chain/merkle.hpp +++ b/libraries/chain/include/eosio/chain/merkle.hpp @@ -5,29 +5,6 @@ namespace eosio::chain { -#if 0 -/** - * Calculates the merkle root of a set of digests. Does not manipulate the digests. - */ -inline digest_type calculate_merkle( deque ids ) { - if (ids.empty()) - return {}; - - while( ids.size() > 1 ) { - if( ids.size() % 2 ) - ids.push_back(ids.back()); - - for (size_t i = 0; i < ids.size() / 2; ++i) { - ids[i] = digest_type::hash(std::make_pair(std::cref(ids[2 * i]), std::cref(ids[(2 * i) + 1]))); - } - - ids.resize(ids.size() / 2); - } - - return ids.front(); -} -#endif - namespace detail { inline digest_type hash_combine(const digest_type& a, const digest_type& b) { @@ -68,42 +45,6 @@ inline digest_type calculate_merkle(const It& start, const It& end) { return hash_combine(calculate_merkle_pow2(start, mid), calculate_merkle(mid, end)); } -// overwrites passed sequence -// -------------------------- -template -requires std::is_same_v::value_type, digest_type> -inline digest_type calculate_merkle_2(const It& start, const It& end) { - assert(end >= start); - auto size = end - start; - if (size <= 1) - return (size == 0) ? digest_type{} : *start; - - auto midpoint = std::bit_floor(static_cast(size)); - - // accumulate the first 2**N digests into start[0] - // ----------------------------------------------- - { - auto remaining = midpoint; - while (remaining > 1) { - for (size_t i = 0; i < remaining / 2; ++i) - start[i] = hash_combine(start[2 * i], start[(2 * i) + 1]); - remaining /= 2; - } - } - - if (midpoint == size) - return start[0]; - - // accumulate the rest of the digests into start[1], recursion limited to - // log2(size) so stack overflow is not a concern. - // ---------------------------------------------------------------------- - start[1] = calculate_merkle_2(start + midpoint, end); - - // combine and return - // ------------------ - return hash_combine(start[0], std::cref(start[1])); -} - } inline digest_type calculate_merkle(const deque& ids) { From 8ea41a4cf3c464df13730b7eb87ee0bd80b3c818 Mon Sep 17 00:00:00 2001 From: greg7mdp Date: Thu, 28 Mar 2024 20:29:00 -0400 Subject: [PATCH 09/26] Add alternative to `std::bit_floor` for cdt which does not support C++20 yet. --- .../include/eosio/chain/incremental_merkle.hpp | 8 -------- libraries/chain/include/eosio/chain/merkle.hpp | 16 ++++++++++++---- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/libraries/chain/include/eosio/chain/incremental_merkle.hpp b/libraries/chain/include/eosio/chain/incremental_merkle.hpp index 7fd7a7ce77..23bbed4ccf 100644 --- a/libraries/chain/include/eosio/chain/incremental_merkle.hpp +++ b/libraries/chain/include/eosio/chain/incremental_merkle.hpp @@ -6,14 +6,6 @@ namespace eosio::chain { -namespace detail { -#if __cplusplus >= 202002L - inline int popcount(uint64_t x) noexcept { return std::popcount(x); } -#else - inline int popcount(uint64_t x) noexcept { return __builtin_popcountll(x); } -#endif -} - class incremental_merkle_tree { public: void append(const digest_type& digest) { diff --git a/libraries/chain/include/eosio/chain/merkle.hpp b/libraries/chain/include/eosio/chain/merkle.hpp index e286b7cfae..b6db3653c5 100644 --- a/libraries/chain/include/eosio/chain/merkle.hpp +++ b/libraries/chain/include/eosio/chain/merkle.hpp @@ -7,6 +7,14 @@ namespace eosio::chain { namespace detail { +#if __cplusplus >= 202002L + inline int popcount(uint64_t x) noexcept { return std::popcount(x); } + inline uint64_t bit_floor(uint64_t x) noexcept { return std::bit_floor(x); } +#else + inline int popcount(uint64_t x) noexcept { return __builtin_popcountll(x); } + inline uint64_t bit_floor(uint64_t x) noexcept { return x == 0 ? 0ull : 1ull << (64 - 1 - __builtin_clzll(x)); } +#endif + inline digest_type hash_combine(const digest_type& a, const digest_type& b) { return digest_type::hash(std::make_pair(std::cref(a), std::cref(b))); } @@ -14,11 +22,11 @@ inline digest_type hash_combine(const digest_type& a, const digest_type& b) { // does not overwrite passed sequence // ---------------------------------- template -requires std::is_same_v::value_type, std::decay_t> +requires std::is_same_v::value_type>, digest_type> inline digest_type calculate_merkle_pow2(const It& start, const It& end) { auto size = end - start; assert(size >= 2); - assert(std::bit_floor(static_cast(size)) == size); + assert(detail::bit_floor(static_cast(size)) == size); if (size == 2) return hash_combine(start[0], start[1]); @@ -30,14 +38,14 @@ inline digest_type calculate_merkle_pow2(const It& start, const It& end) { template -requires std::is_same_v::value_type, digest_type> +requires std::is_same_v::value_type>, digest_type> inline digest_type calculate_merkle(const It& start, const It& end) { assert(end >= start); auto size = end - start; if (size <= 1) return (size == 0) ? digest_type{} : *start; - auto midpoint = std::bit_floor(static_cast(size)); + auto midpoint = detail::bit_floor(static_cast(size)); if (size == midpoint) return calculate_merkle_pow2(start, end); From d2f1c92db4975cb8eedd5aaccba71e49167cc71f Mon Sep 17 00:00:00 2001 From: greg7mdp Date: Fri, 29 Mar 2024 12:58:11 -0400 Subject: [PATCH 10/26] Add perf tests, fix warning. --- .../chain/include/eosio/chain/merkle.hpp | 5 +- unittests/merkle_tree_tests.cpp | 53 +++++++++++++++++++ 2 files changed, 55 insertions(+), 3 deletions(-) diff --git a/libraries/chain/include/eosio/chain/merkle.hpp b/libraries/chain/include/eosio/chain/merkle.hpp index b6db3653c5..0899e617db 100644 --- a/libraries/chain/include/eosio/chain/merkle.hpp +++ b/libraries/chain/include/eosio/chain/merkle.hpp @@ -36,16 +36,15 @@ inline digest_type calculate_merkle_pow2(const It& start, const It& end) { } } - template requires std::is_same_v::value_type>, digest_type> inline digest_type calculate_merkle(const It& start, const It& end) { assert(end >= start); - auto size = end - start; + auto size = static_cast(end - start); if (size <= 1) return (size == 0) ? digest_type{} : *start; - auto midpoint = detail::bit_floor(static_cast(size)); + auto midpoint = detail::bit_floor(size); if (size == midpoint) return calculate_merkle_pow2(start, end); diff --git a/unittests/merkle_tree_tests.cpp b/unittests/merkle_tree_tests.cpp index f74ffb0ed5..2fc708f86c 100644 --- a/unittests/merkle_tree_tests.cpp +++ b/unittests/merkle_tree_tests.cpp @@ -2,6 +2,7 @@ #include #include #include +#include using namespace eosio::chain; using eosio::chain::detail::make_legacy_digest_pair; @@ -254,5 +255,57 @@ BOOST_AUTO_TEST_CASE(stack_check) { } } +class stopwatch { +public: + stopwatch(std::string msg) : _msg(std::move(msg)) { _start = clock::now(); } + + ~stopwatch() { std::cout << _msg << get_time_us()/1000000 << " s\n"; } + + double get_time_us() const { + using duration_t = std::chrono::duration; + return std::chrono::duration_cast(clock::now() - _start).count(); + } + + using clock = std::chrono::high_resolution_clock; + using point = std::chrono::time_point; + + std::string _msg; + point _start; +}; + +BOOST_AUTO_TEST_CASE(perf_test) { + auto perf_test = [](const std::string& type, auto&& incr_tree, auto&& calc_fn) { + using namespace std::string_literals; + constexpr size_t num_digests = 1024ull * 1024ull; + + std::vector digests = create_test_digests(num_digests); + deque deq { digests.begin(), digests.end() }; + + auto incr_root = [&]() { + stopwatch s("time for "s + type + " incremental_merkle: "); + for (const auto& d : digests) + incr_tree.append(d); + return incr_tree.get_root(); + }(); + + auto calc_root = [&]() { + stopwatch s("time for "s + type + " calculate_merkle: "); + return calc_fn(deq); + }(); + + return std::make_pair(incr_root, calc_root); + }; + + { + auto [incr_root, calc_root] = perf_test("new", incremental_merkle_tree(), calculate_merkle); + BOOST_CHECK_EQUAL(incr_root, calc_root); + } + + { + auto [incr_root, calc_root] = perf_test("legacy", incremental_merkle_tree_legacy(), calculate_merkle_legacy); + BOOST_CHECK_EQUAL(incr_root, calc_root); + } +} + BOOST_AUTO_TEST_SUITE_END() From 12e6a53a2a71e9a3a4148c4701876d5c3a43ed00 Mon Sep 17 00:00:00 2001 From: greg7mdp Date: Fri, 29 Mar 2024 15:34:08 -0400 Subject: [PATCH 11/26] Add multithreading to , add test, remove stack size debug code --- .../eosio/chain/incremental_merkle.hpp | 14 ++-- .../chain/include/eosio/chain/merkle.hpp | 32 +++++++-- unittests/merkle_tree_tests.cpp | 67 ++++++++++++++----- 3 files changed, 82 insertions(+), 31 deletions(-) diff --git a/libraries/chain/include/eosio/chain/incremental_merkle.hpp b/libraries/chain/include/eosio/chain/incremental_merkle.hpp index 23bbed4ccf..ffcf909b2a 100644 --- a/libraries/chain/include/eosio/chain/incremental_merkle.hpp +++ b/libraries/chain/include/eosio/chain/incremental_merkle.hpp @@ -9,9 +9,8 @@ namespace eosio::chain { class incremental_merkle_tree { public: void append(const digest_type& digest) { - char c; assert(trees.size() == detail::popcount(mask)); - _append(digest, trees.end(), 0, &c); + _append(digest, trees.end(), 0); assert(trees.size() == detail::popcount(mask)); } @@ -22,8 +21,6 @@ class incremental_merkle_tree { return _get_root(0); }; - int64_t max_stack_depth = 0; - private: friend struct fc::reflector; using vec_it = std::vector::iterator; @@ -35,13 +32,11 @@ class incremental_merkle_tree { digest_type _get_root(size_t idx) const { if (idx + 1 == trees.size()) return trees[idx]; - return detail::hash_combine(trees[idx], _get_root(idx + 1)); + return detail::hash_combine(trees[idx], _get_root(idx + 1)); // log2 recursion OK } // slot points to the current insertion point. *(slot-1) is the digest for the first bit set >= idx - void _append(const digest_type& digest, vec_it slot, size_t idx, const char *p) { - char c; - max_stack_depth = std::max(max_stack_depth, p - &c); + void _append(const digest_type& digest, vec_it slot, size_t idx) { if (is_bit_set(idx)) { assert(!trees.empty()); if (!is_bit_set(idx+1)) { @@ -55,7 +50,8 @@ class incremental_merkle_tree { clear_bit(idx+1); digest_type d = detail::hash_combine(*(slot-2), detail::hash_combine(*(slot-1), digest)); trees.erase(slot-2, slot); - _append(d, slot-2, idx+2, p); + _append(d, slot-2, idx+2); // log2 recursion OK, uses less than 5KB stack space for 32M digests + // appended (or 0.25% of default 2MB thread stack size on Ubuntu) } } else { trees.insert(slot, digest); diff --git a/libraries/chain/include/eosio/chain/merkle.hpp b/libraries/chain/include/eosio/chain/merkle.hpp index 0899e617db..9a94a47cb8 100644 --- a/libraries/chain/include/eosio/chain/merkle.hpp +++ b/libraries/chain/include/eosio/chain/merkle.hpp @@ -2,6 +2,8 @@ #include #include #include +#include +#include namespace eosio::chain { @@ -20,8 +22,11 @@ inline digest_type hash_combine(const digest_type& a, const digest_type& b) { } // does not overwrite passed sequence -// ---------------------------------- -template +// +// log2 recursion OK, uses less than 5KB stack space for 32M digests +// appended (or 0.25% of default 2MB thread stack size on Ubuntu) +// ----------------------------------------------------------------- +template requires std::is_same_v::value_type>, digest_type> inline digest_type calculate_merkle_pow2(const It& start, const It& end) { auto size = end - start; @@ -31,8 +36,23 @@ inline digest_type calculate_merkle_pow2(const It& start, const It& end) { if (size == 2) return hash_combine(start[0], start[1]); else { - auto mid = start + size / 2; - return hash_combine(calculate_merkle_pow2(start, mid), calculate_merkle_pow2(mid, end)); + if (async && size >= 4096) { // below 4096, starting async threads is overkill + std::array, 4> fut; // size dictates the number of threads (must be power of two) + size_t slice_size = size / fut.size(); + + for (size_t i=0; i, + start + slice_size * i, start + slice_size * (i+1)); + + std::array res; + for (size_t i=0; i(start, end); auto mid = start + midpoint; - return hash_combine(calculate_merkle_pow2(start, mid), calculate_merkle(mid, end)); + return hash_combine(calculate_merkle_pow2(start, mid), calculate_merkle(mid, end)); } } diff --git a/unittests/merkle_tree_tests.cpp b/unittests/merkle_tree_tests.cpp index 2fc708f86c..390f4dfa8e 100644 --- a/unittests/merkle_tree_tests.cpp +++ b/unittests/merkle_tree_tests.cpp @@ -241,20 +241,6 @@ BOOST_AUTO_TEST_CASE(consistency_over_large_range) { } } -BOOST_AUTO_TEST_CASE(stack_check) { - constexpr size_t num_digests = 1024ull; - - std::vector digests = create_test_digests(num_digests); - incremental_merkle_tree tree; - for (size_t j=0; j digests = create_test_digests(num_digests); deque deq { digests.begin(), digests.end() }; @@ -308,4 +294,53 @@ BOOST_AUTO_TEST_CASE(perf_test) { } +BOOST_AUTO_TEST_CASE(perf_test_many_small) { + + auto perf_test = [](const std::string& type, auto&& incr_tree, auto&& calc_fn) { + using namespace std::string_literals; + constexpr size_t num_digests = 10000; // don't use exact powers of 2 as it is a special case + constexpr size_t num_runs = 100; + + std::vector digests = create_test_digests(num_digests); + deque deq { digests.begin(), digests.end() }; + + deque results(num_runs); + + auto incr = [&]() { + auto work_tree = incr_tree; + for (const auto& d : digests) + work_tree.append(d); + return work_tree.get_root(); + }; + + auto calc = [&]() { return calc_fn(deq); }; + + auto incr_root = [&]() { + stopwatch s("time for "s + type + " incremental_merkle: "); + for (auto& r : results) + r = incr(); + return calc_fn(results); + }(); + + auto calc_root = [&]() { + stopwatch s("time for "s + type + " calculate_merkle: "); + for (auto& r : results) + r = calc(); + return calc_fn(results); + }(); + + return std::make_pair(incr_root, calc_root); + }; + + { + auto [incr_root, calc_root] = perf_test("new", incremental_merkle_tree(), calculate_merkle); + BOOST_CHECK_EQUAL(incr_root, calc_root); + } + + { + auto [incr_root, calc_root] = perf_test("legacy", incremental_merkle_tree_legacy(), calculate_merkle_legacy); + BOOST_CHECK_EQUAL(incr_root, calc_root); + } +} + BOOST_AUTO_TEST_SUITE_END() From 209090e09b8b486cbe2ffe0f43594b1fd2b5e1d5 Mon Sep 17 00:00:00 2001 From: greg7mdp Date: Fri, 29 Mar 2024 17:00:46 -0400 Subject: [PATCH 12/26] Simplify `canonical` template param from `incremental_merkle_tree_legacy` and simplify `unittests/merkle_tree_tests.cpp` --- .../eosio/chain/incremental_merkle_legacy.hpp | 16 +-- unittests/merkle_tree_tests.cpp | 108 ++++-------------- 2 files changed, 27 insertions(+), 97 deletions(-) diff --git a/libraries/chain/include/eosio/chain/incremental_merkle_legacy.hpp b/libraries/chain/include/eosio/chain/incremental_merkle_legacy.hpp index 43b2586f55..5916fce081 100644 --- a/libraries/chain/include/eosio/chain/incremental_merkle_legacy.hpp +++ b/libraries/chain/include/eosio/chain/incremental_merkle_legacy.hpp @@ -50,7 +50,7 @@ inline void move_nodes(Container& to, Container&& from) { * * @param canonical if true use the merkle make_canonical_pair which sets the left/right bits of the hash */ -template class Container = vector, typename ...Args> +template class Container = vector, typename ...Args> class incremental_merkle_impl { public: incremental_merkle_impl() = default; @@ -144,11 +144,7 @@ class incremental_merkle_impl { // calculate the partially realized node value by implying the "right" value is identical // to the "left" value - if constexpr (canonical) { - top = DigestType::hash(detail::make_legacy_digest_pair(top, top)); - } else { - top = DigestType::hash(std::make_pair(std::cref(top), std::cref(top))); - } + top = DigestType::hash(detail::make_legacy_digest_pair(top, top)); partial = true; } else { // we are collapsing from a "right" value and an fully-realized "left" @@ -164,11 +160,7 @@ class incremental_merkle_impl { } // calculate the node - if constexpr (canonical) { - top = DigestType::hash(detail::make_legacy_digest_pair(left_value, top)); - } else { - top = DigestType::hash(std::make_pair(std::cref(left_value), std::cref(top))); - } + top = DigestType::hash(detail::make_legacy_digest_pair(left_value, top)); } // move up a level in the tree @@ -206,7 +198,7 @@ class incremental_merkle_impl { Container _active_nodes; }; -typedef incremental_merkle_impl incremental_merkle_tree_legacy; +typedef incremental_merkle_impl incremental_merkle_tree_legacy; } /// eosio::chain diff --git a/unittests/merkle_tree_tests.cpp b/unittests/merkle_tree_tests.cpp index 390f4dfa8e..c6e26d5d3f 100644 --- a/unittests/merkle_tree_tests.cpp +++ b/unittests/merkle_tree_tests.cpp @@ -17,6 +17,10 @@ std::vector create_test_digests(size_t n) { constexpr auto hash = eosio::chain::detail::hash_combine; +inline digest_type hash_l(const digest_type& a, const digest_type& b) { + return fc::sha256::hash(make_legacy_digest_pair(a, b)); +} + BOOST_AUTO_TEST_SUITE(merkle_tree_tests) BOOST_AUTO_TEST_CASE(basic_append_and_root_check_legacy) { @@ -46,114 +50,48 @@ BOOST_AUTO_TEST_CASE(multiple_appends_legacy) { BOOST_CHECK_EQUAL(calculate_merkle_legacy({node1}).str(), node1.str()); tree.append(node2); - BOOST_CHECK_EQUAL(tree.get_root().str(), fc::sha256::hash(make_legacy_digest_pair(node1, node2)).str()); - BOOST_CHECK_EQUAL(calculate_merkle_legacy({node1, node2}).str(), fc::sha256::hash(make_legacy_digest_pair(node1, node2)).str()); + BOOST_CHECK_EQUAL(tree.get_root().str(), hash_l(node1, node2).str()); + BOOST_CHECK_EQUAL(calculate_merkle_legacy({node1, node2}).str(), hash_l(node1, node2).str()); tree.append(node3); - BOOST_CHECK_EQUAL(tree.get_root().str(), fc::sha256::hash(make_legacy_digest_pair( - fc::sha256::hash(make_legacy_digest_pair(node1, node2)), - fc::sha256::hash(make_legacy_digest_pair(node3, node3)))).str()); - BOOST_CHECK_EQUAL(calculate_merkle_legacy({node1, node2, node3}).str(), fc::sha256::hash(make_legacy_digest_pair( - fc::sha256::hash(make_legacy_digest_pair(node1, node2)), - fc::sha256::hash(make_legacy_digest_pair(node3, node3)))).str()); + BOOST_CHECK_EQUAL(tree.get_root().str(), hash_l(hash_l(node1, node2),hash_l(node3, node3)).str()); + BOOST_CHECK_EQUAL(calculate_merkle_legacy({node1, node2, node3}).str(), hash_l(hash_l(node1, node2),hash_l(node3, node3)).str()); tree.append(node4); - auto calculated_root = fc::sha256::hash(make_legacy_digest_pair( - fc::sha256::hash(make_legacy_digest_pair(node1, node2)), - fc::sha256::hash(make_legacy_digest_pair(node3, node4)))); + auto calculated_root = hash_l(hash_l(node1, node2),hash_l(node3, node4)); BOOST_CHECK_EQUAL(tree.get_root().str(), calculated_root.str()); - BOOST_CHECK_EQUAL(calculate_merkle_legacy({node1, node2, node3, node4}).str(), calculated_root.str()); + BOOST_CHECK_EQUAL(calculate_merkle_legacy({node1, node2, node3, node4}).str(), + calculated_root.str()); tree.append(node5); - calculated_root = fc::sha256::hash( - make_legacy_digest_pair( - fc::sha256::hash(make_legacy_digest_pair( - fc::sha256::hash(make_legacy_digest_pair(node1, node2)), - fc::sha256::hash(make_legacy_digest_pair(node3, node4)) - )), - fc::sha256::hash(make_legacy_digest_pair( - fc::sha256::hash(make_legacy_digest_pair(node5, node5)), - fc::sha256::hash(make_legacy_digest_pair(node5, node5)) - )) - ) - ); + calculated_root = hash_l(hash_l(hash_l(node1, node2), hash_l(node3, node4)), + hash_l(hash_l(node5, node5), hash_l(node5, node5))); BOOST_CHECK_EQUAL(tree.get_root().str(), calculated_root.str()); BOOST_CHECK_EQUAL(calculate_merkle_legacy({node1, node2, node3, node4, node5}).str(), calculated_root.str()); tree.append(node6); - calculated_root = fc::sha256::hash( - make_legacy_digest_pair( - fc::sha256::hash(make_legacy_digest_pair( - fc::sha256::hash(make_legacy_digest_pair(node1, node2)), - fc::sha256::hash(make_legacy_digest_pair(node3, node4)) - )), - fc::sha256::hash(make_legacy_digest_pair( - fc::sha256::hash(make_legacy_digest_pair(node5, node6)), - fc::sha256::hash(make_legacy_digest_pair(node5, node6)) - )) - ) - ); + calculated_root = hash_l(hash_l(hash_l(node1, node2), hash_l(node3, node4)), + hash_l(hash_l(node5, node6), hash_l(node5, node6))); BOOST_CHECK_EQUAL(tree.get_root().str(), calculated_root.str()); BOOST_CHECK_EQUAL(calculate_merkle_legacy({node1, node2, node3, node4, node5, node6}).str(), calculated_root.str()); tree.append(node7); - calculated_root = fc::sha256::hash( - make_legacy_digest_pair( - fc::sha256::hash(make_legacy_digest_pair( - fc::sha256::hash(make_legacy_digest_pair(node1, node2)), - fc::sha256::hash(make_legacy_digest_pair(node3, node4)) - )), - fc::sha256::hash(make_legacy_digest_pair( - fc::sha256::hash(make_legacy_digest_pair(node5, node6)), - fc::sha256::hash(make_legacy_digest_pair(node7, node7)) - )) - ) - ); + calculated_root = hash_l(hash_l(hash_l(node1, node2), hash_l(node3, node4)), + hash_l(hash_l(node5, node6), hash_l(node7, node7))); BOOST_CHECK_EQUAL(tree.get_root().str(), calculated_root.str()); BOOST_CHECK_EQUAL(calculate_merkle_legacy({node1, node2, node3, node4, node5, node6, node7}).str(), calculated_root.str()); tree.append(node8); - calculated_root = fc::sha256::hash( - make_legacy_digest_pair( - fc::sha256::hash(make_legacy_digest_pair( - fc::sha256::hash(make_legacy_digest_pair(node1, node2)), - fc::sha256::hash(make_legacy_digest_pair(node3, node4)) - )), - fc::sha256::hash(make_legacy_digest_pair( - fc::sha256::hash(make_legacy_digest_pair(node5, node6)), - fc::sha256::hash(make_legacy_digest_pair(node7, node8)) - )) - ) - ); + calculated_root = hash_l(hash_l(hash_l(node1, node2), hash_l(node3, node4)), + hash_l(hash_l(node5, node6), hash_l(node7, node8))); BOOST_CHECK_EQUAL(tree.get_root().str(), calculated_root.str()); BOOST_CHECK_EQUAL(calculate_merkle_legacy({node1, node2, node3, node4, node5, node6, node7, node8}).str(), calculated_root.str()); tree.append(node9); - calculated_root = fc::sha256::hash(make_legacy_digest_pair( - fc::sha256::hash( - make_legacy_digest_pair( - fc::sha256::hash(make_legacy_digest_pair( - fc::sha256::hash(make_legacy_digest_pair(node1, node2)), - fc::sha256::hash(make_legacy_digest_pair(node3, node4)) - )), - fc::sha256::hash(make_legacy_digest_pair( - fc::sha256::hash(make_legacy_digest_pair(node5, node6)), - fc::sha256::hash(make_legacy_digest_pair(node7, node8)) - )) - ) - ), - fc::sha256::hash( - make_legacy_digest_pair( - fc::sha256::hash(make_legacy_digest_pair( - fc::sha256::hash(make_legacy_digest_pair(node9, node9)), - fc::sha256::hash(make_legacy_digest_pair(node9, node9)) - )), - fc::sha256::hash(make_legacy_digest_pair( - fc::sha256::hash(make_legacy_digest_pair(node9, node9)), - fc::sha256::hash(make_legacy_digest_pair(node9, node9)) - )) - ) - ) )); + calculated_root = hash_l(hash_l(hash_l(hash_l(node1, node2), hash_l(node3, node4)), + hash_l(hash_l(node5, node6), hash_l(node7, node8))), + hash_l(hash_l(hash_l(node9, node9),hash_l(node9, node9)), + hash_l(hash_l(node9, node9),hash_l(node9, node9)))); BOOST_CHECK_EQUAL(tree.get_root().str(), calculated_root.str()); BOOST_CHECK_EQUAL(calculate_merkle_legacy({node1, node2, node3, node4, node5, node6, node7, node8, node9}).str(), calculated_root.str()); } From 8ed91d9628b963cbda18aae1dc75ba90036bee23 Mon Sep 17 00:00:00 2001 From: greg7mdp Date: Fri, 29 Mar 2024 17:39:47 -0400 Subject: [PATCH 13/26] Update comments. --- libraries/chain/include/eosio/chain/incremental_merkle.hpp | 2 +- libraries/chain/include/eosio/chain/merkle.hpp | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/libraries/chain/include/eosio/chain/incremental_merkle.hpp b/libraries/chain/include/eosio/chain/incremental_merkle.hpp index ffcf909b2a..9325e3ed29 100644 --- a/libraries/chain/include/eosio/chain/incremental_merkle.hpp +++ b/libraries/chain/include/eosio/chain/incremental_merkle.hpp @@ -50,7 +50,7 @@ class incremental_merkle_tree { clear_bit(idx+1); digest_type d = detail::hash_combine(*(slot-2), detail::hash_combine(*(slot-1), digest)); trees.erase(slot-2, slot); - _append(d, slot-2, idx+2); // log2 recursion OK, uses less than 5KB stack space for 32M digests + _append(d, slot-2, idx+2); // log2 recursion OK, uses less than 5KB stack space for 4 billion digests // appended (or 0.25% of default 2MB thread stack size on Ubuntu) } } else { diff --git a/libraries/chain/include/eosio/chain/merkle.hpp b/libraries/chain/include/eosio/chain/merkle.hpp index 9a94a47cb8..b6e78c73f6 100644 --- a/libraries/chain/include/eosio/chain/merkle.hpp +++ b/libraries/chain/include/eosio/chain/merkle.hpp @@ -23,9 +23,9 @@ inline digest_type hash_combine(const digest_type& a, const digest_type& b) { // does not overwrite passed sequence // -// log2 recursion OK, uses less than 5KB stack space for 32M digests -// appended (or 0.25% of default 2MB thread stack size on Ubuntu) -// ----------------------------------------------------------------- +// log2 recursion OK, uses less than 5KB stack space for 4 billion digests +// appended (or 0.25% of default 2MB thread stack size on Ubuntu). +// ----------------------------------------------------------------------- template requires std::is_same_v::value_type>, digest_type> inline digest_type calculate_merkle_pow2(const It& start, const It& end) { From 0663a3d90ce7176e10786fbf698f07e914487c7f Mon Sep 17 00:00:00 2001 From: greg7mdp Date: Fri, 29 Mar 2024 18:30:39 -0400 Subject: [PATCH 14/26] Add `num_digests_appended()` method, not used but pretty cool doc on what `mask` means. --- libraries/chain/include/eosio/chain/incremental_merkle.hpp | 6 +++++- unittests/merkle_tree_tests.cpp | 4 +++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/libraries/chain/include/eosio/chain/incremental_merkle.hpp b/libraries/chain/include/eosio/chain/incremental_merkle.hpp index 9325e3ed29..0b3d5dd4f4 100644 --- a/libraries/chain/include/eosio/chain/incremental_merkle.hpp +++ b/libraries/chain/include/eosio/chain/incremental_merkle.hpp @@ -19,7 +19,11 @@ class incremental_merkle_tree { return {}; assert(!trees.empty()); return _get_root(0); - }; + } + + uint64_t num_digests_appended() const { + return mask; + } private: friend struct fc::reflector; diff --git a/unittests/merkle_tree_tests.cpp b/unittests/merkle_tree_tests.cpp index c6e26d5d3f..092a5545ba 100644 --- a/unittests/merkle_tree_tests.cpp +++ b/unittests/merkle_tree_tests.cpp @@ -173,8 +173,10 @@ BOOST_AUTO_TEST_CASE(consistency_over_large_range) { std::vector digests = create_test_digests(num_digests); for (size_t i=1; i Date: Fri, 29 Mar 2024 18:33:42 -0400 Subject: [PATCH 15/26] Reduce number of boost test assertions. --- unittests/merkle_tree_tests.cpp | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/unittests/merkle_tree_tests.cpp b/unittests/merkle_tree_tests.cpp index 092a5545ba..4e618783b7 100644 --- a/unittests/merkle_tree_tests.cpp +++ b/unittests/merkle_tree_tests.cpp @@ -173,10 +173,9 @@ BOOST_AUTO_TEST_CASE(consistency_over_large_range) { std::vector digests = create_test_digests(num_digests); for (size_t i=1; i Date: Fri, 29 Mar 2024 21:34:58 -0400 Subject: [PATCH 16/26] Small cleanup of the test. --- unittests/merkle_tree_tests.cpp | 45 +++++++++++++++++---------------- 1 file changed, 23 insertions(+), 22 deletions(-) diff --git a/unittests/merkle_tree_tests.cpp b/unittests/merkle_tree_tests.cpp index 4e618783b7..fb3ec25eb1 100644 --- a/unittests/merkle_tree_tests.cpp +++ b/unittests/merkle_tree_tests.cpp @@ -46,54 +46,55 @@ BOOST_AUTO_TEST_CASE(multiple_appends_legacy) { auto node9 = fc::sha256::hash("Node9"); tree.append(node1); - BOOST_CHECK_EQUAL(tree.get_root().str(), node1.str()); - BOOST_CHECK_EQUAL(calculate_merkle_legacy({node1}).str(), node1.str()); + BOOST_CHECK_EQUAL(tree.get_root(), node1); + BOOST_CHECK_EQUAL(calculate_merkle_legacy({node1}), node1); tree.append(node2); - BOOST_CHECK_EQUAL(tree.get_root().str(), hash_l(node1, node2).str()); - BOOST_CHECK_EQUAL(calculate_merkle_legacy({node1, node2}).str(), hash_l(node1, node2).str()); + auto calculated_root = hash_l(node1, node2); + BOOST_CHECK_EQUAL(tree.get_root(), calculated_root); + BOOST_CHECK_EQUAL(calculate_merkle_legacy({node1, node2}), calculated_root); tree.append(node3); - BOOST_CHECK_EQUAL(tree.get_root().str(), hash_l(hash_l(node1, node2),hash_l(node3, node3)).str()); - BOOST_CHECK_EQUAL(calculate_merkle_legacy({node1, node2, node3}).str(), hash_l(hash_l(node1, node2),hash_l(node3, node3)).str()); + calculated_root = hash_l(hash_l(node1, node2), hash_l(node3, node3)); + BOOST_CHECK_EQUAL(tree.get_root(), calculated_root); + BOOST_CHECK_EQUAL(calculate_merkle_legacy({node1, node2, node3}), calculated_root); tree.append(node4); - auto calculated_root = hash_l(hash_l(node1, node2),hash_l(node3, node4)); - BOOST_CHECK_EQUAL(tree.get_root().str(), calculated_root.str()); - BOOST_CHECK_EQUAL(calculate_merkle_legacy({node1, node2, node3, node4}).str(), - calculated_root.str()); + calculated_root = hash_l(hash_l(node1, node2), hash_l(node3, node4)); + BOOST_CHECK_EQUAL(tree.get_root(), calculated_root); + BOOST_CHECK_EQUAL(calculate_merkle_legacy({node1, node2, node3, node4}), calculated_root); tree.append(node5); calculated_root = hash_l(hash_l(hash_l(node1, node2), hash_l(node3, node4)), hash_l(hash_l(node5, node5), hash_l(node5, node5))); - BOOST_CHECK_EQUAL(tree.get_root().str(), calculated_root.str()); - BOOST_CHECK_EQUAL(calculate_merkle_legacy({node1, node2, node3, node4, node5}).str(), calculated_root.str()); + BOOST_CHECK_EQUAL(tree.get_root(), calculated_root); + BOOST_CHECK_EQUAL(calculate_merkle_legacy({node1, node2, node3, node4, node5}), calculated_root); tree.append(node6); calculated_root = hash_l(hash_l(hash_l(node1, node2), hash_l(node3, node4)), hash_l(hash_l(node5, node6), hash_l(node5, node6))); - BOOST_CHECK_EQUAL(tree.get_root().str(), calculated_root.str()); - BOOST_CHECK_EQUAL(calculate_merkle_legacy({node1, node2, node3, node4, node5, node6}).str(), calculated_root.str()); + BOOST_CHECK_EQUAL(tree.get_root(), calculated_root); + BOOST_CHECK_EQUAL(calculate_merkle_legacy({node1, node2, node3, node4, node5, node6}), calculated_root); tree.append(node7); calculated_root = hash_l(hash_l(hash_l(node1, node2), hash_l(node3, node4)), hash_l(hash_l(node5, node6), hash_l(node7, node7))); - BOOST_CHECK_EQUAL(tree.get_root().str(), calculated_root.str()); - BOOST_CHECK_EQUAL(calculate_merkle_legacy({node1, node2, node3, node4, node5, node6, node7}).str(), calculated_root.str()); + BOOST_CHECK_EQUAL(tree.get_root(), calculated_root); + BOOST_CHECK_EQUAL(calculate_merkle_legacy({node1, node2, node3, node4, node5, node6, node7}), calculated_root); tree.append(node8); calculated_root = hash_l(hash_l(hash_l(node1, node2), hash_l(node3, node4)), hash_l(hash_l(node5, node6), hash_l(node7, node8))); - BOOST_CHECK_EQUAL(tree.get_root().str(), calculated_root.str()); - BOOST_CHECK_EQUAL(calculate_merkle_legacy({node1, node2, node3, node4, node5, node6, node7, node8}).str(), calculated_root.str()); + BOOST_CHECK_EQUAL(tree.get_root(), calculated_root); + BOOST_CHECK_EQUAL(calculate_merkle_legacy({node1, node2, node3, node4, node5, node6, node7, node8}), calculated_root); tree.append(node9); calculated_root = hash_l(hash_l(hash_l(hash_l(node1, node2), hash_l(node3, node4)), hash_l(hash_l(node5, node6), hash_l(node7, node8))), - hash_l(hash_l(hash_l(node9, node9),hash_l(node9, node9)), - hash_l(hash_l(node9, node9),hash_l(node9, node9)))); - BOOST_CHECK_EQUAL(tree.get_root().str(), calculated_root.str()); - BOOST_CHECK_EQUAL(calculate_merkle_legacy({node1, node2, node3, node4, node5, node6, node7, node8, node9}).str(), calculated_root.str()); + hash_l(hash_l(hash_l(node9, node9), hash_l(node9, node9)), + hash_l(hash_l(node9, node9), hash_l(node9, node9)))); + BOOST_CHECK_EQUAL(tree.get_root(), calculated_root); + BOOST_CHECK_EQUAL(calculate_merkle_legacy({node1, node2, node3, node4, node5, node6, node7, node8, node9}), calculated_root); } BOOST_AUTO_TEST_CASE(basic_append_and_root_check) { From b5f522ff69a4157b68b03ace5c4e26f904bff94b Mon Sep 17 00:00:00 2001 From: greg7mdp Date: Fri, 29 Mar 2024 22:00:23 -0400 Subject: [PATCH 17/26] Cleanup messages from performance test. --- unittests/merkle_tree_tests.cpp | 34 ++++++++++++++++++--------------- 1 file changed, 19 insertions(+), 15 deletions(-) diff --git a/unittests/merkle_tree_tests.cpp b/unittests/merkle_tree_tests.cpp index fb3ec25eb1..52e7a74bba 100644 --- a/unittests/merkle_tree_tests.cpp +++ b/unittests/merkle_tree_tests.cpp @@ -171,7 +171,7 @@ BOOST_AUTO_TEST_CASE(multiple_appends) { BOOST_AUTO_TEST_CASE(consistency_over_large_range) { constexpr size_t num_digests = 1001ull; - std::vector digests = create_test_digests(num_digests); + const std::vector digests = create_test_digests(num_digests); for (size_t i=1; i; @@ -204,18 +204,20 @@ BOOST_AUTO_TEST_CASE(perf_test_one_large) { using namespace std::string_literals; constexpr size_t num_digests = 1000ull * 1000ull; // don't use exact powers of 2 as it is a special case - std::vector digests = create_test_digests(num_digests); - deque deq { digests.begin(), digests.end() }; + const std::vector digests = create_test_digests(num_digests); + const deque deq { digests.begin(), digests.end() }; + + auto msg_header = "1 sequence of "s + std::to_string(num_digests) + " digests: time for "s; auto incr_root = [&]() { - stopwatch s("time for "s + type + " incremental_merkle: "); + stopwatch s(msg_header + type + " incremental_merkle: "); for (const auto& d : digests) incr_tree.append(d); return incr_tree.get_root(); }(); auto calc_root = [&]() { - stopwatch s("time for "s + type + " calculate_merkle: "); + stopwatch s(msg_header + type + " calculate_merkle: "); return calc_fn(deq); }(); @@ -223,12 +225,12 @@ BOOST_AUTO_TEST_CASE(perf_test_one_large) { }; { - auto [incr_root, calc_root] = perf_test("new", incremental_merkle_tree(), calculate_merkle); + auto [incr_root, calc_root] = perf_test("savanna", incremental_merkle_tree(), calculate_merkle); BOOST_CHECK_EQUAL(incr_root, calc_root); } { - auto [incr_root, calc_root] = perf_test("legacy", incremental_merkle_tree_legacy(), calculate_merkle_legacy); + auto [incr_root, calc_root] = perf_test("legacy ", incremental_merkle_tree_legacy(), calculate_merkle_legacy); BOOST_CHECK_EQUAL(incr_root, calc_root); } } @@ -236,13 +238,13 @@ BOOST_AUTO_TEST_CASE(perf_test_one_large) { BOOST_AUTO_TEST_CASE(perf_test_many_small) { - auto perf_test = [](const std::string& type, auto&& incr_tree, auto&& calc_fn) { + auto perf_test = [](const std::string& type, const auto& incr_tree, auto&& calc_fn) { using namespace std::string_literals; constexpr size_t num_digests = 10000; // don't use exact powers of 2 as it is a special case constexpr size_t num_runs = 100; - std::vector digests = create_test_digests(num_digests); - deque deq { digests.begin(), digests.end() }; + const std::vector digests = create_test_digests(num_digests); + const deque deq { digests.begin(), digests.end() }; deque results(num_runs); @@ -255,15 +257,17 @@ BOOST_AUTO_TEST_CASE(perf_test_many_small) { auto calc = [&]() { return calc_fn(deq); }; + auto msg_header = std::to_string(num_runs) + " runs for a sequence of "s + std::to_string(num_digests) + " digests: time for "s; + auto incr_root = [&]() { - stopwatch s("time for "s + type + " incremental_merkle: "); + stopwatch s(msg_header + type + " incremental_merkle: "); for (auto& r : results) r = incr(); return calc_fn(results); }(); auto calc_root = [&]() { - stopwatch s("time for "s + type + " calculate_merkle: "); + stopwatch s(msg_header + type + " calculate_merkle: "); for (auto& r : results) r = calc(); return calc_fn(results); @@ -273,12 +277,12 @@ BOOST_AUTO_TEST_CASE(perf_test_many_small) { }; { - auto [incr_root, calc_root] = perf_test("new", incremental_merkle_tree(), calculate_merkle); + auto [incr_root, calc_root] = perf_test("savanna", incremental_merkle_tree(), calculate_merkle); BOOST_CHECK_EQUAL(incr_root, calc_root); } { - auto [incr_root, calc_root] = perf_test("legacy", incremental_merkle_tree_legacy(), calculate_merkle_legacy); + auto [incr_root, calc_root] = perf_test("legacy ", incremental_merkle_tree_legacy(), calculate_merkle_legacy); BOOST_CHECK_EQUAL(incr_root, calc_root); } } From 198e512f9b8d203322b87c7d45f253a1a21a3ca7 Mon Sep 17 00:00:00 2001 From: greg7mdp Date: Sat, 30 Mar 2024 14:47:34 -0400 Subject: [PATCH 18/26] Finish removing references to `canonical`. --- .../eosio/chain/incremental_merkle_legacy.hpp | 2 -- .../chain/include/eosio/chain/merkle_legacy.hpp | 14 +++++++------- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/libraries/chain/include/eosio/chain/incremental_merkle_legacy.hpp b/libraries/chain/include/eosio/chain/incremental_merkle_legacy.hpp index 5916fce081..44211cb19c 100644 --- a/libraries/chain/include/eosio/chain/incremental_merkle_legacy.hpp +++ b/libraries/chain/include/eosio/chain/incremental_merkle_legacy.hpp @@ -47,8 +47,6 @@ inline void move_nodes(Container& to, Container&& from) { * change. This allows proofs based on this merkle to be very stable * after some time has past only needing to update or add a single * value to maintain validity. - * - * @param canonical if true use the merkle make_canonical_pair which sets the left/right bits of the hash */ template class Container = vector, typename ...Args> class incremental_merkle_impl { diff --git a/libraries/chain/include/eosio/chain/merkle_legacy.hpp b/libraries/chain/include/eosio/chain/merkle_legacy.hpp index 35d4508aed..dc4a0ce2f8 100644 --- a/libraries/chain/include/eosio/chain/merkle_legacy.hpp +++ b/libraries/chain/include/eosio/chain/merkle_legacy.hpp @@ -7,15 +7,15 @@ namespace eosio::chain { namespace detail { inline digest_type make_legacy_left_digest(const digest_type& val) { - digest_type canonical_l = val; - canonical_l._hash[0] &= 0xFFFFFFFFFFFFFF7FULL; - return canonical_l; + digest_type left = val; + left._hash[0] &= 0xFFFFFFFFFFFFFF7FULL; + return left; } inline digest_type make_legacy_right_digest(const digest_type& val) { - digest_type canonical_r = val; - canonical_r._hash[0] |= 0x0000000000000080ULL; - return canonical_r; + digest_type right = val; + right._hash[0] |= 0x0000000000000080ULL; + return right; } inline bool is_legacy_left_digest(const digest_type& val) { @@ -34,7 +34,7 @@ namespace detail { /** * Calculates the merkle root of a set of digests, if ids is odd it will duplicate the last id. - * Uses make_canonical_pair which before hashing sets the first bit of the previous hashes + * Uses make_legacy_digest_pair which before hashing sets the first bit of the previous hashes * to 0 or 1 to indicate the side it is on. */ inline digest_type calculate_merkle_legacy( deque ids ) { From 89e3d8df92e53d34ee6f70cbc82337b351378f10 Mon Sep 17 00:00:00 2001 From: greg7mdp Date: Sat, 30 Mar 2024 15:41:21 -0400 Subject: [PATCH 19/26] Address PR comments, templatize `calculate_merkle`. --- libraries/chain/block_state.cpp | 4 +- libraries/chain/controller.cpp | 16 +++---- .../chain/include/eosio/chain/merkle.hpp | 47 ++++++++++++++----- .../include/eosio/chain/merkle_legacy.hpp | 2 +- unittests/merkle_tree_tests.cpp | 26 +++++----- 5 files changed, 59 insertions(+), 36 deletions(-) diff --git a/libraries/chain/block_state.cpp b/libraries/chain/block_state.cpp index 1528284875..bae2ea5d43 100644 --- a/libraries/chain/block_state.cpp +++ b/libraries/chain/block_state.cpp @@ -88,8 +88,8 @@ block_state_ptr block_state::create_if_genesis_block(const block_state_legacy& b result.valid_qc = {}; // best qc received from the network inside block extension, empty until first savanna proper IF block // Calculate Merkle tree root in Savanna way so that it is stored in Leaf Node when building block_state. - auto digests = *bsp.action_receipt_digests_savanna; - auto action_mroot_svnn = calculate_merkle(std::move(digests)); + const auto& digests = *bsp.action_receipt_digests_savanna; + auto action_mroot_svnn = calculate_merkle(digests); // build leaf_node and validation_tree valid_t::finality_leaf_node_t leaf_node { diff --git a/libraries/chain/controller.cpp b/libraries/chain/controller.cpp index 9d481ca7e8..9d51f738e0 100644 --- a/libraries/chain/controller.cpp +++ b/libraries/chain/controller.cpp @@ -708,13 +708,13 @@ struct building_block { auto [transaction_mroot, action_mroot] = std::visit( overloaded{[&](digests_t& trx_receipts) { // calculate the two merkle roots in separate threads auto trx_merkle_fut = - post_async_task(ioc, [&]() { return calculate_merkle(std::move(trx_receipts)); }); + post_async_task(ioc, [&]() { return calculate_merkle(trx_receipts); }); auto action_merkle_fut = - post_async_task(ioc, [&]() { return calculate_merkle(std::move(*action_receipts.digests_s)); }); + post_async_task(ioc, [&]() { return calculate_merkle(*action_receipts.digests_s); }); return std::make_pair(trx_merkle_fut.get(), action_merkle_fut.get()); }, [&](const checksum256_type& trx_checksum) { - return std::make_pair(trx_checksum, calculate_merkle(std::move(*action_receipts.digests_s))); + return std::make_pair(trx_checksum, calculate_merkle(*action_receipts.digests_s)); }}, trx_mroot_or_receipt_digests()); @@ -1308,8 +1308,8 @@ struct controller_impl { // IRREVERSIBLE applies (validates) blocks when irreversible, new_valid will be done after apply in log_irreversible assert(read_mode == db_read_mode::IRREVERSIBLE || legacy->action_receipt_digests_savanna); if (legacy->action_receipt_digests_savanna) { - auto digests = *legacy->action_receipt_digests_savanna; - auto action_mroot = calculate_merkle(std::move(digests)); + const auto& digests = *legacy->action_receipt_digests_savanna; + auto action_mroot = calculate_merkle(digests); // Create the valid structure for producing new_bsp->valid = prev->new_valid(*new_bsp, action_mroot, new_bsp->strong_digest); } @@ -1522,8 +1522,8 @@ struct controller_impl { validator_t{}, skip_validate_signee); // legacy_branch is from head, all should be validated assert(bspl->action_receipt_digests_savanna); - auto digests = *bspl->action_receipt_digests_savanna; - auto action_mroot = calculate_merkle(std::move(digests)); + const auto& digests = *bspl->action_receipt_digests_savanna; + auto action_mroot = calculate_merkle(digests); // Create the valid structure for producing new_bsp->valid = prev->new_valid(*new_bsp, action_mroot, new_bsp->strong_digest); prev = new_bsp; @@ -4066,7 +4066,7 @@ struct controller_impl { // @param if_active true if instant finality is active static checksum256_type calc_merkle( deque&& digests, bool if_active ) { if (if_active) { - return calculate_merkle( std::move(digests) ); + return calculate_merkle( digests ); } else { return calculate_merkle_legacy( std::move(digests) ); } diff --git a/libraries/chain/include/eosio/chain/merkle.hpp b/libraries/chain/include/eosio/chain/merkle.hpp index b6e78c73f6..91d26d5e21 100644 --- a/libraries/chain/include/eosio/chain/merkle.hpp +++ b/libraries/chain/include/eosio/chain/merkle.hpp @@ -21,11 +21,6 @@ inline digest_type hash_combine(const digest_type& a, const digest_type& b) { return digest_type::hash(std::make_pair(std::cref(a), std::cref(b))); } -// does not overwrite passed sequence -// -// log2 recursion OK, uses less than 5KB stack space for 4 billion digests -// appended (or 0.25% of default 2MB thread stack size on Ubuntu). -// ----------------------------------------------------------------------- template requires std::is_same_v::value_type>, digest_type> inline digest_type calculate_merkle_pow2(const It& start, const It& end) { @@ -56,8 +51,26 @@ inline digest_type calculate_merkle_pow2(const It& start, const It& end) { } } +} // namespace detail + +// ************* public interface starts here ************************************************ + +// ------------------------------------------------------------------------ +// calculate_merkle: +// ----------------- +// takes two random access iterators delimiting a sequence of `digest_type`, +// returns the root digest for the provided sequence. +// +// does not overwrite passed sequence +// +// log2 recursion OK, uses less than 5KB stack space for 4 billion digests +// appended (or 0.25% of default 2MB thread stack size on Ubuntu). +// ------------------------------------------------------------------------ template -requires std::is_same_v::value_type>, digest_type> +#if __cplusplus >= 202002L +requires std::random_access_iterator && + std::is_same_v::value_type>, digest_type> +#endif inline digest_type calculate_merkle(const It& start, const It& end) { assert(end >= start); auto size = static_cast(end - start); @@ -66,16 +79,26 @@ inline digest_type calculate_merkle(const It& start, const It& end) { auto midpoint = detail::bit_floor(size); if (size == midpoint) - return calculate_merkle_pow2(start, end); + return detail::calculate_merkle_pow2(start, end); auto mid = start + midpoint; - return hash_combine(calculate_merkle_pow2(start, mid), calculate_merkle(mid, end)); + return detail::hash_combine(detail::calculate_merkle_pow2(start, mid), + calculate_merkle(mid, end)); } -} - -inline digest_type calculate_merkle(const deque& ids) { - return detail::calculate_merkle(ids.cbegin(), ids.cend()); +// -------------------------------------------------------------------------- +// calculate_merkle: +// ----------------- +// takes a container or `std::span` of `digest_type`, returns the root digest +// for the sequence of digests in the container. +// -------------------------------------------------------------------------- +template +#if __cplusplus >= 202002L +requires std::random_access_iterator && + std::is_same_v, digest_type> +#endif +inline digest_type calculate_merkle(const Cont& ids) { + return calculate_merkle(ids.begin(), ids.end()); // cbegin not supported for std::span until C++23. } diff --git a/libraries/chain/include/eosio/chain/merkle_legacy.hpp b/libraries/chain/include/eosio/chain/merkle_legacy.hpp index dc4a0ce2f8..5746f2e07f 100644 --- a/libraries/chain/include/eosio/chain/merkle_legacy.hpp +++ b/libraries/chain/include/eosio/chain/merkle_legacy.hpp @@ -30,7 +30,7 @@ namespace detail { return make_pair(make_legacy_left_digest(l), make_legacy_right_digest(r)); }; -} +} // namespace detail /** * Calculates the merkle root of a set of digests, if ids is odd it will duplicate the last id. diff --git a/unittests/merkle_tree_tests.cpp b/unittests/merkle_tree_tests.cpp index 52e7a74bba..066faafdcd 100644 --- a/unittests/merkle_tree_tests.cpp +++ b/unittests/merkle_tree_tests.cpp @@ -104,7 +104,7 @@ BOOST_AUTO_TEST_CASE(basic_append_and_root_check) { auto node1 = fc::sha256::hash("Node1"); tree.append(node1); BOOST_CHECK_EQUAL(tree.get_root(), node1); - BOOST_CHECK_EQUAL(calculate_merkle({node1}), node1); + BOOST_CHECK_EQUAL(calculate_merkle(std::span(&node1, 1)), node1); } BOOST_AUTO_TEST_CASE(multiple_appends) { @@ -124,48 +124,48 @@ BOOST_AUTO_TEST_CASE(multiple_appends) { tree.append(node1); BOOST_CHECK_EQUAL(tree.get_root(), node1); - BOOST_CHECK_EQUAL(calculate_merkle({node1}), node1); + BOOST_CHECK_EQUAL(calculate_merkle(std::span(first, 1)), node1); tree.append(node2); BOOST_CHECK_EQUAL(tree.get_root(), hash(node1, node2)); - BOOST_CHECK_EQUAL(calculate_merkle({first, first + 2}), hash(node1, node2)); + BOOST_CHECK_EQUAL(calculate_merkle(std::span(first, 2)), hash(node1, node2)); tree.append(node3); auto calculated_root = hash(hash(node1, node2), node3); BOOST_CHECK_EQUAL(tree.get_root(), calculated_root); - BOOST_CHECK_EQUAL(calculate_merkle({first, first + 3}), calculated_root); + BOOST_CHECK_EQUAL(calculate_merkle(std::span(first, 3)), calculated_root); tree.append(node4); auto first_four_tree = hash(hash(node1, node2), hash(node3, node4)); calculated_root = first_four_tree; BOOST_CHECK_EQUAL(tree.get_root(), calculated_root); - BOOST_CHECK_EQUAL(calculate_merkle({first, first + 4}), calculated_root); + BOOST_CHECK_EQUAL(calculate_merkle(std::span(first, 4)), calculated_root); tree.append(node5); calculated_root = hash(first_four_tree, node5); BOOST_CHECK_EQUAL(tree.get_root(), calculated_root); - BOOST_CHECK_EQUAL(calculate_merkle({first, first + 5}), calculated_root); + BOOST_CHECK_EQUAL(calculate_merkle(std::span(first, 5)), calculated_root); tree.append(node6); calculated_root = hash(first_four_tree, hash(node5, node6)); BOOST_CHECK_EQUAL(tree.get_root(), calculated_root); - BOOST_CHECK_EQUAL(calculate_merkle({first, first + 6}), calculated_root); + BOOST_CHECK_EQUAL(calculate_merkle(std::span(first, 6)), calculated_root); tree.append(node7); calculated_root = hash(first_four_tree, hash(hash(node5, node6), node7)); BOOST_CHECK_EQUAL(tree.get_root(), calculated_root); - BOOST_CHECK_EQUAL(calculate_merkle({first, first + 7}), calculated_root); + BOOST_CHECK_EQUAL(calculate_merkle(std::span(first, 7)), calculated_root); tree.append(node8); auto next_four_tree = hash(hash(node5, node6), hash(node7, node8)); calculated_root = hash(first_four_tree, next_four_tree); BOOST_CHECK_EQUAL(tree.get_root(), calculated_root); - BOOST_CHECK_EQUAL(calculate_merkle({first, first + 8}), calculated_root); + BOOST_CHECK_EQUAL(calculate_merkle(std::span(first, 8)), calculated_root); tree.append(node9); calculated_root = hash(hash(first_four_tree, next_four_tree), node9); BOOST_CHECK_EQUAL(tree.get_root(), calculated_root); - BOOST_CHECK_EQUAL(calculate_merkle({first, first + 9}), calculated_root); + BOOST_CHECK_EQUAL(calculate_merkle(std::span(first, 9)), calculated_root); } BOOST_AUTO_TEST_CASE(consistency_over_large_range) { @@ -177,7 +177,7 @@ BOOST_AUTO_TEST_CASE(consistency_over_large_range) { for (size_t j=0; j>); BOOST_CHECK_EQUAL(incr_root, calc_root); } @@ -277,7 +277,7 @@ BOOST_AUTO_TEST_CASE(perf_test_many_small) { }; { - auto [incr_root, calc_root] = perf_test("savanna", incremental_merkle_tree(), calculate_merkle); + auto [incr_root, calc_root] = perf_test("savanna", incremental_merkle_tree(), calculate_merkle>); BOOST_CHECK_EQUAL(incr_root, calc_root); } From b86a162cd797fd6da10d826e2052cec796d1567a Mon Sep 17 00:00:00 2001 From: greg7mdp Date: Sat, 30 Mar 2024 15:52:22 -0400 Subject: [PATCH 20/26] Remove `post_async_task` for the `calculate_merkle` of action/transaction receipts. --- libraries/chain/controller.cpp | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/libraries/chain/controller.cpp b/libraries/chain/controller.cpp index 9d51f738e0..58757b5fa7 100644 --- a/libraries/chain/controller.cpp +++ b/libraries/chain/controller.cpp @@ -706,15 +706,13 @@ struct building_block { [&](building_block_if& bb) -> assembled_block { // compute the action_mroot and transaction_mroot auto [transaction_mroot, action_mroot] = std::visit( - overloaded{[&](digests_t& trx_receipts) { // calculate the two merkle roots in separate threads - auto trx_merkle_fut = - post_async_task(ioc, [&]() { return calculate_merkle(trx_receipts); }); - auto action_merkle_fut = - post_async_task(ioc, [&]() { return calculate_merkle(*action_receipts.digests_s); }); - return std::make_pair(trx_merkle_fut.get(), action_merkle_fut.get()); + overloaded{[&](digests_t& trx_receipts) { + return std::make_pair(calculate_merkle(trx_receipts), // ~15us for 100 digests + calculate_merkle(*action_receipts.digests_s)); }, [&](const checksum256_type& trx_checksum) { - return std::make_pair(trx_checksum, calculate_merkle(*action_receipts.digests_s)); + return std::make_pair(trx_checksum, + calculate_merkle(*action_receipts.digests_s)); }}, trx_mroot_or_receipt_digests()); From f2a3c27e881df2f5f2ff1a50493fe7720377374d Mon Sep 17 00:00:00 2001 From: greg7mdp Date: Sat, 30 Mar 2024 16:51:59 -0400 Subject: [PATCH 21/26] Update `calculate_merkle_pow2` to use 2 threads for sizes from 256 to 2048. --- libraries/chain/controller.cpp | 2 +- .../chain/include/eosio/chain/merkle.hpp | 29 ++++++++++++------- 2 files changed, 20 insertions(+), 11 deletions(-) diff --git a/libraries/chain/controller.cpp b/libraries/chain/controller.cpp index 58757b5fa7..3986ebedce 100644 --- a/libraries/chain/controller.cpp +++ b/libraries/chain/controller.cpp @@ -707,7 +707,7 @@ struct building_block { // compute the action_mroot and transaction_mroot auto [transaction_mroot, action_mroot] = std::visit( overloaded{[&](digests_t& trx_receipts) { - return std::make_pair(calculate_merkle(trx_receipts), // ~15us for 100 digests + return std::make_pair(calculate_merkle(trx_receipts), // ~0.06ms for 300 digests calculate_merkle(*action_receipts.digests_s)); }, [&](const checksum256_type& trx_checksum) { diff --git a/libraries/chain/include/eosio/chain/merkle.hpp b/libraries/chain/include/eosio/chain/merkle.hpp index 91d26d5e21..2538d516f4 100644 --- a/libraries/chain/include/eosio/chain/merkle.hpp +++ b/libraries/chain/include/eosio/chain/merkle.hpp @@ -31,19 +31,28 @@ inline digest_type calculate_merkle_pow2(const It& start, const It& end) { if (size == 2) return hash_combine(start[0], start[1]); else { - if (async && size >= 4096) { // below 4096, starting async threads is overkill - std::array, 4> fut; // size dictates the number of threads (must be power of two) - size_t slice_size = size / fut.size(); + if (async && size >= 256) { + auto async_calculate_merkle_pow2 = [&start, &size](auto fut) { + size_t slice_size = size / fut.size(); - for (size_t i=0; i, - start + slice_size * i, start + slice_size * (i+1)); + for (size_t i=0; i, + start + slice_size * i, start + slice_size * (i+1)); - std::array res; - for (size_t i=0; i res; - return calculate_merkle_pow2(res.begin(), res.end()); + for (size_t i=0; i= 2048) { + // use 4 threads. Future array size dictates the number of threads (must be power of two) + return async_calculate_merkle_pow2(std::array, 4>()); + } + // use 2 threads. Future array size dictates the number of threads (must be power of two) + return async_calculate_merkle_pow2(std::array, 2>()); } else { auto mid = start + size / 2; return hash_combine(calculate_merkle_pow2(start, mid), calculate_merkle_pow2(mid, end)); From 808d5869161618f0a8b3d442b8359bc8d62cd255 Mon Sep 17 00:00:00 2001 From: greg7mdp Date: Sat, 30 Mar 2024 17:36:31 -0400 Subject: [PATCH 22/26] Update comment. --- libraries/chain/controller.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/libraries/chain/controller.cpp b/libraries/chain/controller.cpp index 3986ebedce..599d040dae 100644 --- a/libraries/chain/controller.cpp +++ b/libraries/chain/controller.cpp @@ -707,7 +707,8 @@ struct building_block { // compute the action_mroot and transaction_mroot auto [transaction_mroot, action_mroot] = std::visit( overloaded{[&](digests_t& trx_receipts) { - return std::make_pair(calculate_merkle(trx_receipts), // ~0.06ms for 300 digests + // calculate_merkle takes 3.2ms for 50,000 digests (legacy version took 11.1ms) + return std::make_pair(calculate_merkle(trx_receipts), calculate_merkle(*action_receipts.digests_s)); }, [&](const checksum256_type& trx_checksum) { From 95f06f2e59c3f58ae389d910221ce39b10af05c2 Mon Sep 17 00:00:00 2001 From: greg7mdp Date: Sat, 30 Mar 2024 18:12:12 -0400 Subject: [PATCH 23/26] Fix gcc-11 compiler warnings. --- libraries/chain/include/eosio/chain/incremental_merkle.hpp | 4 ++-- libraries/chain/include/eosio/chain/merkle.hpp | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/libraries/chain/include/eosio/chain/incremental_merkle.hpp b/libraries/chain/include/eosio/chain/incremental_merkle.hpp index 0b3d5dd4f4..526041ed07 100644 --- a/libraries/chain/include/eosio/chain/incremental_merkle.hpp +++ b/libraries/chain/include/eosio/chain/incremental_merkle.hpp @@ -9,9 +9,9 @@ namespace eosio::chain { class incremental_merkle_tree { public: void append(const digest_type& digest) { - assert(trees.size() == detail::popcount(mask)); + assert(trees.size() == static_cast(detail::popcount(mask))); _append(digest, trees.end(), 0); - assert(trees.size() == detail::popcount(mask)); + assert(trees.size() == static_cast(detail::popcount(mask))); } digest_type get_root() const { diff --git a/libraries/chain/include/eosio/chain/merkle.hpp b/libraries/chain/include/eosio/chain/merkle.hpp index 2538d516f4..8bdaa2a308 100644 --- a/libraries/chain/include/eosio/chain/merkle.hpp +++ b/libraries/chain/include/eosio/chain/merkle.hpp @@ -24,9 +24,9 @@ inline digest_type hash_combine(const digest_type& a, const digest_type& b) { template requires std::is_same_v::value_type>, digest_type> inline digest_type calculate_merkle_pow2(const It& start, const It& end) { - auto size = end - start; - assert(size >= 2); - assert(detail::bit_floor(static_cast(size)) == size); + assert(end >= start + 2); + auto size = static_cast(end - start); + assert(detail::bit_floor(size) == size); if (size == 2) return hash_combine(start[0], start[1]); From 064717e28d5eb3018dc9c45214bec0794027d951 Mon Sep 17 00:00:00 2001 From: greg7mdp Date: Sat, 30 Mar 2024 18:40:58 -0400 Subject: [PATCH 24/26] Fix compilation error with gcc10. --- unittests/merkle_tree_tests.cpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/unittests/merkle_tree_tests.cpp b/unittests/merkle_tree_tests.cpp index 066faafdcd..bc49a48578 100644 --- a/unittests/merkle_tree_tests.cpp +++ b/unittests/merkle_tree_tests.cpp @@ -225,7 +225,8 @@ BOOST_AUTO_TEST_CASE(perf_test_one_large) { }; { - auto [incr_root, calc_root] = perf_test("savanna", incremental_merkle_tree(), calculate_merkle>); + auto [incr_root, calc_root] = perf_test("savanna", incremental_merkle_tree(), + [](const deque& d) { return calculate_merkle(d); }); // gcc10 needs a lambda here BOOST_CHECK_EQUAL(incr_root, calc_root); } @@ -277,7 +278,8 @@ BOOST_AUTO_TEST_CASE(perf_test_many_small) { }; { - auto [incr_root, calc_root] = perf_test("savanna", incremental_merkle_tree(), calculate_merkle>); + auto [incr_root, calc_root] = perf_test("savanna", incremental_merkle_tree(), + [](const deque& d) { return calculate_merkle(d); }); // gcc10 needs a lambda here BOOST_CHECK_EQUAL(incr_root, calc_root); } From 54e5d7062bf15da8c6ff30a1f4bb62154af53fae Mon Sep 17 00:00:00 2001 From: greg7mdp Date: Sat, 30 Mar 2024 18:41:19 -0400 Subject: [PATCH 25/26] Fix typo. --- .../chain/include/eosio/chain/incremental_merkle_legacy.hpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/chain/include/eosio/chain/incremental_merkle_legacy.hpp b/libraries/chain/include/eosio/chain/incremental_merkle_legacy.hpp index 44211cb19c..0607ee9e3b 100644 --- a/libraries/chain/include/eosio/chain/incremental_merkle_legacy.hpp +++ b/libraries/chain/include/eosio/chain/incremental_merkle_legacy.hpp @@ -45,7 +45,7 @@ inline void move_nodes(Container& to, Container&& from) { * * Once a sub-tree contains only realized nodes its sub-root will never * change. This allows proofs based on this merkle to be very stable - * after some time has past only needing to update or add a single + * after some time has passed, only needing to update or add a single * value to maintain validity. */ template class Container = vector, typename ...Args> From 82178a7baf87cfbdfc60dc43ce9a8b4597eae554 Mon Sep 17 00:00:00 2001 From: greg7mdp Date: Sat, 30 Mar 2024 18:41:36 -0400 Subject: [PATCH 26/26] Add class comment for `incremental_merkle_tree`. --- .../eosio/chain/incremental_merkle.hpp | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/libraries/chain/include/eosio/chain/incremental_merkle.hpp b/libraries/chain/include/eosio/chain/incremental_merkle.hpp index 526041ed07..3e9f9dbb01 100644 --- a/libraries/chain/include/eosio/chain/incremental_merkle.hpp +++ b/libraries/chain/include/eosio/chain/incremental_merkle.hpp @@ -6,6 +6,28 @@ namespace eosio::chain { +/** + * A balanced merkle tree built in such that the set of leaf nodes can be + * appended to without triggering the reconstruction of previously + * constructed nodes. + * + * this is achieved by keeping all possible power of two size trees, so + * for example: + * - after appending 3 digests, we have one `tree of two` digests, and a + * single digest. the mask os 0b11. + * - when appending another digest, a new `tree of two` is constructed with + * the single digest, and these two `trees of two` are conbined in a `tree + * of four`. The original tree of two is unchanged. + * Only the tree of four is stored, the mask 0b100 indicating its rank (4) + * - when appending another digest, the `tree of four` is unchanged, we store + * the new single digest. The mask 0b101 indicates that the tow digest stored + * are the roots of one `tree of four` and one `tree of one` (single digest) + * + * Once a sub-tree is constructed, its sub-root will never change. + * This allows proofs based on this merkle to be very stable + * after some time has passed, only needing to update or add a single + * value to maintain validity. + */ class incremental_merkle_tree { public: void append(const digest_type& digest) {