#include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include // IWYU pragma: keep #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace xrpl { #pragma push_macro("UNWRAP") #undef UNWRAP #pragma push_macro("TRANSACTION") #undef TRANSACTION #define UNWRAP(...) __VA_ARGS__ #define TRANSACTION(tag, value, name, settings, ...) \ case tag: { \ return (TxSettings UNWRAP settings).privileges & priv; \ } bool hasPrivilege(STTx const& tx, Privilege priv) { switch (tx.getTxnType()) { #include // Deprecated types default: return false; } }; #undef TRANSACTION #pragma pop_macro("TRANSACTION") #undef UNWRAP #pragma pop_macro("UNWRAP") // Returns the human-readable name of a ledger entry's type, falling back to // the numeric type if the format is somehow unknown. static std::string ledgerEntryTypeName(SLE const& sle) { auto const item = LedgerFormats::getInstance().findByType(sle.getType()); if (item == nullptr) { // LCOV_EXCL_START UNREACHABLE("xrpl::ledgerEntryTypeName : ledger entry has no known ledger format"); return std::to_string(sle.getType()); // LCOV_EXCL_STOP } return item->getName(); } void TransactionFeeCheck::visitEntry(bool, SLE::const_ref, SLE::const_ref) { // 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, SLE::const_ref before, SLE::const_ref 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; case ltSPONSORSHIP: if (before->isFieldPresent(sfFeeAmount)) { XRPL_ASSERT( isXRP((*before)[sfFeeAmount]), "XRPNotCreated::visitEntry : Sponsorship.FeeAmount is XRP"); drops_ -= (*before)[sfFeeAmount].xrp().drops(); } break; default: break; } } if (!after) { // LCOV_EXCL_START UNREACHABLE("xrpl::XRPNotCreated::visitEntry : after can't be null"); return; // LCOV_EXCL_STOP } 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; case ltSPONSORSHIP: if (!isDelete && after->isFieldPresent(sfFeeAmount)) { XRPL_ASSERT( isXRP((*after)[sfFeeAmount]), "XRPNotCreated::visitEntry : Sponsorship.FeeAmount is XRP"); drops_ += (*after)[sfFeeAmount].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, SLE::const_ref before, SLE::const_ref 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, SLE::const_ref before, SLE::const_ref 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, SLE::const_ref before, SLE::const_ref 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, SLE::const_ref before, SLE::const_ref) { 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, SLE::const_ref before, SLE::const_ref 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(featureSponsor) || 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 JLOG(j.fatal()) << "Invariant failed: account deletion left behind a " << ledgerEntryTypeName(*sle) << " 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; } // An account should not be deleted with sponsorship fields if (after->isFieldPresent(sfSponsoredOwnerCount) || after->isFieldPresent(sfSponsoringOwnerCount) || after->isFieldPresent(sfSponsoringAccountCount) || after->isFieldPresent(sfSponsor)) { JLOG(j.fatal()) << "Invariant failed: account deletion left " "behind a sponsorship field"; XRPL_ASSERT( enforce, "xrpl::AccountRootsDeletedClean::finalize : " "deleted account has no sponsorship fields"); 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::nftokenPageMin(accountID); Keylet const last = keylet::nftokenPageMax(accountID); std::optional 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, SLE::const_ref before, SLE::const_ref 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 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, SLE::const_ref, SLE::const_ref 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, SLE::const_ref, SLE::const_ref 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, SLE::const_ref before, SLE::const_ref 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 //------------------------------------------------------------------------------ static std::optional clawbackTrustLineBalanceInHolderTerms( SLE::const_pointer const& sle, AccountID const& holder, AccountID const& issuer, Currency const& currency) { if (!sle) return STAmount{Issue{currency, issuer}}; if (sle->getType() != ltRIPPLE_STATE || sle->key() != keylet::trustLine(holder, issuer, currency).key) { return std::nullopt; } STAmount balance = sle->getFieldAmount(sfBalance); if (holder > issuer) balance.negate(); balance.get().account = issuer; return balance; } void ValidClawback::visitEntry(bool isDelete, SLE::const_ref before, SLE::const_ref after) { if (before && before->getType() == ltRIPPLE_STATE) { trustlinesChanged_++; iou_.before = before; } if (!isDelete && after && after->getType() == ltRIPPLE_STATE) iou_.after = after; if (before && before->getType() == ltMPTOKEN) { mptokensChanged_++; mpt_.before = before; } if (!isDelete && after && after->getType() == ltMPTOKEN) mpt_.after = after; } 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_ != 0 && mptokensChanged_ != 0) { JLOG(j.fatal()) << "Invariant failed: trustline and MPToken both changed."; if (mptV2Enabled) return false; } if (trustlinesChanged_ == 1 || (mptV2Enabled && mptokensChanged_ == 1)) { STAmount const& amount = tx.getFieldAmount(sfAmount); return amount.asset().visit( [&](Issue const& issue) { AccountID const issuer = tx.getAccountID(sfAccount); AccountID const& holder = amount.getIssuer(); STAmount const holderBalance = accountHolds( view, holder, issue.currency, issuer, FreezeHandling::IgnoreFreeze, j); if (holderBalance.signum() < 0) { JLOG(j.fatal()) << "Invariant failed: trustline or MPT balance is negative"; return false; } if (!iou_.before) { JLOG(j.fatal()) << "Invariant failed: trustline clawback changed the wrong line"; return !mptV2Enabled; } auto const beforeBalance = clawbackTrustLineBalanceInHolderTerms( iou_.before, holder, issuer, issue.currency); auto const afterBalance = clawbackTrustLineBalanceInHolderTerms( iou_.after, holder, issuer, issue.currency); if (!beforeBalance || !afterBalance) { JLOG(j.fatal()) << "Invariant failed: trustline clawback changed the wrong line"; return !mptV2Enabled; } STAmount clawAmount = amount; clawAmount.get().account = issuer; if (clawAmount <= beast::kZero) { JLOG(j.fatal()) << "Invariant failed: trustline clawback amount is invalid"; return !mptV2Enabled; } if (*afterBalance > *beforeBalance || (*beforeBalance - *afterBalance) != std::min(*beforeBalance, clawAmount)) { JLOG(j.fatal()) << "Invariant failed: trustline clawback balance change is invalid"; return !mptV2Enabled; } return true; }, [&](MPTIssue const& issue) { auto const holder = tx[~sfHolder]; if (!holder) { JLOG(j.fatal()) << "Invariant failed: MPT clawback missing holder"; return !mptV2Enabled; } if (!mpt_.before || !mpt_.after) { JLOG(j.fatal()) << "Invariant failed: MPT clawback token is missing"; return !mptV2Enabled; } if (mpt_.before->getAccountID(sfAccount) != *holder || mpt_.after->getAccountID(sfAccount) != *holder || (*mpt_.before)[sfMPTokenIssuanceID] != issue.getMptID() || (*mpt_.after)[sfMPTokenIssuanceID] != issue.getMptID()) { JLOG(j.fatal()) << "Invariant failed: MPT clawback changed the wrong token"; return !mptV2Enabled; } auto const before = mpt_.before->getFieldU64(sfMPTAmount); auto const after = mpt_.after->getFieldU64(sfMPTAmount); if (amount.negative() || amount.mantissa() == 0) { JLOG(j.fatal()) << "Invariant failed: MPT clawback amount is invalid"; return !mptV2Enabled; } auto const clawAmount = amount.mantissa(); // MPT balances are unsigned, so validate the raw holder // debit instead of routing through accountHolds(). if (after > before || (before - after) != std::min(before, clawAmount)) { JLOG(j.fatal()) << "Invariant failed: MPT clawback balance change is invalid"; return !mptV2Enabled; } return true; }); } } 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, SLE::const_ref before, SLE::const_ref 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. // 5. The SponsoredOwnerCount, SponsoringOwnerCount, SponsoringAccountCount, Sponsor // fields are not set. { std::vector const& fields = getPseudoAccountFields(); auto const numFields = std::ranges::count_if( fields, [&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"); } if (after->isFieldPresent(sfSponsoredOwnerCount) || after->isFieldPresent(sfSponsoringOwnerCount) || after->isFieldPresent(sfSponsor) || after->isFieldPresent(sfSponsoringAccountCount)) { errors_.emplace_back("pseudo-account has a sponsorship field"); } } } } 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 NoModifiedUnmodifiableFields::visitEntry(bool isDelete, SLE::const_ref before, SLE::const_ref after) { if (isDelete || !before) { // Creation and deletion are ignored return; } changedEntries_.emplace(before, after); } bool NoModifiedUnmodifiableFields::finalize( STTx const& tx, TER const, XRPAmount const, ReadView const& view, beast::Journal const& j) { 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)); }; for (auto const& slePair : changedEntries_) { auto const& before = slePair.first; auto const& after = slePair.second; auto const type = after->getType(); bool bad = false; [[maybe_unused]] bool enforce = false; 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. */ enforce = view.rules().enabled(featureLendingProtocol); bad = 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. */ enforce = view.rules().enabled(featureLendingProtocol); bad = kFieldChanged(before, after, sfLedgerEntryType) || kFieldChanged(before, after, sfLedgerIndex) || kFieldChanged(before, after, sfSequence) || 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. */ enforce = view.rules().enabled(featureLendingProtocol); bad = kFieldChanged(before, after, sfLedgerEntryType) || kFieldChanged(before, after, sfLedgerIndex); } XRPL_ASSERT( !bad || enforce, "xrpl::NoModifiedUnmodifiableFields::finalize : no bad " "changes or enforce invariant"); if (bad) { JLOG(j.fatal()) << "Invariant failed: changed an unchangeable field for " << tx.getTransactionID(); if (enforce) return false; } } return true; } void ValidAmounts::visitEntry( bool isDelete, std::shared_ptr const&, std::shared_ptr 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; } void ObjectHasPseudoAccount::visitEntry(bool isDelete, SLE::const_ref before, SLE::const_ref after) { if (!isDelete) return; // Before should never be null when isDelete = true if (!before) { // LCOV_EXCL_START UNREACHABLE( "xrpl::ObjectHasPseudoAccount::visitEntry : deleted ledger entry missing before state"); return; // LCOV_EXCL_STOP } switch (before->getType()) { case ltAMM: case ltVAULT: case ltLOAN_BROKER: deletedObjSles_.push_back(before); break; default: return; } } [[nodiscard]] bool ObjectHasPseudoAccount::finalize( STTx const&, TER const, XRPAmount const, ReadView const& view, beast::Journal const& j) const { if (!view.rules().enabled(fixCleanup3_3_0)) return true; if (deletedObjSles_.empty()) return true; bool failed = false; for (auto const& sle : deletedObjSles_) { if (!sle->isFieldPresent(sfAccount)) { JLOG(j.fatal()) << "Invariant failed: deleted " << ledgerEntryTypeName(*sle) << " is missing pseudo-account field"; failed = true; continue; } // The pseudo-account must NOT exist on the ledger after the object is deleted. if (view.exists(keylet::account(sle->getAccountID(sfAccount)))) { JLOG(j.fatal()) << "Invariant failed: deleted " << ledgerEntryTypeName(*sle) << " without deleting its pseudo-account"; failed = true; } } return !failed; } } // namespace xrpl