Files
rippled/src/libxrpl/shamap/SHAMapDelta.cpp
Bart a09a255ed4 fix: Report unreadable SHAMap nodes instead of throwing
`SHAMap::walkMap` and `walkMapParallel` treat a null `descendNoStore` result as
"this node is missing" and record it in their `missingNodes` output, but
`descendNoStore` used the throwing `fetchNode`, so that branch could never run for
a backed map. `Ledger::walkLedger` propagated the throw instead of returning
false, its "N missing account node(s)" log never printed, and `LedgerCleaner` never
reached the path that clears the ledger and re-acquires it. `descendNoStore` now
uses `fetchNodeNT`, which makes all three behave as written, and the null check
after `fetchNode` in `descend` goes away as the dead code it always was.

`visitNodes` and `visitLeaves` now return whether the walk read every node it
reached, and the walk ends at the first node it cannot read, since no caller can use
a partial result. `SHAMapStoreImp::run` abandons the rotation cycle on false, which
is what its former `catch (SHAMapMissingNode)` did and what `clearPrior` plus the
archive deletion in `rotate` require. `processReplayDeltaRequest` answers
`reNO_NODE`, the code `xrpl.proto` documents for nodes we do not have, rather than
sending a transaction list it knows is short, and clears the header it had already
set so an error reply carries no partial payload. The two `RCLConsensus` walks run
on unbacked maps, which have no node store to fail to read, so they cannot report an
incomplete result.

`walkMapParallel` decided its result from the exceptions its workers caught, yet
those workers record an unreadable child in `missingNodes` instead, so it never
consulted the list it was filling. The result now counts what this call recorded,
measured against the caller's initial vector size. The pass that reads the root's
children runs before any worker and dropped a null child silently, because the loop
that spawns workers skips one; it records the miss itself now. A one-node map
reports complete, which is what `walkMap` already reported for the same input, and
the worker handler catches `std::exception` so that nothing leaves a worker's
thread.

Nine gtests in `src/tests/libxrpl/shamap/SHAMapMissingNode.cpp` cover the three
walks over a partially copied map, the missing-node budget, an early stop by the
visitor, a stop at the root, a map holding only its root, and a map whose root is a
leaf.
2026-09-23 16:07:49 +02:00

416 lines
14 KiB
C++

