mirror of
https://github.com/XRPLF/rippled.git
synced 2026-07-28 17:40:25 +00:00
606 lines
19 KiB
C++
606 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/json_writer.h>
|
|
#include <xrpl/ledger/Ledger.h>
|
|
#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/SHAMapItem.h>
|
|
|
|
#include <algorithm>
|
|
#include <cstddef>
|
|
#include <memory>
|
|
#include <optional>
|
|
#include <utility>
|
|
#include <vector>
|
|
|
|
namespace xrpl {
|
|
|
|
LedgerHistory::LedgerHistory(beast::insight::Collector::ptr const& collector, Application& app)
|
|
: app_(app)
|
|
, collector_(collector)
|
|
, mismatchCounter_(collector->makeCounter("ledger.history", "mismatch"))
|
|
, consensusValidated_(
|
|
"ConsensusValidated",
|
|
64,
|
|
std::chrono::minutes{5},
|
|
stopwatch(),
|
|
app_.getJournal("TaggedCache"))
|
|
, j_(app.getJournal("LedgerHistory"))
|
|
{
|
|
auto lock = ledgerMaps_.lock();
|
|
lock->byHash = std::make_unique<LedgerMaps::LedgersByHash>(
|
|
"LedgerCache",
|
|
app_.config().getValueFor(SizedItem::LedgerSize),
|
|
std::chrono::seconds{app_.config().getValueFor(SizedItem::LedgerAge)},
|
|
stopwatch(),
|
|
app_.getJournal("TaggedCache"));
|
|
}
|
|
|
|
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");
|
|
|
|
auto lock = ledgerMaps_.lock();
|
|
bool const alreadyHad = lock->byHash->canonicalizeReplaceCache(ledger->header().hash, ledger);
|
|
if (validated)
|
|
lock->byIndex[ledger->header().seq] = ledger->header().hash;
|
|
|
|
return alreadyHad;
|
|
}
|
|
|
|
LedgerHash
|
|
LedgerHistory::getLedgerHash(LedgerIndex index)
|
|
{
|
|
auto lock = ledgerMaps_.lock();
|
|
if (auto it = lock->byIndex.find(index); it != lock->byIndex.end())
|
|
return it->second;
|
|
return {};
|
|
}
|
|
|
|
std::shared_ptr<Ledger const>
|
|
LedgerHistory::getLedgerBySeq(LedgerIndex index)
|
|
{
|
|
uint256 hash;
|
|
{
|
|
auto lock = ledgerMaps_.lock();
|
|
if (auto it = lock->byIndex.find(index); it != lock->byIndex.end())
|
|
{
|
|
hash = it->second;
|
|
}
|
|
else
|
|
{
|
|
hash = {};
|
|
}
|
|
}
|
|
|
|
if (!hash.isZero())
|
|
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
|
|
auto lock = ledgerMaps_.lock();
|
|
XRPL_ASSERT(
|
|
ret->isImmutable(), "xrpl::LedgerHistory::getLedgerBySeq : immutable result ledger");
|
|
lock->byHash->canonicalizeReplaceClient(ret->header().hash, ret);
|
|
lock->byIndex[ret->header().seq] = ret->header().hash;
|
|
return (ret->header().seq == index) ? ret : nullptr;
|
|
}
|
|
}
|
|
|
|
std::shared_ptr<Ledger const>
|
|
LedgerHistory::getLedgerByHash(LedgerHash const& hash)
|
|
{
|
|
std::shared_ptr<Ledger const> ret;
|
|
{
|
|
auto lock = ledgerMaps_.lock();
|
|
ret = lock->byHash->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");
|
|
{
|
|
auto lock = ledgerMaps_.lock();
|
|
lock->byHash->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::logMetadataDifference : 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 // nodesDiff
|
|
{
|
|
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);
|
|
|
|
if (!builtLedger || !validLedger)
|
|
{
|
|
JLOG(j_.error()) << "MISMATCH cannot be analyzed:"
|
|
<< " builtLedger: " << to_string(built) << " -> " << builtLedger
|
|
<< " validLedger: " << to_string(valid) << " -> " << validLedger;
|
|
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";
|
|
return;
|
|
}
|
|
|
|
// Disagreement over close time indicates Byzantine failure
|
|
if (builtLedger->header().closeTime != validLedger->header().closeTime)
|
|
{
|
|
JLOG(j_.error()) << "MISMATCH on close time";
|
|
return;
|
|
}
|
|
|
|
if (builtConsensusHash && validatedConsensusHash)
|
|
{
|
|
if (builtConsensusHash != validatedConsensusHash)
|
|
{
|
|
JLOG(j_.error()) << "MISMATCH on consensus transaction set "
|
|
<< " built: " << to_string(*builtConsensusHash)
|
|
<< " validated: " << to_string(*validatedConsensusHash);
|
|
}
|
|
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";
|
|
}
|
|
else
|
|
JLOG(j_.error()) << "MISMATCH with " << builtTx.size() << " built and " << validTx.size()
|
|
<< " valid transactions.";
|
|
|
|
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 index = ledger->header().seq;
|
|
LedgerHash hash = ledger->header().hash;
|
|
XRPL_ASSERT(!hash.isZero(), "xrpl::LedgerHistory::builtLedger : nonzero hash");
|
|
|
|
struct MismatchInputs
|
|
{
|
|
LedgerHash valid;
|
|
std::optional<uint256> validatedConsensusHash;
|
|
json::Value consensus;
|
|
};
|
|
std::optional<MismatchInputs> mismatch;
|
|
|
|
consensusValidated_.fetchAndModify(index, [&](CvEntry& entry) {
|
|
if (entry.validated && !entry.built)
|
|
{
|
|
if (entry.validated.value() != hash)
|
|
{
|
|
JLOG(j_.error()) << "MISMATCH: seq=" << index
|
|
<< " validated:" << entry.validated.value() << " then:" << hash;
|
|
mismatch = MismatchInputs{
|
|
.valid = entry.validated.value(),
|
|
.validatedConsensusHash = entry.validatedConsensusHash,
|
|
.consensus = 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));
|
|
});
|
|
|
|
if (mismatch)
|
|
{
|
|
handleMismatch(
|
|
hash,
|
|
mismatch->valid,
|
|
consensusHash,
|
|
mismatch->validatedConsensusHash,
|
|
mismatch->consensus);
|
|
}
|
|
}
|
|
|
|
void
|
|
LedgerHistory::validatedLedger(
|
|
std::shared_ptr<Ledger const> const& ledger,
|
|
std::optional<uint256> const& consensusHash)
|
|
{
|
|
LedgerIndex index = ledger->header().seq;
|
|
LedgerHash hash = ledger->header().hash;
|
|
XRPL_ASSERT(!hash.isZero(), "xrpl::LedgerHistory::validatedLedger : nonzero hash");
|
|
|
|
struct MismatchInputs
|
|
{
|
|
LedgerHash built;
|
|
std::optional<uint256> builtConsensusHash;
|
|
json::Value consensus;
|
|
};
|
|
std::optional<MismatchInputs> mismatch;
|
|
|
|
consensusValidated_.fetchAndModify(index, [&](CvEntry& entry) {
|
|
if (entry.built && !entry.validated)
|
|
{
|
|
if (entry.built.value() != hash)
|
|
{
|
|
JLOG(j_.error()) << "MISMATCH: seq=" << index << " built:" << entry.built.value()
|
|
<< " then:" << hash;
|
|
mismatch = MismatchInputs{
|
|
.built = entry.built.value(),
|
|
.builtConsensusHash = entry.builtConsensusHash,
|
|
.consensus =
|
|
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;
|
|
});
|
|
|
|
if (mismatch)
|
|
{
|
|
handleMismatch(
|
|
mismatch->built,
|
|
hash,
|
|
mismatch->builtConsensusHash,
|
|
consensusHash,
|
|
mismatch->consensus);
|
|
}
|
|
}
|
|
|
|
/** Ensure byHash doesn't have the wrong hash for a particular index
|
|
*/
|
|
bool
|
|
LedgerHistory::fixIndex(LedgerIndex ledgerIndex, LedgerHash const& ledgerHash)
|
|
{
|
|
auto lock = ledgerMaps_.lock();
|
|
if (auto it = lock->byIndex.find(ledgerIndex); it != lock->byIndex.end())
|
|
{
|
|
if (it->second != ledgerHash)
|
|
{
|
|
lock->byIndex[ledgerIndex] = ledgerHash;
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
void
|
|
LedgerHistory::clearLedgerCachePrior(LedgerIndex seq)
|
|
{
|
|
std::size_t hashesCleared = 0;
|
|
std::size_t indexesCleared = 0;
|
|
std::size_t cacheSize = 0;
|
|
std::size_t indexSize = 0;
|
|
|
|
std::vector<LedgerHash> keys;
|
|
{
|
|
auto lock = ledgerMaps_.lock();
|
|
keys = lock->byHash->getKeys();
|
|
}
|
|
|
|
for (LedgerHash const& it : keys)
|
|
{
|
|
auto const ledger = getLedgerByHash(it);
|
|
if (!ledger || ledger->header().seq < seq)
|
|
{
|
|
auto lock = ledgerMaps_.lock();
|
|
lock->byHash->del(it, false);
|
|
++hashesCleared;
|
|
}
|
|
}
|
|
|
|
{
|
|
auto lock = ledgerMaps_.lock();
|
|
cacheSize = lock->byHash->size();
|
|
|
|
auto it = lock->byIndex.begin();
|
|
while (it != lock->byIndex.end())
|
|
{
|
|
if (it->first < seq)
|
|
{
|
|
it = lock->byIndex.erase(it);
|
|
++indexesCleared;
|
|
}
|
|
else
|
|
{
|
|
++it;
|
|
}
|
|
}
|
|
indexSize = lock->byIndex.size();
|
|
}
|
|
|
|
JLOG(j_.debug()) << "LedgersByHash: cleared " << hashesCleared << " entries before seq " << seq
|
|
<< " (total now " << cacheSize << ")";
|
|
|
|
JLOG(j_.debug()) << "LedgersByIndex: cleared " << indexesCleared << " index entries before seq "
|
|
<< seq << " (total now " << indexSize << ")";
|
|
}
|
|
|
|
} // namespace xrpl
|