mirror of
https://github.com/XRPLF/rippled.git
synced 2026-09-27 15:28:03 +00:00
MetricsRegistry did two jobs. It owned the OTel metrics pipeline, and it registered the observable gauges whose callbacks read live application services. The second job is what made the whole class xrpld-tier, so the pipeline's lifecycle -- the recording() gate and the stop() teardown that closes a use-after-free window -- could not be unit-tested in xrpl_tests. Split it in two: - xrpl::telemetry::MetricsRegistry (libxrpl) owns the exporter, provider, meter, the 16 synchronous instruments, recording(), stop(), and the record*/increment* methods. - xrpl::telemetry::AppMetricGauges (xrpld) owns the 19 observable gauges and their callbacks, holding a reference to the core and to the ServiceRegistry. MetricMacros.h and ValidationTracker move with the core. The macros need only recording() and meter(), both core members; the core holds a tracker by value, and a libxrpl header cannot include one from src/. ApplicationImp owns both objects and sequences them. The core is built in the member-init list, so every synchronous instrument exists before any subsystem can record one. The gauges are armed once overlay_ exists, the last service their callbacks read. Shutdown detaches the gauge callbacks before the core drops the provider, and each shutdown step is isolated so a failure in one cannot skip the others. That detach call is new. detachCallbacks() had no callers, and the flag it sets is read by the gauge callbacks but can no longer be written by the core, so the caller now has to make the ordering explicit. The telemetry module links xrpl.libxrpl.core and xrpl.libxrpl.protocol PUBLIC: ValidationTracker.h takes a LedgerIndex and MetricMacros.h takes a ServiceRegistry, both in interfaces a consumer compiles against. Adds a MetricsRegistry gtest that drives an enabled core with telemetry on and pins the recording() gate, stop() leaving the registry inert, and stop() being idempotent. The libxrpl test tree no longer depends on xrpld.telemetry at all, and the two CMake workarounds that compiled xrpld sources into xrpl_tests are gone. Documentation and dashboard source links follow the code to their new paths, split between the two classes by which one now defines each metric.
558 lines
19 KiB
C++
558 lines
19 KiB
C++
#include <xrpld/app/ledger/LedgerHistory.h>
|
|
|
|
#include <xrpld/app/ledger/LedgerPersistence.h>
|
|
#include <xrpld/app/ledger/LedgerToJson.h>
|
|
#include <xrpld/app/main/Application.h>
|
|
#include <xrpld/core/Config.h>
|
|
|
|
#include <xrpl/basics/Log.h>
|
|
#include <xrpl/basics/base_uint.h>
|
|
#include <xrpl/basics/chrono.h>
|
|
#include <xrpl/basics/contract.h>
|
|
#include <xrpl/beast/insight/Collector.h>
|
|
#include <xrpl/beast/utility/Journal.h>
|
|
#include <xrpl/beast/utility/instrumentation.h>
|
|
#include <xrpl/json/json_value.h>
|
|
#include <xrpl/json/to_string.h> // IWYU pragma: keep
|
|
#include <xrpl/ledger/ReadView.h>
|
|
#include <xrpl/protocol/Protocol.h>
|
|
#include <xrpl/protocol/RippleLedgerHash.h>
|
|
#include <xrpl/protocol/Rules.h>
|
|
#include <xrpl/protocol/TxMeta.h>
|
|
#include <xrpl/shamap/SHAMap.h>
|
|
#include <xrpl/shamap/SHAMapItem.h>
|
|
#include <xrpl/telemetry/MetricsRegistry.h>
|
|
|
|
#include <algorithm>
|
|
#include <memory>
|
|
#include <mutex>
|
|
#include <optional>
|
|
#include <string_view>
|
|
#include <utility>
|
|
#include <vector>
|
|
|
|
namespace xrpl {
|
|
|
|
// FIXME: Need to clean up ledgers by index at some point
|
|
|
|
LedgerHistory::LedgerHistory(beast::insight::Collector::ptr const& collector, Application& app)
|
|
: app_(app)
|
|
, collector_(collector)
|
|
, mismatchCounter_(collector->makeCounter("ledger.history", "mismatch"))
|
|
, ledgersByHash_(
|
|
"LedgerCache",
|
|
app_.config().getValueFor(SizedItem::LedgerSize),
|
|
std::chrono::seconds{app_.config().getValueFor(SizedItem::LedgerAge)},
|
|
stopwatch(),
|
|
app_.getJournal("TaggedCache"))
|
|
, consensusValidated_(
|
|
"ConsensusValidated",
|
|
64,
|
|
std::chrono::minutes{5},
|
|
stopwatch(),
|
|
app_.getJournal("TaggedCache"))
|
|
, j_(app.getJournal("LedgerHistory"))
|
|
{
|
|
}
|
|
|
|
bool
|
|
LedgerHistory::insert(std::shared_ptr<Ledger const> const& ledger, bool validated)
|
|
{
|
|
if (!ledger->isImmutable())
|
|
logicError("mutable Ledger in insert");
|
|
|
|
XRPL_ASSERT(
|
|
ledger->stateMap().getHash().isNonZero(), "xrpl::LedgerHistory::insert : nonzero hash");
|
|
|
|
std::unique_lock const sl(ledgersByHash_.peekMutex());
|
|
|
|
bool const alreadyHad = ledgersByHash_.canonicalizeReplaceCache(ledger->header().hash, ledger);
|
|
if (validated)
|
|
ledgersByIndex_[ledger->header().seq] = ledger->header().hash;
|
|
|
|
return alreadyHad;
|
|
}
|
|
|
|
LedgerHash
|
|
LedgerHistory::getLedgerHash(LedgerIndex index)
|
|
{
|
|
std::unique_lock const sl(ledgersByHash_.peekMutex());
|
|
if (auto it = ledgersByIndex_.find(index); it != ledgersByIndex_.end())
|
|
return it->second;
|
|
return {};
|
|
}
|
|
|
|
std::shared_ptr<Ledger const>
|
|
LedgerHistory::getLedgerBySeq(LedgerIndex index)
|
|
{
|
|
{
|
|
std::unique_lock sl(ledgersByHash_.peekMutex());
|
|
auto it = ledgersByIndex_.find(index);
|
|
|
|
if (it != ledgersByIndex_.end())
|
|
{
|
|
uint256 const hash = it->second;
|
|
sl.unlock();
|
|
return getLedgerByHash(hash);
|
|
}
|
|
}
|
|
|
|
Rules const rules{app_.config().features};
|
|
Fees const fees = app_.config().fees.toFees();
|
|
std::shared_ptr<Ledger const> ret = loadByIndex(index, rules, fees, app_);
|
|
|
|
if (!ret)
|
|
return ret;
|
|
|
|
XRPL_ASSERT(
|
|
ret->header().seq == index, "xrpl::LedgerHistory::getLedgerBySeq : result sequence match");
|
|
|
|
{
|
|
// Add this ledger to the local tracking by index
|
|
std::unique_lock const sl(ledgersByHash_.peekMutex());
|
|
|
|
XRPL_ASSERT(
|
|
ret->isImmutable(), "xrpl::LedgerHistory::getLedgerBySeq : immutable result ledger");
|
|
ledgersByHash_.canonicalizeReplaceClient(ret->header().hash, ret);
|
|
ledgersByIndex_[ret->header().seq] = ret->header().hash;
|
|
return (ret->header().seq == index) ? ret : nullptr;
|
|
}
|
|
}
|
|
|
|
std::shared_ptr<Ledger const>
|
|
LedgerHistory::getLedgerByHash(LedgerHash const& hash)
|
|
{
|
|
auto ret = ledgersByHash_.fetch(hash);
|
|
|
|
if (ret)
|
|
{
|
|
XRPL_ASSERT(
|
|
ret->isImmutable(),
|
|
"xrpl::LedgerHistory::getLedgerByHash : immutable fetched "
|
|
"ledger");
|
|
XRPL_ASSERT(
|
|
ret->header().hash == hash,
|
|
"xrpl::LedgerHistory::getLedgerByHash : fetched ledger hash "
|
|
"match");
|
|
return ret;
|
|
}
|
|
|
|
Rules const rules{app_.config().features};
|
|
Fees const fees = app_.config().fees.toFees();
|
|
ret = loadByHash(hash, rules, fees, app_);
|
|
|
|
if (!ret)
|
|
return ret;
|
|
|
|
XRPL_ASSERT(
|
|
ret->isImmutable(), "xrpl::LedgerHistory::getLedgerByHash : immutable loaded ledger");
|
|
XRPL_ASSERT(
|
|
ret->header().hash == hash,
|
|
"xrpl::LedgerHistory::getLedgerByHash : loaded ledger hash match");
|
|
ledgersByHash_.canonicalizeReplaceClient(ret->header().hash, ret);
|
|
XRPL_ASSERT(
|
|
ret->header().hash == hash, "xrpl::LedgerHistory::getLedgerByHash : result hash match");
|
|
|
|
return ret;
|
|
}
|
|
|
|
static void
|
|
logOne(ReadView const& ledger, uint256 const& tx, char const* msg, beast::Journal& j)
|
|
{
|
|
auto metaData = ledger.txRead(tx).second;
|
|
|
|
if (metaData != nullptr)
|
|
{
|
|
JLOG(j.debug()) << "MISMATCH on TX " << tx << ": " << msg
|
|
<< " is missing this transaction:\n"
|
|
<< metaData->getJson(JsonOptions::Values::None);
|
|
}
|
|
else
|
|
{
|
|
JLOG(j.debug()) << "MISMATCH on TX " << tx << ": " << msg
|
|
<< " is missing this transaction.";
|
|
}
|
|
}
|
|
|
|
static void
|
|
logMetadataDifference(
|
|
ReadView const& builtLedger,
|
|
ReadView const& validLedger,
|
|
uint256 const& tx,
|
|
beast::Journal j)
|
|
{
|
|
auto getMeta = [](ReadView const& ledger, uint256 const& txID) {
|
|
std::optional<TxMeta> ret;
|
|
if (auto meta = ledger.txRead(txID).second)
|
|
ret.emplace(txID, ledger.seq(), *meta);
|
|
return ret;
|
|
};
|
|
|
|
auto validMetaData = getMeta(validLedger, tx);
|
|
auto builtMetaData = getMeta(builtLedger, tx);
|
|
|
|
XRPL_ASSERT(
|
|
validMetaData || builtMetaData, "xrpl::log_metadata_difference : some metadata present");
|
|
|
|
if (validMetaData && builtMetaData)
|
|
{
|
|
auto const& validNodes = validMetaData->getNodes();
|
|
auto const& builtNodes = builtMetaData->getNodes();
|
|
|
|
bool const resultDiff = validMetaData->getResultTER() != builtMetaData->getResultTER();
|
|
|
|
bool const indexDiff = validMetaData->getIndex() != builtMetaData->getIndex();
|
|
|
|
bool const nodesDiff = validNodes != builtNodes;
|
|
|
|
if (!resultDiff && !indexDiff && !nodesDiff)
|
|
{
|
|
JLOG(j.error()) << "MISMATCH on TX " << tx << ": No apparent mismatches detected!";
|
|
return;
|
|
}
|
|
|
|
if (!nodesDiff)
|
|
{
|
|
if (resultDiff && indexDiff)
|
|
{
|
|
JLOG(j.debug()) << "MISMATCH on TX " << tx << ": Different result and index!";
|
|
JLOG(j.debug()) << " Built:"
|
|
<< " Result: " << builtMetaData->getResult()
|
|
<< " Index: " << builtMetaData->getIndex();
|
|
JLOG(j.debug()) << " Valid:"
|
|
<< " Result: " << validMetaData->getResult()
|
|
<< " Index: " << validMetaData->getIndex();
|
|
}
|
|
else if (resultDiff)
|
|
{
|
|
JLOG(j.debug()) << "MISMATCH on TX " << tx << ": Different result!";
|
|
JLOG(j.debug()) << " Built:"
|
|
<< " Result: " << builtMetaData->getResult();
|
|
JLOG(j.debug()) << " Valid:"
|
|
<< " Result: " << validMetaData->getResult();
|
|
}
|
|
else if (indexDiff)
|
|
{
|
|
JLOG(j.debug()) << "MISMATCH on TX " << tx << ": Different index!";
|
|
JLOG(j.debug()) << " Built:"
|
|
<< " Index: " << builtMetaData->getIndex();
|
|
JLOG(j.debug()) << " Valid:"
|
|
<< " Index: " << validMetaData->getIndex();
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (resultDiff && indexDiff)
|
|
{
|
|
JLOG(j.debug()) << "MISMATCH on TX " << tx
|
|
<< ": Different result, index and nodes!";
|
|
JLOG(j.debug()) << " Built:\n" << builtMetaData->getJson(JsonOptions::Values::None);
|
|
JLOG(j.debug()) << " Valid:\n" << validMetaData->getJson(JsonOptions::Values::None);
|
|
}
|
|
else if (resultDiff)
|
|
{
|
|
JLOG(j.debug()) << "MISMATCH on TX " << tx << ": Different result and nodes!";
|
|
JLOG(j.debug()) << " Built:"
|
|
<< " Result: " << builtMetaData->getResult() << " Nodes:\n"
|
|
<< builtNodes.getJson(JsonOptions::Values::None);
|
|
JLOG(j.debug()) << " Valid:"
|
|
<< " Result: " << validMetaData->getResult() << " Nodes:\n"
|
|
<< validNodes.getJson(JsonOptions::Values::None);
|
|
}
|
|
else if (indexDiff)
|
|
{
|
|
JLOG(j.debug()) << "MISMATCH on TX " << tx << ": Different index and nodes!";
|
|
JLOG(j.debug()) << " Built:"
|
|
<< " Index: " << builtMetaData->getIndex() << " Nodes:\n"
|
|
<< builtNodes.getJson(JsonOptions::Values::None);
|
|
JLOG(j.debug()) << " Valid:"
|
|
<< " Index: " << validMetaData->getIndex() << " Nodes:\n"
|
|
<< validNodes.getJson(JsonOptions::Values::None);
|
|
}
|
|
else // nodes_diff
|
|
{
|
|
JLOG(j.debug()) << "MISMATCH on TX " << tx << ": Different nodes!";
|
|
JLOG(j.debug()) << " Built:"
|
|
<< " Nodes:\n"
|
|
<< builtNodes.getJson(JsonOptions::Values::None);
|
|
JLOG(j.debug()) << " Valid:"
|
|
<< " Nodes:\n"
|
|
<< validNodes.getJson(JsonOptions::Values::None);
|
|
}
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
if (validMetaData)
|
|
{
|
|
JLOG(j.error()) << "MISMATCH on TX " << tx << ": Metadata Difference. Valid=\n"
|
|
<< validMetaData->getJson(JsonOptions::Values::None);
|
|
}
|
|
|
|
if (builtMetaData)
|
|
{
|
|
JLOG(j.error()) << "MISMATCH on TX " << tx << ": Metadata Difference. Built=\n"
|
|
<< builtMetaData->getJson(JsonOptions::Values::None);
|
|
}
|
|
}
|
|
|
|
//------------------------------------------------------------------------------
|
|
|
|
// Return list of leaves sorted by key
|
|
static std::vector<SHAMapItem const*>
|
|
leaves(SHAMap const& sm)
|
|
{
|
|
std::vector<SHAMapItem const*> v;
|
|
for (auto const& item : sm)
|
|
v.push_back(&item);
|
|
std::ranges::sort(
|
|
v, [](SHAMapItem const* lhs, SHAMapItem const* rhs) { return lhs->key() < rhs->key(); });
|
|
return v;
|
|
}
|
|
|
|
void
|
|
LedgerHistory::handleMismatch(
|
|
LedgerHash const& built,
|
|
LedgerHash const& valid,
|
|
std::optional<uint256> const& builtConsensusHash,
|
|
std::optional<uint256> const& validatedConsensusHash,
|
|
json::Value const& consensus)
|
|
{
|
|
XRPL_ASSERT(built != valid, "xrpl::LedgerHistory::handleMismatch : unequal hashes");
|
|
++mismatchCounter_;
|
|
|
|
auto builtLedger = getLedgerByHash(built);
|
|
auto validLedger = getLedgerByHash(valid);
|
|
|
|
// Records the classified mismatch reason as a labeled OTel counter so
|
|
// fork diagnosis is a queryable time series, not just a log grep.
|
|
auto recordReason = [this](std::string_view reason) {
|
|
if (auto* mr = app_.getMetricsRegistry())
|
|
mr->incrementLedgerHistoryMismatch(reason);
|
|
};
|
|
|
|
if (!builtLedger || !validLedger)
|
|
{
|
|
JLOG(j_.error()) << "MISMATCH cannot be analyzed:"
|
|
<< " builtLedger: " << to_string(built) << " -> " << builtLedger
|
|
<< " validLedger: " << to_string(valid) << " -> " << validLedger;
|
|
recordReason("unknown");
|
|
return;
|
|
}
|
|
|
|
XRPL_ASSERT(
|
|
builtLedger->header().seq == validLedger->header().seq,
|
|
"xrpl::LedgerHistory::handleMismatch : sequence match");
|
|
|
|
if (auto stream = j_.debug())
|
|
{
|
|
stream << "Built: " << getJson({*builtLedger, {}});
|
|
stream << "Valid: " << getJson({*validLedger, {}});
|
|
stream << "Consensus: " << consensus;
|
|
}
|
|
|
|
// Determine the mismatch reason, distinguishing Byzantine
|
|
// failure from transaction processing difference
|
|
|
|
// Disagreement over prior ledger indicates sync issue
|
|
if (builtLedger->header().parentHash != validLedger->header().parentHash)
|
|
{
|
|
JLOG(j_.error()) << "MISMATCH on prior ledger";
|
|
recordReason("prior_ledger");
|
|
return;
|
|
}
|
|
|
|
// Disagreement over close time indicates Byzantine failure
|
|
if (builtLedger->header().closeTime != validLedger->header().closeTime)
|
|
{
|
|
JLOG(j_.error()) << "MISMATCH on close time";
|
|
recordReason("close_time");
|
|
return;
|
|
}
|
|
|
|
// Tracks whether the mismatch reason has already been recorded. The
|
|
// consensus tx-set hash disagreement is the root cause, so it is counted
|
|
// once here; the tx-level comparison below still logs its diagnostics but
|
|
// must not record a second reason for the same mismatch event.
|
|
bool reasonRecorded = false;
|
|
|
|
if (builtConsensusHash && validatedConsensusHash)
|
|
{
|
|
if (builtConsensusHash != validatedConsensusHash)
|
|
{
|
|
JLOG(j_.error()) << "MISMATCH on consensus transaction set "
|
|
<< " built: " << to_string(*builtConsensusHash)
|
|
<< " validated: " << to_string(*validatedConsensusHash);
|
|
recordReason("consensus_txset");
|
|
reasonRecorded = true;
|
|
}
|
|
else
|
|
{
|
|
JLOG(j_.error()) << "MISMATCH with same consensus transaction set: "
|
|
<< to_string(*builtConsensusHash);
|
|
}
|
|
}
|
|
|
|
// Find differences between built and valid ledgers
|
|
auto const builtTx = leaves(builtLedger->txMap());
|
|
auto const validTx = leaves(validLedger->txMap());
|
|
|
|
if (builtTx == validTx)
|
|
{
|
|
JLOG(j_.error()) << "MISMATCH with same " << builtTx.size() << " transactions";
|
|
if (!reasonRecorded)
|
|
recordReason("same_txset_diff_result");
|
|
}
|
|
else
|
|
{
|
|
JLOG(j_.error()) << "MISMATCH with " << builtTx.size() << " built and " << validTx.size()
|
|
<< " valid transactions.";
|
|
if (!reasonRecorded)
|
|
recordReason("different_txset");
|
|
}
|
|
|
|
JLOG(j_.error()) << "built\n" << getJson({*builtLedger, {}});
|
|
JLOG(j_.error()) << "valid\n" << getJson({*validLedger, {}});
|
|
|
|
// Log all differences between built and valid ledgers
|
|
auto b = builtTx.begin();
|
|
auto v = validTx.begin();
|
|
while (b != builtTx.end() && v != validTx.end())
|
|
{
|
|
if ((*b)->key() < (*v)->key())
|
|
{
|
|
logOne(*builtLedger, (*b)->key(), "valid", j_);
|
|
++b;
|
|
}
|
|
else if ((*b)->key() > (*v)->key())
|
|
{
|
|
logOne(*validLedger, (*v)->key(), "built", j_);
|
|
++v;
|
|
}
|
|
else
|
|
{
|
|
if ((*b)->slice() != (*v)->slice())
|
|
{
|
|
// Same transaction with different metadata
|
|
logMetadataDifference(*builtLedger, *validLedger, (*b)->key(), j_);
|
|
}
|
|
++b;
|
|
++v;
|
|
}
|
|
}
|
|
for (; b != builtTx.end(); ++b)
|
|
logOne(*builtLedger, (*b)->key(), "valid", j_);
|
|
for (; v != validTx.end(); ++v)
|
|
logOne(*validLedger, (*v)->key(), "built", j_);
|
|
}
|
|
|
|
void
|
|
LedgerHistory::builtLedger(
|
|
std::shared_ptr<Ledger const> const& ledger,
|
|
uint256 const& consensusHash,
|
|
json::Value consensus)
|
|
{
|
|
LedgerIndex const index = ledger->header().seq;
|
|
LedgerHash const hash = ledger->header().hash;
|
|
XRPL_ASSERT(!hash.isZero(), "xrpl::LedgerHistory::builtLedger : nonzero hash");
|
|
|
|
std::unique_lock const sl(consensusValidated_.peekMutex());
|
|
|
|
auto entry = std::make_shared<CvEntry>();
|
|
consensusValidated_.canonicalizeReplaceClient(index, entry);
|
|
|
|
if (entry->validated && !entry->built)
|
|
{
|
|
if (entry->validated.value() != hash)
|
|
{
|
|
JLOG(j_.error()) << "MISMATCH: seq=" << index
|
|
<< " validated:" << entry->validated.value() << " then:" << hash;
|
|
handleMismatch(
|
|
hash,
|
|
entry->validated.value(),
|
|
consensusHash,
|
|
entry->validatedConsensusHash,
|
|
consensus);
|
|
}
|
|
else
|
|
{
|
|
// We validated a ledger and then built it locally
|
|
JLOG(j_.debug()) << "MATCH: seq=" << index << " late";
|
|
}
|
|
}
|
|
|
|
entry->built.emplace(hash);
|
|
entry->builtConsensusHash.emplace(consensusHash);
|
|
entry->consensus.emplace(std::move(consensus));
|
|
}
|
|
|
|
void
|
|
LedgerHistory::validatedLedger(
|
|
std::shared_ptr<Ledger const> const& ledger,
|
|
std::optional<uint256> const& consensusHash)
|
|
{
|
|
LedgerIndex const index = ledger->header().seq;
|
|
LedgerHash const hash = ledger->header().hash;
|
|
XRPL_ASSERT(!hash.isZero(), "xrpl::LedgerHistory::validatedLedger : nonzero hash");
|
|
|
|
std::unique_lock const sl(consensusValidated_.peekMutex());
|
|
|
|
auto entry = std::make_shared<CvEntry>();
|
|
consensusValidated_.canonicalizeReplaceClient(index, entry);
|
|
|
|
if (entry->built && !entry->validated)
|
|
{
|
|
if (entry->built.value() != hash)
|
|
{
|
|
JLOG(j_.error()) << "MISMATCH: seq=" << index << " built:" << entry->built.value()
|
|
<< " then:" << hash;
|
|
handleMismatch(
|
|
entry->built.value(),
|
|
hash,
|
|
entry->builtConsensusHash,
|
|
consensusHash,
|
|
entry->consensus.value()); // NOLINT(bugprone-unchecked-optional-access) consensus
|
|
// always emplaced with built
|
|
}
|
|
else
|
|
{
|
|
// We built a ledger locally and then validated it
|
|
JLOG(j_.debug()) << "MATCH: seq=" << index;
|
|
}
|
|
}
|
|
|
|
entry->validated.emplace(hash);
|
|
entry->validatedConsensusHash = consensusHash;
|
|
}
|
|
|
|
/**
|
|
* Ensure ledgers_by_hash_ doesn't have the wrong hash for a particular index
|
|
*/
|
|
bool
|
|
LedgerHistory::fixIndex(LedgerIndex ledgerIndex, LedgerHash const& ledgerHash)
|
|
{
|
|
std::unique_lock const sl(ledgersByHash_.peekMutex());
|
|
auto it = ledgersByIndex_.find(ledgerIndex);
|
|
|
|
if ((it != ledgersByIndex_.end()) && (it->second != ledgerHash))
|
|
{
|
|
it->second = ledgerHash;
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
void
|
|
LedgerHistory::clearLedgerCachePrior(LedgerIndex seq)
|
|
{
|
|
for (LedgerHash const it : ledgersByHash_.getKeys())
|
|
{
|
|
auto const ledger = getLedgerByHash(it);
|
|
if (!ledger || ledger->header().seq < seq)
|
|
ledgersByHash_.del(it, false);
|
|
}
|
|
}
|
|
|
|
} // namespace xrpl
|