#include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace xrpl { void SHAMap::visitLeaves( std::function const& item)> const& leafFunction) const { visitNodes([&leafFunction](SHAMapTreeNode& node) { if (!node.isInner()) leafFunction(safe_downcast(node).peekItem()); return true; }); } void SHAMap::visitNodes(std::function const& function) const { if (!root_) return; function(*root_); if (!root_->isInner()) return; using StackEntry = std::pair>; std::stack> stack; auto node = intr_ptr::static_pointer_cast(root_); int pos = 0; while (true) { while (pos < 16) { if (!node->isEmptyBranch(pos)) { SHAMapTreeNodePtr const child = descendNoStore(*node, pos); if (!function(*child)) return; if (child->isLeaf()) { ++pos; } else { // If there are no more children, don't push this node while ((pos != 15) && (node->isEmptyBranch(pos + 1))) ++pos; if (pos != 15) { // save next position to resume at stack.emplace(pos + 1, std::move(node)); } // descend to the child's first position node = intr_ptr::static_pointer_cast(child); pos = 0; } } else { ++pos; // move to next position } } if (stack.empty()) break; std::tie(pos, node) = stack.top(); stack.pop(); } } void SHAMap::visitDifferences( SHAMap const* have, std::function const& function) const { // Visit every node in this SHAMap that is not present // in the specified SHAMap if (!root_) return; if (root_->getHash().isZero()) return; if ((have != nullptr) && (root_->getHash() == have->root_->getHash())) return; if (root_->isLeaf()) { auto leaf = intr_ptr::static_pointer_cast(root_); if ((have == nullptr) || !have->hasLeafNode(leaf->peekItem()->key(), leaf->getHash())) function(*root_); return; } // contains unexplored non-matching inner node entries using StackEntry = std::pair; std::stack> stack; stack.emplace(safe_downcast(root_.get()), SHAMapNodeID{}); while (!stack.empty()) { auto const [node, nodeID] = stack.top(); stack.pop(); // 1) Add this node to the pack if (!function(*node)) return; // 2) push non-matching child inner nodes for (int i = 0; i < 16; ++i) { if (!node->isEmptyBranch(i)) { auto const& childHash = node->getChildHash(i); SHAMapNodeID const childID = nodeID.getChildNodeID(i); auto next = descendThrow(node, i); if (next->isInner()) { if ((have == nullptr) || !have->hasInnerNode(childID, childHash)) stack.emplace(safe_downcast(next), childID); } else if ( (have == nullptr) || !have->hasLeafNode( safe_downcast(next)->peekItem()->key(), childHash)) { if (!function(*next)) return; } } } } } // Starting at the position referred to by the specfied // StackEntry, process that node and its first resident // children, descending the SHAMap until we complete the // processing of a node. void SHAMap::gmn_ProcessNodes(MissingNodes& mn, MissingNodes::StackEntry& se) { SHAMapInnerNode*& node = std::get<0>(se); SHAMapNodeID& nodeID = std::get<1>(se); int& firstChild = std::get<2>(se); int& currentChild = std::get<3>(se); bool& fullBelow = std::get<4>(se); while (currentChild < 16) { int const branch = (firstChild + currentChild++) % 16; if (node->isEmptyBranch(branch)) continue; auto const& childHash = node->getChildHash(branch); if (mn.missingHashes_.contains(childHash)) { // we already know this child node is missing fullBelow = false; } else if (!backed_ || !f_.getFullBelowCache()->touch_if_exists(childHash.as_uint256())) { bool pending = false; auto d = descendAsync( node, branch, mn.filter_, pending, [node, nodeID, branch, &mn](SHAMapTreeNodePtr found, SHAMapHash const&) { // a read completed asynchronously std::unique_lock const lock{mn.deferLock_}; mn.finishedReads_.emplace_back(node, nodeID, branch, std::move(found)); mn.deferCondVar_.notify_one(); }); if (pending) { fullBelow = false; ++mn.deferred_; } else if (d == nullptr) { // node is not in database fullBelow = false; // for now, not known full below mn.missingHashes_.insert(childHash); mn.missingNodes_.emplace_back( nodeID.getChildNodeID(branch), childHash.as_uint256()); if (--mn.max_ <= 0) return; } else if ( d->isInner() && !safe_downcast(d)->isFullBelow(mn.generation_)) { mn.stack_.push(se); // Switch to processing the child node node = safe_downcast(d); nodeID = nodeID.getChildNodeID(branch); firstChild = rand_int(255); currentChild = 0; fullBelow = true; } } } // We have finished processing an inner node // and thus (for now) all its children if (fullBelow) { // No partial node encountered below this node node->setFullBelowGen(mn.generation_); if (backed_) { f_.getFullBelowCache()->insert(node->getHash().as_uint256()); } } node = nullptr; } // Wait for deferred reads to finish and // process their results void SHAMap::gmn_ProcessDeferredReads(MissingNodes& mn) { // Process all deferred reads int complete = 0; while (complete != mn.deferred_) { std::tuple deferredNode; { std::unique_lock lock{mn.deferLock_}; while (mn.finishedReads_.size() <= complete) mn.deferCondVar_.wait(lock); deferredNode = std::move(mn.finishedReads_[complete++]); } auto parent = std::get<0>(deferredNode); auto const& parentID = std::get<1>(deferredNode); auto branch = std::get<2>(deferredNode); auto nodePtr = std::get<3>(deferredNode); auto const& nodeHash = parent->getChildHash(branch); if (nodePtr) { // Got the node nodePtr = parent->canonicalizeChild(branch, std::move(nodePtr)); // When we finish this stack, we need to restart // with the parent of this node mn.resumes_[parent] = parentID; } else if ((mn.max_ > 0) && (mn.missingHashes_.insert(nodeHash).second)) { mn.missingNodes_.emplace_back(parentID.getChildNodeID(branch), nodeHash.as_uint256()); --mn.max_; } } mn.finishedReads_.clear(); mn.finishedReads_.reserve(mn.maxDefer_); mn.deferred_ = 0; } /** Get a list of node IDs and hashes for nodes that are part of this SHAMap but not available locally. The filter can hold alternate sources of nodes that are not permanently stored locally */ std::vector> SHAMap::getMissingNodes(int max, SHAMapSyncFilter* filter) { XRPL_ASSERT(root_->getHash().isNonZero(), "xrpl::SHAMap::getMissingNodes : nonzero root hash"); XRPL_ASSERT(max > 0, "xrpl::SHAMap::getMissingNodes : valid max input"); MissingNodes mn( max, filter, 512, // number of async reads per pass f_.getFullBelowCache()->getGeneration()); if (!root_->isInner() || intr_ptr::static_pointer_cast(root_)->isFullBelow(mn.generation_)) { clearSynching(); return std::move(mn.missingNodes_); } // Start at the root. // The firstChild value is selected randomly so if multiple threads // are traversing the map, each thread will start at a different // (randomly selected) inner node. This increases the likelihood // that the two threads will produce different request sets (which is // more efficient than sending identical requests). MissingNodes::StackEntry pos{ safe_downcast(root_.get()), SHAMapNodeID(), rand_int(255), 0, true}; auto& node = std::get<0>(pos); auto& nextChild = std::get<3>(pos); auto& fullBelow = std::get<4>(pos); // Traverse the map without blocking do { while ((node != nullptr) && (mn.deferred_ <= mn.maxDefer_)) { gmn_ProcessNodes(mn, pos); if (mn.max_ <= 0) break; if ((node == nullptr) && !mn.stack_.empty()) { // Pick up where we left off with this node's parent bool const was = fullBelow; // was full below pos = mn.stack_.top(); mn.stack_.pop(); if (nextChild == 0) { // This is a node we are processing for the first time fullBelow = true; } else { // This is a node we are continuing to process fullBelow = fullBelow && was; // was and still is } XRPL_ASSERT(node, "xrpl::SHAMap::getMissingNodes : first non-null node"); } } // We have either emptied the stack or // posted as many deferred reads as we can if (mn.deferred_ != 0) gmn_ProcessDeferredReads(mn); if (mn.max_ <= 0) return std::move(mn.missingNodes_); if (node == nullptr) { // We weren't in the middle of processing a node if (mn.stack_.empty() && !mn.resumes_.empty()) { // Recheck nodes we could not finish before for (auto const& [innerNode, nodeId] : mn.resumes_) { if (!innerNode->isFullBelow(mn.generation_)) mn.stack_.emplace(innerNode, nodeId, rand_int(255), 0, true); } mn.resumes_.clear(); } if (!mn.stack_.empty()) { // Resume at the top of the stack pos = mn.stack_.top(); mn.stack_.pop(); XRPL_ASSERT(node, "xrpl::SHAMap::getMissingNodes : second non-null node"); } } // node will only still be nullptr if // we finished the current node, the stack is empty // and we have no nodes to resume } while (node != nullptr); if (mn.missingNodes_.empty()) clearSynching(); return std::move(mn.missingNodes_); } bool SHAMap::getNodeFat( SHAMapNodeID const& wanted, std::vector& data, bool fatLeaves, std::uint32_t depth) const { // Gets a node and some of its children // to a specified depth auto node = root_.get(); SHAMapNodeID nodeID; while ((node != nullptr) && node->isInner() && (nodeID.getDepth() < wanted.getDepth())) { int const branch = selectBranch(nodeID, wanted.getNodeID()); auto inner = safe_downcast(node); if (inner->isEmptyBranch(branch)) return false; node = descendThrow(inner, branch); nodeID = nodeID.getChildNodeID(branch); } if (node == nullptr || wanted != nodeID) { JLOG(journal_.info()) << "peer requested node that is not in the map: " << wanted << " but found " << nodeID; return false; } if (node->isInner() && safe_downcast(node)->isEmpty()) { JLOG(journal_.warn()) << "peer requests empty node"; return false; } std::stack> stack; stack.emplace(node, nodeID, depth); Serializer s(8192); while (!stack.empty()) { std::tie(node, nodeID, depth) = stack.top(); stack.pop(); // Add this node to the reply s.erase(); node->serializeForWire(s); data.push_back({nodeID, s.getData(), node->isLeaf()}); if (node->isInner()) { // We descend inner nodes with only a single child // without decrementing the depth auto inner = safe_downcast(node); int const bc = inner->getBranchCount(); if ((depth > 0) || (bc == 1)) { // We need to process this node's children for (int i = 0; i < 16; ++i) { if (!inner->isEmptyBranch(i)) { auto const childNode = descendThrow(inner, i); auto const childID = nodeID.getChildNodeID(i); if (childNode->isInner() && ((depth > 1) || (bc == 1))) { // If there's more than one child, reduce the depth // If only one child, follow the chain stack.emplace(childNode, childID, (bc > 1) ? (depth - 1) : depth); } else if (childNode->isInner() || fatLeaves) { // Just include this node s.erase(); childNode->serializeForWire(s); data.push_back({childID, s.getData(), childNode->isLeaf()}); } } } } } } return true; } void SHAMap::serializeRoot(Serializer& s) const { root_->serializeForWire(s); } SHAMapAddNode SHAMap::addRootNode( SHAMapHash const& hash, SHAMapTreeNodePtr rootNode, SHAMapSyncFilter const* filter) { XRPL_ASSERT(rootNode, "xrpl::SHAMap::addRootNode : non-null root node"); if (!rootNode) { JLOG(journal_.error()) << "Null node received"; return SHAMapAddNode::invalid(); } // we already have a root_ node if (root_->getHash().isNonZero()) { JLOG(journal_.trace()) << "got root node, already have one"; XRPL_ASSERT(root_->getHash() == hash, "xrpl::SHAMap::addRootNode : valid hash input"); return SHAMapAddNode::duplicate(); } XRPL_ASSERT(cowid_ >= 1, "xrpl::SHAMap::addRootNode : valid cowid"); if (rootNode->getHash() != hash) { JLOG(journal_.warn()) << "Corrupt node received"; return SHAMapAddNode::invalid(); } if (backed_) canonicalize(hash, rootNode); root_ = std::move(rootNode); if (root_->isLeaf()) clearSynching(); if (filter != nullptr) { Serializer s; root_->serializeWithPrefix(s); filter->gotNode( false, root_->getHash(), ledgerSeq_, std::move(s.modData()), root_->getType()); } return SHAMapAddNode::useful(); } SHAMapAddNode SHAMap::addKnownNode( SHAMapNodeID const& nodeID, SHAMapTreeNodePtr treeNode, SHAMapSyncFilter const* filter) { XRPL_ASSERT(!nodeID.isRoot(), "xrpl::SHAMap::addKnownNode : valid node input"); if (nodeID.isRoot()) { JLOG(journal_.error()) << "Root node received"; return SHAMapAddNode::invalid(); } XRPL_ASSERT(treeNode, "xrpl::SHAMap::addKnownNode : non-null tree node"); if (!treeNode) { JLOG(journal_.error()) << "Null node received"; return SHAMapAddNode::invalid(); } if (!isSynching()) { JLOG(journal_.trace()) << "AddKnownNode while not synching"; return SHAMapAddNode::duplicate(); } auto const generation = f_.getFullBelowCache()->getGeneration(); SHAMapNodeID currNodeID; auto currNode = root_.get(); while (currNode->isInner() && !safe_downcast(currNode)->isFullBelow(generation) && (currNodeID.getDepth() < nodeID.getDepth())) { int const branch = selectBranch(currNodeID, nodeID.getNodeID()); XRPL_ASSERT(branch >= 0, "xrpl::SHAMap::addKnownNode : valid branch"); auto inner = safe_downcast(currNode); if (inner->isEmptyBranch(branch)) { JLOG(journal_.warn()) << "Add known node for empty branch" << nodeID; return SHAMapAddNode::invalid(); } auto childHash = inner->getChildHash(branch); if (f_.getFullBelowCache()->touch_if_exists(childHash.as_uint256())) { return SHAMapAddNode::duplicate(); } auto prevNode = inner; std::tie(currNode, currNodeID) = descend(inner, currNodeID, branch, filter); if (currNode != nullptr) continue; if (childHash != treeNode->getHash()) { JLOG(journal_.warn()) << "Corrupt node received"; return SHAMapAddNode::invalid(); } // Inner nodes must be at a level strictly less than 64 // but leaf nodes (while notionally at level 64) can be // at any depth up to and including 64: if ((currNodeID.getDepth() > leafDepth) || (treeNode->isInner() && currNodeID.getDepth() == leafDepth)) { // Map is provably invalid state_ = SHAMapState::Invalid; return SHAMapAddNode::useful(); } if (currNodeID != nodeID) { // Either this node is broken or we didn't request it (yet) JLOG(journal_.warn()) << "unable to hook node " << nodeID; JLOG(journal_.info()) << " stuck at " << currNodeID; JLOG(journal_.info()) << "got depth=" << nodeID.getDepth() << ", walked to= " << currNodeID.getDepth(); return SHAMapAddNode::useful(); } if (backed_) canonicalize(childHash, treeNode); treeNode = prevNode->canonicalizeChild(branch, std::move(treeNode)); if (filter != nullptr) { Serializer s; treeNode->serializeWithPrefix(s); filter->gotNode( false, childHash, ledgerSeq_, std::move(s.modData()), treeNode->getType()); } return SHAMapAddNode::useful(); } JLOG(journal_.trace()) << "got node, already had it (late)"; return SHAMapAddNode::duplicate(); } bool SHAMap::deepCompare(SHAMap& other) const { // Intended for debug/test only std::stack> stack; stack.emplace(root_.get(), other.root_.get()); while (!stack.empty()) { auto const [node, otherNode] = stack.top(); stack.pop(); if ((node == nullptr) || (otherNode == nullptr)) { JLOG(journal_.info()) << "unable to fetch node"; return false; } if (otherNode->getHash() != node->getHash()) { JLOG(journal_.warn()) << "node hash mismatch"; return false; } if (node->isLeaf()) { if (!otherNode->isLeaf()) return false; auto& nodePeek = safe_downcast(node)->peekItem(); auto& otherNodePeek = safe_downcast(otherNode)->peekItem(); if (nodePeek->key() != otherNodePeek->key()) return false; if (nodePeek->slice() != otherNodePeek->slice()) return false; } else if (node->isInner()) { if (!otherNode->isInner()) return false; auto node_inner = safe_downcast(node); auto other_inner = safe_downcast(otherNode); for (int i = 0; i < 16; ++i) { if (node_inner->isEmptyBranch(i)) { if (!other_inner->isEmptyBranch(i)) return false; } else { if (other_inner->isEmptyBranch(i)) return false; auto next = descend(node_inner, i); auto otherNext = other.descend(other_inner, i); if ((next == nullptr) || (otherNext == nullptr)) { JLOG(journal_.warn()) << "unable to fetch inner node"; return false; } stack.emplace(next, otherNext); } } } } return true; } /** Does this map have this inner node? */ bool SHAMap::hasInnerNode(SHAMapNodeID const& targetNodeID, SHAMapHash const& targetNodeHash) const { auto node = root_.get(); SHAMapNodeID nodeID; while (node->isInner() && (nodeID.getDepth() < targetNodeID.getDepth())) { int const branch = selectBranch(nodeID, targetNodeID.getNodeID()); auto inner = safe_downcast(node); if (inner->isEmptyBranch(branch)) return false; node = descendThrow(inner, branch); nodeID = nodeID.getChildNodeID(branch); } return (node->isInner()) && (node->getHash() == targetNodeHash); } /** Does this map have this leaf node? */ bool SHAMap::hasLeafNode(uint256 const& tag, SHAMapHash const& targetNodeHash) const { auto node = root_.get(); SHAMapNodeID nodeID; if (!node->isInner()) // only one leaf node in the tree return node->getHash() == targetNodeHash; do { int const branch = selectBranch(nodeID, tag); auto inner = safe_downcast(node); if (inner->isEmptyBranch(branch)) return false; // Dead end, node must not be here if (inner->getChildHash(branch) == targetNodeHash) // Matching leaf, no need to retrieve it return true; node = descendThrow(inner, branch); nodeID = nodeID.getChildNodeID(branch); } while (node->isInner()); return false; // If this was a matching leaf, we would have caught it // already } std::optional> SHAMap::getProofPath(uint256 const& key) const { SharedPtrNodeStack stack; walkTowardsKey(key, &stack); if (stack.empty()) { JLOG(journal_.debug()) << "no path to " << key; return {}; } if (auto const& node = stack.top().first; !node || node->isInner() || intr_ptr::static_pointer_cast(node)->peekItem()->key() != key) { JLOG(journal_.debug()) << "no path to " << key; return {}; } std::vector path; path.reserve(stack.size()); while (!stack.empty()) { Serializer s; stack.top().first->serializeForWire(s); path.emplace_back(std::move(s.modData())); stack.pop(); } JLOG(journal_.debug()) << "getPath for key " << key << ", path length " << path.size(); return path; } bool SHAMap::verifyProofPath(uint256 const& rootHash, uint256 const& key, std::vector const& path) { if (path.empty() || path.size() > 65) return false; SHAMapHash hash{rootHash}; try { for (auto rit = path.rbegin(); rit != path.rend(); ++rit) { auto const& blob = *rit; auto node = SHAMapTreeNode::makeFromWire(makeSlice(blob)); if (!node) return false; node->updateHash(); if (node->getHash() != hash) return false; auto depth = std::distance(path.rbegin(), rit); if (node->isInner()) { auto nodeId = SHAMapNodeID::createID(depth, key); hash = safe_downcast(node.get()) ->getChildHash(selectBranch(nodeId, key)); } else { // should exhaust all the blobs now return depth + 1 == path.size(); } } } catch (std::exception const&) { // the data in the path may come from the network, // exception could be thrown when parsing the data return false; } return false; } } // namespace xrpl