Files
rippled/src/tests/libxrpl/shamap/SHAMapSync.cpp
Bart 05c5998634 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. The workers share one missing-node budget, so the critical section tests it
before it adds to the list and not only after, which keeps the total within the cap
the caller asked for.

Eleven gtests in `src/tests/libxrpl/shamap/SHAMapMissingNode.cpp` cover the three
walks over a partially copied map, the missing-node budget both within one walk and
across the workers, an empty branch of the root, 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. A
case in `src/test/app/LedgerReplay_test.cpp` asks for a replay delta on a ledger
whose transaction map holds nothing below its root, and checks the error reply.
2026-09-24 06:06:09 +02:00

176 lines
5.0 KiB
C++

#include <xrpl/basics/SHAMapHash.h>
#include <xrpl/basics/Slice.h>
#include <xrpl/basics/base_uint.h>
#include <xrpl/basics/random.h>
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/beast/xor_shift_engine.h>
#include <xrpl/shamap/SHAMap.h>
#include <xrpl/shamap/SHAMapItem.h>
#include <xrpl/shamap/SHAMapMissingNode.h>
#include <xrpl/shamap/SHAMapTreeNode.h>
#include <boost/smart_ptr/intrusive_ptr.hpp>
#include <gtest/gtest.h>
#include <helpers/TestSink.h>
#include <shamap/common.h>
#include <chrono>
#include <cstddef>
#include <list>
#include <utility>
#include <vector>
namespace xrpl::tests {
class SHAMapSyncTest : public ::testing::Test
{
protected:
beast::Journal const j_{TestSink::instance()};
beast::xor_shift_engine eng_;
boost::intrusive_ptr<SHAMapItem>
makeRandomAS()
{
return makeRandomAccountStateItem(eng_);
}
bool
confuseMap(SHAMap& map, std::size_t count)
{
// add a bunch of random states to a map, then remove them
// map should be the same
SHAMapHash const beforeHash = map.getHash();
std::list<uint256> items;
for (auto i = 0uz; i < count; ++i)
{
auto item = makeRandomAS();
items.push_back(item->key());
if (!map.addItem(SHAMapNodeType::TnAccountState, item))
{
ADD_FAILURE() << "Unable to add item to map";
return false;
}
}
for (auto const& item : items)
{
if (!map.delItem(item))
{
ADD_FAILURE() << "Unable to remove item from map";
return false;
}
}
if (beforeHash != map.getHash())
{
ADD_FAILURE() << "Hashes do not match " << beforeHash << " " << map.getHash();
return false;
}
return true;
}
};
TEST_F(SHAMapSyncTest, sync)
{
TestNodeFamily f{j_}, f2{j_};
SHAMap source{SHAMapType::FREE, f};
SHAMap destination{SHAMapType::FREE, f2};
static constexpr auto kItemCount = 10000uz;
static constexpr auto kInvariantInterval = 100uz;
static constexpr auto kNodesToConfuse = 500uz;
static constexpr auto kMaxNodesPerRequest = 2048;
for (auto i = 0uz; i < kItemCount; ++i)
{
source.addItem(SHAMapNodeType::TnAccountState, makeRandomAS());
if (i % kInvariantInterval == 0)
source.invariants();
}
source.invariants();
ASSERT_TRUE(confuseMap(source, kNodesToConfuse));
source.invariants();
source.setImmutable();
std::size_t count = 0;
source.visitLeaves([&count]([[maybe_unused]] auto const& item) { ++count; });
EXPECT_EQ(count, kItemCount);
std::vector<SHAMapMissingNode> missingNodes;
source.walkMap(missingNodes, kMaxNodesPerRequest);
EXPECT_TRUE(missingNodes.empty());
destination.setSynching();
{
std::vector<SHAMapNodeData> a;
ASSERT_TRUE(source.getNodeFat(SHAMapNodeID(), a, randBool(eng_), randInt(eng_, 2)));
ASSERT_FALSE(a.empty()) << "NodeSize";
auto node = SHAMapTreeNode::makeFromWire(makeSlice(a[0].data));
if (!node)
FAIL() << "Could not create node";
ASSERT_TRUE(destination.addRootNode(source.getHash(), std::move(node), nullptr).isGood());
}
do
{
f.clock().advance(std::chrono::seconds(1));
// get the list of nodes we know we need
auto nodesMissing = destination.getMissingNodes(kMaxNodesPerRequest, nullptr);
if (nodesMissing.empty())
break;
// get as many nodes as possible based on this information
std::vector<SHAMapNodeData> b;
for (auto& it : nodesMissing)
{
// Keep failures fatal here because this loop is data-dependent.
// non-deterministic number of times and the number of tests run
// should be deterministic
if (!source.getNodeFat(it.first, b, randBool(eng_), randInt(eng_, 2)))
FAIL() << "Unable to fetch node";
}
// Keep failures fatal here because this loop is data-dependent.
// non-deterministic number of times and the number of tests run
// should be deterministic
if (b.empty())
FAIL() << "No nodes returned";
for (auto const& i : b)
{
// Keep failures fatal here because this loop is data-dependent.
// non-deterministic number of times and the number of tests run
// should be deterministic
auto node = SHAMapTreeNode::makeFromWire(makeSlice(i.data));
if (!node)
FAIL() << "Could not create node";
if (i.isLeaf != node->isLeaf())
FAIL() << "Node is not a leaf";
if (!destination.addKnownNode(i.nodeID, std::move(node), nullptr).isUseful())
FAIL() << "Known node was not useful";
}
} while (true);
destination.clearSynching();
EXPECT_TRUE(source.deepCompare(destination));
destination.invariants();
}
} // namespace xrpl::tests