#include <xrpl/basics/IntrusivePointer.h> // IWYU pragma: keep
#include <xrpl/basics/IntrusivePointer.ipp> // IWYU pragma: keep
#include <xrpl/basics/Log.h>
#include <xrpl/basics/contract.h>
#include <xrpl/basics/safe_cast.h>
#include <xrpl/beast/utility/instrumentation.h>
#include <xrpl/shamap/SHAMap.h>
#include <xrpl/shamap/SHAMapInnerNode.h>
#include <xrpl/shamap/SHAMapItem.h>
#include <xrpl/shamap/SHAMapMissingNode.h>
#include <xrpl/shamap/SHAMapTreeNode.h>
#include <boost/smart_ptr/intrusive_ptr.hpp>
#include <array>
#include <exception>
#include <mutex>
#include <sstream>
#include <stack>
#include <string>
#include <thread>
#include <utility>
#include <vector>
namespace xrpl {
// This code is used to compare another node's transaction tree
// to our own. It returns a map containing all items that are different
// between two SHA maps. It is optimized not to descend down tree
// branches with the same branch hash. A limit can be passed so
// that we will abort early if a node sends a map to us that
// makes no sense at all. (And our sync algorithm will avoid
// synchronizing matching branches too.)
bool
SHAMap::walkBranch(
SHAMapTreeNode* node,
boost::intrusive_ptr<SHAMapItem const> const& otherMapItem,
bool isFirstMap,
Delta& differences,
int& maxCount) const
{
// Walk a branch of a SHAMap that's matched by an empty branch or single
// item in the other map
std::stack<SHAMapTreeNode*, std::vector<SHAMapTreeNode*>> nodeStack;
nodeStack.push(node);
bool emptyBranch = !otherMapItem;
while (!nodeStack.empty())
{
node = nodeStack.top();
nodeStack.pop();
if (node->isInner())
{
// This is an inner node, add all non-empty branches
auto inner = safeDowncast<SHAMapInnerNode*>(node);
for (auto i = 0u; i < SHAMapInnerNode::kBranchFactor; ++i)
{
if (!inner->isEmptyBranch(i))
nodeStack.push({descendThrow(inner, i)});
}
}
else
{
// This is a leaf node, process its item
auto item = safeDowncast<SHAMapLeafNode*>(node)->peekItem();
if (emptyBranch || (item->key() != otherMapItem->key()))
{
// unmatched
if (isFirstMap)
{
differences.insert(std::make_pair(item->key(), DeltaRef(item, nullptr)));
}
else
{
differences.insert(std::make_pair(item->key(), DeltaRef(nullptr, item)));
}
if (--maxCount <= 0)
return false;
}
else if (item->slice() != otherMapItem->slice())
{
// non-matching items with same tag
if (isFirstMap)
{
differences.insert(std::make_pair(item->key(), DeltaRef(item, otherMapItem)));
}
else
{
differences.insert(std::make_pair(item->key(), DeltaRef(otherMapItem, item)));
}
if (--maxCount <= 0)
return false;
emptyBranch = true;
}
else
{
// exact match
emptyBranch = true;
}
}
}
if (!emptyBranch)
{
// otherMapItem was unmatched, must add
if (isFirstMap)
{ // this is first map, so other item is from second
differences.insert(
std::make_pair(otherMapItem->key(), DeltaRef(nullptr, otherMapItem)));
}
else
{
differences.insert(
std::make_pair(otherMapItem->key(), DeltaRef(otherMapItem, nullptr)));
}
if (--maxCount <= 0)
return false;
}
return true;
}
bool
SHAMap::compare(SHAMap const& otherMap, Delta& differences, int maxCount) const
{
// compare two hash trees, add up to maxCount differences to the difference
// table return value: true=complete table of differences given, false=too
// many differences throws on corrupt tables or missing nodes CAUTION:
// otherMap is not locked and must be immutable
XRPL_ASSERT(
isValid() && otherMap.isValid(), "xrpl::SHAMap::compare : valid state and valid input");
if (getHash() == otherMap.getHash())
return true;
using StackEntry = std::pair<SHAMapTreeNode*, SHAMapTreeNode*>;
std::stack<StackEntry, std::vector<StackEntry>> nodeStack; // track nodes we've pushed
nodeStack.emplace(root_.get(), otherMap.root_.get());
while (!nodeStack.empty())
{
auto [ourNode, otherNode] = nodeStack.top();
nodeStack.pop();
if ((ourNode == nullptr) || (otherNode == nullptr))
{
// LCOV_EXCL_START
UNREACHABLE("xrpl::SHAMap::compare : missing a node");
Throw<SHAMapMissingNode>(type_, uint256());
// LCOV_EXCL_STOP
}
if (ourNode->isLeaf() && otherNode->isLeaf())
{
// two leaves
auto ours = safeDowncast<SHAMapLeafNode*>(ourNode);
auto other = safeDowncast<SHAMapLeafNode*>(otherNode);
if (ours->peekItem()->key() == other->peekItem()->key())
{
if (ours->peekItem()->slice() != other->peekItem()->slice())
{
differences.insert(
std::make_pair(
ours->peekItem()->key(),
DeltaRef(ours->peekItem(), other->peekItem())));
if (--maxCount <= 0)
return false;
}
}
else
{
differences.insert(
std::make_pair(ours->peekItem()->key(), DeltaRef(ours->peekItem(), nullptr)));
if (--maxCount <= 0)
return false;
differences.insert(
std::make_pair(other->peekItem()->key(), DeltaRef(nullptr, other->peekItem())));
if (--maxCount <= 0)
return false;
}
}
else if (ourNode->isInner() && otherNode->isLeaf())
{
auto ours = safeDowncast<SHAMapInnerNode*>(ourNode);
auto other = safeDowncast<SHAMapLeafNode*>(otherNode);
if (!walkBranch(ours, other->peekItem(), true, differences, maxCount))
return false;
}
else if (ourNode->isLeaf() && otherNode->isInner())
{
auto ours = safeDowncast<SHAMapLeafNode*>(ourNode);
auto other = safeDowncast<SHAMapInnerNode*>(otherNode);
if (!otherMap.walkBranch(other, ours->peekItem(), false, differences, maxCount))
return false;
}
else if (ourNode->isInner() && otherNode->isInner())
{
auto ours = safeDowncast<SHAMapInnerNode*>(ourNode);
auto other = safeDowncast<SHAMapInnerNode*>(otherNode);
for (auto i = 0u; i < SHAMapInnerNode::kBranchFactor; ++i)
{
if (ours->getChildHash(i) != other->getChildHash(i))
{
if (other->isEmptyBranch(i))
{
// We have a branch, the other tree does not
SHAMapTreeNode* iNode = descendThrow(ours, i);
if (!walkBranch(iNode, nullptr, true, differences, maxCount))
return false;
}
else if (ours->isEmptyBranch(i))
{
// The other tree has a branch, we do not
SHAMapTreeNode* iNode = otherMap.descendThrow(other, i);
if (!otherMap.walkBranch(iNode, nullptr, false, differences, maxCount))
return false;
}
else
{ // The two trees have different non-empty branches
nodeStack.emplace(descendThrow(ours, i), otherMap.descendThrow(other, i));
}
}
}
}
else
{
// LCOV_EXCL_START
UNREACHABLE("xrpl::SHAMap::compare : invalid node");
// LCOV_EXCL_STOP
}
}
return true;
}
void
SHAMap::walkMap(std::vector<SHAMapMissingNode>& missingNodes, int maxMissing) const
{
if (!root_->isInner()) // root_ is only node, and we have it
return;
using StackEntry = intr_ptr::SharedPtr<SHAMapInnerNode>;
std::stack<StackEntry, std::vector<StackEntry>> nodeStack;
nodeStack.push(intr_ptr::staticPointerCast<SHAMapInnerNode>(root_));
while (!nodeStack.empty())
{
intr_ptr::SharedPtr<SHAMapInnerNode> const node = std::move(nodeStack.top());
nodeStack.pop();
for (auto i = 0u; i < SHAMapInnerNode::kBranchFactor; ++i)
{
if (!node->isEmptyBranch(i))
{
SHAMapTreeNodePtr const nextNode = descendNoStore(*node, i);
if (nextNode)
{
if (nextNode->isInner())
nodeStack.push(intr_ptr::staticPointerCast<SHAMapInnerNode>(nextNode));
}
else
{
missingNodes.emplace_back(type_, node->getChildHash(i));
if (--maxMissing <= 0)
return;
}
}
}
}
}
bool
SHAMap::walkMapParallel(std::vector<SHAMapMissingNode>& missingNodes, int maxMissing) const
{
if (!root_->isInner()) // root_ is only node, and we have it
return true;
// Only the nodes this call records count towards the result, so remember what the
// caller already had.
auto const initialMissing = missingNodes.size();
using StackEntry = intr_ptr::SharedPtr<SHAMapInnerNode>;
std::array<SHAMapTreeNodePtr, SHAMapInnerNode::kBranchFactor> topChildren;
{
// This loop runs before the workers start, so it needs no lock.
auto const& innerRoot = intr_ptr::staticPointerCast<SHAMapInnerNode>(root_);
for (auto i = 0u; i < SHAMapInnerNode::kBranchFactor; ++i)
{
if (innerRoot->isEmptyBranch(i))
continue;
topChildren[i] = descendNoStore(*innerRoot, i);
if (!topChildren[i])
{
// A root child that cannot be read hides its whole subtree. Record it here,
// because the loop below skips a null child without visiting it.
missingNodes.emplace_back(type_, innerRoot->getChildHash(i));
if (--maxMissing <= 0)
return false;
}
}
}
std::vector<std::thread> workers;
workers.reserve(SHAMapInnerNode::kBranchFactor);
std::vector<std::string> exceptions;
exceptions.reserve(SHAMapInnerNode::kBranchFactor);
std::array<std::stack<StackEntry, std::vector<StackEntry>>, SHAMapInnerNode::kBranchFactor>
nodeStacks;
// This mutex is used inside the worker threads to protect `missingNodes`
// and `maxMissing` from race conditions
std::mutex m;
for (auto rootChildIndex = 0u; rootChildIndex < SHAMapInnerNode::kBranchFactor;
++rootChildIndex)
{
auto const& child = topChildren[rootChildIndex];
if (!child || !child->isInner())
continue;
nodeStacks[rootChildIndex].push(intr_ptr::staticPointerCast<SHAMapInnerNode>(child));
JLOG(journal_.debug()) << "starting worker " << rootChildIndex;
workers.emplace_back(
[&m, &missingNodes, &maxMissing, &exceptions, this](
std::stack<StackEntry, std::vector<StackEntry>> nodeStack) {
try
{
while (!nodeStack.empty())
{
intr_ptr::SharedPtr<SHAMapInnerNode> const node =
std::move(nodeStack.top());
XRPL_ASSERT(node, "xrpl::SHAMap::walkMapParallel : non-null node");
nodeStack.pop();
for (auto i = 0u; i < SHAMapInnerNode::kBranchFactor; ++i)
{
if (node->isEmptyBranch(i))
continue;
SHAMapTreeNodePtr const nextNode = descendNoStore(*node, i);
if (nextNode)
{
if (nextNode->isInner())
{
nodeStack.push(
intr_ptr::staticPointerCast<SHAMapInnerNode>(nextNode));
}
}
else
{
std::scoped_lock const l{m};
missingNodes.emplace_back(type_, node->getChildHash(i));
if (--maxMissing <= 0)
return;
}
}
}
}
catch (std::exception const& e)
{
// LCOV_EXCL_START
// A worker must not let an exception leave its thread, so record it
// and let the join below report it.
std::scoped_lock const l(m);
exceptions.emplace_back(e.what());
// LCOV_EXCL_STOP
}
},
std::move(nodeStacks[rootChildIndex]));
}
for (std::thread& worker : workers)
worker.join();
std::scoped_lock const l(m);
if (!exceptions.empty())
{
// LCOV_EXCL_START
std::stringstream ss;
ss << "Exception(s) in ledger load: ";
for (auto const& e : exceptions)
ss << e << ", ";
JLOG(journal_.error()) << ss.str();
return false;
// LCOV_EXCL_STOP
}
// A node the workers could not read is recorded in `missingNodes` rather than thrown,
// so the result has to consult it too.
auto const found = missingNodes.size() - initialMissing;
if (found != 0)
{
JLOG(journal_.error()) << "Missing node(s) in ledger load: " << found
<< ", first: " << missingNodes[initialMissing].what();
return false;
}
return true;
}
} // namespace xrpl