Files
rippled/src/test/app/LedgerHistory_test.cpp
Bart adb73e2584 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.

All fourteen call sites now branch 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.
2026-08-25 17:58:00 -04:00

192 lines
6.5 KiB
C++

#include <test/jtx/Account.h>
#include <test/jtx/CheckMessageLogs.h>
#include <test/jtx/Env.h>
#include <test/jtx/JTx.h>
#include <test/jtx/amount.h>
#include <test/jtx/envconfig.h>
#include <test/jtx/noop.h>
#include <xrpld/app/ledger/LedgerHistory.h>
#include <xrpld/app/ledger/LedgerMaster.h>
#include <xrpl/basics/base_uint.h>
#include <xrpl/basics/chrono.h>
#include <xrpl/basics/contract.h>
#include <xrpl/beast/insight/NullCollector.h>
#include <xrpl/beast/unit_test/suite.h>
#include <xrpl/ledger/ApplyView.h>
#include <xrpl/ledger/Ledger.h>
#include <xrpl/ledger/OpenView.h>
#include <xrpl/nodestore/NodeObject.h>
#include <xrpl/protocol/STTx.h>
#include <xrpl/tx/apply.h>
#include <cassert>
#include <memory>
#include <stdexcept>
#include <vector>
namespace xrpl::test {
class LedgerHistory_test : public beast::unit_test::Suite
{
public:
/**
* Generate a new ledger by hand, applying a specific close time offset
* and optionally inserting a transaction.
*
* If prev is nullptr, then the genesis ledger is made and no offset or
* transaction is applied.
*/
static std::shared_ptr<Ledger>
makeLedger(
std::shared_ptr<Ledger const> const& prev,
jtx::Env& env,
LedgerHistory& lh,
NetClock::duration closeOffset,
std::shared_ptr<STTx const> stx = {})
{
if (!prev)
{
assert(!stx);
return std::make_shared<Ledger>(
kCreateGenesis,
Rules{env.app().config().features},
env.app().config().fees.toFees(),
std::vector<uint256>{},
env.app().getNodeFamily());
}
auto res = std::make_shared<Ledger>(*prev, prev->header().closeTime + closeOffset);
if (stx)
{
OpenView accum(&*res);
applyTransaction(env.app(), accum, *stx, false, TapNone, env.journal);
accum.apply(*res);
}
res->updateSkipList();
{
res->stateMap().flushDirty(NodeObjectType::AccountNode);
res->txMap().flushDirty(NodeObjectType::TransactionNode);
}
res->unshare();
// Accept ledger. Thrown rather than asserted because a failure leaves res unusable, and
// an assert would let a Release build hand every caller below a broken ledger. This helper
// is static, so BEAST_EXPECT is out of reach; the suite runner reports the throw.
if (!res->setAccepted(
res->header().closeTime,
res->header().closeTimeResolution,
true /* close time correct*/))
{
Throw<std::runtime_error>("makeLedger: ledger could not be accepted");
}
lh.insert(res, false);
return res;
}
void
testHandleMismatch()
{
testcase("LedgerHistory mismatch");
using namespace jtx;
using namespace std::chrono;
// No mismatch
{
bool found = false;
Env env{*this, envconfig(), std::make_unique<CheckMessageLogs>("MISMATCH ", &found)};
LedgerHistory lh{beast::insight::NullCollector::make(), env.app()};
auto const genesis = makeLedger({}, env, lh, 0s);
uint256 const dummyTxHash{1};
lh.builtLedger(genesis, dummyTxHash, {});
lh.validatedLedger(genesis, dummyTxHash);
BEAST_EXPECT(!found);
}
// Close time mismatch
{
bool found = false;
Env env{
*this,
envconfig(),
std::make_unique<CheckMessageLogs>("MISMATCH on close time", &found)};
LedgerHistory lh{beast::insight::NullCollector::make(), env.app()};
auto const genesis = makeLedger({}, env, lh, 0s);
auto const ledgerA = makeLedger(genesis, env, lh, 4s);
auto const ledgerB = makeLedger(genesis, env, lh, 40s);
uint256 const dummyTxHash{1};
lh.builtLedger(ledgerA, dummyTxHash, {});
lh.validatedLedger(ledgerB, dummyTxHash);
BEAST_EXPECT(found);
}
// Prior ledger mismatch
{
bool found = false;
Env env{
*this,
envconfig(),
std::make_unique<CheckMessageLogs>("MISMATCH on prior ledger", &found)};
LedgerHistory lh{beast::insight::NullCollector::make(), env.app()};
auto const genesis = makeLedger({}, env, lh, 0s);
auto const ledgerA = makeLedger(genesis, env, lh, 4s);
auto const ledgerB = makeLedger(genesis, env, lh, 40s);
auto const ledgerAC = makeLedger(ledgerA, env, lh, 4s);
auto const ledgerBD = makeLedger(ledgerB, env, lh, 4s);
uint256 const dummyTxHash{1};
lh.builtLedger(ledgerAC, dummyTxHash, {});
lh.validatedLedger(ledgerBD, dummyTxHash);
BEAST_EXPECT(found);
}
// Simulate a bug in which consensus may agree on transactions, but
// somehow generate different ledgers
for (bool const txBug : {true, false})
{
std::string const msg = txBug ? "MISMATCH with same consensus transaction set"
: "MISMATCH on consensus transaction set";
bool found = false;
Env env{*this, envconfig(), std::make_unique<CheckMessageLogs>(msg, &found)};
LedgerHistory lh{beast::insight::NullCollector::make(), env.app()};
Account const alice{"A1"};
Account const bob{"A2"};
env.fund(XRP(1000), alice, bob);
env.close();
auto const ledgerBase = env.app().getLedgerMaster().getClosedLedger();
JTx const txAlice = env.jt(noop(alice));
auto const ledgerA = makeLedger(ledgerBase, env, lh, 4s, txAlice.stx);
JTx const txBob = env.jt(noop(bob));
auto const ledgerB = makeLedger(ledgerBase, env, lh, 4s, txBob.stx);
lh.builtLedger(ledgerA, txAlice.stx->getTransactionID(), {});
// Simulate the bug by claiming ledgerB had the same consensus hash
// as ledgerA, but somehow generated different ledgers
lh.validatedLedger(
ledgerB, txBug ? txAlice.stx->getTransactionID() : txBob.stx->getTransactionID());
BEAST_EXPECT(found);
}
}
void
run() override
{
testHandleMismatch();
}
};
BEAST_DEFINE_TESTSUITE(LedgerHistory, app, xrpl);
} // namespace xrpl::test