mirror of
https://github.com/XRPLF/rippled.git
synced 2026-09-02 07:10:34 +00:00
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.
192 lines
6.5 KiB
C++
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
|