mirror of
https://github.com/XRPLF/rippled.git
synced 2026-10-01 01:08:03 +00:00
fix: Refuse to make an invalid SHAMap or Ledger immutable
SHAMap::setImmutable() now returns [[nodiscard]] bool and refuses a map already proven impossible. Every state change goes through trySetState(), the only writer of state_ past construction, so the order between the states is stated once: its compare-exchange can't leave Invalid however it interleaves with another thread's, and setInvalid() stores through the same funnel rather than behind its back. Invalid is stored unconditionally there, since only the map itself reaches that verdict and a walk that reaches it has to win against a thread settling the map; refusing to overwrite Immutable would leave a map proven impossible reporting itself sound, which is what nothing downstream could recover from. Ledger::setImmutable()/setAccepted() do the same one level up, checking mapsValid() before touching anything and settling both maps independently so neither is left mid-sync because the other refused. The map hashes are read before the maps are settled, since getHash() can unshare a dirty tree, but written to the header only once both maps have made it. A walk that invalidates a map in between therefore leaves the header describing what the ledger was built from rather than a map that has since been abandoned. Every call site now branches on the result. The two genesis paths and buildLedgerImpl() call logicError(), since consensus can't tolerate an invalid ledger; the load paths return early instead; InboundLedger and TransactionAcquire withdraw complete_ alongside the failure, since for them a refusal is an outcome a peer can produce. A test helper that cannot reach BEAST_EXPECT throws instead, so a refusal cannot hand a broken ledger to the assertions below.
This commit is contained in:
@@ -257,13 +257,43 @@ public:
|
||||
header_.validated = true;
|
||||
}
|
||||
|
||||
void
|
||||
/**
|
||||
* Mark this ledger as accepted and attempt to make it immutable.
|
||||
*
|
||||
* The close-time fields are recorded before the maps are settled, since the
|
||||
* ledger hash covers them.
|
||||
*
|
||||
* @param closeTime The consensus-agreed close time.
|
||||
* @param closeResolution The close time resolution.
|
||||
* @param correctCloseTime Whether consensus agreed on the close time; if
|
||||
* false, kSLcfNoConsensusTime is recorded in closeFlags instead.
|
||||
* @return What setImmutable() returned, so false means the ledger must be
|
||||
* discarded rather than retried.
|
||||
*/
|
||||
[[nodiscard]] bool
|
||||
setAccepted(
|
||||
NetClock::time_point closeTime,
|
||||
NetClock::duration closeResolution,
|
||||
bool correctCloseTime);
|
||||
|
||||
void
|
||||
/**
|
||||
* Mark this ledger as immutable, so it can no longer be modified.
|
||||
*
|
||||
* A ledger built or loaded locally cannot have an invalid map, since only a
|
||||
* map syncing against hashes from outside can be proven impossible (see
|
||||
* SHAMap::addKnownNode), so a caller on such a path may treat a false return
|
||||
* as a broken internal invariant. A caller assembling a ledger from peer data
|
||||
* may not: for it, false is an outcome a peer can produce.
|
||||
*
|
||||
* @param rehash Whether to recompute the ledger hash from the header fields.
|
||||
* The transaction and account hashes are recomputed from the maps too,
|
||||
* but only the first time.
|
||||
* @return false if either map is Invalid, leaving the immutable flag unset and
|
||||
* the header untouched. A map invalidated partway through can still
|
||||
* leave the other one immutable, so a false return means the ledger
|
||||
* must be discarded rather than retried.
|
||||
*/
|
||||
[[nodiscard]] bool
|
||||
setImmutable(bool rehash = true);
|
||||
|
||||
bool
|
||||
@@ -272,16 +302,28 @@ public:
|
||||
return immutable_;
|
||||
}
|
||||
|
||||
/* Mark this ledger as "should be full".
|
||||
/**
|
||||
* Whether neither map has been found invalid.
|
||||
*
|
||||
* Read by whatever assembles the ledger, which cannot tell a map that
|
||||
* is merely incomplete from one that has been abandoned by looking at
|
||||
* what a walk returned. See SHAMap::isValid().
|
||||
*
|
||||
* @return Whether both maps can still be the maps the header names.
|
||||
*/
|
||||
[[nodiscard]] bool
|
||||
mapsValid() const
|
||||
{
|
||||
return txMap_.isValid() && stateMap_.isValid();
|
||||
}
|
||||
|
||||
"Full" is metadata property of the ledger, it indicates
|
||||
that the local server wants all the corresponding nodes
|
||||
in durable storage.
|
||||
|
||||
This is marked `const` because it reflects metadata
|
||||
and not data that is in common with other nodes on the
|
||||
network.
|
||||
*/
|
||||
/**
|
||||
* Mark this ledger as "should be full", indicating that the local server
|
||||
* wants all the corresponding nodes in durable storage.
|
||||
*
|
||||
* Const because it reflects metadata, not data this ledger shares with
|
||||
* other nodes on the network.
|
||||
*/
|
||||
void
|
||||
setFull() const
|
||||
{
|
||||
@@ -421,6 +463,24 @@ private:
|
||||
static std::pair<std::shared_ptr<STTx const>, std::shared_ptr<STObject const>>
|
||||
deserializeTxPlusMeta(SHAMapItem const& item);
|
||||
|
||||
/**
|
||||
* Make both maps immutable, without short-circuiting.
|
||||
*
|
||||
* A concurrent walk can invalidate one map after the other is settled,
|
||||
* so both calls are always made rather than one guarding the other:
|
||||
* each map becomes Immutable or stays Invalid on its own, and neither
|
||||
* is left mid-sync because the other refused.
|
||||
*
|
||||
* @return Whether both maps are immutable.
|
||||
*/
|
||||
[[nodiscard]] bool
|
||||
setMapsImmutable()
|
||||
{
|
||||
bool const txImmutable = txMap_.setImmutable();
|
||||
bool const stateImmutable = stateMap_.setImmutable();
|
||||
return txImmutable && stateImmutable;
|
||||
}
|
||||
|
||||
bool immutable_;
|
||||
|
||||
// A SHAMap containing the transactions associated with this ledger.
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
#include <xrpl/basics/Blob.h>
|
||||
#include <xrpl/basics/IntrusivePointer.h>
|
||||
#include <xrpl/basics/Log.h>
|
||||
#include <xrpl/basics/SHAMapHash.h>
|
||||
#include <xrpl/basics/base_uint.h>
|
||||
#include <xrpl/beast/utility/Journal.h>
|
||||
@@ -180,7 +181,14 @@ public:
|
||||
SHAMap&
|
||||
operator=(SHAMap const&) = delete;
|
||||
|
||||
// Take a snapshot of the given map:
|
||||
/**
|
||||
* Take a snapshot of the given map.
|
||||
*
|
||||
* @param other The map to snapshot. An Invalid source yields an Invalid
|
||||
* snapshot, since the two share the same node structure.
|
||||
* @param isMutable Whether the snapshot may be modified. Ignored when other
|
||||
* is Invalid, since that state outranks both alternatives.
|
||||
*/
|
||||
SHAMap(SHAMap const& other, bool isMutable);
|
||||
|
||||
// build new map
|
||||
@@ -218,8 +226,15 @@ public:
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
// Returns a new map that's a snapshot of this one.
|
||||
// Handles copy on write for mutable snapshots.
|
||||
/**
|
||||
* Return a new map that is a snapshot of this one.
|
||||
*
|
||||
* Handles copy on write for mutable snapshots. An invalid map yields an
|
||||
* invalid snapshot, since the two share the same node structure.
|
||||
*
|
||||
* @param isMutable Whether the snapshot may be modified.
|
||||
* @return The snapshot.
|
||||
*/
|
||||
std::shared_ptr<SHAMap>
|
||||
snapShot(bool isMutable) const;
|
||||
|
||||
@@ -408,7 +423,13 @@ public:
|
||||
SHAMapTreeNodePtr treeNode,
|
||||
SHAMapSyncFilter const* filter);
|
||||
|
||||
void
|
||||
/**
|
||||
* Mark this map as immutable, so it can no longer be modified.
|
||||
*
|
||||
* @return false if the map is Invalid and was left unchanged, true
|
||||
* otherwise.
|
||||
*/
|
||||
[[nodiscard]] bool
|
||||
setImmutable();
|
||||
|
||||
/**
|
||||
@@ -418,8 +439,24 @@ public:
|
||||
*/
|
||||
[[nodiscard]] bool
|
||||
isSynching() const;
|
||||
|
||||
/**
|
||||
* Mark this map as syncing, fixing its hash while still allowing missing
|
||||
* nodes to be added.
|
||||
*
|
||||
* Left unchanged if the map is Invalid, which is terminal - though that
|
||||
* case is itself treated as unreachable (and asserts in a build with
|
||||
* assertions enabled), since nothing should call this on a map that has
|
||||
* already been judged.
|
||||
*/
|
||||
void
|
||||
setSynching();
|
||||
|
||||
/**
|
||||
* Mark this map as no longer syncing, so it can be modified again.
|
||||
*
|
||||
* Does nothing if the map is Invalid, which is terminal.
|
||||
*/
|
||||
void
|
||||
clearSynching();
|
||||
|
||||
@@ -499,11 +536,27 @@ private:
|
||||
* Record that the map is provably not the one it claims to be.
|
||||
*
|
||||
* Private because only the map itself can prove that, from a node that
|
||||
* contradicts the hashes it is syncing against.
|
||||
* contradicts the hashes it is syncing against. Cannot fail, since
|
||||
* Invalid outranks every other state; see trySetState().
|
||||
*/
|
||||
void
|
||||
setInvalid();
|
||||
|
||||
/**
|
||||
* Move the map to a new state, atomically.
|
||||
*
|
||||
* The only writer of state_ past construction, so the order between
|
||||
* the states lives in one place: Invalid outranks all of them and is
|
||||
* always stored, while every other transition is refused once the map
|
||||
* is Invalid, which is what makes that verdict terminal.
|
||||
*
|
||||
* @param desired The state to move to.
|
||||
* @return false if the map is Invalid and the requested state is not,
|
||||
* leaving it unchanged; true otherwise.
|
||||
*/
|
||||
bool
|
||||
trySetState(SHAMapState desired);
|
||||
|
||||
// tree node cache operations
|
||||
SHAMapTreeNodePtr
|
||||
cacheLookup(SHAMapHash const& hash) const;
|
||||
@@ -740,11 +793,41 @@ SHAMap::state() const
|
||||
return state_.load(std::memory_order_acquire);
|
||||
}
|
||||
|
||||
inline void
|
||||
inline bool
|
||||
SHAMap::trySetState(SHAMapState desired)
|
||||
{
|
||||
// Stored outright rather than exchanged, since Invalid outranks every other state: only the map
|
||||
// itself reaches that verdict, from a node that contradicts the hashes it is syncing against,
|
||||
// so a walk that reaches it has to win however it interleaves with a thread settling the map.
|
||||
// An exchange that refused to overwrite Immutable would leave a map proven impossible reporting
|
||||
// itself sound, which is the one thing nothing downstream could recover from.
|
||||
if (desired == SHAMapState::Invalid)
|
||||
{
|
||||
state_.store(SHAMapState::Invalid, std::memory_order_release);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Compare-exchange rather than check-then-store, so the refusal to leave Invalid holds no
|
||||
// matter how this call interleaves with another thread's. Invalid is the only state this
|
||||
// refuses to leave; the loop simply retries if another one is stored meanwhile. No load ahead
|
||||
// of it, since a failed exchange both reports the state and refreshes expected.
|
||||
auto expected = SHAMapState::Modifying;
|
||||
while (expected != SHAMapState::Invalid)
|
||||
{
|
||||
if (state_.compare_exchange_weak(
|
||||
expected, desired, std::memory_order_acq_rel, std::memory_order_acquire))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
inline bool
|
||||
SHAMap::setImmutable()
|
||||
{
|
||||
XRPL_ASSERT(isValid(), "xrpl::SHAMap::setImmutable : state is valid");
|
||||
state_.store(SHAMapState::Immutable, std::memory_order_release);
|
||||
SOMETIMES(!isValid(), "xrpl::SHAMap::setImmutable : map is invalid");
|
||||
return trySetState(SHAMapState::Immutable);
|
||||
}
|
||||
|
||||
inline bool
|
||||
@@ -756,13 +839,32 @@ SHAMap::isSynching() const
|
||||
inline void
|
||||
SHAMap::setSynching()
|
||||
{
|
||||
state_.store(SHAMapState::Synching, std::memory_order_release);
|
||||
// Guarded, so this is not a way out of Invalid, matching clearSynching().
|
||||
if (!trySetState(SHAMapState::Synching))
|
||||
{
|
||||
// Unreachable today, though not because the map is Modifying: a ledger built from a header
|
||||
// starts out with both maps Synching already. It is unreachable because this is only ever
|
||||
// called on a map that has just been constructed, so nothing can have synced against it and
|
||||
// reached a verdict on it yet.
|
||||
// LCOV_EXCL_START
|
||||
UNREACHABLE("xrpl::SHAMap::setSynching : map is invalid");
|
||||
// LCOV_EXCL_STOP
|
||||
}
|
||||
}
|
||||
|
||||
inline void
|
||||
SHAMap::clearSynching()
|
||||
{
|
||||
state_.store(SHAMapState::Modifying, std::memory_order_release);
|
||||
// Guarded, so an invalid map stays invalid rather than being moved back to Modifying, which
|
||||
// passes isValid(). Refusing is the contract rather than a broken invariant, so this reports
|
||||
// instead of asserting: peer data produces the verdict, so an UNREACHABLE here would be an
|
||||
// abort a peer could ask for.
|
||||
SOMETIMES(!isValid(), "xrpl::SHAMap::clearSynching : map is invalid");
|
||||
if (!trySetState(SHAMapState::Modifying))
|
||||
{
|
||||
JLOG(journal_.warn()) << "Refused to clear synching on an invalid map, root hash "
|
||||
<< root_->getHash();
|
||||
}
|
||||
}
|
||||
|
||||
inline bool
|
||||
@@ -774,7 +876,9 @@ SHAMap::isValid() const
|
||||
inline void
|
||||
SHAMap::setInvalid()
|
||||
{
|
||||
state_.store(SHAMapState::Invalid, std::memory_order_release);
|
||||
// Through trySetState() like every other transition, so nothing writes state_ behind its back
|
||||
// and the order between the states is stated once. Cannot fail: Invalid outranks all of them.
|
||||
trySetState(SHAMapState::Invalid);
|
||||
}
|
||||
|
||||
inline void
|
||||
|
||||
Reference in New Issue
Block a user