Compare commits

..
Author SHA1 Message Date
tequ 8845f4672e fix: keep ledger_history in standalone mode for rwdb null node store
Since #728, [node_db] type=rwdb is the null node store. SHAMapStoreImp
sizes LedgerMaster's in-memory retained-ledger window from ledger_history
and throws when it is 0 or "full". Config::setup, however, unconditionally
forced LEDGER_HISTORY to 0 in standalone mode, so any standalone node
using rwdb (including the shipped cfg/rippled.cfg, and tools such as
xrpld-lab) aborted at startup with:

    RWDB null mode requires a finite ledger_history > 0

jtx builds its Config without Config::setup, so the unit test suite never
exercised this path.

Gate the standalone reset on !nullBackend() instead of removing it: for
persistent backends standalone behaviour is unchanged, and for rwdb the
configured value (default 256) is kept, matching what jtx already runs
with. cfg/rippled.cfg needs no change.

Add a Config_test case that goes through Config::setup in standalone mode
and checks ledger_history is kept for type=rwdb and still zeroed for a
persistent backend.
2026-10-14 10:18:36 +09:00
43 changed files with 408 additions and 2956 deletions

No files matched your search

-1
View File
@@ -127,7 +127,6 @@ test.rpc > xrpl.basics
test.rpc > xrpld.app
test.rpc > xrpld.core
test.rpc > xrpld.net
test.rpc > xrpld.nodestore
test.rpc > xrpld.overlay
test.rpc > xrpld.rpc
test.rpc > xrpld.shamap
@@ -34,10 +34,6 @@
// If you add an amendment here, then do not forget to increment `numFeatures`
// in include/xrpl/protocol/Feature.h.
// Xahau testnet (NetworkID 21338) only. Registered as unsupported here; the
// server promotes it to Supported::yes / DefaultYes at startup if and only
// if it is configured with network_id 21338 (see Application.cpp).
XRPL_FIX (XahauTestnetGenesis, Supported::no, VoteBehavior::DefaultNo)
XRPL_FEATURE(RNG, Supported::yes, VoteBehavior::DefaultNo)
XRPL_FEATURE(PWALoader, Supported::yes, VoteBehavior::DefaultNo)
XRPL_FIX (20261005, Supported::yes, VoteBehavior::DefaultYes)
-2
View File
@@ -171,7 +171,6 @@ JSS(amendment_blocked); // out: NetworkOPs
JSS(amm_account); // in: amm_info
JSS(amount); // out: AccountChannels, amm_info
JSS(amount2); // out: amm_info
JSS(anchored_through); // out: CatalogueLoad
JSS(api_version); // in: many, out: Version
JSS(api_version_low); // out: Version
JSS(applied); // out: SubmitTransaction
@@ -745,7 +744,6 @@ JSS(TRANSACTION_FLAGS_INDICES); // out: RPC server_definitions
JSS(type_hex); // out: STPathSet
JSS(unl); // out: UnlList
JSS(unlimited); // out: Connection.h
JSS(unpinned); // out: CatalogueUnpin
JSS(uptime); // out: GetCounts
JSS(uri); // out: ValidatorSites
JSS(url); // in/out: Subscribe, Unsubscribe
-74
View File
@@ -1271,79 +1271,6 @@ public:
BEAST_EXPECT(table->needValidatedLedger(257));
}
void
testIgnoredUnsupported()
{
testcase("ignore_amendments");
using namespace std::chrono_literals;
weeks constexpr w(1);
test::jtx::Env env{*this, makeConfig()};
static std::vector<AmendmentTable::FeatureInfo> const supported =
makeDefaultYes(yes_);
// Ignore every unsupported_ amendment except the first one.
BEAST_EXPECT(unsupported_.size() > 1);
std::vector<std::string> const ignoredNames(
unsupported_.begin() + 1, unsupported_.end());
auto table = make_AmendmentTable(
env.app(),
w,
supported,
emptySection_,
emptySection_,
journal_,
makeSection(ignoredNames));
for (auto const& a : ignoredNames)
BEAST_EXPECT(table->isSupported(amendmentId(a)));
BEAST_EXPECT(!table->isSupported(amendmentId(unsupported_[0])));
majorityAmendments_t const majority;
// Enabling only ignored amendments must not block.
std::set<uint256> enabled;
for (auto const& a : ignoredNames)
enabled.insert(amendmentId(a));
table->doValidatedLedger(1, enabled, majority);
BEAST_EXPECT(!table->hasUnsupportedEnabled());
BEAST_EXPECT(table->unsupportedAmendments().empty());
// An ignored amendment must never be voted for.
auto const desired = table->getDesired();
for (auto const& a : ignoredNames)
BEAST_EXPECT(
std::find(desired.begin(), desired.end(), amendmentId(a)) ==
desired.end());
// A non-ignored unsupported amendment still blocks.
enabled.insert(amendmentId(unsupported_[0]));
table->doValidatedLedger(2, enabled, majority);
BEAST_EXPECT(table->hasUnsupportedEnabled());
// Bad entries are rejected.
Section bad("ignore_amendments");
bad.append("NotARealFeatureName");
try
{
make_AmendmentTable(
env.app(),
w,
supported,
emptySection_,
emptySection_,
journal_,
bad);
fail("expected bad [ignore_amendments] entry to throw");
}
catch (std::runtime_error const&)
{
pass();
}
}
void
testFeature(FeatureBitset const& feat)
{
@@ -1367,7 +1294,6 @@ public:
testBadConfig();
testEnableVeto();
testHasUnsupported();
testIgnoredUnsupported();
testFeature(all - fixMajorityCalc);
testFeature(all);
}
-146
View File
@@ -33,11 +33,9 @@
#include <xrpl/protocol/jss.h>
#include <boost/filesystem.hpp>
#include <chrono>
#include <exception>
#include <limits>
#include <string>
#include <thread>
namespace ripple {
namespace test {
@@ -95,27 +93,6 @@ class SHAMapStore_test : public beast::unit_test::suite
return cfg;
}
// Online delete with DatabasePinned over in-memory backends. Unlike
// pinnedPersistent, the hot store keeps nodes (type=rwdb is the null
// node store), so ledgers closed in the test can be fetched. There is
// no database_path, so every SHAMapStoreImp opens its own private
// state.db: a second instance built by a test does not see (or trip
// over) the rotation state of the Env's own.
static auto
pinnedInMemory(
std::unique_ptr<Config> cfg,
std::string const& nodePath,
std::string const& pinnedPath)
{
cfg = onlineDelete(std::move(cfg));
auto& section = cfg->section(ConfigSection::nodeDatabase());
section.set("type", "memory");
section.set("path", nodePath);
section.set("pinned_type", "memory");
section.set("pinned_path", pinnedPath);
return cfg;
}
bool
goodLedger(
jtx::Env& env,
@@ -1176,127 +1153,6 @@ public:
}
}
// catalogue_unpin of everything (or a rolled-back first load) leaves the
// pinned set empty. That must persist as "no pins", not as a string that
// fails to parse on the next start.
void
testPinnedRangeRestoreEmpty()
{
testcase("pinned range restore of an empty set");
using namespace jtx;
beast::temp_dir tempDir;
auto const tempPath = boost::filesystem::path(tempDir.path());
auto const nodePath = (tempPath / "node").string();
auto const pinnedPath = (tempPath / "pinned").string();
Env env(*this, envconfig(pinnedInMemory, nodePath, pinnedPath));
NodeStoreScheduler scheduler(env.app().getJobQueue());
auto const journal = env.app().journal("SHAMapStoreTest");
try
{
SHAMapStoreImp restoreCheck(env.app(), scheduler, journal);
RangeSet<std::uint32_t> ranges;
ranges.insert(range(100u, 200u));
restoreCheck.setPinnedRanges(ranges);
restoreCheck.setPinnedRanges(RangeSet<std::uint32_t>{});
restoreCheck.start();
restoreCheck.stop();
pass();
}
catch (std::exception const& e)
{
fail(
std::string("restart with no pinned ranges threw: ") +
e.what());
}
BEAST_EXPECT(
env.app().getLedgerMaster().getPinnedLedgersRangeSet().empty());
}
// A crash between a pinned-range checkpoint and the backend committing
// the last ledgers leaves the recorded interval's tail missing. Startup
// trims it back to the last ledger present instead of refusing to start.
void
testPinnedRangeRestoreTrimsMissingTail()
{
testcase("pinned range restore trims a missing tail");
using namespace jtx;
beast::temp_dir tempDir;
auto const tempPath = boost::filesystem::path(tempDir.path());
auto const nodePath = (tempPath / "node").string();
auto const pinnedPath = (tempPath / "pinned").string();
Env env(*this, envconfig(pinnedInMemory, nodePath, pinnedPath));
for (int i = 0; i < 4; ++i)
env.close();
auto& rdb = env.app().getRelationalDatabase();
auto const last = env.app().getLedgerMaster().getValidLedgerIndex();
auto const first = last - 2;
// Ledger saves may complete asynchronously; wait for the SQL rows.
for (int i = 0; i < 100 && rdb.getHashByIndex(last).isZero(); ++i)
std::this_thread::sleep_for(std::chrono::milliseconds(10));
if (!BEAST_EXPECT(
rdb.getHashByIndex(first).isNonZero() &&
rdb.getHashByIndex(last).isNonZero()))
return;
NodeStoreScheduler scheduler(env.app().getJobQueue());
auto const journal = env.app().journal("SHAMapStoreTest");
try
{
SHAMapStoreImp restoreCheck(env.app(), scheduler, journal);
// Recorded through last + 10, but only [first, last] exist.
RangeSet<std::uint32_t> recorded;
recorded.insert(range(first, last + 10));
restoreCheck.setPinnedRanges(recorded);
restoreCheck.start();
restoreCheck.stop();
}
catch (std::exception const& e)
{
fail(std::string("restart with a missing tail threw: ") + e.what());
return;
}
RangeSet<std::uint32_t> expected;
expected.insert(range(first, last));
BEAST_EXPECT(
env.app().getLedgerMaster().getPinnedLedgersRangeSet() == expected);
// A missing tail longer than the trim limit (65536 ledgers) is not
// an interrupted load: it means the wrong pinned store, so startup
// still refuses.
try
{
SHAMapStoreImp restoreCheck(env.app(), scheduler, journal);
RangeSet<std::uint32_t> recorded;
recorded.insert(range(first, last + 70000));
restoreCheck.setPinnedRanges(recorded);
restoreCheck.start();
restoreCheck.stop();
fail("Expected startup failure for an over-long missing tail");
}
catch (std::runtime_error const& e)
{
BEAST_EXPECT(
std::string(e.what()).find("Persisted pinned interval") !=
std::string::npos);
}
}
// Helper: compare two RangeSet<uint32_t> for set-equality. The
// test cares about which sequences are in the result, not the
// particular interval representation.
@@ -1446,8 +1302,6 @@ public:
testNullFactoryDropsWrites();
testRotate();
testPinnedRangeRestoreRequiresPinnedData();
testPinnedRangeRestoreEmpty();
testPinnedRangeRestoreTrimsMissingTail();
}
};
-232
View File
@@ -18,7 +18,6 @@
#include <test/jtx.h>
#include <test/jtx/envconfig.h>
#include <xrpld/app/ledger/LedgerMaster.h>
#include <xrpld/app/misc/AmendmentTable.h>
#include <xrpld/app/misc/HashRouter.h>
#include <xrpld/app/tx/apply.h>
#include <xrpld/app/tx/detail/XahauGenesis.h>
@@ -5892,236 +5891,6 @@ struct XahauGenesis_test : public beast::unit_test::suite
testNoClaimLate(features);
}
// inject an enabling ttAMENDMENT pseudo for `amendment` and close
void
injectAmendment(jtx::Env& env, uint256 const& amendment)
{
uint32_t const seq =
env.app().getLedgerMaster().getClosedLedger()->info().seq + 1;
env.app().openLedger().modify(
[&](OpenView& view, beast::Journal j) -> bool {
STTx tx(ttAMENDMENT, [&](auto& obj) {
obj.setAccountID(sfAccount, AccountID());
obj.setFieldH256(sfAmendment, amendment);
obj.setFieldU32(sfLedgerSequence, seq);
});
uint256 txID = tx.getTransactionID();
auto s = std::make_shared<ripple::Serializer>();
tx.add(*s);
env.app().getHashRouter().setFlags(txID, SF_PRIVATE2);
view.rawTxInsert(txID, std::move(s), nullptr);
return true;
});
env.close();
}
// check the genesis account is in the shape fixXahauTestnetGenesis
// leaves it in: blackholed, governance + reward hooks, Evernode-only seat,
// no hook state
void
checkTestnetGenesis(jtx::Env& env, AccountID const& evernode)
{
auto const acc = env.le(keylet::account(genesisAccID));
BEAST_REQUIRE(!!acc);
BEAST_EXPECT(acc->getAccountID(sfRegularKey) == noAccount());
BEAST_EXPECT(acc->getFieldU32(sfFlags) == lsfDisableMaster);
BEAST_EXPECT(!acc->isFieldPresent(sfHookStateCount));
BEAST_EXPECT(!acc->isFieldPresent(sfHookNamespaces));
// governance: 1 + IRR, IRD, IS\0, IMC; reward: 1
BEAST_EXPECT(acc->getFieldU32(sfOwnerCount) == 6);
BEAST_EXPECT(!env.le(keylet::signers(genesisAccID)));
BEAST_EXPECT(!env.le(keylet::hookStateDir(genesisAccID, beast::zero)));
auto const hooksLE = env.le(keylet::hook(genesisAccID));
BEAST_REQUIRE(!!hooksLE);
auto const& hooks = hooksLE->getFieldArray(sfHooks);
BEAST_REQUIRE(hooks.size() == 2);
BEAST_EXPECT(hooks[0].getFieldH256(sfHookHash) == governHookHash);
BEAST_EXPECT(hooks[1].getFieldH256(sfHookHash) == rewardHookHash);
std::vector<std::pair<std::vector<uint8_t>, std::vector<uint8_t>>>
expected{XahauGenesis::GovernanceParameters};
expected.emplace_back(
std::vector<uint8_t>{'I', 'S', 0},
std::vector<uint8_t>(evernode.data(), evernode.data() + 20));
expected.emplace_back(
std::vector<uint8_t>{'I', 'M', 'C'}, std::vector<uint8_t>{1});
auto const& params = hooks[0].getFieldArray(sfHookParameters);
BEAST_REQUIRE(params.size() == expected.size());
for (std::size_t i = 0; i < expected.size(); ++i)
{
BEAST_EXPECT(
params[i].getFieldVL(sfHookParameterName) == expected[i].first);
BEAST_EXPECT(
params[i].getFieldVL(sfHookParameterValue) ==
expected[i].second);
}
BEAST_EXPECT(hooks[1].getFieldArray(sfHookParameters).empty());
for (auto const& h : {governHookHash, rewardHookHash})
{
auto const def = env.le(keylet::hookDefinition(h));
BEAST_EXPECT(def && def->getFieldU64(sfReferenceCount) == 1);
}
}
void
testXahauTestnetGenesisFix(FeatureBitset features)
{
using namespace jtx;
testcase("fixXahauTestnetGenesis");
auto const evernodePK = parseBase58<PublicKey>(
TokenType::NodePublic,
"nHUetpkzTgf8c6A1DTGemRrhL4nMjsiJrtcsmmg75ufzV5EDMbWU");
BEAST_REQUIRE(!!evernodePK);
AccountID const evernode = calcAccountID(*evernodePK);
auto stateAt = [&](Env& env, std::vector<uint8_t> const& k) {
uint8_t key[32]{};
std::copy(k.begin(), k.end(), key + 32 - k.size());
return env.le(keylet::hookState(
genesisAccID, uint256::fromVoid(key), beast::zero));
};
std::vector<uint8_t> const mcKey{'M', 'C'};
// only supported on 21338
{
Env env{*this, makeNetworkConfig(21337), features};
BEAST_EXPECT(!env.app().getAmendmentTable().isSupported(
fixXahauTestnetGenesis));
}
{
Env env{*this, makeNetworkConfig(21338), features};
BEAST_EXPECT(env.app().getAmendmentTable().isSupported(
fixXahauTestnetGenesis));
}
// wrongly seated governance with stale hook state gets reset
{
Env env{
*this,
makeNetworkConfig(21338),
features - featureXahauGenesis};
auto const alice = Account("alice");
auto const bob = Account("bob");
auto const invoker = Account("invoker");
auto const user = Account("user");
env.fund(XRP(10000), alice, bob, invoker, user);
env.close();
XahauGenesis::TestNonGovernanceDistribution.clear();
XahauGenesis::TestL1Membership.clear();
XahauGenesis::TestL2Membership.clear();
XahauGenesis::TestL1Membership.emplace_back(
alice.human(), XRPAmount(10000000000));
XahauGenesis::TestL1Membership.emplace_back(
bob.human(), XRPAmount(10000000000));
activate(__LINE__, env, true, false, true, {}, 2 + 3 + 2);
XahauGenesis::TestL1Membership.clear();
Json::Value invoke;
invoke[jss::TransactionType] = "Invoke";
invoke[jss::Account] = invoker.human();
invoke[jss::Destination] = env.master.human();
env(invoke, fee(XRP(10)));
env.close();
{
auto const mc = stateAt(env, mcKey);
BEAST_REQUIRE(!!mc);
BEAST_EXPECT(
mc->getFieldVL(sfHookStateData) == std::vector<uint8_t>{2});
}
injectAmendment(env, fixXahauTestnetGenesis);
BEAST_EXPECT(
env.current()->rules().enabled(fixXahauTestnetGenesis));
checkTestnetGenesis(env, evernode);
BEAST_EXPECT(!stateAt(env, mcKey));
// governance re-initialises from the new parameters
env(invoke, fee(XRP(10)));
env.close();
{
auto const mc = stateAt(env, mcKey);
BEAST_REQUIRE(!!mc);
BEAST_EXPECT(
mc->getFieldVL(sfHookStateData) == std::vector<uint8_t>{1});
auto const seat = stateAt(
env,
std::vector<uint8_t>(
evernode.data(), evernode.data() + 20));
BEAST_EXPECT(
seat &&
seat->getFieldVL(sfHookStateData) ==
std::vector<uint8_t>{0});
BEAST_EXPECT(!stateAt(
env,
std::vector<uint8_t>(
alice.id().data(), alice.id().data() + 20)));
}
// alice no longer holds a seat
env(vote(
__LINE__,
alice,
env.master,
'R',
'R',
{1, 2, 3, 4, 5, 6, 7, 8}),
fee(XRP(1)),
ter(tecHOOK_REJECTED));
env.close();
// reward claim reaches the reward hook
env(claimReward(user, env.master), fee(XRP(1)), ter(tesSUCCESS));
env.close();
// enabling it again is refused and nothing is reset
auto const ownerCount = env.le(keylet::account(genesisAccID))
->getFieldU32(sfOwnerCount);
injectAmendment(env, fixXahauTestnetGenesis);
BEAST_EXPECT(stateAt(env, mcKey));
BEAST_EXPECT(
env.le(keylet::account(genesisAccID))
->getFieldU32(sfOwnerCount) == ownerCount);
}
// never-configured genesis with a regular key, signer list and a
// foreign hook gets configured and blackholed
{
Env env{*this, makeNetworkConfig(21338), features};
auto const alice = Account("alice");
auto const user = Account("user");
env.fund(XRP(10000), alice, user);
env.memoize(env.master);
env(genesis::setAcceptHook(env.master), fee(XRP(10)));
env(signers(env.master, 1, {{alice, 1}}));
env(regkey(env.master, alice));
env.close();
BEAST_EXPECT(env.le(keylet::hookDefinition(acceptHookHash)));
injectAmendment(env, fixXahauTestnetGenesis);
checkTestnetGenesis(env, evernode);
BEAST_EXPECT(!env.le(keylet::hookDefinition(acceptHookHash)));
env(claimReward(user, env.master), fee(XRP(1)), ter(tesSUCCESS));
env.close();
}
// never touches state off 21338 even if enabled
{
Env env{*this, makeNetworkConfig(21337), features};
injectAmendment(env, fixXahauTestnetGenesis);
BEAST_EXPECT(!env.le(keylet::hook(genesisAccID)));
}
}
void
testGovernHookWithFeats(FeatureBitset features)
{
@@ -6131,7 +5900,6 @@ struct XahauGenesis_test : public beast::unit_test::suite
testWithRegularKey(features);
testGovernanceL1(features);
testGovernanceL2(features);
testXahauTestnetGenesisFix(features);
}
void
-8
View File
@@ -66,14 +66,6 @@ public:
// Write the batch
storeBatch(*backend, batch);
// Durability on demand: RocksDB drains its write queue and
// syncs its WAL; in-memory backends have nothing to wait for;
// NuDB commits on its own thread and cannot promise it.
backend->store(batch.front());
bool const durable = backend->syncDurable();
BEAST_EXPECT(durable == (type != "nudb"));
backend->sync();
{
// Read it back in
Batch copy;
+1 -640
View File
@@ -18,22 +18,16 @@
//==============================================================================
#include <test/jtx.h>
#include <xrpld/app/ledger/Ledger.h>
#include <xrpld/app/ledger/LedgerMaster.h>
#include <xrpld/core/ConfigSections.h>
#include <xrpld/nodestore/detail/DatabasePinnedImp.h>
#include <xrpl/beast/unit_test.h>
#include <xrpl/beast/utility/temp_dir.h>
#include <xrpl/protocol/jss.h>
#include <boost/filesystem.hpp>
#include <chrono>
#include <cstddef>
#include <cstdlib>
#include <cstring>
#include <fstream>
#include <grpc/impl/codegen/compression_types.h>
#include <iterator>
#include <optional>
#include <thread>
namespace ripple {
@@ -108,28 +102,14 @@ struct CatalogueEnvOpts
};
// envconfig with pinned_type set so catalogue_load passes the config guard.
//
// envconfig() selects [node_db] type=rwdb, which is the tree-only null node
// store: SHAMapStore never builds DatabasePinned on it, so catalogue_load
// would refuse to run. Use the in-memory "memory" store for the hot side
// instead. It is shared by path within the process, so each config gets its
// own path (SHAMapStore creates the directory; nothing is written to it).
// The pinned side uses rwdb, an in-memory persistent backend.
// Uses rwdb for in-process speed (allowed in standalone).
inline std::unique_ptr<Config>
catalogueEnvconfig(CatalogueEnvOpts const& opts = {})
{
auto cfg = test::jtx::envconfig();
auto& nodeDb = cfg->section(ConfigSection::nodeDatabase());
if (opts.withPinnedType)
{
nodeDb.set("type", "memory");
nodeDb.set(
"path",
(boost::filesystem::temp_directory_path() /
boost::filesystem::unique_path("catalogue-test-%%%%-%%%%-%%%%"))
.string());
nodeDb.set("pinned_type", "rwdb");
}
nodeDb.set("online_delete", opts.onlineDelete);
return cfg;
}
@@ -1087,621 +1067,6 @@ class Catalogue_test : public beast::unit_test::suite
}
public:
// ---- Editing uncompressed catalogues -----------------------------------
//
// Ledger record: seq, hash, txHash, accountHash, parentHash, drops,
// closeFlags, closeTimeResolution, closeTime, parentCloseTime; then the
// state map and the tx map, each a run of leaf records ending in
// tnTERMINAL. A leaf record is type (1), key (32), and, unless it is a
// tnREMOVE, size (4) and data.
static constexpr std::size_t catLedgerHeaderSize =
4 + 32 * 4 + 8 + sizeof(int) + 4 + 8 + 8;
struct CatLedgerRecord
{
std::size_t header; // first byte of the ledger header
std::size_t state; // first byte of the state map
std::size_t tx; // first byte of the tx map
std::size_t end; // one past the tx map's tnTERMINAL
};
// Position just past the map that starts at `pos`.
static std::size_t
skipCatalogueMap(std::vector<std::uint8_t> const& bytes, std::size_t pos)
{
while (pos < bytes.size())
{
auto const type = bytes[pos++];
if (type == 255) // tnTERMINAL
return pos;
pos += 32;
if (type == 254) // tnREMOVE
continue;
if (pos + 4 > bytes.size())
break;
std::uint32_t size = 0;
std::memcpy(&size, &bytes[pos], 4);
pos += 4 + size;
}
return bytes.size();
}
// Position of the size field of the first leaf (not a removal) in the
// map that starts at `pos`, if the map has one.
static std::optional<std::size_t>
firstLeafSizeField(std::vector<std::uint8_t> const& bytes, std::size_t pos)
{
while (pos < bytes.size())
{
auto const type = bytes[pos];
if (type == 255)
return std::nullopt;
if (type != 254)
return pos + 1 + 32;
pos += 1 + 32;
}
return std::nullopt;
}
static std::vector<CatLedgerRecord>
indexCatalogue(std::vector<std::uint8_t> const& bytes)
{
std::vector<CatLedgerRecord> records;
std::size_t pos = sizeof(TestCATLHeader);
while (pos + catLedgerHeaderSize <= bytes.size())
{
CatLedgerRecord r;
r.header = pos;
r.state = pos + catLedgerHeaderSize;
r.tx = skipCatalogueMap(bytes, r.state);
r.end = skipCatalogueMap(bytes, r.tx);
records.push_back(r);
pos = r.end;
}
return records;
}
static std::vector<std::uint8_t>
readCatalogue(std::string const& path)
{
std::ifstream in(path, std::ios::binary);
return {
std::istreambuf_iterator<char>(in),
std::istreambuf_iterator<char>()};
}
static void
writeCatalogue(std::string const& path, std::vector<std::uint8_t> const& b)
{
std::ofstream out(path, std::ios::binary | std::ios::trunc);
out.write(reinterpret_cast<char const*>(b.data()), b.size());
}
static Json::Value
loadCatalogue(test::jtx::Env& env, std::string const& path, bool ignoreHash)
{
Json::Value params{Json::objectValue};
params[jss::input_file] = path;
if (ignoreHash)
params[jss::ignore_hash] = true;
return env.client().invoke("catalogue_load", params)[jss::result];
}
static bool
errorContains(Json::Value const& result, std::string const& what)
{
return result[jss::status] == "error" &&
result[jss::error_message].asString().find(what) !=
std::string::npos;
}
void
testCatalogueLoadFailures(FeatureBitset features)
{
testcase("catalogue_load: failures part way through a file");
using namespace test::jtx;
using beast::severities::kDisabled;
boost::filesystem::path tempDir =
boost::filesystem::temp_directory_path() /
boost::filesystem::unique_path();
boost::filesystem::create_directories(tempDir);
auto const goodPath = (tempDir / "good.catl").string();
// An uncompressed catalogue of ledgers 3-6, so records can be edited.
{
Env env{*this, catalogueEnvconfig(), features, nullptr, kDisabled};
prepareLedgerData(env, 3);
Json::Value params{Json::objectValue};
params[jss::min_ledger] = 3;
params[jss::max_ledger] = 6;
params[jss::output_file] = goodPath;
params[jss::compression_level] = 0;
auto const result =
env.client().invoke("catalogue_create", params)[jss::result];
BEAST_EXPECT(result[jss::status] == jss::success);
}
auto const good = readCatalogue(goodPath);
auto const records = indexCatalogue(good);
if (!BEAST_EXPECT(
records.size() == 4 && records.back().end == good.size()))
return;
// Write `bytes` to a new file, load it into a fresh server, and hand
// the result and the resulting pinned set to `check`.
int fileNo = 0;
auto loadEdited = [&](std::vector<std::uint8_t> const& bytes,
bool ignoreHash,
auto&& check) {
auto const path =
(tempDir / ("edited" + std::to_string(++fileNo) + ".catl"))
.string();
writeCatalogue(path, bytes);
Env env{*this, catalogueEnvconfig(), features, nullptr, kDisabled};
auto const result = loadCatalogue(env, path, ignoreHash);
check(
result, env.app().getLedgerMaster().getPinnedLedgersRangeSet());
};
auto const hashField = offsetof(TestCATLHeader, hash);
auto const maxLedgerField = offsetof(TestCATLHeader, max_ledger);
// Offsets within a ledger header record.
constexpr std::size_t accountHashAt = 4 + 32 * 2;
constexpr std::size_t dropsAt = 4 + 32 * 4;
// The stored checksum is wrong but every ledger is intact: all four
// load, then the whole-file check fails and the run is rolled back.
{
auto bytes = good;
bytes[hashField] ^= 0xff;
loadEdited(
bytes, false, [&](auto const& result, auto const& pinned) {
BEAST_EXPECT(errorContains(
result, "catalogue hash verification failed"));
BEAST_EXPECT(pinned.empty());
});
}
// A ledger part way through is corrupt. The load stops there, the
// rest of the file is still hashed, the checksum fails, and the
// ledgers loaded before it are rolled back too.
{
auto bytes = good;
bytes[records[2].header + dropsAt] ^= 0x01;
loadEdited(
bytes, false, [&](auto const& result, auto const& pinned) {
BEAST_EXPECT(errorContains(
result, "catalogue hash verification failed"));
BEAST_EXPECT(pinned.empty());
});
}
// A checksum failure only rolls back what this run added: ledgers
// that were already pinned stay pinned.
{
auto bytes = good;
bytes[hashField] ^= 0xff;
auto const badPath = (tempDir / "badsum.catl").string();
writeCatalogue(badPath, bytes);
Env env{*this, catalogueEnvconfig(), features, nullptr, kDisabled};
BEAST_EXPECT(
loadCatalogue(env, goodPath, false)[jss::status] ==
jss::success);
BEAST_EXPECT(errorContains(
loadCatalogue(env, badPath, false),
"catalogue hash verification failed"));
auto& lm = env.app().getLedgerMaster();
BEAST_EXPECT(to_string(lm.getPinnedLedgersRangeSet()) == "3-6");
// The pinned store: an empty batch is a no-op, an in-memory
// backend is durable on demand, and a fetch made for
// online_delete's copy pass (duplicate == true) finds nodes
// that exist only in the pinned store.
auto* const pinnedDb = dynamic_cast<NodeStore::DatabasePinnedImp*>(
&env.app().getNodeStore());
if (BEAST_EXPECT(pinnedDb))
{
pinnedDb->storePinnedBatch({});
BEAST_EXPECT(pinnedDb->syncPinned());
auto const stateRoot =
uint256::fromVoid(&good[records[0].header + accountHashAt]);
BEAST_EXPECT(env.app().getNodeStore().fetchNodeObject(
stateRoot, 0, NodeStore::FetchType::synchronous, true));
}
}
// Out of sequence: ledger 4's record claims to be ledger 9.
{
auto bytes = good;
std::uint32_t const nine = 9;
std::memcpy(&bytes[records[1].header], &nine, 4);
loadEdited(
bytes, true, [&](auto const& result, auto const& pinned) {
BEAST_EXPECT(errorContains(result, "out of sequence"));
BEAST_EXPECT(boost::icl::contains(pinned, 3u));
BEAST_EXPECT(!boost::icl::contains(pinned, 4u));
});
}
// The header promises a ledger 7 the file does not contain. Ledgers
// 3-6 load (hash checks are off) before the file runs out.
{
auto bytes = good;
std::uint32_t const seven = 7;
std::memcpy(&bytes[maxLedgerField], &seven, 4);
loadEdited(
bytes, true, [&](auto const& result, auto const& pinned) {
BEAST_EXPECT(errorContains(
result, "Unexpected end of catalogue file"));
BEAST_EXPECT(to_string(pinned) == "3-6");
});
}
// An absurd leaf size in each kind of map is rejected while parsing.
auto withHugeLeaf = [&](std::size_t sizeField) {
auto bytes = good;
std::uint32_t const huge = 0xFFFFFFFF;
std::memcpy(&bytes[sizeField], &huge, 4);
return bytes;
};
// ... in the base ledger's state map.
if (auto const field = firstLeafSizeField(good, records[0].state);
BEAST_EXPECT(field))
{
loadEdited(
withHugeLeaf(*field),
true,
[&](auto const& result, auto const& pinned) {
BEAST_EXPECT(errorContains(
result, "Failed to load base ledger state"));
BEAST_EXPECT(pinned.empty());
});
}
// ... in a later ledger's state delta.
{
std::optional<std::size_t> index;
std::optional<std::size_t> field;
for (std::size_t i = 1; i < records.size() && !field; ++i)
if ((field = firstLeafSizeField(good, records[i].state)))
index = i;
if (BEAST_EXPECT(field))
{
std::uint32_t const seq = 3 + *index;
loadEdited(
withHugeLeaf(*field),
true,
[&](auto const& result, auto const& pinned) {
BEAST_EXPECT(errorContains(
result, "Failed to apply ledger delta"));
BEAST_EXPECT(boost::icl::contains(pinned, seq - 1));
BEAST_EXPECT(!boost::icl::contains(pinned, seq));
});
}
}
// ... in a ledger's transaction map.
{
std::optional<std::size_t> index;
std::optional<std::size_t> field;
for (std::size_t i = 0; i < records.size() && !field; ++i)
if ((field = firstLeafSizeField(good, records[i].tx)))
index = i;
if (BEAST_EXPECT(field))
{
std::uint32_t const seq = 3 + *index;
loadEdited(
withHugeLeaf(*field),
true,
[&](auto const& result, auto const& pinned) {
BEAST_EXPECT(errorContains(
result, "Failed to apply ledger delta"));
BEAST_EXPECT(!boost::icl::contains(pinned, seq));
});
}
}
boost::filesystem::remove_all(tempDir);
}
void
testCataloguePinnedStoreRequired(FeatureBitset features)
{
testcase("catalogue_load / catalogue_unpin: pinned store required");
using namespace test::jtx;
using beast::severities::kDisabled;
// pinned_type is set, but [node_db] type=rwdb is the null node
// store, on which SHAMapStore never builds the pinned store.
{
auto cfg = test::jtx::envconfig();
auto& nodeDb = cfg->section(ConfigSection::nodeDatabase());
nodeDb.set("pinned_type", "rwdb");
nodeDb.set("online_delete", "256");
Env env{*this, std::move(cfg), features, nullptr, kDisabled};
Json::Value params{Json::objectValue};
params[jss::input_file] = "/nonexistent.catl";
auto const result =
env.client().invoke("catalogue_load", params)[jss::result];
BEAST_EXPECT(
errorContains(result, "requires the pinned node store"));
}
// catalogue_unpin needs pinned_type configured at all.
{
Env env{
*this,
catalogueEnvconfig({.withPinnedType = false}),
features,
nullptr,
kDisabled};
Json::Value params{Json::objectValue};
params[jss::ledger_index_min] = 1;
params[jss::ledger_index_max] = 2;
auto const result =
env.client().invoke("catalogue_unpin", params)[jss::result];
BEAST_EXPECT(errorContains(result, "pinned_type"));
}
}
void
testCatalogueChainLinkage(FeatureBitset features)
{
testcase("catalogue_load: Ledgers must chain");
using namespace test::jtx;
Env env{
*this,
catalogueEnvconfig(),
features,
nullptr,
beast::severities::kDisabled,
};
prepareLedgerData(env, 3);
boost::filesystem::path tempDir =
boost::filesystem::temp_directory_path() /
boost::filesystem::unique_path();
boost::filesystem::create_directories(tempDir);
auto cataloguePath = (tempDir / "test.catl").string();
// Uncompressed, so ledger records can be located and edited.
{
Json::Value params{Json::objectValue};
params[jss::min_ledger] = 3;
params[jss::max_ledger] = 6;
params[jss::output_file] = cataloguePath;
params[jss::compression_level] = 0;
auto const result =
env.client().invoke("catalogue_create", params)[jss::result];
BEAST_EXPECT(result[jss::status] == jss::success);
}
std::vector<std::uint8_t> bytes;
{
std::ifstream in(cataloguePath, std::ios::binary);
bytes.assign(
std::istreambuf_iterator<char>(in),
std::istreambuf_iterator<char>());
}
// Ledger record: seq, hash, txHash, accountHash, parentHash, drops,
// closeFlags, closeTimeResolution, closeTime, parentCloseTime; then
// the state map and tx map as leaf records ending in tnTERMINAL.
constexpr std::size_t ledgerHeaderSize =
4 + 32 * 4 + 8 + sizeof(int) + 4 + 8 + 8;
auto skipMap = [&](std::size_t pos) -> std::size_t {
while (pos < bytes.size())
{
auto const type = bytes[pos++];
if (type == 255) // tnTERMINAL
return pos;
pos += 32;
if (type == 254) // tnREMOVE
continue;
if (pos + 4 > bytes.size())
break;
std::uint32_t size = 0;
std::memcpy(&size, &bytes[pos], 4);
pos += 4 + size;
}
return bytes.size();
};
// Skip the base ledger (3) to reach ledger 4.
std::size_t pos = sizeof(TestCATLHeader) + ledgerHeaderSize;
pos = skipMap(skipMap(pos));
if (!BEAST_EXPECT(pos + ledgerHeaderSize <= bytes.size()))
return;
LedgerInfo info;
std::size_t p = pos;
auto take = [&](void* dst, std::size_t n) {
std::memcpy(dst, &bytes[p], n);
p += n;
};
std::uint64_t drops = 0, closeTime = 0, parentCloseTime = 0;
std::uint32_t resolution = 0;
take(&info.seq, 4);
std::size_t const hashOffset = p;
take(info.hash.data(), 32);
take(info.txHash.data(), 32);
take(info.accountHash.data(), 32);
std::size_t const parentOffset = p;
take(info.parentHash.data(), 32);
take(&drops, 8);
take(&info.closeFlags, sizeof(info.closeFlags));
take(&resolution, 4);
take(&closeTime, 8);
take(&parentCloseTime, 8);
info.drops = drops;
info.closeTime = NetClock::time_point{NetClock::duration{closeTime}};
info.parentCloseTime =
NetClock::time_point{NetClock::duration{parentCloseTime}};
info.closeTimeResolution = NetClock::duration{resolution};
BEAST_EXPECT(info.seq == 4);
// Sanity: the record parses to a ledger whose hash checks out.
BEAST_EXPECT(calculateLedgerHash(info) == info.hash);
// Re-parent ledger 4 and fix up its hash, so the file stays
// internally consistent ledger by ledger; only the chain check can
// catch it.
info.parentHash = ~info.parentHash;
info.hash = calculateLedgerHash(info);
std::memcpy(&bytes[parentOffset], info.parentHash.data(), 32);
std::memcpy(&bytes[hashOffset], info.hash.data(), 32);
{
std::ofstream out(
cataloguePath, std::ios::binary | std::ios::trunc);
out.write(
reinterpret_cast<char const*>(bytes.data()), bytes.size());
}
Env loadEnv{
*this,
catalogueEnvconfig(),
features,
nullptr,
beast::severities::kDisabled,
};
Json::Value params{Json::objectValue};
params[jss::input_file] = cataloguePath;
params[jss::ignore_hash] = true; // the file was edited
auto const result =
loadEnv.client().invoke("catalogue_load", params)[jss::result];
BEAST_EXPECT(result[jss::status] == "error");
BEAST_EXPECT(
result[jss::error_message].asString().find(
"does not chain to its predecessor") != std::string::npos);
// The base ledger verified and stays pinned; nothing after it does.
auto const pinned =
loadEnv.app().getLedgerMaster().getPinnedLedgersRangeSet();
BEAST_EXPECT(boost::icl::contains(pinned, 3u));
BEAST_EXPECT(!boost::icl::contains(pinned, 4u));
boost::filesystem::remove_all(tempDir);
}
void
testCatalogueUnpin(FeatureBitset features)
{
testcase("catalogue_unpin");
using namespace test::jtx;
Env env{
*this,
catalogueEnvconfig(),
features,
nullptr,
beast::severities::kDisabled,
};
prepareLedgerData(env, 5);
boost::filesystem::path tempDir =
boost::filesystem::temp_directory_path() /
boost::filesystem::unique_path();
boost::filesystem::create_directories(tempDir);
auto cataloguePath = (tempDir / "test.catl").string();
{
Json::Value params{Json::objectValue};
params[jss::min_ledger] = 3;
params[jss::max_ledger] = 8;
params[jss::output_file] = cataloguePath;
auto const result =
env.client().invoke("catalogue_create", params)[jss::result];
BEAST_EXPECT(result[jss::status] == jss::success);
}
{
Json::Value params{Json::objectValue};
params[jss::input_file] = cataloguePath;
auto const result =
env.client().invoke("catalogue_load", params)[jss::result];
BEAST_EXPECT(result[jss::status] == jss::success);
}
auto& lm = env.app().getLedgerMaster();
BEAST_EXPECT(to_string(lm.getPinnedLedgersRangeSet()) == "3-8");
auto unpin = [&](std::optional<Json::Value> min,
std::optional<Json::Value> max) {
Json::Value params{Json::objectValue};
if (min)
params[jss::ledger_index_min] = *min;
if (max)
params[jss::ledger_index_max] = *max;
return env.client().invoke("catalogue_unpin", params)[jss::result];
};
// Bad parameters
BEAST_EXPECT(unpin({}, {})[jss::status] == "error");
BEAST_EXPECT(unpin(5, {})[jss::status] == "error");
BEAST_EXPECT(unpin(6, 5)[jss::status] == "error");
BEAST_EXPECT(unpin(-1, 5)[jss::status] == "error");
BEAST_EXPECT(unpin("x", 5)[jss::status] == "error");
BEAST_EXPECT(to_string(lm.getPinnedLedgersRangeSet()) == "3-8");
// Cut a hole
{
auto const result = unpin(5, 6);
BEAST_EXPECT(result[jss::status] == jss::success);
BEAST_EXPECT(result[jss::unpinned] == "5-6");
BEAST_EXPECT(result[jss::complete_ledgers_pinned] == "3-4,7-8");
BEAST_EXPECT(lm.isPinned(4) && lm.isPinned(7));
BEAST_EXPECT(!lm.isPinned(5) && !lm.isPinned(6));
}
// A range only partly pinned unpins just the pinned part
{
auto const result = unpin(1, 4);
BEAST_EXPECT(result[jss::status] == jss::success);
BEAST_EXPECT(result[jss::unpinned] == "3-4");
BEAST_EXPECT(result[jss::complete_ledgers_pinned] == "7-8");
}
// Nothing pinned in range: success, nothing unpinned. (Unsigned
// JSON values are accepted as well as non-negative signed ones.)
{
auto const result = unpin(Json::UInt(5), Json::UInt(6));
BEAST_EXPECT(result[jss::status] == jss::success);
BEAST_EXPECT(result[jss::unpinned] == "empty");
BEAST_EXPECT(result[jss::complete_ledgers_pinned] == "7-8");
}
// Unpinning the rest leaves nothing pinned. (state.db then records
// an empty set, which must not stop the next start; see
// SHAMapStore_test.)
{
auto const result = unpin(1, 100);
BEAST_EXPECT(result[jss::status] == jss::success);
BEAST_EXPECT(result[jss::unpinned] == "7-8");
BEAST_EXPECT(result[jss::complete_ledgers_pinned] == "empty");
BEAST_EXPECT(lm.getPinnedLedgersRangeSet().empty());
}
// Re-loading the file pins it again
{
Json::Value params{Json::objectValue};
params[jss::input_file] = cataloguePath;
auto const result =
env.client().invoke("catalogue_load", params)[jss::result];
BEAST_EXPECT(result[jss::status] == jss::success);
BEAST_EXPECT(to_string(lm.getPinnedLedgersRangeSet()) == "3-8");
}
boost::filesystem::remove_all(tempDir);
}
void
run() override
{
@@ -1716,10 +1081,6 @@ public:
testCatalogueFileSize(all);
testCatalogueCompression(all);
testCatalogueStatus(all);
testCatalogueChainLinkage(all);
testCatalogueUnpin(all);
testCatalogueLoadFailures(all);
testCataloguePinnedStoreRequired(all);
}
};
-50
View File
@@ -1934,56 +1934,6 @@ static RPCCallTestData const rpcCallTestArray[] = {
RPCCallTestData::bad_cast,
R"()"},
// catalogue_unpin
// ------------------------------------------------------------
{"catalogue_unpin: minimal.",
__LINE__,
{"catalogue_unpin", "5000001", "6000000"},
RPCCallTestData::no_exception,
R"({
"method" : "catalogue_unpin",
"params" : [
{
"api_version" : %API_VER%,
"ledger_index_max" : 6000000,
"ledger_index_min" : 5000001
}
]
})"},
{"catalogue_unpin: too few arguments.",
__LINE__,
{"catalogue_unpin", "5000001"},
RPCCallTestData::no_exception,
R"({
"method" : "catalogue_unpin",
"params" : [
{
"error" : "badSyntax",
"error_code" : 1,
"error_message" : "Syntax error."
}
]
})"},
{"catalogue_unpin: too many arguments.",
__LINE__,
{"catalogue_unpin", "1", "2", "3"},
RPCCallTestData::no_exception,
R"({
"method" : "catalogue_unpin",
"params" : [
{
"error" : "badSyntax",
"error_code" : 1,
"error_message" : "Syntax error."
}
]
})"},
{"catalogue_unpin: non-integer bound.",
__LINE__,
{"catalogue_unpin", "first", "6000000"},
RPCCallTestData::bad_cast,
R"()"},
// channel_authorize
// -----------------------------------------------------------
{"channel_authorize: minimal.",
-79
View File
@@ -29,9 +29,7 @@
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/protocol/Serializer.h>
#include <algorithm>
#include <optional>
#include <vector>
namespace ripple {
namespace tests {
@@ -133,7 +131,6 @@ public:
testCanonicalizeMergesInnerChildren(journal);
testCanonicalizeSharesDuplicateContent(journal);
testGetLinkedNode(journal);
testUpsertAndFlushToSink(journal);
}
void
@@ -287,82 +284,6 @@ public:
BEAST_EXPECT(liveLinked > 0);
}
void
testUpsertAndFlushToSink(beast::Journal const& journal)
{
testcase("upsertGiveItem and flushDirty to a sink");
tests::TestNodeFamily f(journal);
SHAMap a{SHAMapType::FREE, f}; // built with upsertGiveItem
SHAMap b{SHAMapType::FREE, f}; // reference: addItem/updateGiveItem
// Distinct first nibbles: a root with three leaf children.
constexpr uint256 h1(
"092891fe4ef6cee585fdc6fda0e09eb4d386363158ec3321b8123e5a772c6ca7");
constexpr uint256 h2(
"436ccbac3347baa1f1e53baeef1f43334da88f1f6d70d963b833afd6dfa289fe");
constexpr uint256 h3(
"b92891fe4ef6cee585fdc6fda1e09eb4d386363158ec3321b8123e5a772c6ca8");
auto const nm = SHAMapNodeType::tnTRANSACTION_NM;
// Absent keys are added.
BEAST_EXPECT(a.upsertGiveItem(nm, make_shamapitem(h1, IntToVUC(1))));
BEAST_EXPECT(a.upsertGiveItem(nm, make_shamapitem(h2, IntToVUC(2))));
BEAST_EXPECT(a.upsertGiveItem(nm, make_shamapitem(h3, IntToVUC(3))));
// A present key has its data replaced.
BEAST_EXPECT(a.upsertGiveItem(nm, make_shamapitem(h2, IntToVUC(20))));
// A change of leaf type is refused and leaves the item untouched.
BEAST_EXPECT(!a.upsertGiveItem(
SHAMapNodeType::tnTRANSACTION_MD,
make_shamapitem(h1, IntToVUC(10))));
BEAST_EXPECT(b.addItem(nm, make_shamapitem(h1, IntToVUC(1))));
BEAST_EXPECT(b.addItem(nm, make_shamapitem(h2, IntToVUC(2))));
BEAST_EXPECT(b.addItem(nm, make_shamapitem(h3, IntToVUC(3))));
BEAST_EXPECT(b.updateGiveItem(nm, make_shamapitem(h2, IntToVUC(20))));
{
auto const v20 = IntToVUC(20);
auto const& item = a.peekItem(h2);
BEAST_EXPECT(
item && item->slice() == Slice(v20.data(), v20.size()));
auto const v1 = IntToVUC(1);
auto const& item1 = a.peekItem(h1);
BEAST_EXPECT(
item1 && item1->slice() == Slice(v1.data(), v1.size()));
}
// Flush to a sink. Do not call getHash() on `a` first: it unshares
// the map and the flush would then have nothing to write.
auto const cacheBefore = f.getTreeNodeCache()->size();
std::vector<uint256> written;
bool sinkArgsOk = true;
SHAMap::FlushSink const sink =
[&](NodeObjectType t, Blob&& data, uint256 const& hash) {
sinkArgsOk =
sinkArgsOk && t == hotTRANSACTION_NODE && !data.empty();
written.push_back(hash);
};
a.flushDirty(hotTRANSACTION_NODE, sink);
BEAST_EXPECT(sinkArgsOk);
BEAST_EXPECT(written.size() == 4); // root + three leaves
// Unlike flushDirty(t), nothing was canonicalized into the cache.
BEAST_EXPECT(f.getTreeNodeCache()->size() == cacheBefore);
// The upserted map matches the reference, and its root was written.
BEAST_EXPECT(a.getHash() == b.getHash());
BEAST_EXPECT(
std::find(
written.begin(), written.end(), a.getHash().as_uint256()) !=
written.end());
// Everything is now shared: a second flush writes nothing.
written.clear();
a.flushDirty(hotTRANSACTION_NODE, sink);
BEAST_EXPECT(written.empty());
}
void
testGetLinkedNode(beast::Journal const& journal)
{
+2 -3
View File
@@ -25,8 +25,7 @@ namespace ripple {
AcceptedLedger::AcceptedLedger(
std::shared_ptr<ReadView const> const& ledger,
Application& app,
bool withJson)
Application& app)
: mLedger(ledger)
{
transactions_.reserve(256);
@@ -34,7 +33,7 @@ AcceptedLedger::AcceptedLedger(
auto insertAll = [&](auto const& txns) {
for (auto const& item : txns)
transactions_.emplace_back(std::make_unique<AcceptedLedgerTx>(
ledger, item.first, item.second, withJson));
ledger, item.first, item.second));
};
transactions_.reserve(256);
+1 -7
View File
@@ -44,15 +44,9 @@ namespace ripple {
class AcceptedLedger : public CountedObject<AcceptedLedger>
{
public:
/** @param withJson build each transaction's publishable JSON. Pass
false only for ledgers that will never be published from this
object (e.g. pinned catalogue history being saved to SQL);
getJson() on such transactions returns a null value.
*/
AcceptedLedger(
std::shared_ptr<ReadView const> const& ledger,
Application& app,
bool withJson = true);
Application& app);
std::shared_ptr<ReadView const> const&
getLedger() const
+1 -7
View File
@@ -28,8 +28,7 @@ namespace ripple {
AcceptedLedgerTx::AcceptedLedgerTx(
std::shared_ptr<ReadView const> const& ledger,
std::shared_ptr<STTx const> const& txn,
std::shared_ptr<STObject const> const& met,
bool withJson)
std::shared_ptr<STObject const> const& met)
: mTxn(txn)
, mMeta(txn->getTransactionID(), ledger->seq(), *met)
, mAffected(mMeta.getAffectedAccounts())
@@ -42,11 +41,6 @@ AcceptedLedgerTx::AcceptedLedgerTx(
met->add(s);
mRawMeta = std::move(s.modData());
// The JSON (full tx + meta + hex raw meta per transaction) is only
// needed for publishing. Bulk SQL saves of pinned history skip it.
if (!withJson)
return;
mJson = Json::objectValue;
mJson[jss::transaction] = mTxn->getJson(JsonOptions::none);
+1 -3
View File
@@ -46,8 +46,7 @@ public:
AcceptedLedgerTx(
std::shared_ptr<ReadView const> const& ledger,
std::shared_ptr<STTx const> const&,
std::shared_ptr<STObject const> const&,
bool withJson = true);
std::shared_ptr<STObject const> const&);
std::shared_ptr<STTx const> const&
getTxn() const
@@ -89,7 +88,6 @@ public:
std::string
getEscMeta() const;
/** Null if constructed with withJson == false. */
Json::Value const&
getJson() const
{
-13
View File
@@ -228,19 +228,6 @@ public:
isPinned(std::uint32_t seq);
void
unpinLedger(std::uint32_t seq);
/** Remove `ranges` from the pinned set.
@param dropFromComplete of the ledgers actually unpinned, those to
also remove from complete_ledgers now (used to roll back a
load). Otherwise unpinned ledgers stay complete until online
delete releases them.
@return the ledgers that were pinned and no longer are.
*/
RangeSet<std::uint32_t>
unpinLedgers(
RangeSet<std::uint32_t> const& ranges,
RangeSet<std::uint32_t> const& dropFromComplete = {});
bool
isValidated(ReadView const& ledger);
bool
+1 -16
View File
@@ -501,9 +501,7 @@ LedgerMaster::storeLedger(std::shared_ptr<Ledger const> ledger, bool pin)
std::lock_guard sl(mPinnedLock);
mPinnedLedgers.insert(range(seq, seq));
}
// catalogue_load pins every ledger it imports; at info this would
// emit one line per ledger for the whole file.
JLOG(m_journal.debug()) << "Pinned ledger : " << seq;
JLOG(m_journal.info()) << "Pinned ledger : " << seq;
}
return true;
}
@@ -588,19 +586,6 @@ LedgerMaster::unpinLedger(std::uint32_t seq)
mPinnedLedgers.erase(range(seq, seq));
}
RangeSet<std::uint32_t>
LedgerMaster::unpinLedgers(
RangeSet<std::uint32_t> const& ranges,
RangeSet<std::uint32_t> const& dropFromComplete)
{
std::scoped_lock lock(mCompleteLock, mPinnedLock);
RangeSet<std::uint32_t> removed = mPinnedLedgers & ranges;
mPinnedLedgers -= removed;
if (!dropFromComplete.empty())
mCompleteLedgers -= (removed & dropFromComplete);
return removed;
}
bool
LedgerMaster::isValidated(ReadView const& ledger)
{
+3 -29
View File
@@ -1242,10 +1242,10 @@ ApplicationImp::setup(boost::program_options::variables_map const& cmdline)
// Configure the amendments the server supports
{
auto const supported = [this]() {
auto const supported = []() {
auto const& amendments = detail::supportedAmendments();
std::vector<AmendmentTable::FeatureInfo> supported;
supported.reserve(amendments.size() + 1);
supported.reserve(amendments.size());
for (auto const& [a, vote] : amendments)
{
auto const f = ripple::getRegisteredFeature(a);
@@ -1254,45 +1254,19 @@ ApplicationImp::setup(boost::program_options::variables_map const& cmdline)
if (f)
supported.emplace_back(a, *f, vote);
}
// fixXahauTestnetGenesis is registered Supported::no. It is only
// supported (and voted for) by servers configured for the Xahau
// testnet. Everywhere else it stays unsupported.
if (config_->NETWORK_ID == 21338)
supported.emplace_back(
"fixXahauTestnetGenesis",
fixXahauTestnetGenesis,
VoteBehavior::DefaultYes);
return supported;
}();
Section const& downVoted = config_->section(SECTION_VETO_AMENDMENTS);
Section const& upVoted = config_->section(SECTION_AMENDMENTS);
Section const& ignored = config_->section(SECTION_IGNORE_AMENDMENTS);
// [ignore_amendments] exists to keep long-lived test networks syncable
// after an amendment they activated is removed from the codebase. It
// must never be usable on a production network.
if (!ignored.lines().empty() &&
(config_->NETWORK_ID == 0 || config_->NETWORK_ID == 21337))
{
JLOG(m_journal.fatal()) << "[" SECTION_IGNORE_AMENDMENTS
"] is not permitted on "
"network_id "
<< config_->NETWORK_ID;
return false;
}
m_amendmentTable = make_AmendmentTable(
*this,
config().AMENDMENT_MAJORITY_TIME,
supported,
upVoted,
downVoted,
logs_->journal("Amendments"),
ignored);
logs_->journal("Amendments"));
}
Pathfinder::initPathTable();
+1 -2
View File
@@ -211,8 +211,7 @@ make_AmendmentTable(
std::vector<AmendmentTable::FeatureInfo> const& supported,
Section const& enabled,
Section const& vetoed,
beast::Journal journal,
Section const& ignored = Section{});
beast::Journal journal);
} // namespace ripple
+9 -51
View File
@@ -381,9 +381,7 @@ void
SHAMapStoreImp::loadPinnedRanges()
{
auto rangesStr = state_db_.getPinnedRanges();
// "empty" is what to_string() renders for an empty set; accept it in
// case it was stored before setPinnedRanges() learned to write "".
if (rangesStr.empty() || rangesStr == "empty")
if (rangesStr.empty())
return;
auto const& nscfg = app_.config().section(ConfigSection::nodeDatabase());
@@ -413,62 +411,22 @@ SHAMapStoreImp::loadPinnedRanges()
static_cast<bool>(app_.getNodeStore().fetchNodeObject(hash, seq));
};
//
// catalogue_load records ranges at checkpoints, after handing the pinned
// backend every node of the recorded ledgers, but not every backend can
// make those writes durable on demand (NuDB commits on its own thread).
// A crash can therefore leave the tail of an interval recorded but not
// on disk. Each ledger's nodes reach the backend before its header, and
// ledgers in sequence, so what survives is a prefix: trim the upper bound
// back to the last ledger whose header is present instead of refusing to
// start. A missing lower bound still means the configured pinned store
// is not the one these ranges were loaded into.
constexpr std::uint32_t maxTailTrim = 65536;
RangeSet<std::uint32_t> verified;
for (auto const& interval : persistedRanges)
{
auto const lower = interval.lower();
auto upper = interval.upper();
auto const notPresent = [&]() {
return std::runtime_error(
"Persisted pinned interval " + std::to_string(lower) + "-" +
if (!hasLedgerData(interval.lower()) ||
!hasLedgerData(interval.upper()))
{
Throw<std::runtime_error>(
"Persisted pinned interval " +
std::to_string(interval.lower()) + "-" +
std::to_string(interval.upper()) +
" is not present in the currently configured pinned store");
};
if (!hasLedgerData(lower))
Throw<std::runtime_error>(notPresent());
std::uint32_t trimmed = 0;
while (upper > lower && !hasLedgerData(upper))
{
if (++trimmed > maxTailTrim)
Throw<std::runtime_error>(notPresent());
--upper;
}
if (trimmed)
{
JLOG(journal_.warn())
<< "Pinned interval " << lower << "-" << interval.upper()
<< " was recorded but its last " << trimmed
<< " ledger(s) are not in the pinned store (likely an "
"interrupted catalogue_load); trimmed to "
<< lower << "-" << upper
<< ". Re-run catalogue_load to restore them.";
}
verified.insert(range(lower, upper));
}
if (verified != persistedRanges)
state_db_.setPinnedRanges(to_string(verified));
JLOG(journal_.info()) << "Loaded pinned ranges from database: "
<< to_string(verified);
app_.getLedgerMaster().setPinnedLedgersRangeSet(verified);
<< rangesStr;
app_.getLedgerMaster().setPinnedLedgersRangeSet(persistedRanges);
}
void
+3 -5
View File
@@ -184,11 +184,9 @@ public:
void
setPinnedRanges(RangeSet<std::uint32_t> const& ranges) override
{
// Save pinned ranges to database for persistence. to_string()
// renders an empty set as "empty", which from_string() rejects, so
// store "" instead: loadPinnedRanges() treats that as nothing pinned.
state_db_.setPinnedRanges(
ranges.empty() ? std::string{} : to_string(ranges));
// Save pinned ranges to database for persistence
std::string rangesStr = to_string(ranges);
state_db_.setPinnedRanges(rangesStr);
}
void
+6 -78
View File
@@ -302,15 +302,6 @@ struct AmendmentState
/** Indicates an amendment that this server has code support for. */
bool supported = false;
/** Indicates an amendment listed in [ignore_amendments]. The server has
no code for it, but must not become amendment blocked if it is (or
becomes) enabled on the ledger. Such an amendment is reported as
`supported` internally so every blocking check (LedgerMaster,
NetworkOPs, Change) passes, but is never voted for and is shown as
unsupported + ignored over RPC.
*/
bool ignored = false;
/** The name of this amendment, possibly empty. */
std::string name;
@@ -485,8 +476,7 @@ public:
std::vector<FeatureInfo> const& supported,
Section const& enabled,
Section const& vetoed,
beast::Journal journal,
Section const& ignored);
beast::Journal journal);
uint256
find(std::string const& name) const override;
@@ -553,8 +543,7 @@ AmendmentTableImpl::AmendmentTableImpl(
std::vector<FeatureInfo> const& supported,
Section const& enabled,
Section const& vetoed,
beast::Journal journal,
Section const& ignored)
beast::Journal journal)
: lastUpdateSeq_(0)
, majorityTime_(majorityTime)
, unsupportedEnabled_(false)
@@ -597,59 +586,6 @@ AmendmentTableImpl::AmendmentTableImpl(
<< " voted by default if not enabled on the ledger.";
}
// Parse ignored amendments from config. Each line is either a registered
// feature name (e.g. "HookOnV2") or "<64 hex amendment ID> [name]". These
// are deliberately NOT persisted to wallet.db: removing the line from the
// config must restore normal amendment-blocking behaviour.
for (auto const& line : ignored.lines())
{
std::string const trimmed = boost::algorithm::trim_copy(line);
if (trimmed.empty())
continue;
std::vector<std::string> parts;
boost::algorithm::split(
parts,
trimmed,
boost::algorithm::is_space(),
boost::algorithm::token_compress_on);
uint256 id;
std::string name;
if (id.parseHex(parts[0]))
name = parts.size() > 1 ? parts[1] : std::string{};
else if (auto const f = getRegisteredFeature(parts[0]))
{
id = *f;
name = parts[0];
}
else
Throw<std::runtime_error>(
"Invalid entry '" + line + "' in [" + ignored.name() +
"]: expected a registered feature name or amendment ID");
AmendmentState& s = add(id, lock);
if (s.supported)
{
JLOG(j_.warn()) << "[" << ignored.name() << "] lists amendment "
<< id << " (" << s.name
<< ") which this server supports; entry has no "
"effect.";
continue;
}
if (!name.empty())
s.name = name;
s.supported = true;
s.ignored = true;
s.vote = AmendmentVote::obsolete;
JLOG(j_.warn()) << "Amendment " << id << " (" << s.name
<< ") is unsupported but listed in [" << ignored.name()
<< "]: this server will NOT become amendment blocked "
"if it is enabled on the ledger.";
}
hash_set<uint256> detect_conflict;
// Parse enabled amendments from config
for (std::pair<uint256, std::string> const& a : parseSection(enabled))
@@ -839,12 +775,7 @@ AmendmentTableImpl::enable(uint256 const& amendment)
s.enabled = true;
if (s.ignored)
{
JLOG(j_.warn()) << "Ignored unsupported amendment " << amendment << " ("
<< s.name << ") is enabled on the ledger.";
}
else if (!s.supported)
if (!s.supported)
{
JLOG(j_.error()) << "Unsupported amendment " << amendment
<< " activated.";
@@ -1113,9 +1044,7 @@ AmendmentTableImpl::injectJson(
if (!fs.name.empty())
v[jss::name] = fs.name;
v[jss::supported] = fs.supported && !fs.ignored;
if (fs.ignored)
v["ignored"] = true;
v[jss::supported] = fs.supported;
if (!fs.enabled && isAdmin)
{
if (fs.vote == AmendmentVote::obsolete)
@@ -1184,11 +1113,10 @@ make_AmendmentTable(
std::vector<AmendmentTable::FeatureInfo> const& supported,
Section const& enabled,
Section const& vetoed,
beast::Journal journal,
Section const& ignored)
beast::Journal journal)
{
return std::make_unique<AmendmentTableImpl>(
app, majorityTime, supported, enabled, vetoed, journal, ignored);
app, majorityTime, supported, enabled, vetoed, journal);
}
} // namespace ripple
+20 -28
View File
@@ -258,50 +258,42 @@ saveValidatedLedger(
ledger->info().txHash == ledger->txMap().getHash().as_uint256(),
"ripple::detail::saveValidatedLedger : transaction hash match");
bool const pinned = app.getLedgerMaster().isPinned(seq);
// Save the ledger header in the hashed object store
{
Serializer s(128);
s.add32(HashPrefix::ledgerMaster);
addRaw(ledger->info(), s);
// Use pinnedLEDGER type for pinned ledgers, hotLEDGER for others
auto ledgerType = pinned ? pinnedLEDGER : hotLEDGER;
auto ledgerType =
app.getLedgerMaster().isPinned(seq) ? pinnedLEDGER : hotLEDGER;
app.getNodeStore().store(
ledgerType, std::move(s.modData()), ledger->info().hash, seq);
}
// Pinned (catalogue) ledgers are saved in bulk: they are never cached
// or published from here, so their AcceptedLedger skips the per-tx JSON
// and is not built at all when there are no tx tables to fill.
std::shared_ptr<AcceptedLedger> aLedger;
if (!pinned || app.config().useTxTables())
try
{
try
aLedger = app.getAcceptedLedgerCache().fetch(ledger->info().hash);
if (!aLedger)
{
aLedger = app.getAcceptedLedgerCache().fetch(ledger->info().hash);
if (!aLedger)
{
aLedger = std::make_shared<AcceptedLedger>(
ledger, app, /*withJson=*/!pinned);
aLedger = std::make_shared<AcceptedLedger>(ledger, app);
// Only cache if the ledger is NOT in the pinned range
if (!pinned)
{
app.getAcceptedLedgerCache().canonicalize_replace_client(
ledger->info().hash, aLedger);
}
// Only cache if the ledger is NOT in the pinned range
if (!app.getLedgerMaster().isPinned(ledger->info().seq))
{
app.getAcceptedLedgerCache().canonicalize_replace_client(
ledger->info().hash, aLedger);
}
}
catch (std::exception const&)
{
JLOG(j.warn()) << "An accepted ledger was missing nodes";
app.getLedgerMaster().failedSave(seq, ledger->info().hash);
// Clients can now trust the database for information about this
// ledger sequence.
app.pendingSaves().finishWork(seq);
return false;
}
}
catch (std::exception const&)
{
JLOG(j.warn()) << "An accepted ledger was missing nodes";
app.getLedgerMaster().failedSave(seq, ledger->info().hash);
// Clients can now trust the database for information about this
// ledger sequence.
app.pendingSaves().finishWork(seq);
return false;
}
{
-331
View File
@@ -988,335 +988,6 @@ Change::activateXahauGenesis()
ctx_.rawView().rawDestroyXRP(destroyedXRP);
}
// fixXahauTestnetGenesis: one-off repair of the genesis account on the Xahau
// testnet (NetworkID 21338). The genesis hooks there were never set up the
// way featureXahauGenesis sets them up on mainnet, so reward claims misbehave.
//
// This runs exactly once: only from applyAmendment(), at the moment the
// amendment is enabled, which is after the tefALREADY check on the
// Amendments object. It is never reached from any rules().enabled() path.
//
// It resets the genesis account to the mainnet shape:
// - signer list removed, regular key blackholed, master disabled
// - every existing hook removed and every hook state entry wiped, so the
// governance hook re-runs its initial setup on the next Invoke
// - governance + reward hooks installed from the current binaries, with
// only Evernode's L1 seat filled (IMC = 1) and no L2 tables
// Balances are not touched and nothing is minted or burned.
void
Change::activateXahauTestnetGenesisFix()
{
JLOG(j_.warn()) << "fixXahauTestnetGenesis amendment activation starting";
using namespace XahauGenesis;
// Belt and braces: the amendment is only Supported on 21338, but never
// touch state on any other network even if it were somehow enabled.
if (ctx_.app.config().NETWORK_ID != 21338)
{
JLOG(j_.warn()) << "fixXahauTestnetGenesis: NetworkID is not 21338, "
"no changes made";
return;
}
auto [ng_entries, l1_entries, l2_entries, gov_params] =
normalizeXahauGenesis(
{}, TestnetL1Membership, {}, GovernanceParameters, j_);
if (l1_entries.size() != 1 || !l2_entries.empty())
{
JLOG(j_.warn()) << "fixXahauTestnetGenesis: expected exactly one L1 "
"seat and no L2 tables, bailing";
return;
}
std::vector<std::tuple<
uint256, // hook on
std::vector<uint8_t>, // hook code
std::vector<
std::pair<std::vector<uint8_t>, std::vector<uint8_t>>>>> const
genesis_hooks = {
{GovernanceHookOn, GovernanceHook, gov_params},
{RewardHookOn, RewardHook, {}}};
Sandbox sb(&view());
static auto const accid = calcAccountID(
generateKeyPair(KeyType::secp256k1, generateSeed("masterpassphrase"))
.first);
if (!sb.exists(keylet::account(accid)))
{
JLOG(j_.warn()) << "fixXahauTestnetGenesis: genesis account doesn't "
"exist, bailing";
return;
}
// Step 1: remove any signer list. This adjusts OwnerCount itself, so it
// happens before we start our own OwnerCount bookkeeping below.
if (sb.exists(keylet::signers(accid)))
{
if (auto const ter =
SetSignerList::removeFromLedger(ctx_.app, sb, accid, j_);
!isTesSuccess(ter))
{
JLOG(j_.warn()) << "fixXahauTestnetGenesis: could not remove "
"signer list: "
<< transToken(ter) << ", bailing";
return;
}
}
auto sle = sb.peek(keylet::account(accid));
std::int64_t ownerCount = sle->getFieldU32(sfOwnerCount);
// Step 2: blackhole
sle->setAccountID(sfRegularKey, noAccount());
sle->setFieldU32(sfFlags, lsfDisableMaster);
// Step 3: release every existing hook. Definitions are only decremented
// here; zero-reference ones are erased after the new hooks are installed
// so a definition we are about to reuse is never erased and re-inserted.
std::set<uint256> releasedDefs;
auto sleHooks = sb.peek(keylet::hook(accid));
if (sleHooks && sleHooks->isFieldPresent(sfHooks))
{
for (auto const& h : sleHooks->getFieldArray(sfHooks))
{
ownerCount -= SetHook::computeHookReserve(h);
if (!h.isFieldPresent(sfHookHash))
continue;
auto const defKL =
keylet::hookDefinition(h.getFieldH256(sfHookHash));
auto def = sb.peek(defKL);
if (!def)
continue;
std::uint64_t const rc = def->isFieldPresent(sfReferenceCount)
? def->getFieldU64(sfReferenceCount)
: 0;
def->setFieldU64(sfReferenceCount, rc > 0 ? rc - 1 : 0);
sb.update(def);
releasedDefs.emplace(defKL.key);
}
}
// Step 4: wipe all hook state, in every namespace
{
std::uint16_t const scale = sle->isFieldPresent(sfHookStateScale)
? sle->getFieldU16(sfHookStateScale)
: 1;
std::uint32_t const stateCount = sle->isFieldPresent(sfHookStateCount)
? sle->getFieldU32(sfHookStateCount)
: 0;
std::set<uint256> namespaces{uint256{beast::zero}};
if (sle->isFieldPresent(sfHookNamespaces))
for (auto const& ns : sle->getFieldV256(sfHookNamespaces))
namespaces.emplace(ns);
for (auto const& ns : namespaces)
{
auto const dirKL = keylet::hookStateDir(accid, ns);
if (!sb.exists(dirKL))
continue;
std::vector<uint256> entries;
std::shared_ptr<SLE const> page;
unsigned int index{0};
uint256 entry;
if (cdirFirst(sb, dirKL.key, page, index, entry))
{
do
{
entries.push_back(entry);
} while (cdirNext(sb, dirKL.key, page, index, entry));
}
for (auto const& key : entries)
{
auto item = sb.peek(Keylet{ltHOOK_STATE, key});
if (!item)
{
JLOG(j_.warn()) << "fixXahauTestnetGenesis: hook state "
"directory references a missing or "
"non-state entry, bailing";
return;
}
if (!sb.dirRemove(dirKL, (*item)[sfOwnerNode], key, false))
{
JLOG(j_.warn()) << "fixXahauTestnetGenesis: could not "
"remove hook state entry, bailing";
return;
}
sb.erase(item);
}
// dirRemove drops the root once empty; this is just in case
if (auto dir = sb.peek(dirKL))
sb.erase(dir);
}
ownerCount -= static_cast<std::int64_t>(stateCount) * scale;
if (sle->isFieldPresent(sfHookStateCount))
sle->makeFieldAbsent(sfHookStateCount);
if (sle->isFieldPresent(sfHookStateScale))
sle->makeFieldAbsent(sfHookStateScale);
if (sle->isFieldPresent(sfHookNamespaces))
sle->makeFieldAbsent(sfHookNamespaces);
}
// Step 5: install governance + reward hooks
STArray hooks{sfHooks, genesis_hooks.size()};
std::int64_t hookReserve = 0;
for (auto const& [hookOn, wasmBytes, params] : genesis_hooks)
{
auto const hookHash = ripple::sha512Half_s(
ripple::Slice(wasmBytes.data(), wasmBytes.size()));
auto const defKL = keylet::hookDefinition(hookHash);
STObject hookObj{sfHook};
hookObj.setFieldH256(sfHookHash, hookHash);
if (auto def = sb.peek(defKL))
{
// Reuse the existing definition. It may have been created by an
// ordinary SetHook with other defaults, so pin the fields the
// genesis hooks depend on at the hook level where they differ.
std::uint64_t const rc = def->isFieldPresent(sfReferenceCount)
? def->getFieldU64(sfReferenceCount)
: 0;
def->setFieldU64(sfReferenceCount, rc + 1);
sb.update(def);
if (!def->isFieldPresent(sfHookOn) ||
def->getFieldH256(sfHookOn) != hookOn ||
def->isFieldPresent(sfHookOnIncoming) ||
def->isFieldPresent(sfHookOnOutgoing))
hookObj.setFieldH256(sfHookOn, hookOn);
if (def->getFieldH256(sfHookNamespace) != beast::zero)
hookObj.setFieldH256(sfHookNamespace, uint256{beast::zero});
if (def->isFieldPresent(sfHookCanEmit))
hookObj.setFieldH256(sfHookCanEmit, UINT256_BIT[ttHOOK_SET]);
}
else
{
std::ostringstream loggerStream;
auto result = validateGuards(
wasmBytes,
loggerStream,
"rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh",
hook_api::getImportWhitelist(ctx_.view().rules()),
hook_api::getGuardRulesVersion(ctx_.view().rules()));
if (!result)
{
JLOG(j_.warn()) << "fixXahauTestnetGenesis: genesis hook "
"failed guard validation, bailing: "
<< loggerStream.str();
return;
}
if (auto err = hook::HookExecutor::validateWasm(
wasmBytes.data(), (size_t)wasmBytes.size()))
{
JLOG(j_.warn()) << "fixXahauTestnetGenesis: genesis hook "
"failed wasm validation, bailing: "
<< *err;
return;
}
// def is null here (the peek above found nothing)
def = std::make_shared<SLE>(defKL);
def->setFieldH256(sfHookHash, hookHash);
def->setFieldH256(sfHookOn, hookOn);
def->setFieldH256(sfHookNamespace, uint256{beast::zero});
def->setFieldU16(sfHookApiVersion, 0);
def->setFieldVL(sfCreateCode, wasmBytes);
def->setFieldH256(sfHookSetTxnID, ctx_.tx.getTransactionID());
def->setFieldU64(sfReferenceCount, 1);
def->setFieldAmount(
sfFee, XRPAmount{hook::computeExecutionFee(result->first)});
if (result->second > 0)
def->setFieldAmount(
sfHookCallbackFee,
XRPAmount{hook::computeExecutionFee(result->second)});
sb.insert(def);
}
{
std::vector<STObject> vec;
for (auto const& [k, v] : params)
{
STObject param(sfHookParameter);
param.setFieldVL(sfHookParameterName, k);
param.setFieldVL(sfHookParameterValue, v);
vec.emplace_back(std::move(param));
}
hookObj.setFieldArray(
sfHookParameters, STArray(vec, sfHookParameters));
}
hookReserve += SetHook::computeHookReserve(hookObj);
hooks.push_back(hookObj);
}
if (sleHooks)
{
sleHooks->setFieldArray(sfHooks, hooks);
sb.update(sleHooks);
}
else
{
sleHooks = std::make_shared<SLE>(keylet::hook(accid));
sleHooks->setFieldArray(sfHooks, hooks);
sleHooks->setAccountID(sfAccount, accid);
auto const page = sb.dirInsert(
keylet::ownerDir(accid),
keylet::hook(accid),
describeOwnerDir(accid));
if (!page)
{
JLOG(j_.warn()) << "fixXahauTestnetGenesis: genesis directory full "
"when inserting hooks object, bailing";
return;
}
sleHooks->setFieldU64(sfOwnerNode, *page);
sb.insert(sleHooks);
}
// Step 6: drop definitions nothing references any more
for (auto const& key : releasedDefs)
{
auto def = sb.peek(Keylet{ltHOOK_DEFINITION, key});
if (def && def->isFieldPresent(sfReferenceCount) &&
def->getFieldU64(sfReferenceCount) == 0)
sb.erase(def);
}
// Step 7: owner count
ownerCount += hookReserve;
if (ownerCount < 0)
{
JLOG(j_.warn()) << "fixXahauTestnetGenesis: owner count went negative ("
<< ownerCount << "), clamping to hook reserve";
ownerCount = hookReserve;
}
sle->setFieldU32(sfOwnerCount, static_cast<std::uint32_t>(ownerCount));
sb.update(sle);
sb.apply(ctx_.rawView());
JLOG(j_.warn()) << "fixXahauTestnetGenesis amendment executed successfully";
}
void
Change::activateTrustLinesToSelfFix()
{
@@ -1479,8 +1150,6 @@ Change::applyAmendment()
activateTrustLinesToSelfFix();
else if (amendment == featureXahauGenesis)
activateXahauGenesis();
else if (amendment == fixXahauTestnetGenesis)
activateXahauTestnetGenesisFix();
ctx_.app.getAmendmentTable().enable(amendment);
-3
View File
@@ -62,9 +62,6 @@ private:
void
activateXahauGenesis();
void
activateXahauTestnetGenesisFix();
TER
applyAmendment();
-9
View File
@@ -115,15 +115,6 @@ inline std::vector<std::pair<std::string, XRPAmount>> const L1Membership{
TableAmount}, // L2 Table Exchanges
};
// Governance seating used by fixXahauTestnetGenesis on the Xahau testnet
// (NetworkID 21338). Only Evernode's L1 seat is filled, every other seat is
// left empty and there are no L2 tables. The amount is unused: the fix does
// not mint or burn anything.
inline std::vector<std::pair<std::string, XRPAmount>> const TestnetL1Membership{
{"nHUetpkzTgf8c6A1DTGemRrhL4nMjsiJrtcsmmg75ufzV5EDMbWU",
InfraAmount}, // Evernode
};
// this data structure is used for testing the amendment only,
// if the ttEnableAmendment has an optional flag (that cannot be added in
// production) then whatever is in this array is used
-3
View File
@@ -102,9 +102,6 @@ struct ConfigSection
#define SECTION_VALIDATORS "validators"
#define SECTION_VALIDATOR_TOKEN "validator_token"
#define SECTION_VETO_AMENDMENTS "veto_amendments"
// Testnet-only: amendments enabled on-ledger that this node should not treat
// as unsupported (i.e. should not become amendment blocked over).
#define SECTION_IGNORE_AMENDMENTS "ignore_amendments"
#define SECTION_WORKERS "workers"
} // namespace ripple
-17
View File
@@ -851,22 +851,6 @@ private:
return jvRequest;
}
// catalogue_unpin <ledger_index_min> <ledger_index_max>
Json::Value
parseCatalogueUnpin(Json::Value const& jvParams)
{
Json::Value jvRequest(Json::objectValue);
if (jvParams.size() >= 2)
{
// asUInt() throws on non-numeric input rather than yielding 0
jvRequest[jss::ledger_index_min] = jvParams[0u].asUInt();
jvRequest[jss::ledger_index_max] = jvParams[1u].asUInt();
}
return jvRequest;
}
// catalogue_status - no parameters required
Json::Value
parseCatalogueStatus(Json::Value const& jvParams)
@@ -1486,7 +1470,6 @@ public:
{"catalogue_create", &RPCParser::parseCatalogueCreate, 3, 4},
{"catalogue_load", &RPCParser::parseCatalogueLoad, 1, 2},
{"catalogue_status", &RPCParser::parseCatalogueStatus, 0, 0},
{"catalogue_unpin", &RPCParser::parseCatalogueUnpin, 2, 2},
{"channel_authorize", &RPCParser::parseChannelAuthorize, 3, 4},
{"channel_verify", &RPCParser::parseChannelVerify, 4, 4},
{"connect", &RPCParser::parseConnect, 1, 2},
-16
View File
@@ -121,22 +121,6 @@ public:
virtual void
sync() = 0;
/** Make every object previously passed to store() or storeBatch()
durable before returning.
@return true if, on return, those writes are as durable as this
backend ever makes them (trivially so for in-memory
backends); false if the backend cannot do this on demand and
they become durable later on its own schedule (e.g. NuDB's
commit thread).
*/
virtual bool
syncDurable()
{
sync();
return false;
}
/** Visit every object in the database
This is usually called during import.
@note This routine will not be called concurrently with itself
@@ -186,13 +186,6 @@ public:
{
}
bool
syncDurable() override
{
// Nothing here outlives the process, so there is nothing to wait for.
return true;
}
void
for_each(std::function<void(std::shared_ptr<NodeObject>)> f) override
{
@@ -291,9 +291,6 @@ public:
void
sync() override
{
// NuDB commits on its context thread (about once a second) and
// offers no public barrier while that thread runs, so this cannot
// force durability; syncDurable() therefore reports false.
}
void
@@ -81,13 +81,6 @@ public:
{
}
bool
syncDurable() override
{
// Nothing here outlives the process, so there is nothing to wait for.
return true;
}
void
for_each(std::function<void(std::shared_ptr<NodeObject>)> f) override
{
@@ -164,13 +164,6 @@ public:
{
}
bool
syncDurable() override
{
// Nothing here outlives the process, so there is nothing to wait for.
return true;
}
void
for_each(std::function<void(std::shared_ptr<NodeObject>)> f) override
{
@@ -383,33 +383,6 @@ public:
void
sync() override
{
(void)syncDurable();
}
bool
syncDurable() override
{
// Drain anything queued through store() into RocksDB, then flush
// and fsync the WAL so every write so far survives a crash.
m_batch.waitForWriting();
if (!m_db)
return false;
auto const ret = m_db->FlushWAL(/*sync=*/true);
if (ret.ok())
return true;
// Some user-supplied options (e.g. allow_mmap_writes) make WAL sync
// unavailable. Report "not durable on demand" rather than failing:
// callers then fall back to the non-durable checkpoint path.
if (ret.IsNotSupported())
{
JLOG(m_journal.warn())
<< "RocksDB cannot sync its WAL on demand: " << ret.ToString();
return false;
}
Throw<std::runtime_error>("sync failed: " + ret.ToString());
}
void
+2 -5
View File
@@ -74,16 +74,13 @@ public:
int
getWriteLoad();
/** Block until every object passed to store() so far has been handed
to the callback's writeBatch(). */
void
waitForWriting();
private:
void
performScheduledTask() override;
void
writeBatch();
void
waitForWriting();
private:
using LockType = std::recursive_mutex;
@@ -91,36 +91,6 @@ DatabasePinnedImp::store(
}
}
void
DatabasePinnedImp::storePinnedBatch(Batch const& batch)
{
if (batch.empty())
return;
std::uint64_t bytes = 0;
for (auto const& obj : batch)
{
XRPL_ASSERT(
obj && !isPinnedType(obj->getType()),
"ripple::NodeStore::DatabasePinnedImp::storePinnedBatch : "
"serializable type");
bytes += obj->getData().size();
}
// The persistent backends allowed for pinned_type (NuDB, RocksDB, and
// rwdb/memory in standalone) are safe to write from several threads,
// so this may overlap a BatchWriter flush of single store() calls.
persistent_->storeBatch(batch);
pinnedStoreCount_ += batch.size();
storeStats(batch.size(), bytes);
}
bool
DatabasePinnedImp::syncPinned()
{
return persistent_->syncDurable();
}
std::shared_ptr<NodeObject>
DatabasePinnedImp::fetchNodeObject(
uint256 const& hash,
@@ -142,11 +112,7 @@ DatabasePinnedImp::fetchNodeObject(
{
// Try persistent backend first (pinned data)
if (auto obj = tryPersistent(hash, fetchReport))
{
if (duplicate)
copyToRotating(*obj, hash, ledgerSeq);
return obj;
}
// Fallback: try rotating backends
return rotating_.fetchNodeObject(
@@ -164,30 +130,7 @@ DatabasePinnedImp::fetchNodeObject(
return obj;
// Fallback: try persistent backend (pinned data)
auto obj = tryPersistent(hash, fetchReport);
// `duplicate` is set by online_delete's copy of the current validated
// state into the new writable backend. A hot ledger can share nodes
// that exist only in persistent storage (e.g. after catalogue_load
// switched the LCL to its last ledger); copy those too, so the hot
// store becomes self-contained after one rotation and pinned history
// can be unpinned and its store discarded without breaking live state.
// The pinned-first path above does the same, so the copy happens
// whichever backend is tried first.
if (obj && duplicate)
copyToRotating(*obj, hash, ledgerSeq);
return obj;
}
void
DatabasePinnedImp::copyToRotating(
NodeObject const& obj,
uint256 const& hash,
std::uint32_t ledgerSeq)
{
Blob data(obj.getData());
rotating_.store(obj.getType(), std::move(data), hash, ledgerSeq);
return tryPersistent(hash, fetchReport);
}
void
@@ -319,8 +262,8 @@ DatabasePinnedImp::tryPersistent(uint256 const& hash, FetchReport& fetchReport)
if (status == ok && nodeObject)
{
fetchReport.wasFound = true;
// Copying into rotating storage for duplicate=true is handled by
// the caller (see fetchNodeObject's default path).
// Note: We do NOT copy pinned data to rotating storage even if
// duplicate=true. Pinned data stays in persistent storage.
}
return nodeObject;
}
@@ -121,20 +121,6 @@ public:
std::uint32_t ledgerSeq) override;
void
sync() override;
/** Write node objects of pinned history straight to the persistent
backend in one batch. Objects must carry hot (serializable) types.
Used by catalogue_load, which builds whole batches per ledger.
*/
void
storePinnedBatch(Batch const& batch);
/** Make everything written to the persistent backend durable.
@return false if the backend cannot guarantee that on demand.
*/
bool
syncPinned();
bool
storeLedger(std::shared_ptr<Ledger const> const& srcLedger) override;
void
@@ -155,14 +141,6 @@ private:
bool
likelyPinned(std::uint32_t ledgerSeq) const;
/** Copy an object found only in persistent storage into the writable
rotating backend (the `duplicate` path of fetchNodeObject). */
void
copyToRotating(
NodeObject const& obj,
uint256 const& hash,
std::uint32_t ledgerSeq);
std::shared_ptr<NodeObject>
tryPersistent(uint256 const& hash, FetchReport& fetchReport);
+13 -65
View File
@@ -213,19 +213,13 @@ deserializeSHAMapFromStream(
CatalogueInputStream& stream,
SHAMapNodeType nodeType,
NodeObjectType flushType,
SHAMap::FlushSink const* sink,
bool allowRemoval,
beast::Journal const& j)
{
try
{
// Reused across leaves: resize() keeps capacity, so steady-state
// parsing does no per-leaf allocation beyond the SHAMapItem itself.
std::vector<std::uint8_t> data;
auto deserializeLeaf =
[&shaMap, &stream, &j, &data, nodeType, allowRemoval](
SHAMapNodeType& parsedType) -> bool {
auto deserializeLeaf = [&shaMap, &stream, &j, nodeType, allowRemoval](
SHAMapNodeType& parsedType) -> bool {
stream.read(reinterpret_cast<char*>(&parsedType), 1);
if (parsedType == SHAMapNodeType::tnTERMINAL)
@@ -254,8 +248,7 @@ deserializeSHAMapFromStream(
return false;
}
// delItem walks once and reports absence itself.
if (!shaMap.delItem(key))
if (!shaMap.hasItem(key))
{
JLOG(j.error())
<< "Deserialization: removal of key " << to_string(key)
@@ -263,6 +256,7 @@ deserializeSHAMapFromStream(
return false;
}
shaMap.delItem(key);
return true;
}
@@ -285,7 +279,7 @@ deserializeSHAMapFromStream(
return false;
}
data.resize(size);
std::vector<std::uint8_t> data(size);
stream.read(reinterpret_cast<char*>(data.data()), size);
if (stream.fail())
@@ -296,8 +290,11 @@ deserializeSHAMapFromStream(
return false;
}
auto item = make_shamapitem(key, Slice(data.data(), size));
return shaMap.upsertGiveItem(nodeType, std::move(item));
auto item = make_shamapitem(key, makeSlice(data));
if (shaMap.hasItem(key))
return shaMap.updateGiveItem(nodeType, std::move(item));
return shaMap.addGiveItem(nodeType, std::move(item));
};
auto lastParsed = SHAMapNodeType::tnREMOVE;
@@ -311,10 +308,7 @@ deserializeSHAMapFromStream(
return false;
}
if (sink)
shaMap.flushDirty(flushType, *sink);
else
shaMap.flushDirty(flushType);
shaMap.flushDirty(flushType);
return true;
}
catch (std::exception const& e)
@@ -351,13 +345,7 @@ deserializeStateMapFromStream(
beast::Journal const& j)
{
return deserializeSHAMapFromStream(
stateMap,
stream,
SHAMapNodeType::tnACCOUNT_STATE,
flushType,
nullptr,
true,
j);
stateMap, stream, SHAMapNodeType::tnACCOUNT_STATE, flushType, true, j);
}
bool
@@ -368,47 +356,7 @@ deserializeTxMapFromStream(
beast::Journal const& j)
{
return deserializeSHAMapFromStream(
txMap,
stream,
SHAMapNodeType::tnTRANSACTION_MD,
flushType,
nullptr,
false,
j);
}
bool
deserializeStateMapFromStream(
SHAMap& stateMap,
CatalogueInputStream& stream,
SHAMap::FlushSink const& sink,
beast::Journal const& j)
{
return deserializeSHAMapFromStream(
stateMap,
stream,
SHAMapNodeType::tnACCOUNT_STATE,
hotACCOUNT_NODE,
&sink,
true,
j);
}
bool
deserializeTxMapFromStream(
SHAMap& txMap,
CatalogueInputStream& stream,
SHAMap::FlushSink const& sink,
beast::Journal const& j)
{
return deserializeSHAMapFromStream(
txMap,
stream,
SHAMapNodeType::tnTRANSACTION_MD,
hotTRANSACTION_NODE,
&sink,
false,
j);
txMap, stream, SHAMapNodeType::tnTRANSACTION_MD, flushType, false, j);
}
} // namespace RPC
-18
View File
@@ -59,24 +59,6 @@ deserializeTxMapFromStream(
NodeObjectType flushType,
beast::Journal const& j);
/** As above, but dirty nodes are flushed to `sink` (tagged with
hotACCOUNT_NODE / hotTRANSACTION_NODE) instead of the nodestore, and are
not canonicalized into the TreeNodeCache. See SHAMap::flushDirty(t, sink).
*/
bool
deserializeStateMapFromStream(
SHAMap& stateMap,
CatalogueInputStream& stream,
SHAMap::FlushSink const& sink,
beast::Journal const& j);
bool
deserializeTxMapFromStream(
SHAMap& txMap,
CatalogueInputStream& stream,
SHAMap::FlushSink const& sink,
beast::Journal const& j);
} // namespace RPC
} // namespace ripple
-1
View File
@@ -199,7 +199,6 @@ Handler const handlerArray[]{
{"catalogue_create", byRef(&doCatalogueCreate), Role::ADMIN, NO_CONDITION},
{"catalogue_status", byRef(&doCatalogueStatus), Role::ADMIN, NO_CONDITION},
{"catalogue_load", byRef(&doCatalogueLoad), Role::ADMIN, NO_CONDITION},
{"catalogue_unpin", byRef(&doCatalogueUnpin), Role::ADMIN, NO_CONDITION},
};
class HandlerTable
File diff suppressed because it is too large. Load diff
-2
View File
@@ -184,8 +184,6 @@ Json::Value
doCatalogueStatus(RPC::JsonContext&);
Json::Value
doCatalogueLoad(RPC::JsonContext&);
Json::Value
doCatalogueUnpin(RPC::JsonContext&);
} // namespace ripple
#endif
+3 -44
View File
@@ -36,7 +36,6 @@
#include <xrpl/beast/utility/instrumentation.h>
#include <boost/iostreams/filtering_stream.hpp>
#include <cassert>
#include <functional>
#include <stack>
#include <vector>
@@ -213,19 +212,6 @@ public:
SHAMapNodeType type,
boost::intrusive_ptr<SHAMapItem const> item);
/** Replace the item with the same key if present, otherwise add it.
Equivalent to `hasItem(key) ? updateGiveItem(..) : addGiveItem(..)`
but walks the tree once when the key already exists.
@return false if the key exists with a different leaf type, or if
the add fails.
*/
bool
upsertGiveItem(
SHAMapNodeType type,
boost::intrusive_ptr<SHAMapItem const> item);
// Save a copy if you need to extend the life
// of the SHAMapItem beyond this SHAMap
boost::intrusive_ptr<SHAMapItem const> const&
@@ -381,26 +367,6 @@ public:
int
flushDirty(NodeObjectType t);
/** Receives each node written by flushDirty(t, sink): the type passed
to flushDirty, the node serialized with its hash prefix, and its
hash.
*/
using FlushSink =
std::function<void(NodeObjectType, Blob&&, uint256 const&)>;
/** Flush modified nodes to `sink` instead of the nodestore and convert
them to shared.
Unlike flushDirty(t), written nodes are NOT canonicalized into the
family's TreeNodeCache. Bulk writers of history (catalogue_load)
must use this: every cached inner node holds strong references to
its children, so canonicalizing pins every superseded node version
in memory until the cache sweeps it, which on a fast import grows
without practical bound.
*/
int
flushDirty(NodeObjectType t, FlushSink const& sink);
void
walkMap(std::vector<SHAMapMissingNode>& missingNodes, int maxMissing) const;
bool
@@ -483,13 +449,9 @@ private:
std::shared_ptr<Node>
preFlushNode(std::shared_ptr<Node> node) const;
/** write and canonicalize modified node; if `sink` is set, hand the
serialized node to it instead and do not canonicalize */
/** write and canonicalize modified node */
std::shared_ptr<SHAMapTreeNode>
writeNode(
NodeObjectType t,
std::shared_ptr<SHAMapTreeNode> node,
FlushSink const* sink = nullptr) const;
writeNode(NodeObjectType t, std::shared_ptr<SHAMapTreeNode> node) const;
// returns the first item at or below this node
SHAMapLeafNode*
@@ -572,10 +534,7 @@ private:
Delta& differences,
int& maxCount) const;
int
walkSubTree(
bool doWrite,
NodeObjectType t,
FlushSink const* sink = nullptr);
walkSubTree(bool doWrite, NodeObjectType t);
// Structure to track information about call to
// getMissingNodes while it's in progress
+6 -65
View File
@@ -1059,42 +1059,6 @@ SHAMap::updateGiveItem(
return true;
}
bool
SHAMap::upsertGiveItem(
SHAMapNodeType type,
boost::intrusive_ptr<SHAMapItem const> item)
{
XRPL_ASSERT(
state_ != SHAMapState::Immutable,
"ripple::SHAMap::upsertGiveItem : not immutable");
uint256 const tag = item->key();
SharedPtrNodeStack stack;
auto const leaf = walkTowardsKey(tag, &stack);
if (!leaf || leaf->peekItem()->key() != tag)
return addGiveItem(type, std::move(item));
// From here this mirrors updateGiveItem, reusing the walk above.
auto node = std::static_pointer_cast<SHAMapLeafNode>(stack.top().first);
auto const nodeID = stack.top().second;
stack.pop();
if (node->getType() != type)
{
JLOG(journal_.fatal()) << "SHAMap::upsertGiveItem: cross-type change!";
return false;
}
node = unshareNode(std::move(node), nodeID);
if (node->setItem(std::move(item)))
dirtyUp(stack, tag, node);
return true;
}
bool
SHAMap::fetchRoot(SHAMapHash const& hash, SHAMapSyncFilter* filter)
{
@@ -1144,24 +1108,10 @@ SHAMap::fetchRoot(SHAMapHash const& hash, SHAMapSyncFilter* filter)
first call SHAMapTreeNode::unshare().
*/
std::shared_ptr<SHAMapTreeNode>
SHAMap::writeNode(
NodeObjectType t,
std::shared_ptr<SHAMapTreeNode> node,
FlushSink const* sink) const
SHAMap::writeNode(NodeObjectType t, std::shared_ptr<SHAMapTreeNode> node) const
{
XRPL_ASSERT(
node->cowid() == 0, "ripple::SHAMap::writeNode : valid input node");
if (sink)
{
// Bulk-write path: no TreeNodeCache, no direct store. The caller
// owns persistence (see flushDirty(t, sink)).
Serializer s;
node->serializeWithPrefix(s);
(*sink)(t, std::move(s.modData()), node->getHash().as_uint256());
return node;
}
XRPL_ASSERT(backed_, "ripple::SHAMap::writeNode : is backed");
canonicalize(node->getHash(), node);
@@ -1209,19 +1159,10 @@ SHAMap::flushDirty(NodeObjectType t)
}
int
SHAMap::flushDirty(NodeObjectType t, FlushSink const& sink)
{
// The sink decides where nodes go, so this writes whether or not the
// map is backed.
return walkSubTree(true, t, &sink);
}
int
SHAMap::walkSubTree(bool doWrite, NodeObjectType t, FlushSink const* sink)
SHAMap::walkSubTree(bool doWrite, NodeObjectType t)
{
XRPL_ASSERT(
!doWrite || backed_ || sink,
"ripple::SHAMap::walkSubTree : valid input");
!doWrite || backed_, "ripple::SHAMap::walkSubTree : valid input");
int flushed = 0;
@@ -1235,7 +1176,7 @@ SHAMap::walkSubTree(bool doWrite, NodeObjectType t, FlushSink const* sink)
root_->unshare();
if (doWrite)
root_ = writeNode(t, std::move(root_), sink);
root_ = writeNode(t, std::move(root_));
return 1;
}
@@ -1304,7 +1245,7 @@ SHAMap::walkSubTree(bool doWrite, NodeObjectType t, FlushSink const* sink)
child->unshare();
if (doWrite)
child = writeNode(t, std::move(child), sink);
child = writeNode(t, std::move(child));
node->shareChild(branch, child);
}
@@ -1320,7 +1261,7 @@ SHAMap::walkSubTree(bool doWrite, NodeObjectType t, FlushSink const* sink)
if (doWrite)
node = std::static_pointer_cast<SHAMapInnerNode>(
writeNode(t, std::move(node), sink));
writeNode(t, std::move(node)));
++flushed;