Files
rippled/src/libxrpl/tx/invariants/InvariantCheck.cpp
2026-05-29 09:44:43 -04:00

1193 lines
36 KiB
C++

#include <xrpl/tx/invariants/InvariantCheck.h>
#include <xrpl/basics/Log.h>
#include <xrpl/basics/base_uint.h>
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/beast/utility/Zero.h>
#include <xrpl/beast/utility/instrumentation.h>
#include <xrpl/ledger/ReadView.h>
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
#include <xrpl/ledger/helpers/TokenHelpers.h>
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/Issue.h>
#include <xrpl/protocol/Keylet.h>
#include <xrpl/protocol/LedgerFormats.h>
#include <xrpl/protocol/MPTIssue.h>
#include <xrpl/protocol/Protocol.h>
#include <xrpl/protocol/Rules.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/SOTemplate.h>
#include <xrpl/protocol/STAmount.h>
#include <xrpl/protocol/STLedgerEntry.h>
#include <xrpl/protocol/STNumber.h> // IWYU pragma: keep
#include <xrpl/protocol/STTx.h>
#include <xrpl/protocol/SystemParameters.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol/TxFormats.h>
#include <xrpl/protocol/UintTypes.h>
#include <xrpl/protocol/XRPAmount.h>
#include <xrpl/tx/invariants/InvariantCheckPrivilege.h>
#include <algorithm>
#include <cstdint>
#include <functional>
#include <memory>
#include <optional>
#include <sstream>
#include <vector>
namespace xrpl {
#pragma push_macro("TRANSACTION")
#undef TRANSACTION
#define TRANSACTION(tag, value, name, delegable, amendment, privileges, ...) \
case tag: { \
return (privileges) & priv; \
}
bool
hasPrivilege(STTx const& tx, Privilege priv)
{
switch (tx.getTxnType())
{
#include <xrpl/protocol/detail/transactions.macro>
// Deprecated types
default:
return false;
}
};
#undef TRANSACTION
#pragma pop_macro("TRANSACTION")
void
TransactionFeeCheck::visitEntry(
bool,
std::shared_ptr<SLE const> const&,
std::shared_ptr<SLE const> const&)
{
// nothing to do
}
bool
TransactionFeeCheck::finalize(
STTx const& tx,
TER const,
XRPAmount const fee,
ReadView const&,
beast::Journal const& j)
{
// We should never charge a negative fee
if (fee.drops() < 0)
{
JLOG(j.fatal()) << "Invariant failed: fee paid was negative: " << fee.drops();
return false;
}
// We should never charge a fee that's greater than or equal to the
// entire XRP supply.
if (fee >= kInitialXrp)
{
JLOG(j.fatal()) << "Invariant failed: fee paid exceeds system limit: " << fee.drops();
return false;
}
// We should never charge more for a transaction than the transaction
// authorizes. It's possible to charge less in some circumstances.
if (fee > tx.getFieldAmount(sfFee).xrp())
{
JLOG(j.fatal()) << "Invariant failed: fee paid is " << fee.drops()
<< " exceeds fee specified in transaction.";
return false;
}
return true;
}
//------------------------------------------------------------------------------
void
XRPNotCreated::visitEntry(
bool isDelete,
std::shared_ptr<SLE const> const& before,
std::shared_ptr<SLE const> const& after)
{
/* We go through all modified ledger entries, looking only at account roots,
* escrow payments, and payment channels. We remove from the total any
* previous XRP values and add to the total any new XRP values. The net
* balance of a payment channel is computed from two fields (amount and
* balance) and deletions are ignored for paychan and escrow because the
* amount fields have not been adjusted for those in the case of deletion.
*/
if (before)
{
switch (before->getType())
{
case ltACCOUNT_ROOT:
drops_ -= (*before)[sfBalance].xrp().drops();
break;
case ltPAYCHAN:
drops_ -= ((*before)[sfAmount] - (*before)[sfBalance]).xrp().drops();
break;
case ltESCROW:
if (isXRP((*before)[sfAmount]))
drops_ -= (*before)[sfAmount].xrp().drops();
break;
default:
break;
}
}
if (after)
{
switch (after->getType())
{
case ltACCOUNT_ROOT:
drops_ += (*after)[sfBalance].xrp().drops();
break;
case ltPAYCHAN:
if (!isDelete)
drops_ += ((*after)[sfAmount] - (*after)[sfBalance]).xrp().drops();
break;
case ltESCROW:
if (!isDelete && isXRP((*after)[sfAmount]))
drops_ += (*after)[sfAmount].xrp().drops();
break;
default:
break;
}
}
}
bool
XRPNotCreated::finalize(
STTx const& tx,
TER const,
XRPAmount const fee,
ReadView const&,
beast::Journal const& j) const
{
// The net change should never be positive, as this would mean that the
// transaction created XRP out of thin air. That's not possible.
if (drops_ > 0)
{
JLOG(j.fatal()) << "Invariant failed: XRP net change was positive: " << drops_;
return false;
}
// The negative of the net change should be equal to actual fee charged.
if (-drops_ != fee.drops())
{
JLOG(j.fatal()) << "Invariant failed: XRP net change of " << drops_ << " doesn't match fee "
<< fee.drops();
return false;
}
return true;
}
//------------------------------------------------------------------------------
void
XRPBalanceChecks::visitEntry(
bool,
std::shared_ptr<SLE const> const& before,
std::shared_ptr<SLE const> const& after)
{
auto isBad = [](STAmount const& balance) {
if (!balance.native())
return true;
auto const drops = balance.xrp();
// Can't have more than the number of drops instantiated
// in the genesis ledger.
if (drops > kInitialXrp)
return true;
// Can't have a negative balance (0 is OK)
if (drops < XRPAmount{0})
return true;
return false;
};
if (before && before->getType() == ltACCOUNT_ROOT)
bad_ |= isBad((*before)[sfBalance]);
if (after && after->getType() == ltACCOUNT_ROOT)
bad_ |= isBad((*after)[sfBalance]);
}
bool
XRPBalanceChecks::finalize(
STTx const&,
TER const,
XRPAmount const,
ReadView const&,
beast::Journal const& j) const
{
if (bad_)
{
JLOG(j.fatal()) << "Invariant failed: incorrect account XRP balance";
return false;
}
return true;
}
//------------------------------------------------------------------------------
void
NoBadOffers::visitEntry(
bool isDelete,
std::shared_ptr<SLE const> const& before,
std::shared_ptr<SLE const> const& after)
{
auto isBad = [](STAmount const& pays, STAmount const& gets) {
// An offer should never be negative
if (pays < beast::kZero)
return true;
if (gets < beast::kZero)
return true;
// Can't have an XRP to XRP offer:
return pays.native() && gets.native();
};
if (before && before->getType() == ltOFFER)
bad_ |= isBad((*before)[sfTakerPays], (*before)[sfTakerGets]);
if (after && after->getType() == ltOFFER)
bad_ |= isBad((*after)[sfTakerPays], (*after)[sfTakerGets]);
}
bool
NoBadOffers::finalize(
STTx const&,
TER const,
XRPAmount const,
ReadView const&,
beast::Journal const& j) const
{
if (bad_)
{
JLOG(j.fatal()) << "Invariant failed: offer with a bad amount";
return false;
}
return true;
}
//------------------------------------------------------------------------------
void
NoZeroEscrow::visitEntry(
bool isDelete,
std::shared_ptr<SLE const> const& before,
std::shared_ptr<SLE const> const& after)
{
auto isBad = [](STAmount const& amount) {
// XRP case
if (amount.native())
{
if (amount.xrp() <= XRPAmount{0})
return true;
if (amount.xrp() >= kInitialXrp)
return true;
}
else
{
return amount.asset().visit(
[&](Issue const& issue) {
// IOU case
if (amount <= beast::kZero)
return true;
if (badCurrency() == issue.currency)
return true;
return false;
}
// MPT case
,
[&](MPTIssue const&) {
if (amount <= beast::kZero)
return true;
if (amount.mpt() > MPTAmount{kMaxMpTokenAmount})
return true; // LCOV_EXCL_LINE
return false;
});
}
return false;
};
if (before && before->getType() == ltESCROW)
bad_ |= isBad((*before)[sfAmount]);
if (after && after->getType() == ltESCROW)
bad_ |= isBad((*after)[sfAmount]);
auto checkAmount = [this](std::int64_t amount) {
if (amount > kMaxMpTokenAmount || amount < 0)
bad_ |= true;
};
bool const overwriteFixEnabled = isFeatureEnabled(fixCleanup3_1_3, true);
if (after && after->getType() == ltMPTOKEN_ISSUANCE)
{
auto const outstanding = (*after)[sfOutstandingAmount];
checkAmount(outstanding);
if (auto const locked = (*after)[~sfLockedAmount])
{
checkAmount(*locked);
bool const isBad = outstanding < *locked;
if (overwriteFixEnabled)
{
bad_ |= isBad;
}
else
{
bad_ = isBad;
}
}
}
if (after && after->getType() == ltMPTOKEN)
{
auto const mptAmount = (*after)[sfMPTAmount];
checkAmount(mptAmount);
if (auto const locked = (*after)[~sfLockedAmount])
{
checkAmount(*locked);
}
}
}
bool
NoZeroEscrow::finalize(
STTx const& txn,
TER const,
XRPAmount const,
ReadView const&,
beast::Journal const& j) const
{
if (bad_)
{
JLOG(j.fatal()) << "Invariant failed: escrow specifies invalid amount";
return false;
}
return true;
}
//------------------------------------------------------------------------------
void
AccountRootsNotDeleted::visitEntry(
bool isDelete,
std::shared_ptr<SLE const> const& before,
std::shared_ptr<SLE const> const&)
{
if (isDelete && before && before->getType() == ltACCOUNT_ROOT)
accountsDeleted_++;
}
bool
AccountRootsNotDeleted::finalize(
STTx const& tx,
TER const result,
XRPAmount const,
ReadView const&,
beast::Journal const& j) const
{
// AMM account root can be deleted as the result of AMM withdraw/delete
// transaction when the total AMM LP Tokens balance goes to 0.
// A successful AccountDelete or AMMDelete MUST delete exactly
// one account root.
if (hasPrivilege(tx, MustDeleteAcct) && isTesSuccess(result))
{
if (accountsDeleted_ == 1)
return true;
if (accountsDeleted_ == 0)
{
JLOG(j.fatal()) << "Invariant failed: account deletion "
"succeeded without deleting an account";
}
else
JLOG(j.fatal()) << "Invariant failed: account deletion "
"succeeded but deleted multiple accounts!";
return false;
}
// A successful AMMWithdraw/AMMClawback MAY delete one account root
// when the total AMM LP Tokens balance goes to 0. Not every AMM withdraw
// deletes the AMM account, accountsDeleted_ is set if it is deleted.
if (hasPrivilege(tx, MayDeleteAcct) && isTesSuccess(result) && accountsDeleted_ == 1)
return true;
if (accountsDeleted_ == 0)
return true;
JLOG(j.fatal()) << "Invariant failed: an account root was deleted";
return false;
}
//------------------------------------------------------------------------------
void
AccountRootsDeletedClean::visitEntry(
bool isDelete,
std::shared_ptr<SLE const> const& before,
std::shared_ptr<SLE const> const& after)
{
if (isDelete && before && before->getType() == ltACCOUNT_ROOT)
accountsDeleted_.emplace_back(before, after);
}
bool
AccountRootsDeletedClean::finalize(
STTx const& tx,
TER const result,
XRPAmount const,
ReadView const& view,
beast::Journal const& j)
{
// Always check for objects in the ledger, but to prevent differing
// transaction processing results, however unlikely, only fail if the
// feature is enabled. Enabled, or not, though, a fatal-level message will
// be logged
[[maybe_unused]] bool const enforce = view.rules().enabled(fixCleanup3_2_0) ||
view.rules().enabled(featureSingleAssetVault) ||
view.rules().enabled(featureLendingProtocol);
auto const objectExists = [&view, enforce, &j](auto const& keylet) {
(void)enforce;
if (auto const sle = view.read(keylet))
{
// Finding the object is bad
auto const typeName = [&sle]() {
auto item = LedgerFormats::getInstance().findByType(sle->getType());
if (item != nullptr)
return item->getName();
return std::to_string(sle->getType());
}();
JLOG(j.fatal()) << "Invariant failed: account deletion left behind a " << typeName
<< " object";
// The comment above starting with "assert(enforce)" explains this
// assert.
XRPL_ASSERT(
enforce,
"xrpl::AccountRootsDeletedClean::finalize::objectExists : "
"account deletion left no objects behind");
return true;
}
return false;
};
for (auto const& [before, after] : accountsDeleted_)
{
auto const accountID = before->getAccountID(sfAccount);
// An account should not be deleted with a balance
if (after->at(sfBalance) != beast::kZero)
{
JLOG(j.fatal()) << "Invariant failed: account deletion left "
"behind a non-zero balance";
XRPL_ASSERT(
enforce,
"xrpl::AccountRootsDeletedClean::finalize : "
"deleted account has zero balance");
if (enforce)
return false;
}
// An account should not be deleted with a non-zero owner count
if (after->at(sfOwnerCount) != 0)
{
JLOG(j.fatal()) << "Invariant failed: account deletion left "
"behind a non-zero owner count";
XRPL_ASSERT(
enforce,
"xrpl::AccountRootsDeletedClean::finalize : "
"deleted account has zero owner count");
if (enforce)
return false;
}
// Simple types
for (auto const& [keyletfunc, _1, _2] : kDirectAccountKeylets)
{
// TODO: use '_' for both unused variables above once we are in C++26
if (objectExists(std::invoke(keyletfunc, accountID)) && enforce)
return false;
}
{
// NFT pages. nftpage_min and nftpage_max were already explicitly
// checked above as entries in directAccountKeylets. This uses
// view.succ() to check for any NFT pages in between the two
// endpoints.
Keylet const first = keylet::nftpageMin(accountID);
Keylet const last = keylet::nftpageMax(accountID);
std::optional<uint256> key = view.succ(first.key, last.key.next());
// current page
if (key && objectExists(Keylet{ltNFTOKEN_PAGE, *key}) && enforce)
return false;
}
// If the account is a pseudo account, then the linked object must
// also be deleted. e.g. AMM, Vault, etc.
for (auto const& field : getPseudoAccountFields())
{
if (before->isFieldPresent(*field))
{
auto const key = before->getFieldH256(*field);
if (objectExists(keylet::unchecked(key)) && enforce)
return false;
}
}
}
return true;
}
//------------------------------------------------------------------------------
void
LedgerEntryTypesMatch::visitEntry(
bool,
std::shared_ptr<SLE const> const& before,
std::shared_ptr<SLE const> const& after)
{
if (before && after && before->getType() != after->getType())
typeMismatch_ = true;
if (after)
{
#pragma push_macro("LEDGER_ENTRY")
#undef LEDGER_ENTRY
#define LEDGER_ENTRY(tag, ...) case tag:
switch (after->getType())
{
#include <xrpl/protocol/detail/ledger_entries.macro>
break;
default:
invalidTypeAdded_ = true;
break;
}
#undef LEDGER_ENTRY
#pragma pop_macro("LEDGER_ENTRY")
}
}
bool
LedgerEntryTypesMatch::finalize(
STTx const&,
TER const,
XRPAmount const,
ReadView const&,
beast::Journal const& j) const
{
if ((!typeMismatch_) && (!invalidTypeAdded_))
return true;
if (typeMismatch_)
{
JLOG(j.fatal()) << "Invariant failed: ledger entry type mismatch";
}
if (invalidTypeAdded_)
{
JLOG(j.fatal()) << "Invariant failed: invalid ledger entry type added";
}
return false;
}
//------------------------------------------------------------------------------
void
NoXRPTrustLines::visitEntry(
bool,
std::shared_ptr<SLE const> const&,
std::shared_ptr<SLE const> const& after)
{
bool const overwriteFixEnabled = isFeatureEnabled(fixCleanup3_1_3, true);
if (after && after->getType() == ltRIPPLE_STATE)
{
// checking the issue directly here instead of
// relying on .native() just in case native somehow
// were systematically incorrect
bool const isXrp = after->getFieldAmount(sfLowLimit).asset() == xrpIssue() ||
after->getFieldAmount(sfHighLimit).asset() == xrpIssue();
if (overwriteFixEnabled)
{
xrpTrustLine_ |= isXrp;
}
else
{
xrpTrustLine_ = isXrp;
}
}
}
bool
NoXRPTrustLines::finalize(
STTx const&,
TER const,
XRPAmount const,
ReadView const&,
beast::Journal const& j) const
{
if (!xrpTrustLine_)
return true;
JLOG(j.fatal()) << "Invariant failed: an XRP trust line was created";
return false;
}
//------------------------------------------------------------------------------
void
NoDeepFreezeTrustLinesWithoutFreeze::visitEntry(
bool,
std::shared_ptr<SLE const> const&,
std::shared_ptr<SLE const> const& after)
{
if (after && after->getType() == ltRIPPLE_STATE)
{
bool const overwriteFixEnabled = isFeatureEnabled(fixCleanup3_1_3, true);
bool const lowFreeze = after->isFlag(lsfLowFreeze);
bool const lowDeepFreeze = after->isFlag(lsfLowDeepFreeze);
bool const highFreeze = after->isFlag(lsfHighFreeze);
bool const highDeepFreeze = after->isFlag(lsfHighDeepFreeze);
bool const bad = (lowDeepFreeze && !lowFreeze) || (highDeepFreeze && !highFreeze);
if (overwriteFixEnabled)
{
deepFreezeWithoutFreeze_ |= bad;
}
else
{
deepFreezeWithoutFreeze_ = bad;
}
}
}
bool
NoDeepFreezeTrustLinesWithoutFreeze::finalize(
STTx const&,
TER const,
XRPAmount const,
ReadView const&,
beast::Journal const& j) const
{
if (!deepFreezeWithoutFreeze_)
return true;
JLOG(j.fatal()) << "Invariant failed: a trust line with deep freeze flag "
"without normal freeze was created";
return false;
}
//------------------------------------------------------------------------------
void
ValidNewAccountRoot::visitEntry(
bool,
std::shared_ptr<SLE const> const& before,
std::shared_ptr<SLE const> const& after)
{
if (!before && after->getType() == ltACCOUNT_ROOT)
{
accountsCreated_++;
accountSeq_ = (*after)[sfSequence];
pseudoAccount_ = isPseudoAccount(after);
flags_ = after->getFlags();
}
}
bool
ValidNewAccountRoot::finalize(
STTx const& tx,
TER const result,
XRPAmount const,
ReadView const& view,
beast::Journal const& j) const
{
if (accountsCreated_ == 0)
return true;
if (accountsCreated_ > 1)
{
JLOG(j.fatal()) << "Invariant failed: multiple accounts "
"created in a single transaction";
return false;
}
// From this point on we know exactly one account was created.
if (hasPrivilege(tx, CreateAcct | CreatePseudoAcct) && isTesSuccess(result))
{
bool const pseudoAccount =
(pseudoAccount_ &&
(view.rules().enabled(featureSingleAssetVault) ||
view.rules().enabled(featureLendingProtocol)));
if (pseudoAccount && !hasPrivilege(tx, CreatePseudoAcct))
{
JLOG(j.fatal()) << "Invariant failed: pseudo-account created by a "
"wrong transaction type";
return false;
}
std::uint32_t const startingSeq = pseudoAccount ? 0 : view.seq();
if (accountSeq_ != startingSeq)
{
JLOG(j.fatal()) << "Invariant failed: account created with "
"wrong starting sequence number";
return false;
}
if (pseudoAccount)
{
std::uint32_t const expected = (lsfDisableMaster | lsfDefaultRipple | lsfDepositAuth);
if (flags_ != expected)
{
JLOG(j.fatal()) << "Invariant failed: pseudo-account created with "
"wrong flags";
return false;
}
}
return true;
}
JLOG(j.fatal()) << "Invariant failed: account root created illegally";
return false;
} // namespace xrpl
//------------------------------------------------------------------------------
void
ValidClawback::visitEntry(
bool,
std::shared_ptr<SLE const> const& before,
std::shared_ptr<SLE const> const&)
{
if (before && before->getType() == ltRIPPLE_STATE)
trustlinesChanged_++;
if (before && before->getType() == ltMPTOKEN)
mptokensChanged_++;
}
bool
ValidClawback::finalize(
STTx const& tx,
TER const result,
XRPAmount const,
ReadView const& view,
beast::Journal const& j) const
{
if (tx.getTxnType() != ttCLAWBACK)
return true;
if (isTesSuccess(result))
{
if (trustlinesChanged_ > 1)
{
JLOG(j.fatal()) << "Invariant failed: more than one trustline changed.";
return false;
}
if (mptokensChanged_ > 1)
{
JLOG(j.fatal()) << "Invariant failed: more than one mptokens changed.";
return false;
}
bool const mptV2Enabled = view.rules().enabled(featureMPTokensV2);
if (trustlinesChanged_ == 1 || (mptV2Enabled && mptokensChanged_ == 1))
{
AccountID const issuer = tx.getAccountID(sfAccount);
STAmount const& amount = tx.getFieldAmount(sfAmount);
AccountID const& holder = amount.getIssuer();
STAmount const holderBalance = amount.asset().visit(
[&](Issue const& issue) {
return accountHolds(
view, holder, issue.currency, issuer, FreezeHandling::IgnoreFreeze, j);
},
[&](MPTIssue const& issue) {
return accountHolds(
view,
holder,
issue,
FreezeHandling::IgnoreFreeze,
AuthHandling::IgnoreAuth,
j);
});
if (holderBalance.signum() < 0)
{
JLOG(j.fatal()) << "Invariant failed: trustline or MPT balance is negative";
return false;
}
}
}
else
{
if (trustlinesChanged_ != 0)
{
JLOG(j.fatal()) << "Invariant failed: some trustlines were changed "
"despite failure of the transaction.";
return false;
}
if (mptokensChanged_ != 0)
{
JLOG(j.fatal()) << "Invariant failed: some mptokens were changed "
"despite failure of the transaction.";
return false;
}
}
return true;
}
//------------------------------------------------------------------------------
void
ValidPseudoAccounts::visitEntry(
bool isDelete,
std::shared_ptr<SLE const> const& before,
std::shared_ptr<SLE const> const& after)
{
if (isDelete)
{
// Deletion is ignored
return;
}
if (after && after->getType() == ltACCOUNT_ROOT)
{
bool const isPseudo = [&]() {
// isPseudoAccount checks that any of the pseudo-account fields are
// set.
if (isPseudoAccount(after))
return true;
// Not all pseudo-accounts have a zero sequence, but all accounts
// with a zero sequence had better be pseudo-accounts.
if (after->at(sfSequence) == 0)
return true;
return false;
}();
if (isPseudo)
{
// Pseudo accounts must have the following properties:
// 1. Exactly one of the pseudo-account fields is set.
// 2. The sequence number is not changed.
// 3. The lsfDisableMaster, lsfDefaultRipple, and lsfDepositAuth
// flags are set.
// 4. The RegularKey is not set.
{
std::vector<SField const*> const& fields = getPseudoAccountFields();
auto const numFields =
std::count_if(fields.begin(), fields.end(), [&after](SField const* sf) -> bool {
return after->isFieldPresent(*sf);
});
if (numFields != 1)
{
std::stringstream error;
error << "pseudo-account has " << numFields << " pseudo-account fields set";
errors_.emplace_back(error.str());
}
}
if (before && before->at(sfSequence) != after->at(sfSequence))
{
errors_.emplace_back("pseudo-account sequence changed");
}
if (!after->isFlag(lsfDisableMaster | lsfDefaultRipple | lsfDepositAuth))
{
errors_.emplace_back("pseudo-account flags are not set");
}
if (after->isFieldPresent(sfRegularKey))
{
errors_.emplace_back("pseudo-account has a regular key");
}
}
}
}
bool
ValidPseudoAccounts::finalize(
STTx const& tx,
TER const,
XRPAmount const,
ReadView const& view,
beast::Journal const& j)
{
bool const enforce = view.rules().enabled(featureSingleAssetVault);
XRPL_ASSERT(
errors_.empty() || enforce,
"xrpl::ValidPseudoAccounts::finalize : no bad "
"changes or enforce invariant");
if (!errors_.empty())
{
for (auto const& error : errors_)
{
JLOG(j.fatal()) << "Invariant failed: " << error;
}
if (enforce)
return false;
}
return true;
}
//------------------------------------------------------------------------------
void
NoModifiedImmutableFields::visitEntry(
bool isDelete,
std::shared_ptr<SLE const> const& before,
std::shared_ptr<SLE const> const& after)
{
if (isDelete || !before)
{
// Creation and deletion are ignored
return;
}
changedEntries_.emplace(before, after);
}
// Check whether any constant (or unannotated) fields in the given template
// have been modified between before and after.
// TODO(future): recurse into STObject and STArray fields using InnerObjectFormats
static bool
hasConstantFieldChanged(STObject const& before, STObject const& after, SOTemplate const& tmpl)
{
for (auto const& elem : tmpl)
{
auto const& sf = elem.sField();
auto const constant = elem.constant();
auto const* bField = before.peekAtPField(sf);
auto const* aField = after.peekAtPField(sf);
bool const bPresent = (bField != nullptr) && bField->getSType() != STI_NOTPRESENT;
bool const aPresent = (aField != nullptr) && aField->getSType() != STI_NOTPRESENT;
if (constant == SoeImmutable)
{
// Field must not change at all, including transitions between
// default (not-present) and explicit values.
if (bPresent != aPresent || (bPresent && aPresent && *bField != *aField))
return true;
}
else if (constant == SoeImmutableSetOnce)
{
XRPL_ASSERT(
elem.style() == SoeOptional,
"xrpl::hasConstantFieldChanged : set-once fields must be optional");
// Field may be set once, but cannot be removed or changed after
// it is present.
if (bPresent && (!aPresent || *bField != *aField))
return true;
}
// SoeMutable fields may change freely — no recursion
// into inner objects/arrays is needed because the parent
// field explicitly allows changes to its entire contents.
}
return false;
}
bool
NoModifiedImmutableFields::finalize(
STTx const& tx,
TER const,
XRPAmount const,
ReadView const& view,
beast::Journal const& j)
{
// LedgerStateFix repairs ledger invariants, so it may need to modify
// fields that are otherwise immutable.
if (tx.getTxnType() == ttLEDGER_STATE_FIX)
return true;
static auto const kFieldChanged = [](auto const& before, auto const& after, auto const& field) {
bool const beforeField = before->isFieldPresent(field);
bool const afterField = after->isFieldPresent(field);
return beforeField != afterField || (afterField && before->at(field) != after->at(field));
};
bool const useTemplate = view.rules().enabled(fixConstantInvariant);
for (auto const& slePair : changedEntries_)
{
auto const& before = slePair.first;
auto const& after = slePair.second;
auto const type = after->getType();
// New template-based check. This is a superset of the old hardcoded
// path below: the common template includes sfLedgerEntryType, the
// explicit check below covers sfLedgerIndex, and the loan/loan-broker
// fields from the old switch are marked immutable in their ledger
// templates.
if (useTemplate)
{
bool bad = false;
auto const* format = LedgerFormats::getInstance().findByType(type);
if (format != nullptr)
bad = hasConstantFieldChanged(*before, *after, format->getSOTemplate());
// sfLedgerIndex is a non-serialized (discardable) field
// that is not reliably present via peekAtPField, so we
// check it explicitly.
if (!bad)
bad = kFieldChanged(before, after, sfLedgerIndex);
if (bad)
{
JLOG(j.fatal()) << "Invariant failed: changed an unchangeable field for "
<< tx.getTransactionID();
return false;
}
continue;
}
// Old hardcoded check
bool badOld = false;
bool const enforceOld = view.rules().enabled(featureLendingProtocol);
switch (type)
{
case ltLOAN_BROKER:
/*
* We check this invariant regardless of lending protocol
* amendment status, allowing for detection and logging of
* potential issues even when the amendment is disabled.
*/
badOld = kFieldChanged(before, after, sfLedgerEntryType) ||
kFieldChanged(before, after, sfLedgerIndex) ||
kFieldChanged(before, after, sfSequence) ||
kFieldChanged(before, after, sfOwnerNode) ||
kFieldChanged(before, after, sfVaultNode) ||
kFieldChanged(before, after, sfVaultID) ||
kFieldChanged(before, after, sfAccount) ||
kFieldChanged(before, after, sfOwner) ||
kFieldChanged(before, after, sfManagementFeeRate) ||
kFieldChanged(before, after, sfCoverRateMinimum) ||
kFieldChanged(before, after, sfCoverRateLiquidation);
break;
case ltLOAN:
/*
* We check this invariant regardless of lending protocol
* amendment status, allowing for detection and logging of
* potential issues even when the amendment is disabled.
*/
badOld = kFieldChanged(before, after, sfLedgerEntryType) ||
kFieldChanged(before, after, sfLedgerIndex) ||
kFieldChanged(before, after, sfLoanSequence) ||
kFieldChanged(before, after, sfOwnerNode) ||
kFieldChanged(before, after, sfLoanBrokerNode) ||
kFieldChanged(before, after, sfLoanBrokerID) ||
kFieldChanged(before, after, sfBorrower) ||
kFieldChanged(before, after, sfLoanOriginationFee) ||
kFieldChanged(before, after, sfLoanServiceFee) ||
kFieldChanged(before, after, sfLatePaymentFee) ||
kFieldChanged(before, after, sfClosePaymentFee) ||
kFieldChanged(before, after, sfOverpaymentFee) ||
kFieldChanged(before, after, sfInterestRate) ||
kFieldChanged(before, after, sfLateInterestRate) ||
kFieldChanged(before, after, sfCloseInterestRate) ||
kFieldChanged(before, after, sfOverpaymentInterestRate) ||
kFieldChanged(before, after, sfStartDate) ||
kFieldChanged(before, after, sfPaymentInterval) ||
kFieldChanged(before, after, sfGracePeriod) ||
kFieldChanged(before, after, sfLoanScale);
break;
default:
/*
* We check this invariant regardless of lending protocol
* amendment status, allowing for detection and logging of
* potential issues even when the amendment is disabled.
*
* We use the lending protocol as a gate, even though
* all transactions are affected because that's when it
* was added.
*/
badOld = kFieldChanged(before, after, sfLedgerEntryType) ||
kFieldChanged(before, after, sfLedgerIndex);
}
XRPL_ASSERT(
!badOld || enforceOld,
"xrpl::NoModifiedImmutableFields::finalize : no bad "
"changes or enforce invariant");
if (badOld)
{
JLOG(j.fatal()) << "Invariant failed: changed an unchangeable field for "
<< tx.getTransactionID();
if (enforceOld)
return false;
}
}
return true;
}
void
ValidAmounts::visitEntry(
bool isDelete,
std::shared_ptr<SLE const> const&,
std::shared_ptr<SLE const> const& after)
{
if (!isDelete && after)
afterEntries_.push_back(after);
}
bool
ValidAmounts::finalize(
STTx const&,
TER const,
XRPAmount const,
ReadView const& view,
beast::Journal const& j) const
{
bool const badLedgerEntry = std::ranges::any_of(
afterEntries_, [&](auto const& sle) { return hasInvalidAmount(*sle, j); });
if (badLedgerEntry)
{
JLOG(j.fatal())
<< "Invariant failed: ledger entry contains non-canonical MPT or XRP amount";
return !view.rules().enabled(fixCleanup3_2_0);
}
return true;
}
} // namespace xrpl