mirror of
https://github.com/XRPLF/rippled.git
synced 2026-09-27 23:38:08 +00:00
A walk that reaches a position only a leaf may occupy now marks the map Invalid and abandons the descent instead of continuing: SHAMapNodeID::getChildNodeID() throws past kLeafDepth, uncaught, all the way to std::terminate(). It's reachable without going through addKnownNode() at all - InboundLedgers::gotStaleData() stores any parseable node from an unsolicited liAS_NODE reply into the fetch pack by its own hash with no relatedness check - making this a conditioned remote denial of service, not just a single bad packet. As in addKnownNode(), the depth check runs before the full-below cache lookup, for the same cache-doesn't-cover-depth reason. Callers must re-check isValid() before reading an empty result as nothing left to fetch, which getMissingNodes()'s docstring now says.
964 lines
32 KiB
C++
964 lines
32 KiB
C++
#include <xrpl/basics/Blob.h>
|
|
#include <xrpl/basics/IntrusivePointer.h>
|
|
#include <xrpl/basics/Log.h>
|
|
#include <xrpl/basics/Slice.h>
|
|
#include <xrpl/basics/base_uint.h>
|
|
#include <xrpl/basics/random.h>
|
|
#include <xrpl/basics/safe_cast.h>
|
|
#include <xrpl/beast/utility/instrumentation.h>
|
|
#include <xrpl/protocol/Serializer.h>
|
|
#include <xrpl/shamap/SHAMap.h>
|
|
#include <xrpl/shamap/SHAMapAddNode.h>
|
|
#include <xrpl/shamap/SHAMapInnerNode.h>
|
|
#include <xrpl/shamap/SHAMapItem.h>
|
|
#include <xrpl/shamap/SHAMapLeafNode.h>
|
|
#include <xrpl/shamap/SHAMapNodeID.h>
|
|
#include <xrpl/shamap/SHAMapSyncFilter.h>
|
|
#include <xrpl/shamap/SHAMapTreeNode.h>
|
|
|
|
#include <boost/smart_ptr/intrusive_ptr.hpp>
|
|
|
|
#include <cstdint>
|
|
#include <exception>
|
|
#include <functional>
|
|
#include <iterator>
|
|
#include <mutex>
|
|
#include <optional>
|
|
#include <stack>
|
|
#include <tuple>
|
|
#include <utility>
|
|
#include <vector>
|
|
|
|
namespace xrpl {
|
|
|
|
namespace {
|
|
|
|
/**
|
|
* Whether a depth is one only a leaf may occupy.
|
|
*
|
|
* Nibbles run out at SHAMap::kLeafDepth, so an inner node there would need two
|
|
* keys agreeing in all 64 nibbles. Spelled once, since the sync path tests it
|
|
* for a node, for a node's child, and for a position a descent reached.
|
|
*
|
|
* @param depth The depth to judge.
|
|
* @return Whether an inner node at that depth would make the map impossible.
|
|
*/
|
|
[[nodiscard]] bool
|
|
isLeafDepth(unsigned int depth)
|
|
{
|
|
return depth >= SHAMap::kLeafDepth;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
void
|
|
SHAMap::visitLeaves(
|
|
std::function<void(boost::intrusive_ptr<SHAMapItem const> const& item)> const& leafFunction)
|
|
const
|
|
{
|
|
visitNodes([&leafFunction](SHAMapTreeNode& node) {
|
|
if (!node.isInner())
|
|
leafFunction(safeDowncast<SHAMapLeafNode&>(node).peekItem());
|
|
return true;
|
|
});
|
|
}
|
|
|
|
void
|
|
SHAMap::visitNodes(std::function<bool(SHAMapTreeNode&)> const& function) const
|
|
{
|
|
if (!root_)
|
|
return;
|
|
|
|
function(*root_);
|
|
|
|
if (!root_->isInner())
|
|
return;
|
|
|
|
using StackEntry = std::pair<unsigned int, intr_ptr::SharedPtr<SHAMapInnerNode>>;
|
|
std::stack<StackEntry, std::vector<StackEntry>> stack;
|
|
|
|
auto node = intr_ptr::staticPointerCast<SHAMapInnerNode>(root_);
|
|
auto pos = 0u;
|
|
|
|
while (true)
|
|
{
|
|
while (pos < kBranchFactor)
|
|
{
|
|
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 != kBranchFactor - 1u) && (node->isEmptyBranch(pos + 1)))
|
|
++pos;
|
|
|
|
if (pos != kBranchFactor - 1u)
|
|
{
|
|
// save next position to resume at
|
|
stack.emplace(pos + 1, std::move(node));
|
|
}
|
|
|
|
// descend to the child's first position
|
|
node = intr_ptr::staticPointerCast<SHAMapInnerNode>(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* map,
|
|
std::function<bool(SHAMapTreeNode const&)> 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 ((map != nullptr) && (root_->getHash() == map->root_->getHash()))
|
|
return;
|
|
|
|
if (root_->isLeaf())
|
|
{
|
|
auto leaf = intr_ptr::staticPointerCast<SHAMapLeafNode>(root_);
|
|
if ((map == nullptr) || !map->hasLeafNode(leaf->peekItem()->key(), leaf->getHash()))
|
|
function(*root_);
|
|
return;
|
|
}
|
|
// contains unexplored non-matching inner node entries
|
|
using StackEntry = std::pair<SHAMapInnerNode*, SHAMapNodeID>;
|
|
std::stack<StackEntry, std::vector<StackEntry>> stack;
|
|
|
|
stack.emplace(safeDowncast<SHAMapInnerNode*>(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;
|
|
|
|
// Nibbles run out at kLeafDepth, so only a leaf belongs there. A well-formed map never
|
|
// holds an inner node at that depth: addKnownNode marks the map invalid rather than hooking
|
|
// one in, and fetch-pack data is hash-verified against a validated root, so reaching this
|
|
// means a defect or a corrupt store, not something a peer can provoke. Report the node
|
|
// anyway - the wire form carries no depth, and the recipient hooks blobs in by hash - but
|
|
// skip the children rather than letting getChildNodeID throw on them.
|
|
if (nodeID.getDepth() >= kLeafDepth)
|
|
{
|
|
// LCOV_EXCL_START
|
|
UNREACHABLE("xrpl::SHAMap::visitDifferences : inner node at leaf depth");
|
|
continue;
|
|
// LCOV_EXCL_STOP
|
|
}
|
|
|
|
// 2) push non-matching child inner nodes
|
|
for (auto i = 0u; i < kBranchFactor; ++i)
|
|
{
|
|
if (!node->isEmptyBranch(i))
|
|
{
|
|
auto const& childHash = node->getChildHash(i);
|
|
auto const childID = nodeID.getChildNodeID(i);
|
|
auto next = descendThrow(node, i);
|
|
|
|
if (next->isInner())
|
|
{
|
|
if ((map == nullptr) || !map->hasInnerNode(childID, childHash))
|
|
stack.emplace(safeDowncast<SHAMapInnerNode*>(next), childID);
|
|
}
|
|
else if ((map == nullptr) || !map->hasLeafNode(leafKey(*next), 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::gmnProcessNodes(MissingNodes& mn, MissingNodes::StackEntry& se)
|
|
{
|
|
SHAMapInnerNode*& node = std::get<0>(se);
|
|
SHAMapNodeID& nodeID = std::get<1>(se);
|
|
auto& firstChild = std::get<2>(se);
|
|
auto& currentChild = std::get<3>(se);
|
|
bool& fullBelow = std::get<4>(se);
|
|
|
|
while (currentChild < kBranchFactor)
|
|
{
|
|
auto const branch = (firstChild + currentChild++) % kBranchFactor;
|
|
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;
|
|
}
|
|
// The depth test precedes the cache lookup for the same reason it does in addKnownNode():
|
|
// the cache is keyed by node hash and shared across maps, and a hash covers a node's
|
|
// children but not its depth, so a hit would skip the depth guard below. Skipping the
|
|
// shortcut only forgoes an optimization.
|
|
else if (
|
|
!backed_ || isLeafDepth(nodeID.getDepth() + 1) ||
|
|
!f_.getFullBelowCache()->touchIfExists(childHash.asUInt256()))
|
|
{
|
|
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<std::mutex> 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.asUInt256());
|
|
|
|
if (--mn.max <= 0)
|
|
return;
|
|
}
|
|
else if (d->isInner() && isLeafDepth(nodeID.getDepth() + 1))
|
|
{
|
|
// Only a leaf belongs that deep (see isLeafDepth and SHAMap::addKnownNode). A node
|
|
// resolved locally never passes through addKnownNode(), so the walk has to reach
|
|
// this verdict itself. Ordered ahead of the full-below test below, which
|
|
// canonicalization shares across maps, or a node already marked full below would go
|
|
// unjudged.
|
|
JLOG(journal_.warn()) << "Inner node at branch " << branch << " below " << nodeID
|
|
<< " makes the map invalid";
|
|
setInvalid();
|
|
return;
|
|
}
|
|
else if (d->isInner() && !safeDowncast<SHAMapInnerNode*>(d)->isFullBelow(mn.generation))
|
|
{
|
|
mn.stack.push(se);
|
|
|
|
// Switch to processing the child node
|
|
node = safeDowncast<SHAMapInnerNode*>(d);
|
|
nodeID = nodeID.getChildNodeID(branch);
|
|
firstChild = randInt(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().asUInt256());
|
|
}
|
|
}
|
|
|
|
node = nullptr;
|
|
}
|
|
|
|
// Wait for deferred reads to finish and
|
|
// process their results
|
|
void
|
|
SHAMap::gmnProcessDeferredReads(MissingNodes& mn)
|
|
{
|
|
// Process all deferred reads
|
|
int complete = 0;
|
|
while (complete != mn.deferred)
|
|
{
|
|
MissingNodes::DeferredNode deferredNode;
|
|
{
|
|
std::unique_lock<std::mutex> 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.asUInt256());
|
|
--mn.max;
|
|
}
|
|
}
|
|
|
|
mn.finishedReads.clear();
|
|
mn.finishedReads.reserve(mn.maxDefer);
|
|
mn.deferred = 0;
|
|
}
|
|
|
|
std::vector<std::pair<SHAMapNodeID, uint256>>
|
|
SHAMap::getMissingNodes(int max, SHAMapSyncFilter const* filter)
|
|
{
|
|
XRPL_ASSERT(root_->getHash().isNonZero(), "xrpl::SHAMap::getMissingNodes : nonzero root hash");
|
|
XRPL_ASSERT(max > 0, "xrpl::SHAMap::getMissingNodes : valid max input");
|
|
|
|
// An already-invalid map short-circuits here instead of re-deriving the verdict in the walk
|
|
// below, which reaches it on its own.
|
|
if (!isValid())
|
|
{
|
|
// journal_ is the family journal, shared by every map, so name which one this is. The root
|
|
// node's own hash rather than SHAMap::getHash(), which unshares the tree on a zero hash.
|
|
JLOG(journal_.warn()) << "getMissingNodes called on an invalid map, root hash "
|
|
<< root_->getHash() << " seq " << ledgerSeq();
|
|
return {};
|
|
}
|
|
|
|
MissingNodes mn(
|
|
max,
|
|
filter,
|
|
512, // number of async reads per pass
|
|
f_.getFullBelowCache()->getGeneration());
|
|
|
|
if (!root_->isInner() ||
|
|
intr_ptr::staticPointerCast<SHAMapInnerNode>(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{
|
|
safeDowncast<SHAMapInnerNode*>(root_.get()), SHAMapNodeID(), randInt(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))
|
|
{
|
|
gmnProcessNodes(mn, pos);
|
|
|
|
// The walk just invalidated the map. Stop descending, but fall through to the drain
|
|
// below rather than returning, since posted reads hold a reference to mn.
|
|
if (!isValid())
|
|
break;
|
|
|
|
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)
|
|
gmnProcessDeferredReads(mn);
|
|
|
|
// Reads are drained, so the map can now be abandoned. Whatever was collected belongs to
|
|
// a tree that cannot exist, so discard it.
|
|
if (!isValid())
|
|
return {};
|
|
|
|
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, randInt(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);
|
|
|
|
// Tested once more, since an addKnownNode() on another thread can write the verdict after the
|
|
// loop's own test above, and an empty result would then be read as "satisfied". clearSynching()
|
|
// refuses either way, so this is about not asking rather than about the state it would leave.
|
|
if (!isValid())
|
|
return {};
|
|
|
|
if (mn.missingNodes.empty())
|
|
clearSynching();
|
|
|
|
return std::move(mn.missingNodes);
|
|
}
|
|
|
|
bool
|
|
SHAMap::getNodeFat(
|
|
SHAMapNodeID const& wanted,
|
|
std::vector<SHAMapNodeData>& 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()))
|
|
{
|
|
auto const branch = selectBranch(nodeID, wanted.getNodeID());
|
|
auto inner = safeDowncast<SHAMapInnerNode*>(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() && safeDowncast<SHAMapInnerNode*>(node)->isEmpty())
|
|
{
|
|
JLOG(journal_.warn()) << "peer requests empty node";
|
|
return false;
|
|
}
|
|
|
|
std::stack<std::tuple<SHAMapTreeNode*, SHAMapNodeID, std::uint32_t>> 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.emplace_back(nodeID, node->isLeaf(), s.getData());
|
|
|
|
if (node->isInner())
|
|
{
|
|
// We descend inner nodes with only a single child
|
|
// without decrementing the depth
|
|
auto inner = safeDowncast<SHAMapInnerNode*>(node);
|
|
auto const bc = inner->getBranchCount();
|
|
|
|
if ((depth > 0) || (bc == 1))
|
|
{
|
|
// We need to process this node's children
|
|
for (auto i = 0u; i < kBranchFactor; ++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.emplace_back(childID, childNode->isLeaf(), s.getData());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
void
|
|
SHAMap::serializeRoot(Serializer& s) const
|
|
{
|
|
root_->serializeForWire(s);
|
|
}
|
|
|
|
SHAMapAddNode
|
|
SHAMap::addRootNode(
|
|
SHAMapHash const& hash,
|
|
SHAMapTreeNodePtr rootNode,
|
|
SHAMapSyncFilter const* filter)
|
|
{
|
|
XRPL_ASSERT(cowid_ >= 1, "xrpl::SHAMap::addRootNode : valid cowid");
|
|
XRPL_ASSERT(rootNode, "xrpl::SHAMap::addRootNode : non-null root node");
|
|
|
|
// 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");
|
|
return SHAMapAddNode::duplicate();
|
|
}
|
|
|
|
if (rootNode->getHash() != hash)
|
|
{
|
|
JLOG(journal_.warn()) << "Corrupt root node received: expected hash " << hash << ", got "
|
|
<< rootNode->getHash();
|
|
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");
|
|
XRPL_ASSERT(treeNode, "xrpl::SHAMap::addKnownNode : non-null tree node");
|
|
XRPL_ASSERT_IF(
|
|
treeNode->isLeaf(),
|
|
nodeID.isPrefixOf(leafKey(*treeNode)),
|
|
"xrpl::SHAMap::addKnownNode : leaf position consistent with node ID");
|
|
|
|
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() &&
|
|
!safeDowncast<SHAMapInnerNode*>(currNode)->isFullBelow(generation) &&
|
|
(currNodeID.getDepth() < nodeID.getDepth()))
|
|
{
|
|
auto const branch = selectBranch(currNodeID, nodeID.getNodeID());
|
|
auto inner = safeDowncast<SHAMapInnerNode*>(currNode);
|
|
if (inner->isEmptyBranch(branch))
|
|
{
|
|
JLOG(journal_.warn()) << "Add known node " << nodeID << " for empty branch " << branch
|
|
<< " at " << currNodeID;
|
|
return SHAMapAddNode::invalid();
|
|
}
|
|
|
|
auto childHash = inner->getChildHash(branch);
|
|
|
|
// The depth test precedes the cache lookup deliberately: the cache is keyed by node hash
|
|
// and shared across every map of this family, and a hash covers a node's children but not
|
|
// its depth, so the same subtree hash can be cached as complete at one depth and reached at
|
|
// another. Taking the shortcut first would make the verdict below depend on what an
|
|
// unrelated map cached, and the acquisition paths rely on it being deterministic. Skipping
|
|
// the shortcut only forgoes an optimization.
|
|
if (!isLeafDepth(currNodeID.getDepth() + 1) &&
|
|
f_.getFullBelowCache()->touchIfExists(childHash.asUInt256()))
|
|
{
|
|
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 " << nodeID << " received: expected hash "
|
|
<< childHash << ", got " << treeNode->getHash();
|
|
return SHAMapAddNode::invalid();
|
|
}
|
|
|
|
// Only leaves may sit at kLeafDepth (see isLeafDepth), so an inner node there makes the map
|
|
// impossible. Nothing is hooked in, so this is bad data rather than progress.
|
|
//
|
|
// Every node from the root down hash-verified to get here, so it is the requested root hash
|
|
// itself that commits to a shape no valid tree can have. The verdict therefore belongs to
|
|
// that hash rather than to our copy of the tree: no peer can satisfy it, retrying is
|
|
// futile, and it cannot arise by accident. The acquisition paths rely on all three.
|
|
//
|
|
// A charge is a deterrent rather than a control: the same node can reach a map through a
|
|
// fetch pack or an unsolicited object reply, neither of which comes through here, so
|
|
// nothing may assume the sender of such a node was made to pay for it.
|
|
bool const badDepth = treeNode->isInner() && isLeafDepth(currNodeID.getDepth());
|
|
SOMETIMES(badDepth, "xrpl::SHAMap::addKnownNode : map is invalid");
|
|
if (badDepth)
|
|
{
|
|
JLOG(journal_.warn()) << "Node " << nodeID << " makes the map invalid at "
|
|
<< currNodeID;
|
|
setInvalid();
|
|
return SHAMapAddNode::invalid();
|
|
}
|
|
|
|
// The data hashes to the child we need at currNodeID but is labeled as belonging at nodeID,
|
|
// so it cannot be hooked anywhere. Only the label is wrong, so the map stays sound and the
|
|
// node is still obtainable from another sender.
|
|
bool const badPosition = (currNodeID != nodeID);
|
|
SOMETIMES(badPosition, "xrpl::SHAMap::addKnownNode : node ID does not match its position");
|
|
if (badPosition)
|
|
{
|
|
JLOG(journal_.warn()) << "Unable to hook node " << nodeID << ", stuck at "
|
|
<< currNodeID;
|
|
return SHAMapAddNode::invalid();
|
|
}
|
|
|
|
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<std::pair<SHAMapTreeNode*, SHAMapTreeNode*>> 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 = safeDowncast<SHAMapLeafNode*>(node)->peekItem();
|
|
auto& otherNodePeek = safeDowncast<SHAMapLeafNode*>(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 nodeInner = safeDowncast<SHAMapInnerNode*>(node);
|
|
auto otherInner = safeDowncast<SHAMapInnerNode*>(otherNode);
|
|
for (auto i = 0u; i < kBranchFactor; ++i)
|
|
{
|
|
if (nodeInner->isEmptyBranch(i))
|
|
{
|
|
if (!otherInner->isEmptyBranch(i))
|
|
return false;
|
|
}
|
|
else
|
|
{
|
|
if (otherInner->isEmptyBranch(i))
|
|
return false;
|
|
|
|
auto next = descend(nodeInner, i);
|
|
auto otherNext = other.descend(otherInner, 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()))
|
|
{
|
|
auto const branch = selectBranch(nodeID, targetNodeID.getNodeID());
|
|
auto inner = safeDowncast<SHAMapInnerNode*>(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
|
|
{
|
|
// Same kLeafDepth hazard as in visitDifferences above. That guard bounds the caller's own
|
|
// traversal, not the map queried here, and the loop below descends from this map's root
|
|
// independently, so this check is what keeps a malformed map from reaching getChildNodeID.
|
|
if (isLeafDepth(nodeID.getDepth()))
|
|
{
|
|
// LCOV_EXCL_START
|
|
UNREACHABLE("xrpl::SHAMap::hasLeafNode : inner node at leaf depth");
|
|
return false;
|
|
// LCOV_EXCL_STOP
|
|
}
|
|
|
|
auto const branch = selectBranch(nodeID, tag);
|
|
auto inner = safeDowncast<SHAMapInnerNode*>(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<std::vector<Blob>>
|
|
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::staticPointerCast<SHAMapLeafNode>(node)->peekItem()->key() != key)
|
|
{
|
|
JLOG(journal_.debug()) << "no path to " << key;
|
|
return {};
|
|
}
|
|
|
|
std::vector<Blob> 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<Blob> const& path)
|
|
{
|
|
if (path.empty() || path.size() > kLeafDepth + 1u)
|
|
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 const depth = static_cast<unsigned int>(std::distance(path.rbegin(), rit));
|
|
if (node->isInner())
|
|
{
|
|
// Nibbles run out at kLeafDepth, so only the leaf terminating the path may sit
|
|
// there. These nodes come off the wire, so a peer can still claim an inner one;
|
|
// reject it rather than passing this depth to selectBranch.
|
|
SOMETIMES(
|
|
depth >= kLeafDepth, "xrpl::SHAMap::verifyProofPath : inner at leaf depth");
|
|
if (depth >= kLeafDepth)
|
|
return false;
|
|
|
|
auto nodeId = SHAMapNodeID::createID(depth, key);
|
|
hash = safeDowncast<SHAMapInnerNode*>(node.get())
|
|
->getChildHash(selectBranch(nodeId, key));
|
|
}
|
|
else
|
|
{
|
|
// The hash chain up to rootHash only proves this leaf sits where the path claims,
|
|
// not that it is the leaf for `key`: a peer could substitute any other leaf whose
|
|
// subtree hashes to the same value at every level above it. Checking the terminal
|
|
// leaf's own key is what ties the proof to `key` specifically.
|
|
if (leafKey(*node) != key)
|
|
return false;
|
|
|
|
// 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
|