diff --git a/include/xrpl/shamap/SHAMapAccountStateLeafNode.h b/include/xrpl/shamap/SHAMapAccountStateLeafNode.h index e388d205d1..2110bc767d 100644 --- a/include/xrpl/shamap/SHAMapAccountStateLeafNode.h +++ b/include/xrpl/shamap/SHAMapAccountStateLeafNode.h @@ -45,6 +45,12 @@ public: hash_ = SHAMapHash{sha512Half(HashPrefix::LeafNode, item_->slice(), item_->key())}; } + std::size_t + sizeForWire() const final + { + return item_->size() + uint256::kBytes + sizeof(std::uint8_t); + } + void serializeForWire(Serializer& s) const final { diff --git a/include/xrpl/shamap/SHAMapInnerNode.h b/include/xrpl/shamap/SHAMapInnerNode.h index cafb498218..7dd5c0f45d 100644 --- a/include/xrpl/shamap/SHAMapInnerNode.h +++ b/include/xrpl/shamap/SHAMapInnerNode.h @@ -17,6 +17,12 @@ public: /** Each inner node has 16 children (the 'radix tree' part of the map) */ static constexpr unsigned int kBranchFactor = 16; + /** Branch count below which compressed wire format is used instead of full. + Compressed format encodes only populated branches, costing 33 bytes each + plus a 1-byte type tag. Full format always emits all 16 hashes (513 + bytes). At this threshold, compressed is still smaller than full. */ + static constexpr unsigned int kCompressedThreshold = 12; + private: /** Opaque type that contains the `hashes` array (array of type `SHAMapHash`) and the `children` array (array of type @@ -149,6 +155,9 @@ public: void updateHashDeep(); + std::size_t + sizeForWire() const override; + void serializeForWire(Serializer&) const override; diff --git a/include/xrpl/shamap/SHAMapLeafNode.h b/include/xrpl/shamap/SHAMapLeafNode.h index af5ed29702..5b3994197b 100644 --- a/include/xrpl/shamap/SHAMapLeafNode.h +++ b/include/xrpl/shamap/SHAMapLeafNode.h @@ -36,6 +36,9 @@ public: return false; } + std::size_t + sizeForWire() const override = 0; + void invariants(bool isRoot = false) const final; diff --git a/include/xrpl/shamap/SHAMapTreeNode.h b/include/xrpl/shamap/SHAMapTreeNode.h index 5cca2ea41a..9ee5ac9806 100644 --- a/include/xrpl/shamap/SHAMapTreeNode.h +++ b/include/xrpl/shamap/SHAMapTreeNode.h @@ -142,6 +142,10 @@ public: virtual bool isInner() const = 0; + /** Returns the exact number of bytes that serializeForWire will emit. */ + virtual std::size_t + sizeForWire() const = 0; + /** Serialize the node in a format appropriate for sending over the wire */ virtual void serializeForWire(Serializer&) const = 0; diff --git a/include/xrpl/shamap/SHAMapTxLeafNode.h b/include/xrpl/shamap/SHAMapTxLeafNode.h index 49f4f90906..6fc7b79a9b 100644 --- a/include/xrpl/shamap/SHAMapTxLeafNode.h +++ b/include/xrpl/shamap/SHAMapTxLeafNode.h @@ -44,6 +44,12 @@ public: hash_ = SHAMapHash{sha512Half(HashPrefix::TransactionId, item_->slice())}; } + std::size_t + sizeForWire() const final + { + return item_->size() + sizeof(std::uint8_t); + } + void serializeForWire(Serializer& s) const final { diff --git a/include/xrpl/shamap/SHAMapTxPlusMetaLeafNode.h b/include/xrpl/shamap/SHAMapTxPlusMetaLeafNode.h index 3f4163ac41..ad105c1899 100644 --- a/include/xrpl/shamap/SHAMapTxPlusMetaLeafNode.h +++ b/include/xrpl/shamap/SHAMapTxPlusMetaLeafNode.h @@ -45,6 +45,12 @@ public: hash_ = SHAMapHash{sha512Half(HashPrefix::TxNode, item_->slice(), item_->key())}; } + std::size_t + sizeForWire() const final + { + return item_->size() + uint256::kBytes + sizeof(std::uint8_t); + } + void serializeForWire(Serializer& s) const final { diff --git a/src/libxrpl/shamap/SHAMapInnerNode.cpp b/src/libxrpl/shamap/SHAMapInnerNode.cpp index ee6ebf7f3f..c86f92e14e 100644 --- a/src/libxrpl/shamap/SHAMapInnerNode.cpp +++ b/src/libxrpl/shamap/SHAMapInnerNode.cpp @@ -219,13 +219,22 @@ SHAMapInnerNode::updateHashDeep() updateHash(); } +std::size_t +SHAMapInnerNode::sizeForWire() const +{ + auto const n = getBranchCount(); + if (n < kCompressedThreshold) + return n * (uint256::kBytes + sizeof(std::uint8_t)) + sizeof(std::uint8_t); + return kBranchFactor * uint256::kBytes + sizeof(std::uint8_t); +} + void SHAMapInnerNode::serializeForWire(Serializer& s) const { XRPL_ASSERT(!isEmpty(), "xrpl::SHAMapInnerNode::serializeForWire : is non-empty"); // If the node is sparse, then only send non-empty branches: - if (getBranchCount() < 12) + if (getBranchCount() < kCompressedThreshold) { // compressed node auto hashes = hashesAndChildren_.getHashes(); diff --git a/src/libxrpl/shamap/SHAMapSync.cpp b/src/libxrpl/shamap/SHAMapSync.cpp index dc4f4ffe27..71cb849f06 100644 --- a/src/libxrpl/shamap/SHAMapSync.cpp +++ b/src/libxrpl/shamap/SHAMapSync.cpp @@ -454,9 +454,7 @@ SHAMap::getNodeFat( std::tie(node, nodeID, depth) = stack.top(); stack.pop(); - // Use a fresh Serializer per node and move its buffer into `data` rather than copying it - // via Serializer::getData(): the move is O(1) whereas the copy was O(node size). - Serializer s; + Serializer s(node->sizeForWire()); node->serializeForWire(s); data.emplace_back(nodeID, std::move(s.modData())); @@ -486,7 +484,7 @@ SHAMap::getNodeFat( else if (childNode->isInner() || fatLeaves) { // Just include this node - Serializer cs; + Serializer cs(childNode->sizeForWire()); childNode->serializeForWire(cs); data.emplace_back(childID, std::move(cs.modData())); } @@ -793,7 +791,7 @@ SHAMap::getProofPath(uint256 const& key) const path.reserve(stack.size()); while (!stack.empty()) { - Serializer s; + Serializer s(stack.top().first->sizeForWire()); stack.top().first->serializeForWire(s); path.emplace_back(std::move(s.modData())); stack.pop();