Background ledger acquisition reads and writes SHAMap::state_, SHAMap::full_, SHAMap::ledgerSeq_ and
SHAMapInnerNode::fullBelowGen_ concurrently with the thread driving it, so all four are std::atomic.
state_ is read through state() and written through setInvalid() and the existing setters, and
SHAMapState carries an explicit std::uint8_t underlying type. finishFetch() withdraws full_ with an
exchange behind a relaxed load, so exactly one of the reader threads that miss reports the gap, and a
map that is already not full stays off the exclusive-write path: full_ shares a cache line with
state_ and ledgerSeq_, and a walk posts up to 512 reads per pass. ledgerSeq_ is read through
ledgerSeq() and relaxed both ways, since it only serves as a lookup hint for a nodestore keyed by
hash.
Ledger::setFull() sets each map's ledger sequence before its full flag. A release store publishes
only what is sequenced before it, and the finishFetch() thread that wins the exchange on the flag
reads the sequence after that, so this is the order that makes the sequence visible to the once-only
gap report.
Static assertions pin the three SHAMap members lock-free, and pin fullBelowGen_'s size and alignment
to those of a plain std::uint32_t, so SHAMapInnerNode's packed layout stays byte-identical and
isFullBelow() takes no mutex once per node of every walk. Its accessors are relaxed, since a
generation is only ever compared for equality and the children it vouches for are published through
the node's own child lock. Three tests cover this: sixteen unresolvable branches posted at a backed
map with four nodestore reader threads, so finishFetch() runs concurrently for one map and the single
gap report is observable; that Ledger::setFull() publishes the sequence that report names; and that
every node the sync path hands to a filter carries it.
tryDB() decides an acquisition can never succeed on two paths: a header whose hash or sequence does
not match what was asked for, and a zero account hash. Both set failed_, and init() and trigger() now
call done() on that path, so the object signals whatever is waiting on it and logFailure() records
the hash in recentFailures_. That is what stops the next round asking for the same doomed ledger.
checkLocal() is the third route into tryDB() and does the same, so all three agree.
testLocalFailureSignalsDone drives both of the entry points that were silent. The first goes through
InboundLedgers::acquire(), the only caller of init(); the second constructs an acquisition with no
header and triggers it, which is the trigger() route. Each uses a hash of its own, since
recentFailures_ is shared and keyed by hash, and each asserts on recentFailures_ rather than on the
flags, since being remembered as a failure is the caller-visible consequence of having signalled.
src/tests/libxrpl/shamap/DeepChain.h builds the node chains both acquisition suites need. It offers
two shapes: inner nodes running all the way to SHAMap::kLeafDepth, a depth only a leaf may occupy,
so feeding one leaves the map provably impossible; and toLeaf(), which stops at a real transaction
leaf and so completes an acquisition. fill() and addOffendingNode() divide a chain at its deepest
node, so a caller populates a map and then offers that one node itself, and every entry point takes
a seed, since caches and fetch packs are keyed by hash and two chains must not resolve each other's
nodes. It sits under src/tests/libxrpl because building a chain needs nothing outside libxrpl, while
the peer harness that wraps it for these suites is xrpld-only.
src/test/app/AcquireTestHelpers.h holds the fakes both acquisition suites need: ChargeRecordingPeer,
which records what it was charged and is otherwise inert; RequestCountingPeerSet, which selects peers
the way the real one does and whose counters are safe to read while the retry timer runs, so a case
can see which peers an acquisition chose and what limit it asked for; packetFor(), which wraps a
DeepChain's nodes as a TMLedgerData reply so a case can go through the real gotData() dispatch rather
than reproducing it; waitFor(), for the paths that finish on another thread; and tallyIs(), for
reading a verdict as counts.
Both classes carry the seams the suites reach through, stated where the reader meets them.
TransactionAcquire and InboundLedger drop final and gain a defaulted retryInterval constructor
parameter beside a kRetryInterval constant, so a case can run a whole timeout chain in a fraction of
a second; TransactionAcquire::map_ is protected, since nothing else publishes the map's state; and
InboundLedger keeps TriggerReason, trigger() and done() protected, so a case can drive an acquisition
the way the timer chain does without routing through the JobQueue. No production call site
passes one, since TimeoutCounter already takes the interval, and its onTimer() hook documents that
the lock it is handed is this object's own recursive mutex rather than anything belonging to a
PeerSet.
Ten cases pin existing behavior across the two suites: a set completes, asks for nothing further and
reaches InboundTransactions; two peers each supplying a different piece are both accepted without
penalty; a root that does not hash-match leaves the acquisition able to try another peer and the map
untouched; a repeated root and a repeated non-root node are each free, while a reply whose node data
cannot be deserialized is charged, which is what gives the free-of-charge assertions their teeth;
init() asks only the peers claiming to have the set; a ledger that resolves locally completes on the
spot and is handed to LedgerMaster; and each suite's retry timer re-asks and then gives up. Each
suite shares one jtx::Env, which costs far more to build than any case, and hands out a fresh chain
seed per case so nothing one case fed can resolve another's nodes.
ConsensusTransSetSF::gotNode() also names its parse threshold kMinTxNodeBytesToParse, which the
helper asserts a chain's leaf payload stays below so a fabricated leaf is never parsed and
resubmitted as a transaction. It is sizeof(std::uint32_t) + kMinShaMapItemBytes + 1, or 17, and the
docstring says it is the long-standing threshold rather than a derived bound: the smallest
hash-prefixed leaf is 16 bytes and nothing that size is a signed transaction either.
SHAMapAddNode gains getBad() and getDuplicate() beside getGood(), so a verdict can be read as
counts. Every accessor, mutator and factory is documented, and reset(), get() and operator+= move
after the static factories, so the counters and the ways to read or combine them stay grouped. get()
is a log format, and src/tests/libxrpl/shamap/SHAMapAddNode.cpp is the one place that depends on its
wording: it pins that format, the counts the three accessors report, and the way one tally
accumulates into another.