Compare commits

...

9 Commits

Author SHA1 Message Date
Vito
759b22dabf Merge remote-tracking branch 'origin/develop' into tapanito/invariant-assertion
# Conflicts:
#	include/xrpl/tx/Transactor.h
#	include/xrpl/tx/invariants/AMMInvariant.h
#	include/xrpl/tx/invariants/FreezeInvariant.h
#	include/xrpl/tx/invariants/InvariantCheck.h
#	include/xrpl/tx/invariants/InvariantRunner.h
#	include/xrpl/tx/invariants/LoanBrokerInvariant.h
#	include/xrpl/tx/invariants/LoanInvariant.h
#	include/xrpl/tx/invariants/MPTInvariant.h
#	include/xrpl/tx/invariants/NFTInvariant.h
#	include/xrpl/tx/invariants/PermissionedDEXInvariant.h
#	include/xrpl/tx/invariants/PermissionedDomainInvariant.h
#	include/xrpl/tx/invariants/SponsorshipInvariant.h
#	include/xrpl/tx/invariants/VaultInvariant.h
#	src/libxrpl/tx/invariants/AMMInvariant.cpp
#	src/libxrpl/tx/invariants/FreezeInvariant.cpp
#	src/libxrpl/tx/invariants/InvariantCheck.cpp
#	src/libxrpl/tx/invariants/InvariantRunner.cpp
#	src/libxrpl/tx/invariants/LoanBrokerInvariant.cpp
#	src/libxrpl/tx/invariants/LoanInvariant.cpp
#	src/libxrpl/tx/invariants/MPTInvariant.cpp
#	src/libxrpl/tx/invariants/NFTInvariant.cpp
#	src/libxrpl/tx/invariants/PermissionedDEXInvariant.cpp
#	src/libxrpl/tx/invariants/PermissionedDomainInvariant.cpp
#	src/libxrpl/tx/invariants/SponsorshipInvariant.cpp
#	src/libxrpl/tx/invariants/VaultInvariant.cpp
#	src/test/app/invariants/InvariantsMisc_test.cpp
2026-10-01 10:15:08 +02:00
Vito
4b81928ef8 test: Cover the runner failing an erase with no before state
Drive a rawErase of a key absent from the base view through
checkInvariants and expect tecINVARIANT_FAILED, so the InvariantEntry
contract is exercised end to end rather than only on the constructor.
2026-09-30 13:53:31 +02:00
Vito
753a4e8f89 docs: Clarify InvariantEntry ownership and contract
InvariantEntry holds shared pointers to the before and after SLEs and is
neither a non-owning view nor copyable. State the construction contract
on the class so visitEntry implementations know which pointers they may
dereference.
2026-09-30 13:26:44 +02:00
Vito
05c6f23db8 Merge remote-tracking branch 'origin/develop' into tapanito/invariant-assertion 2026-09-30 10:18:17 +02:00
Vito
22ed49685a fix: Keep recording deleted holdings in ValidLoanBroker visitEntry 2026-09-09 15:05:31 +02:00
Vito
6b8c0e3faf Merge remote-tracking branch 'origin/develop' into tapanito/invariant-assertion 2026-09-09 14:38:10 +02:00
Vito
696351e41e chore: Apply clang-tidy include-cleaner and pass-by-value on InvariantEntry
Give checkers a direct InvariantEntry include and take SLE pointers by
value so clang-tidy -Werror matches CI.
2026-08-27 14:23:24 +02:00
Vito
4f2eaeedbc refactor: Validate visitEntry arguments via InvariantEntry
Own before/after as shared_ptrs so a malformed create/delete cannot
reach checkers, and throw std::logic_error so the runner can fail
the transaction in all builds.
2026-08-27 14:21:51 +02:00
Vito
d3bf7a73fd refactor: Enforce visitEntry SLE nullability in checkInvariants
Throw if after is null or a deletion lacks before, then drop the
duplicate checker-level guards that assumed those pointers might be missing.
2026-08-27 13:39:29 +02:00
31 changed files with 495 additions and 325 deletions

View File

@@ -20,6 +20,7 @@
#include <xrpl/protocol/XRPAmount.h>
#include <xrpl/tx/ApplyContext.h>
#include <xrpl/tx/applySteps.h>
#include <xrpl/tx/invariants/InvariantEntry.h>
#include <xrpl/tx/invariants/InvariantRunner.h>
#include <cstddef>
@@ -590,9 +591,9 @@ private:
* ordering is enforced.
*/
void
visitEntry(bool isDelete, SLE::ConstRef before, SLE::ConstRef after) final
visitEntry(InvariantEntry const& entry) final
{
visitInvariantEntry(isDelete, before, after);
visitInvariantEntry(entry.isDelete(), entry.before(), entry.after());
}
[[nodiscard]] bool

View File

@@ -4,10 +4,10 @@
#include <xrpl/ledger/ReadView.h>
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/STAmount.h>
#include <xrpl/protocol/STLedgerEntry.h>
#include <xrpl/protocol/STTx.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol/XRPAmount.h>
#include <xrpl/tx/invariants/InvariantEntry.h>
#include <optional>
@@ -27,7 +27,7 @@ public:
ValidAMM() = default;
void
visitEntry(bool, SLE::ConstRef, SLE::ConstRef);
visitEntry(InvariantEntry const&);
bool
finalize(STTx const&, TER const, XRPAmount const, ReadView const&, beast::Journal const&);

View File

@@ -4,12 +4,10 @@
#include <xrpl/basics/base_uint.h>
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/ledger/ReadView.h>
#include <xrpl/protocol/STLedgerEntry.h>
#include <xrpl/protocol/STTx.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol/XRPAmount.h>
#include <memory>
#include <xrpl/tx/invariants/InvariantEntry.h>
namespace xrpl {
@@ -20,7 +18,7 @@ class ValidBookDirectory
public:
void
visitEntry(bool, std::shared_ptr<SLE const> const&, std::shared_ptr<SLE const> const&);
visitEntry(InvariantEntry const&);
bool
finalize(STTx const&, TER const, XRPAmount const, ReadView const&, beast::Journal const&);

View File

@@ -10,6 +10,7 @@
#include <xrpl/protocol/STTx.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol/XRPAmount.h>
#include <xrpl/tx/invariants/InvariantEntry.h>
#include <map>
#include <optional>
@@ -44,7 +45,7 @@ class TransfersNotFrozen
public:
void
visitEntry(bool, SLE::ConstRef, SLE::ConstRef);
visitEntry(InvariantEntry const&);
bool
finalize(STTx const&, TER const, XRPAmount const, ReadView const&, beast::Journal const&);

View File

@@ -9,6 +9,7 @@
#include <xrpl/tx/invariants/AMMInvariant.h>
#include <xrpl/tx/invariants/DirectoryInvariant.h>
#include <xrpl/tx/invariants/FreezeInvariant.h>
#include <xrpl/tx/invariants/InvariantEntry.h>
#include <xrpl/tx/invariants/LoanBrokerInvariant.h>
#include <xrpl/tx/invariants/LoanInvariant.h>
#include <xrpl/tx/invariants/MPTInvariant.h>
@@ -71,21 +72,15 @@ public:
/**
* @brief called for each ledger entry in the current transaction.
*
* @param isDelete true if the SLE is being deleted.
* @param before ledger entry before modification by the transaction. `before` will be null if
* the entry is new.
* @param after ledger entry after modification by the transaction. Always non-null. When
* deleting, `after` may differ from `before`. Whether that is important is up to the
* individual invariant check.
* @param entry validated before/after pair for the modified ledger entry.
* The entry holds shared pointers to the SLEs and keeps them alive for
* its own lifetime. It is passed by reference and is not copyable.
*
* @note `after` IS NEVER NULL. `isDelete` is the only correct way to check for deletions.
* Do not make logic or branching decisions on whether on `after` is set, because it will
* always be set. Treat a null `after` as a programming error (with XRPL_ASSERT). An
* invariant MAY check for null defensively, if it makes more sense, but an assertion is
* preferred for new invariants.
* @note entry.after() is never null; construction throws otherwise.
* entry.isDelete() is the only correct way to check for deletions.
*/
void
visitEntry(bool isDelete, SLE::ConstRef before, SLE::ConstRef after);
visitEntry(InvariantEntry const& entry);
/**
* @brief called after all ledger entries have been visited to determine
@@ -123,7 +118,7 @@ class TransactionFeeCheck
{
public:
void
visitEntry(bool, SLE::ConstRef, SLE::ConstRef);
visitEntry(InvariantEntry const&);
static bool
finalize(STTx const&, TER const, XRPAmount const, ReadView const&, beast::Journal const&);
@@ -143,7 +138,7 @@ class XRPNotCreated
public:
void
visitEntry(bool, SLE::ConstRef, SLE::ConstRef);
visitEntry(InvariantEntry const&);
[[nodiscard]] bool
finalize(STTx const&, TER const, XRPAmount const, ReadView const&, beast::Journal const&) const;
@@ -163,7 +158,7 @@ class AccountRootsNotDeleted
public:
void
visitEntry(bool, SLE::ConstRef, SLE::ConstRef);
visitEntry(InvariantEntry const&);
[[nodiscard]] bool
finalize(STTx const&, TER const, XRPAmount const, ReadView const&, beast::Journal const&) const;
@@ -190,7 +185,7 @@ class AccountRootsDeletedClean
public:
void
visitEntry(bool, SLE::ConstRef, SLE::ConstRef);
visitEntry(InvariantEntry const&);
bool
finalize(STTx const&, TER const, XRPAmount const, ReadView const&, beast::Journal const&);
@@ -209,7 +204,7 @@ class XRPBalanceChecks
public:
void
visitEntry(bool, SLE::ConstRef, SLE::ConstRef);
visitEntry(InvariantEntry const&);
[[nodiscard]] bool
finalize(STTx const&, TER const, XRPAmount const, ReadView const&, beast::Journal const&) const;
@@ -226,7 +221,7 @@ class LedgerEntryTypesMatch
public:
void
visitEntry(bool, SLE::ConstRef, SLE::ConstRef);
visitEntry(InvariantEntry const&);
[[nodiscard]] bool
finalize(STTx const&, TER const, XRPAmount const, ReadView const&, beast::Journal const&) const;
@@ -244,7 +239,7 @@ class NoXRPTrustLines
public:
void
visitEntry(bool, SLE::ConstRef, SLE::ConstRef);
visitEntry(InvariantEntry const&);
[[nodiscard]] bool
finalize(STTx const&, TER const, XRPAmount const, ReadView const&, beast::Journal const&) const;
@@ -263,7 +258,7 @@ class NoDeepFreezeTrustLinesWithoutFreeze
public:
void
visitEntry(bool, SLE::ConstRef, SLE::ConstRef);
visitEntry(InvariantEntry const&);
[[nodiscard]] bool
finalize(STTx const&, TER const, XRPAmount const, ReadView const&, beast::Journal const&) const;
@@ -282,7 +277,7 @@ class NoBadOffers
public:
void
visitEntry(bool, SLE::ConstRef, SLE::ConstRef);
visitEntry(InvariantEntry const&);
[[nodiscard]] bool
finalize(STTx const&, TER const, XRPAmount const, ReadView const&, beast::Journal const&) const;
@@ -298,7 +293,7 @@ class NoZeroEscrow
public:
void
visitEntry(bool, SLE::ConstRef, SLE::ConstRef);
visitEntry(InvariantEntry const&);
[[nodiscard]] bool
finalize(STTx const&, TER const, XRPAmount const, ReadView const&, beast::Journal const&) const;
@@ -318,7 +313,7 @@ class ValidNewAccountRoot
public:
void
visitEntry(bool, SLE::ConstRef, SLE::ConstRef);
visitEntry(InvariantEntry const&);
[[nodiscard]] bool
finalize(STTx const&, TER const, XRPAmount const, ReadView const&, beast::Journal const&) const;
@@ -348,7 +343,7 @@ class ValidClawback
public:
void
visitEntry(bool, SLE::ConstRef, SLE::ConstRef);
visitEntry(InvariantEntry const&);
[[nodiscard]] bool
finalize(STTx const&, TER const, XRPAmount const, ReadView const&, beast::Journal const&) const;
@@ -368,7 +363,7 @@ class ValidPseudoAccounts
public:
void
visitEntry(bool, SLE::ConstRef, SLE::ConstRef);
visitEntry(InvariantEntry const&);
bool
finalize(STTx const&, TER const, XRPAmount const, ReadView const&, beast::Journal const&);
@@ -388,7 +383,7 @@ class NoModifiedUnmodifiableFields
public:
void
visitEntry(bool, SLE::ConstRef, SLE::ConstRef);
visitEntry(InvariantEntry const&);
bool
finalize(STTx const&, TER const, XRPAmount const, ReadView const&, beast::Journal const&);
@@ -404,7 +399,7 @@ class ValidAmounts
public:
void
visitEntry(bool, SLE::ConstRef, SLE::ConstRef);
visitEntry(InvariantEntry const&);
[[nodiscard]] bool
finalize(STTx const&, TER const, XRPAmount const, ReadView const&, beast::Journal const&) const;
@@ -421,7 +416,7 @@ class ObjectHasPseudoAccount
{
public:
void
visitEntry(bool, SLE::ConstRef, SLE::ConstRef);
visitEntry(InvariantEntry const&);
[[nodiscard]] bool
finalize(STTx const&, TER const, XRPAmount const, ReadView const&, beast::Journal const&) const;

View File

@@ -0,0 +1,60 @@
#pragma once
#include <xrpl/basics/contract.h>
#include <xrpl/protocol/STLedgerEntry.h>
#include <stdexcept>
#include <utility>
namespace xrpl {
/**
* @brief Before/after pair for one ledger entry touched by a transaction.
*
* Construction throws std::logic_error if after is null, or if isDelete is
* true and before is null. visitEntry implementations may therefore
* dereference after() unconditionally and before() whenever isDelete() is
* true. before() is null for newly created entries. The entry holds shared
* pointers to the SLEs and keeps them alive for its own lifetime. It is
* passed by reference and is not copyable.
*/
class InvariantEntry
{
bool isDelete_;
SLE::const_pointer before_;
SLE::const_pointer after_;
public:
InvariantEntry(bool isDelete, SLE::const_pointer before, SLE::const_pointer after)
: isDelete_(isDelete), before_(std::move(before)), after_(std::move(after))
{
if (after_ == nullptr)
Throw<std::logic_error>("InvariantEntry: after is never null");
if (isDelete_ && before_ == nullptr)
Throw<std::logic_error>("InvariantEntry: deleted entry missing before state");
}
InvariantEntry(InvariantEntry const&) = delete;
InvariantEntry&
operator=(InvariantEntry const&) = delete;
[[nodiscard]] bool
isDelete() const
{
return isDelete_;
}
[[nodiscard]] SLE::ConstRef
before() const
{
return before_;
}
[[nodiscard]] SLE::ConstRef
after() const
{
return after_;
}
};
} // namespace xrpl

View File

@@ -2,11 +2,11 @@
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/ledger/ReadView.h>
#include <xrpl/protocol/STLedgerEntry.h>
#include <xrpl/protocol/STTx.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol/XRPAmount.h>
#include <xrpl/tx/ApplyContext.h>
#include <xrpl/tx/invariants/InvariantEntry.h>
#include <functional>
#include <optional>
@@ -66,16 +66,12 @@ public:
/**
* @brief Called for each ledger entry modified by the transaction.
*
* @param isDelete true if the SLE is being deleted.
* @param before the entry's state before the transaction (nullptr for
* newly created entries).
* @param after the entry's state after the transaction. For deletions
* this is the SLE being erased; use @p isDelete rather than
* a null @p after to detect deletions. @p after is
* never null.
* @param entry validated before/after pair for the modified entry. It
* holds shared pointers to the SLEs and keeps them alive for its
* own lifetime. It is passed by reference and is not copyable.
*/
virtual void
visitEntry(bool isDelete, SLE::ConstRef before, SLE::ConstRef after) = 0;
visitEntry(InvariantEntry const& entry) = 0;
/**
* @brief Called after all entries have been visited.

View File

@@ -7,6 +7,7 @@
#include <xrpl/protocol/STTx.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol/XRPAmount.h>
#include <xrpl/tx/invariants/InvariantEntry.h>
#include <map>
#include <vector>
@@ -62,7 +63,7 @@ class ValidLoanBroker
public:
void
visitEntry(bool, SLE::ConstRef, SLE::ConstRef);
visitEntry(InvariantEntry const&);
bool
finalize(STTx const&, TER const, XRPAmount const, ReadView const&, beast::Journal const&);

View File

@@ -6,6 +6,7 @@
#include <xrpl/protocol/STTx.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol/XRPAmount.h>
#include <xrpl/tx/invariants/InvariantEntry.h>
#include <utility>
#include <vector>
@@ -57,7 +58,7 @@ class ValidLoan
public:
void
visitEntry(bool, SLE::ConstRef, SLE::ConstRef);
visitEntry(InvariantEntry const&);
bool
finalize(STTx const&, TER const, XRPAmount const, ReadView const&, beast::Journal const&);

View File

@@ -10,6 +10,7 @@
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol/UintTypes.h>
#include <xrpl/protocol/XRPAmount.h>
#include <xrpl/tx/invariants/InvariantEntry.h>
#include <array>
#include <cstdint>
@@ -54,12 +55,10 @@ public:
* @brief Track MPT issuance and holding creations, deletions, and
* mutations.
*
* @param isDelete Whether the ledger entry is being deleted.
* @param before The ledger entry before transaction application.
* @param after The ledger entry after transaction application.
* @param entry The ledger entry change being checked.
*/
void
visitEntry(bool isDelete, SLE::ConstRef before, SLE::ConstRef after);
visitEntry(InvariantEntry const& entry);
/**
* @brief Verify MPT issuance invariants after transaction application.
@@ -106,12 +105,10 @@ public:
/**
* @brief Track MPT amount and outstanding amount changes.
*
* @param isDelete Whether the ledger entry is being deleted.
* @param before The ledger entry before transaction application.
* @param after The ledger entry after transaction application.
* @param entry The ledger entry change being checked.
*/
void
visitEntry(bool isDelete, SLE::ConstRef before, SLE::ConstRef after);
visitEntry(InvariantEntry const& entry);
/**
* @brief Verify public MPT payment accounting invariants.
@@ -173,15 +170,10 @@ public:
/**
* @brief Track confidential MPT balance, issuance, and version changes.
*
* @param isDelete Whether the ledger entry is being deleted.
* @param before The ledger entry before transaction application.
* @param after The ledger entry after transaction application.
* @param entry The ledger entry change being checked.
*/
void
visitEntry(
bool isDelete,
std::shared_ptr<SLE const> const& before,
std::shared_ptr<SLE const> const& after);
visitEntry(InvariantEntry const& entry);
/**
* @brief Verify confidential MPT accounting and encrypted-field
@@ -227,15 +219,10 @@ public:
/**
* @brief Track MPT balance changes and deleted authorization state.
*
* @param isDelete Whether the ledger entry is being deleted.
* @param before The ledger entry before transaction application.
* @param after The ledger entry after transaction application.
* @param entry The ledger entry change being checked.
*/
void
visitEntry(
bool isDelete,
std::shared_ptr<SLE const> const& before,
std::shared_ptr<SLE const> const& after);
visitEntry(InvariantEntry const& entry);
/**
* @brief Verify MPT transfer authorization invariants.

View File

@@ -2,10 +2,10 @@
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/ledger/ReadView.h>
#include <xrpl/protocol/STLedgerEntry.h>
#include <xrpl/protocol/STTx.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol/XRPAmount.h>
#include <xrpl/tx/invariants/InvariantEntry.h>
#include <cstdint>
@@ -34,7 +34,7 @@ class ValidNFTokenPage
public:
void
visitEntry(bool, SLE::ConstRef, SLE::ConstRef);
visitEntry(InvariantEntry const&);
[[nodiscard]] bool
finalize(STTx const&, TER const, XRPAmount const, ReadView const&, beast::Journal const&) const;
@@ -62,7 +62,7 @@ class NFTokenCountTracking
public:
void
visitEntry(bool, SLE::ConstRef, SLE::ConstRef);
visitEntry(InvariantEntry const&);
[[nodiscard]] bool
finalize(STTx const&, TER const, XRPAmount const, ReadView const&, beast::Journal const&) const;

View File

@@ -4,10 +4,10 @@
#include <xrpl/basics/base_uint.h>
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/ledger/ReadView.h>
#include <xrpl/protocol/STLedgerEntry.h>
#include <xrpl/protocol/STTx.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol/XRPAmount.h>
#include <xrpl/tx/invariants/InvariantEntry.h>
namespace xrpl {
@@ -22,7 +22,7 @@ class ValidPermissionedDEX
public:
void
visitEntry(bool, SLE::ConstRef, SLE::ConstRef);
visitEntry(InvariantEntry const&);
bool
finalize(STTx const&, TER const, XRPAmount const, ReadView const&, beast::Journal const&);

View File

@@ -2,10 +2,10 @@
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/ledger/ReadView.h>
#include <xrpl/protocol/STLedgerEntry.h>
#include <xrpl/protocol/STTx.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol/XRPAmount.h>
#include <xrpl/tx/invariants/InvariantEntry.h>
#include <cstddef>
#include <vector>
@@ -35,7 +35,7 @@ class ValidPermissionedDomain
public:
void
visitEntry(bool, SLE::ConstRef, SLE::ConstRef);
visitEntry(InvariantEntry const&);
bool
finalize(STTx const&, TER const, XRPAmount const, ReadView const&, beast::Journal const&);

View File

@@ -2,10 +2,10 @@
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/ledger/ReadView.h>
#include <xrpl/protocol/STLedgerEntry.h>
#include <xrpl/protocol/STTx.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol/XRPAmount.h>
#include <xrpl/tx/invariants/InvariantEntry.h>
#include <cstdint>
@@ -31,7 +31,7 @@ class SponsorshipOwnerCountsMatch
public:
void
visitEntry(bool, SLE::ConstRef, SLE::ConstRef);
visitEntry(InvariantEntry const&);
[[nodiscard]] bool
finalize(STTx const&, TER const, XRPAmount const, ReadView const&, beast::Journal const&) const;
@@ -52,7 +52,7 @@ class SponsorshipAccountCountMatchesField
public:
void
visitEntry(bool, SLE::ConstRef, SLE::ConstRef);
visitEntry(InvariantEntry const&);
[[nodiscard]] bool
finalize(STTx const&, TER const, XRPAmount const, ReadView const&, beast::Journal const&) const;

View File

@@ -13,6 +13,7 @@
#include <xrpl/protocol/STTx.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol/XRPAmount.h>
#include <xrpl/tx/invariants/InvariantEntry.h>
#include <cstdint>
#include <optional>
@@ -223,7 +224,7 @@ public:
computeCoarsestScale(std::vector<DeltaInfo> const& numbers);
void
visitEntry(bool, SLE::ConstRef, SLE::ConstRef);
visitEntry(InvariantEntry const&);
bool
finalize(STTx const&, TER const, XRPAmount const, ReadView const&, beast::Journal const&);

View File

@@ -18,14 +18,19 @@
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol/TxFormats.h>
#include <xrpl/protocol/XRPAmount.h>
#include <xrpl/tx/invariants/InvariantEntry.h>
#include <string>
namespace xrpl {
void
ValidAMM::visitEntry(bool isDelete, SLE::ConstRef before, SLE::ConstRef after)
ValidAMM::visitEntry(InvariantEntry const& entry)
{
auto const isDelete = entry.isDelete();
auto const& before = entry.before();
auto const& after = entry.after();
if (isDelete)
{
if (before && before->getType() == ltAMM)
@@ -36,23 +41,20 @@ ValidAMM::visitEntry(bool isDelete, SLE::ConstRef before, SLE::ConstRef after)
return;
}
if (after)
auto const type = after->getType();
// AMM object changed
if (type == ltAMM)
{
auto const type = after->getType();
// AMM object changed
if (type == ltAMM)
{
ammAccount_ = after->getAccountID(sfAccount);
lptAMMBalanceAfter_ = after->getFieldAmount(sfLPTokenBalance);
}
// AMM pool changed
else if (
(type == ltRIPPLE_STATE && after->isFlag(lsfAMMNode)) ||
(type == ltACCOUNT_ROOT && after->isFieldPresent(sfAMMID)) ||
(type == ltMPTOKEN && after->isFlag(lsfMPTAMM)))
{
ammPoolChanged_ = true;
}
ammAccount_ = after->getAccountID(sfAccount);
lptAMMBalanceAfter_ = after->getFieldAmount(sfLPTokenBalance);
}
// AMM pool changed
else if (
(type == ltRIPPLE_STATE && after->isFlag(lsfAMMNode)) ||
(type == ltACCOUNT_ROOT && after->isFieldPresent(sfAMMID)) ||
(type == ltMPTOKEN && after->isFlag(lsfMPTAMM)))
{
ammPoolChanged_ = true;
}
if (before)

View File

@@ -12,9 +12,9 @@
#include <xrpl/protocol/STTx.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol/XRPAmount.h>
#include <xrpl/tx/invariants/InvariantEntry.h>
#include <algorithm>
#include <memory>
namespace xrpl {
@@ -41,19 +41,19 @@ badExchangeRate(SLE const& dir)
} // namespace
void
ValidBookDirectory::visitEntry(
bool isDelete,
std::shared_ptr<SLE const> const& before,
std::shared_ptr<SLE const> const& after)
ValidBookDirectory::visitEntry(InvariantEntry const& entry)
{
auto const isDelete = entry.isDelete();
auto const& before = entry.before();
auto const& after = entry.after();
// New root directories must have matching exchange-rate metadata. New
// child directories, and modified directories that change sfRootIndex, must
// point to an existing root.
// Only validate newly-created directories and sfRootIndex changes;
// LedgerStateFix handles legacy bad exchange-rate metadata. Skip deletions
// because `after` is not guaranteed to be null.
if (badBookDirectory_ || isDelete || !after || after->getType() != ltDIR_NODE)
// LedgerStateFix handles legacy bad exchange-rate metadata. Skip deletions.
if (badBookDirectory_ || isDelete || after->getType() != ltDIR_NODE)
return;
auto const rootIndex = after->getFieldH256(sfRootIndex);

View File

@@ -17,6 +17,7 @@
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol/XRPAmount.h>
#include <xrpl/tx/invariants/InvariantCheckPrivilege.h>
#include <xrpl/tx/invariants/InvariantEntry.h>
#include <algorithm>
#include <optional>
@@ -25,8 +26,12 @@
namespace xrpl {
void
TransfersNotFrozen::visitEntry(bool isDelete, SLE::ConstRef before, SLE::ConstRef after)
TransfersNotFrozen::visitEntry(InvariantEntry const& entry)
{
auto const isDelete = entry.isDelete();
auto const& before = entry.before();
auto const& after = entry.after();
/*
* A trust line freeze state alone doesn't determine if a transfer is
* frozen. The transfer must be examined "end-to-end" because both sides of
@@ -116,13 +121,6 @@ TransfersNotFrozen::finalize(
bool
TransfersNotFrozen::isValidEntry(SLE::ConstRef before, SLE::ConstRef after)
{
// `after` can never be null, even if the trust line is deleted.
XRPL_ASSERT(after, "xrpl::TransfersNotFrozen::isValidEntry : valid after.");
if (!after)
{
return false;
}
if (after->getType() == ltACCOUNT_ROOT)
{
possibleIssuers_.emplace(after->at(sfAccount), after);

View File

@@ -29,11 +29,11 @@
#include <xrpl/protocol/UintTypes.h>
#include <xrpl/protocol/XRPAmount.h>
#include <xrpl/tx/invariants/InvariantCheckPrivilege.h>
#include <xrpl/tx/invariants/InvariantEntry.h>
#include <algorithm>
#include <cstdint>
#include <functional>
#include <memory>
#include <optional>
#include <sstream>
#include <string>
@@ -88,7 +88,7 @@ ledgerEntryTypeName(SLE const& sle)
}
void
TransactionFeeCheck::visitEntry(bool, SLE::ConstRef, SLE::ConstRef)
TransactionFeeCheck::visitEntry(InvariantEntry const&)
{
// nothing to do
}
@@ -131,8 +131,12 @@ TransactionFeeCheck::finalize(
//------------------------------------------------------------------------------
void
XRPNotCreated::visitEntry(bool isDelete, SLE::ConstRef before, SLE::ConstRef after)
XRPNotCreated::visitEntry(InvariantEntry const& entry)
{
auto const isDelete = entry.isDelete();
auto const& before = entry.before();
auto const& after = entry.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
@@ -168,13 +172,6 @@ XRPNotCreated::visitEntry(bool isDelete, SLE::ConstRef before, SLE::ConstRef aft
}
}
if (!after)
{
// LCOV_EXCL_START
UNREACHABLE("xrpl::XRPNotCreated::visitEntry : after can't be null");
return;
// LCOV_EXCL_STOP
}
switch (after->getType())
{
case ltACCOUNT_ROOT:
@@ -232,8 +229,11 @@ XRPNotCreated::finalize(
//------------------------------------------------------------------------------
void
XRPBalanceChecks::visitEntry(bool, SLE::ConstRef before, SLE::ConstRef after)
XRPBalanceChecks::visitEntry(InvariantEntry const& entry)
{
auto const& before = entry.before();
auto const& after = entry.after();
auto isBad = [](STAmount const& balance) {
if (!balance.native())
return true;
@@ -255,7 +255,7 @@ XRPBalanceChecks::visitEntry(bool, SLE::ConstRef before, SLE::ConstRef after)
if (before && before->getType() == ltACCOUNT_ROOT)
bad_ |= isBad((*before)[sfBalance]);
if (after && after->getType() == ltACCOUNT_ROOT)
if (after->getType() == ltACCOUNT_ROOT)
bad_ |= isBad((*after)[sfBalance]);
}
@@ -279,8 +279,11 @@ XRPBalanceChecks::finalize(
//------------------------------------------------------------------------------
void
NoBadOffers::visitEntry(bool isDelete, SLE::ConstRef before, SLE::ConstRef after)
NoBadOffers::visitEntry(InvariantEntry const& entry)
{
auto const& before = entry.before();
auto const& after = entry.after();
auto isBad = [](STAmount const& pays, STAmount const& gets) {
// An offer should never be negative
if (pays < beast::kZero)
@@ -296,7 +299,7 @@ NoBadOffers::visitEntry(bool isDelete, SLE::ConstRef before, SLE::ConstRef after
if (before && before->getType() == ltOFFER)
bad_ |= isBad((*before)[sfTakerPays], (*before)[sfTakerGets]);
if (after && after->getType() == ltOFFER)
if (after->getType() == ltOFFER)
bad_ |= isBad((*after)[sfTakerPays], (*after)[sfTakerGets]);
}
@@ -320,8 +323,11 @@ NoBadOffers::finalize(
//------------------------------------------------------------------------------
void
NoZeroEscrow::visitEntry(bool isDelete, SLE::ConstRef before, SLE::ConstRef after)
NoZeroEscrow::visitEntry(InvariantEntry const& entry)
{
auto const& before = entry.before();
auto const& after = entry.after();
auto isBad = [](STAmount const& amount) {
// XRP case
if (amount.native())
@@ -364,7 +370,7 @@ NoZeroEscrow::visitEntry(bool isDelete, SLE::ConstRef before, SLE::ConstRef afte
if (before && before->getType() == ltESCROW)
bad_ |= isBad((*before)[sfAmount]);
if (after && after->getType() == ltESCROW)
if (after->getType() == ltESCROW)
bad_ |= isBad((*after)[sfAmount]);
auto checkAmount = [this](std::int64_t amount) {
@@ -374,7 +380,7 @@ NoZeroEscrow::visitEntry(bool isDelete, SLE::ConstRef before, SLE::ConstRef afte
bool const overwriteFixEnabled = isFeatureEnabled(fixCleanup3_1_3, true);
if (after && after->getType() == ltMPTOKEN_ISSUANCE)
if (after->getType() == ltMPTOKEN_ISSUANCE)
{
auto const outstanding = (*after)[sfOutstandingAmount];
checkAmount(outstanding);
@@ -393,7 +399,7 @@ NoZeroEscrow::visitEntry(bool isDelete, SLE::ConstRef before, SLE::ConstRef afte
}
}
if (after && after->getType() == ltMPTOKEN)
if (after->getType() == ltMPTOKEN)
{
auto const mptAmount = (*after)[sfMPTAmount];
checkAmount(mptAmount);
@@ -424,8 +430,11 @@ NoZeroEscrow::finalize(
//------------------------------------------------------------------------------
void
AccountRootsNotDeleted::visitEntry(bool isDelete, SLE::ConstRef before, SLE::ConstRef)
AccountRootsNotDeleted::visitEntry(InvariantEntry const& entry)
{
auto const isDelete = entry.isDelete();
auto const& before = entry.before();
if (isDelete && before && before->getType() == ltACCOUNT_ROOT)
accountsDeleted_++;
}
@@ -476,8 +485,12 @@ AccountRootsNotDeleted::finalize(
//------------------------------------------------------------------------------
void
AccountRootsDeletedClean::visitEntry(bool isDelete, SLE::ConstRef before, SLE::ConstRef after)
AccountRootsDeletedClean::visitEntry(InvariantEntry const& entry)
{
auto const isDelete = entry.isDelete();
auto const& before = entry.before();
auto const& after = entry.after();
if (isDelete && before && before->getType() == ltACCOUNT_ROOT)
accountsDeleted_.emplace_back(before, after);
}
@@ -599,31 +612,31 @@ AccountRootsDeletedClean::finalize(
//------------------------------------------------------------------------------
void
LedgerEntryTypesMatch::visitEntry(bool, SLE::ConstRef before, SLE::ConstRef after)
LedgerEntryTypesMatch::visitEntry(InvariantEntry const& entry)
{
if (before && after && before->getType() != after->getType())
auto const& before = entry.before();
auto const& after = entry.after();
if (before && 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())
{
switch (after->getType())
{
#include <xrpl/protocol/detail/ledger_entries.macro>
break;
default:
invalidTypeAdded_ = true;
break;
default:
invalidTypeAdded_ = true;
break;
}
}
#undef LEDGER_ENTRY
#pragma pop_macro("LEDGER_ENTRY")
}
}
bool
@@ -653,11 +666,13 @@ LedgerEntryTypesMatch::finalize(
//------------------------------------------------------------------------------
void
NoXRPTrustLines::visitEntry(bool, SLE::ConstRef, SLE::ConstRef after)
NoXRPTrustLines::visitEntry(InvariantEntry const& entry)
{
auto const& after = entry.after();
bool const overwriteFixEnabled = isFeatureEnabled(fixCleanup3_1_3, true);
if (after && after->getType() == ltRIPPLE_STATE)
if (after->getType() == ltRIPPLE_STATE)
{
// checking the issue directly here instead of
// relying on .native() just in case native somehow
@@ -693,9 +708,11 @@ NoXRPTrustLines::finalize(
//------------------------------------------------------------------------------
void
NoDeepFreezeTrustLinesWithoutFreeze::visitEntry(bool, SLE::ConstRef, SLE::ConstRef after)
NoDeepFreezeTrustLinesWithoutFreeze::visitEntry(InvariantEntry const& entry)
{
if (after && after->getType() == ltRIPPLE_STATE)
auto const& after = entry.after();
if (after->getType() == ltRIPPLE_STATE)
{
bool const overwriteFixEnabled = isFeatureEnabled(fixCleanup3_1_3, true);
@@ -736,8 +753,11 @@ NoDeepFreezeTrustLinesWithoutFreeze::finalize(
//------------------------------------------------------------------------------
void
ValidNewAccountRoot::visitEntry(bool, SLE::ConstRef before, SLE::ConstRef after)
ValidNewAccountRoot::visitEntry(InvariantEntry const& entry)
{
auto const& before = entry.before();
auto const& after = entry.after();
if (!before && after->getType() == ltACCOUNT_ROOT)
{
accountsCreated_++;
@@ -834,15 +854,19 @@ clawbackTrustLineBalanceInHolderTerms(
}
void
ValidClawback::visitEntry(bool isDelete, SLE::ConstRef before, SLE::ConstRef after)
ValidClawback::visitEntry(InvariantEntry const& entry)
{
auto const isDelete = entry.isDelete();
auto const& before = entry.before();
auto const& after = entry.after();
if (before && before->getType() == ltRIPPLE_STATE)
{
trustlinesChanged_++;
iou_.before = before;
}
if (!isDelete && after && after->getType() == ltRIPPLE_STATE)
if (!isDelete && after->getType() == ltRIPPLE_STATE)
iou_.after = after;
if (before && before->getType() == ltMPTOKEN)
@@ -851,7 +875,7 @@ ValidClawback::visitEntry(bool isDelete, SLE::ConstRef before, SLE::ConstRef aft
mpt_.before = before;
}
if (!isDelete && after && after->getType() == ltMPTOKEN)
if (!isDelete && after->getType() == ltMPTOKEN)
mpt_.after = after;
}
@@ -1009,15 +1033,19 @@ ValidClawback::finalize(
//------------------------------------------------------------------------------
void
ValidPseudoAccounts::visitEntry(bool isDelete, SLE::ConstRef before, SLE::ConstRef after)
ValidPseudoAccounts::visitEntry(InvariantEntry const& entry)
{
auto const isDelete = entry.isDelete();
auto const& before = entry.before();
auto const& after = entry.after();
if (isDelete)
{
// Deletion is ignored
return;
}
if (after && after->getType() == ltACCOUNT_ROOT)
if (after->getType() == ltACCOUNT_ROOT)
{
bool const isPseudo = [&]() {
// isPseudoAccount checks that any of the pseudo-account fields are
@@ -1104,8 +1132,12 @@ ValidPseudoAccounts::finalize(
//------------------------------------------------------------------------------
void
NoModifiedUnmodifiableFields::visitEntry(bool isDelete, SLE::ConstRef before, SLE::ConstRef after)
NoModifiedUnmodifiableFields::visitEntry(InvariantEntry const& entry)
{
auto const isDelete = entry.isDelete();
auto const& before = entry.before();
auto const& after = entry.after();
if (isDelete || !before)
{
// Creation and deletion are ignored
@@ -1249,12 +1281,12 @@ NoModifiedUnmodifiableFields::finalize(
}
void
ValidAmounts::visitEntry(
bool isDelete,
std::shared_ptr<SLE const> const&,
std::shared_ptr<SLE const> const& after)
ValidAmounts::visitEntry(InvariantEntry const& entry)
{
if (!isDelete && after)
auto const isDelete = entry.isDelete();
auto const& after = entry.after();
if (!isDelete)
afterEntries_.push_back(after);
}
@@ -1280,21 +1312,14 @@ ValidAmounts::finalize(
}
void
ObjectHasPseudoAccount::visitEntry(bool isDelete, SLE::ConstRef before, SLE::ConstRef after)
ObjectHasPseudoAccount::visitEntry(InvariantEntry const& entry)
{
auto const isDelete = entry.isDelete();
auto const& before = entry.before();
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:

View File

@@ -10,6 +10,7 @@
#include <xrpl/protocol/XRPAmount.h>
#include <xrpl/tx/ApplyContext.h>
#include <xrpl/tx/invariants/InvariantCheck.h>
#include <xrpl/tx/invariants/InvariantEntry.h>
#include <algorithm>
#include <array>
@@ -48,9 +49,11 @@ checkInvariantsHelper(
auto checkers = getInvariantChecks();
ctx.visit([&](UInt256 const&, bool isDelete, SLE::ConstRef before, SLE::ConstRef after) {
InvariantEntry const entry{isDelete, before, after};
if (txCheck)
txCheck->get().visitEntry(isDelete, before, after);
(..., std::get<Is>(checkers).visitEntry(isDelete, before, after));
txCheck->get().visitEntry(entry);
(..., std::get<Is>(checkers).visitEntry(entry));
});
if (txCheck)

View File

@@ -19,22 +19,28 @@
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol/TxFormats.h>
#include <xrpl/protocol/XRPAmount.h>
#include <xrpl/tx/invariants/InvariantEntry.h>
#include <algorithm>
namespace xrpl {
void
ValidLoanBroker::visitEntry(bool isDelete, SLE::ConstRef before, SLE::ConstRef after)
ValidLoanBroker::visitEntry(InvariantEntry const& entry)
{
auto const isDelete = entry.isDelete();
auto const& before = entry.before();
auto const& after = entry.after();
// Track LoanBroker deletions so finalize() can enforce:
// (a) only ttLOAN_BROKER_DELETE removes a broker
// (b) at most one broker is removed per transaction
// (c) DebtTotal and OwnerCount were zero before deletion
// `before` is the pre-transaction state, which is what
// LoanBrokerDelete::preclaim reads. Erased trust lines and MPTokens need no
// special handling here: the `if (after)` branch below already records them.
if (isDelete && before && before->getType() == ltLOAN_BROKER)
// special handling here: after is never null, so the type dispatch below
// still records them.
if (isDelete && before->getType() == ltLOAN_BROKER)
{
if (deletedBroker_)
{
@@ -45,28 +51,26 @@ ValidLoanBroker::visitEntry(bool isDelete, SLE::ConstRef before, SLE::ConstRef a
deletedBroker_ = before;
}
}
if (after)
if (after->getType() == ltLOAN_BROKER)
{
if (after->getType() == ltLOAN_BROKER)
{
auto& broker = brokers_[after->key()];
broker.brokerBefore = before;
broker.brokerAfter = after;
}
else if (after->getType() == ltACCOUNT_ROOT && after->isFieldPresent(sfLoanBrokerID))
{
auto const& loanBrokerID = after->at(sfLoanBrokerID);
// create an entry if one doesn't already exist
brokers_.emplace(loanBrokerID, BrokerInfo{});
}
else if (after->getType() == ltRIPPLE_STATE)
{
lines_.emplace_back(after);
}
else if (after->getType() == ltMPTOKEN)
{
mpts_.emplace_back(after);
}
auto& broker = brokers_[after->key()];
broker.brokerBefore = before;
broker.brokerAfter = after;
}
else if (after->getType() == ltACCOUNT_ROOT && after->isFieldPresent(sfLoanBrokerID))
{
auto const& loanBrokerID = after->at(sfLoanBrokerID);
// create an entry if one doesn't already exist
brokers_.emplace(loanBrokerID, BrokerInfo{});
}
else if (after->getType() == ltRIPPLE_STATE)
{
lines_.emplace_back(after);
}
else if (after->getType() == ltMPTOKEN)
{
mpts_.emplace_back(after);
}
}

View File

@@ -12,28 +12,32 @@
#include <xrpl/protocol/LedgerFormats.h>
#include <xrpl/protocol/Protocol.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/STLedgerEntry.h>
#include <xrpl/protocol/STNumber.h> // IWYU pragma: keep
#include <xrpl/protocol/STTx.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol/TxFormats.h>
#include <xrpl/protocol/XRPAmount.h>
#include <xrpl/tx/invariants/InvariantEntry.h>
#include <cstdint>
namespace xrpl {
void
ValidLoan::visitEntry(bool isDelete, SLE::ConstRef before, SLE::ConstRef after)
ValidLoan::visitEntry(InvariantEntry const& entry)
{
auto const isDelete = entry.isDelete();
auto const& before = entry.before();
auto const& after = entry.after();
// Classify here, but leave the decision about which checks apply to
// finalize(), which is the only place that can see the Rules.
if (isDelete)
{
if (before && before->getType() == ltLOAN)
if (before->getType() == ltLOAN)
deletedLoans_.emplace_back(before, after);
}
else if (after && after->getType() == ltLOAN)
else if (after->getType() == ltLOAN)
{
loans_.emplace_back(before, after);
}

View File

@@ -24,6 +24,7 @@
#include <xrpl/protocol/UintTypes.h>
#include <xrpl/protocol/XRPAmount.h>
#include <xrpl/tx/invariants/InvariantCheckPrivilege.h>
#include <xrpl/tx/invariants/InvariantEntry.h>
#include <algorithm>
#include <array>
@@ -67,8 +68,12 @@ subtractMPTAmountDelta(std::int64_t delta, std::uint64_t amount)
} // namespace
void
ValidMPTIssuance::visitEntry(bool isDelete, SLE::ConstRef before, SLE::ConstRef after)
ValidMPTIssuance::visitEntry(InvariantEntry const& entry)
{
auto const isDelete = entry.isDelete();
auto const& before = entry.before();
auto const& after = entry.after();
// The sfReferenceHolding tracking and the deleted-holding capture are
// only meaningful post-fixCleanup3_2_0 (the field is never set
// pre-amendment, and the holding-deletion rule does not apply).
@@ -76,7 +81,7 @@ ValidMPTIssuance::visitEntry(bool isDelete, SLE::ConstRef before, SLE::ConstRef
// on the hot path.
bool const fix320Enabled = isFeatureEnabled(fixCleanup3_2_0);
if (after && after->getType() == ltMPTOKEN_ISSUANCE)
if (after->getType() == ltMPTOKEN_ISSUANCE)
{
if (isDelete)
{
@@ -103,7 +108,7 @@ ValidMPTIssuance::visitEntry(bool isDelete, SLE::ConstRef before, SLE::ConstRef
}
}
if (after && after->getType() == ltMPTOKEN)
if (after->getType() == ltMPTOKEN)
{
if (isDelete)
{
@@ -122,7 +127,7 @@ ValidMPTIssuance::visitEntry(bool isDelete, SLE::ConstRef before, SLE::ConstRef
// Capture deleted RippleState SLEs so finalize() can verify none of
// them were owned by a vault pseudo-account outside VaultDelete.
if (fix320Enabled && isDelete && after && after->getType() == ltRIPPLE_STATE)
if (fix320Enabled && isDelete && after->getType() == ltRIPPLE_STATE)
deletedHoldings_.push_back(after);
}
@@ -428,8 +433,11 @@ ValidMPTIssuance::finalize(
}
void
ValidMPTBalanceChanges::visitEntry(bool, SLE::ConstRef before, SLE::ConstRef after)
ValidMPTBalanceChanges::visitEntry(InvariantEntry const& entry)
{
auto const& before = entry.before();
auto const& after = entry.after();
if (overflow_)
return;
@@ -478,15 +486,11 @@ ValidMPTBalanceChanges::visitEntry(bool, SLE::ConstRef before, SLE::ConstRef aft
if (before && !update(*before, Order::Before))
return;
if (after)
if (after->getType() == ltMPTOKEN_ISSUANCE)
{
if (after->getType() == ltMPTOKEN_ISSUANCE)
{
overflow_ = (*after)[sfOutstandingAmount] > maxMPTAmount(*after);
}
if (!update(*after, Order::After))
return;
overflow_ = (*after)[sfOutstandingAmount] > maxMPTAmount(*after);
}
update(*after, Order::After);
}
bool
@@ -556,11 +560,12 @@ ValidMPTBalanceChanges::finalize(
}
void
ValidConfidentialMPToken::visitEntry(
bool isDelete,
std::shared_ptr<SLE const> const& before,
std::shared_ptr<SLE const> const& after)
ValidConfidentialMPToken::visitEntry(InvariantEntry const& entry)
{
auto const isDelete = entry.isDelete();
auto const& before = entry.before();
auto const& after = entry.after();
// Helper to get MPToken Issuance ID safely
auto const getMptID = [](std::shared_ptr<SLE const> const& sle) -> UInt192 {
if (!sle)
@@ -593,7 +598,7 @@ ValidConfidentialMPToken::visitEntry(
}
}
if (after && after->getType() == ltMPTOKEN)
if (after->getType() == ltMPTOKEN)
{
UInt192 const id = getMptID(after);
auto& change = changes_[id];
@@ -645,7 +650,7 @@ ValidConfidentialMPToken::visitEntry(
change.outstandingDelta, before->getFieldU64(sfOutstandingAmount));
}
if (after && after->getType() == ltMPTOKEN_ISSUANCE)
if (after->getType() == ltMPTOKEN_ISSUANCE)
{
UInt192 const id = getMptID(after);
auto& change = changes_[id];
@@ -665,7 +670,7 @@ ValidConfidentialMPToken::visitEntry(
change.badCOA = true;
}
if (before && after && before->getType() == ltMPTOKEN && after->getType() == ltMPTOKEN)
if (before && before->getType() == ltMPTOKEN && after->getType() == ltMPTOKEN)
{
UInt192 const id = getMptID(after);
@@ -811,11 +816,12 @@ ValidConfidentialMPToken::finalize(
}
void
ValidMPTTransfer::visitEntry(
bool isDelete,
std::shared_ptr<SLE const> const& before,
std::shared_ptr<SLE const> const& after)
ValidMPTTransfer::visitEntry(InvariantEntry const& entry)
{
auto const isDelete = entry.isDelete();
auto const& before = entry.before();
auto const& after = entry.after();
// Record the before/after MPTAmount for each (issuanceID, account) pair
// so finalize() can determine whether a transfer actually occurred.
auto update = [&](SLE const& sle, bool isBefore) {
@@ -842,8 +848,7 @@ ValidMPTTransfer::visitEntry(
if (before)
update(*before, true);
if (after)
update(*after, false);
update(*after, false);
// Record whether every touched AccountRoot was a pseudo-account BEFORE
// the transaction applied (true and false). A transaction that erases a

View File

@@ -18,6 +18,7 @@
#include <xrpl/protocol/XRPAmount.h>
#include <xrpl/protocol/nftPageMask.h>
#include <xrpl/tx/invariants/InvariantCheckPrivilege.h>
#include <xrpl/tx/invariants/InvariantEntry.h>
#include <cstddef>
#include <optional>
@@ -25,13 +26,16 @@
namespace xrpl {
void
ValidNFTokenPage::visitEntry(bool isDelete, SLE::ConstRef before, SLE::ConstRef after)
ValidNFTokenPage::visitEntry(InvariantEntry const& entry)
{
auto const isDelete = entry.isDelete();
auto const& before = entry.before();
auto const& after = entry.after();
static constexpr UInt256 const& kPageBits = nft::kPageMask;
static constexpr UInt256 kAccountBits = ~kPageBits;
if ((before && before->getType() != ltNFTOKEN_PAGE) ||
(after && after->getType() != ltNFTOKEN_PAGE))
if ((before && before->getType() != ltNFTOKEN_PAGE) || after->getType() != ltNFTOKEN_PAGE)
return;
auto check = [this, isDelete](SLE::ConstRef sle) {
@@ -107,10 +111,9 @@ ValidNFTokenPage::visitEntry(bool isDelete, SLE::ConstRef before, SLE::ConstRef
}
}
if (after)
check(after);
check(after);
if (!isDelete && before && after)
if (!isDelete && before)
{
// If the NFTokenPage
// 1. Has a NextMinPage field in before, but loses it in after, and
@@ -183,15 +186,18 @@ ValidNFTokenPage::finalize(
//------------------------------------------------------------------------------
void
NFTokenCountTracking::visitEntry(bool, SLE::ConstRef before, SLE::ConstRef after)
NFTokenCountTracking::visitEntry(InvariantEntry const& entry)
{
auto const& before = entry.before();
auto const& after = entry.after();
if (before && before->getType() == ltACCOUNT_ROOT)
{
beforeMintedTotal_ += (*before)[~sfMintedNFTokens].value_or(0);
beforeBurnedTotal_ += (*before)[~sfBurnedNFTokens].value_or(0);
}
if (after && after->getType() == ltACCOUNT_ROOT)
if (after->getType() == ltACCOUNT_ROOT)
{
afterMintedTotal_ += (*after)[~sfMintedNFTokens].value_or(0);
afterBurnedTotal_ += (*after)[~sfBurnedNFTokens].value_or(0);

View File

@@ -15,16 +15,15 @@
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol/TxFormats.h>
#include <xrpl/protocol/XRPAmount.h>
#include <xrpl/tx/invariants/InvariantEntry.h>
namespace xrpl {
void
ValidPermissionedDEX::visitEntry(bool isDelete, SLE::ConstRef, SLE::ConstRef after)
ValidPermissionedDEX::visitEntry(InvariantEntry const& entry)
{
// Post-fixCleanup3_4_0: skip when after is null (defensive).
// Pre-amendment: original after-only path via the `if (after && ...)` checks below.
if (isFeatureEnabled(fixCleanup3_4_0) && !after)
return;
auto const isDelete = entry.isDelete();
auto const& after = entry.after();
auto trackDomain = [this, isDelete](UInt256 const& domain) {
domainsOld_.insert(domain);
@@ -32,13 +31,13 @@ ValidPermissionedDEX::visitEntry(bool isDelete, SLE::ConstRef, SLE::ConstRef aft
domains_.insert(domain);
};
if (after && after->getType() == ltDIR_NODE)
if (after->getType() == ltDIR_NODE)
{
if (after->isFieldPresent(sfDomainID))
trackDomain(after->getFieldH256(sfDomainID));
}
if (after && after->getType() == ltOFFER)
if (after->getType() == ltOFFER)
{
if (after->isFieldPresent(sfDomainID))
{

View File

@@ -15,17 +15,22 @@
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol/TxFormats.h>
#include <xrpl/protocol/XRPAmount.h>
#include <xrpl/tx/invariants/InvariantEntry.h>
#include <vector>
namespace xrpl {
void
ValidPermissionedDomain::visitEntry(bool isDel, SLE::ConstRef before, SLE::ConstRef after)
ValidPermissionedDomain::visitEntry(InvariantEntry const& entry)
{
auto const isDel = entry.isDelete();
auto const& before = entry.before();
auto const& after = entry.after();
if (before && before->getType() != ltPERMISSIONED_DOMAIN)
return;
if (after && after->getType() != ltPERMISSIONED_DOMAIN)
if (after->getType() != ltPERMISSIONED_DOMAIN)
return;
auto check = [isDel](std::vector<SleStatus>& sleStatus, SLE::ConstRef sle) {
@@ -54,8 +59,7 @@ ValidPermissionedDomain::visitEntry(bool isDel, SLE::ConstRef before, SLE::Const
sleStatus.emplace_back(ss);
};
if (after)
check(sleStatus_, after);
check(sleStatus_, after);
}
bool

View File

@@ -10,6 +10,7 @@
#include <xrpl/protocol/STTx.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol/XRPAmount.h>
#include <xrpl/tx/invariants/InvariantEntry.h>
#include <cstdint>
@@ -17,8 +18,12 @@ namespace xrpl {
// Add new sponsorship-related invariants implementations
void
SponsorshipOwnerCountsMatch::visitEntry(bool isDelete, SLE::ConstRef before, SLE::ConstRef after)
SponsorshipOwnerCountsMatch::visitEntry(InvariantEntry const& entry)
{
auto const isDelete = entry.isDelete();
auto const& before = entry.before();
auto const& after = entry.after();
auto getSponsored = [](SLE::ConstRef sle) -> std::uint32_t {
if (sle && sle->getType() == ltACCOUNT_ROOT)
return sle->getFieldU32(sfSponsoredOwnerCount);
@@ -116,8 +121,11 @@ SponsorshipOwnerCountsMatch::finalize(
}
void
SponsorshipAccountCountMatchesField::visitEntry(bool, SLE::ConstRef before, SLE::ConstRef after)
SponsorshipAccountCountMatchesField::visitEntry(InvariantEntry const& entry)
{
auto const& before = entry.before();
auto const& after = entry.after();
auto getSponsoringAccountCount = [](SLE::ConstRef sle) -> std::uint32_t {
if (sle && sle->getType() == ltACCOUNT_ROOT)
return sle->getFieldU32(sfSponsoringAccountCount);

View File

@@ -23,6 +23,7 @@
#include <xrpl/protocol/XRPAmount.h>
#include <xrpl/tx/Transactor.h>
#include <xrpl/tx/invariants/InvariantCheckPrivilege.h>
#include <xrpl/tx/invariants/InvariantEntry.h>
#include <algorithm>
#include <cstdint>
@@ -84,14 +85,11 @@ ValidVault::Shares::make(SLE const& from)
}
void
ValidVault::visitEntry(bool isDelete, SLE::ConstRef before, SLE::ConstRef after)
ValidVault::visitEntry(InvariantEntry const& entry)
{
// If `before` is empty, this means an object is being created, in which
// case `isDelete` must be false. Otherwise `before` and `after` are set and
// `isDelete` indicates whether an object is being deleted or modified.
XRPL_ASSERT(
after != nullptr && (before != nullptr || !isDelete),
"xrpl::ValidVault::visitEntry : some object is available");
auto const isDelete = entry.isDelete();
auto const& before = entry.before();
auto const& after = entry.after();
// Number balanceDelta will capture the difference (delta) between "before"
// state (zero if created) and "after" state (zero if destroyed), and
@@ -145,7 +143,7 @@ ValidVault::visitEntry(bool isDelete, SLE::ConstRef before, SLE::ConstRef after)
}
}
if (!isDelete && after)
if (!isDelete)
{
switch (after->getType())
{

View File

@@ -71,7 +71,10 @@ class InvariantsAMM_test : public InvariantsBase
ValidAMM invariant;
if (deletedLPBalance)
invariant.visitEntry(true, makeAMM(*deletedLPBalance), nullptr);
{
auto const sleAMM = makeAMM(*deletedLPBalance);
invariant.visitEntry(InvariantEntry{true, sleAMM, sleAMM});
}
bool const actual = invariant.finalize(
STTx{txType, [](STObject&) {}}, result, XRPAmount{}, *env.current(), jlog);

View File

@@ -43,6 +43,7 @@
#include <xrpl/tx/ApplyContext.h>
#include <xrpl/tx/Transactor.h>
#include <xrpl/tx/applySteps.h>
#include <xrpl/tx/invariants/InvariantEntry.h>
#include <xrpl/tx/invariants/InvariantRunner.h>
#include <array>
@@ -53,6 +54,7 @@
#include <optional>
#include <stdexcept>
#include <string>
#include <string_view>
#include <utility>
#include <vector>
@@ -1435,6 +1437,39 @@ class InvariantsMisc_test : public InvariantsBase
}
}
void
testInvariantEntry()
{
testcase << "invariant entry validation";
SLE::const_pointer const null;
SLE::const_pointer const sle = std::make_shared<SLE>(keylet::amendments());
InvariantEntry const created{false, null, sle};
BEAST_EXPECT(!created.isDelete());
BEAST_EXPECT(!created.before());
BEAST_EXPECT(created.after() == sle);
auto const expectLogicError = [&](bool isDelete,
SLE::ConstRef before,
SLE::ConstRef after,
std::string_view message) {
try
{
InvariantEntry const entry{isDelete, before, after};
(void)entry;
BEAST_EXPECT(false);
}
catch (std::logic_error const& ex)
{
BEAST_EXPECT(std::string_view{ex.what()}.contains(message));
}
};
expectLogicError(false, null, null, "after is never null");
expectLogicError(true, null, sle, "deleted entry missing before state");
}
void
testTxCheckException()
{
@@ -1457,7 +1492,7 @@ class InvariantsMisc_test : public InvariantsBase
}
void
visitEntry(bool, SLE::ConstRef, SLE::ConstRef) override
visitEntry(InvariantEntry const&) override
{
if (throwFrom == ThrowFrom::VisitEntry)
throw std::runtime_error("test-injected visitEntry exception");
@@ -1507,6 +1542,54 @@ class InvariantsMisc_test : public InvariantsBase
}
}
void
testTxCheckEraseMissingBase()
{
testcase << "txCheck erase of entry missing from base";
using namespace jtx;
// A no-op TxInvariantCheck, so any failure comes from building the
// InvariantEntry during the state table traversal rather than from
// a check.
struct NoopTxInvariantCheck : TxInvariantCheck
{
void
visitEntry(InvariantEntry const&) override
{
}
[[nodiscard]] bool
finalize(STTx const&, TER, XRPAmount, ReadView const&, beast::Journal const&) override
{
return true;
}
};
Env env{*this};
Account const alice{"alice"};
env.fund(XRP(1000), alice);
env.close();
OpenView ov{*env.current()};
STTx const tx{ttACCOUNT_SET, [](STObject&) {}};
test::StreamSink sink{beast::Severity::Warning};
beast::Journal const jlog{sink};
ApplyContext ac{env.app(), ov, tx, tesSUCCESS, env.current()->fees().base, TapNone, jlog};
CurrentTransactionRulesGuard const rulesGuard(ov.rules());
// Erase a key that does not exist in the base ledger. The state
// table reports an Erase with no before state, which InvariantEntry
// rejects.
auto const missing = keylet::escrow(alice.id(), SeqProxy::rawSequence(999));
BEAST_EXPECT(!env.current()->exists(missing));
ac.rawView().rawErase(std::make_shared<SLE>(missing));
NoopTxInvariantCheck noop;
TER const result = checkInvariants(ac, tesSUCCESS, XRPAmount{}, noop);
BEAST_EXPECT(result == tecINVARIANT_FAILED);
BEAST_EXPECT(sink.messages().str().contains("deleted entry missing before state"));
}
void
testTxCheckFinalizeFalse()
{
@@ -1521,7 +1604,7 @@ class InvariantsMisc_test : public InvariantsBase
struct FailingTxInvariantCheck : TxInvariantCheck
{
void
visitEntry(bool, SLE::ConstRef, SLE::ConstRef) override
visitEntry(InvariantEntry const&) override
{
}
@@ -1575,7 +1658,9 @@ class InvariantsMisc_test : public InvariantsBase
testInvariantOverwrite(all_ - fixCleanup3_1_3);
testObjectHasPseudoAccount();
testSponsorship();
testInvariantEntry();
testTxCheckException();
testTxCheckEraseMissingBase();
testTxCheckFinalizeFalse();
}
};

View File

@@ -640,83 +640,67 @@ class InvariantsPermissioned_test : public InvariantsBase
{
using namespace test::jtx;
testcase << "PermissionedDEX null after";
testcase << "PermissionedDEX deleted offer after";
// Tx is OfferCreate on pd2. Tracking pd1 fails the invariant iff that
// domain lands in the set finalize consults. after == null is never
// tracked (pre-340: after-only; post-340: early return) — same result,
// both sides are coverage/regression that we do not fall back to before.
auto const check = [this](
FeatureBitset features,
bool const afterIsNull,
bool const isDelete,
bool const expectInvariantFailure) {
Env env(*this, features);
// domain lands in the set finalize consults.
auto const check =
[this](FeatureBitset features, bool const isDelete, bool const expectInvariantFailure) {
Env env(*this, features);
Account const a1{"A1"};
Account const a2{"A2"};
env.fund(XRP(1000), a1, a2);
env.close();
Account const a1{"A1"};
Account const a2{"A2"};
env.fund(XRP(1000), a1, a2);
env.close();
[[maybe_unused]] auto [seq1, pd1] = createPermissionedDomainEnv(env, a1, a2);
[[maybe_unused]] auto [seq2, pd2] = createPermissionedDomainEnv(env, a1, a2);
env.close();
[[maybe_unused]] auto [seq1, pd1] = createPermissionedDomainEnv(env, a1, a2);
[[maybe_unused]] auto [seq2, pd2] = createPermissionedDomainEnv(env, a1, a2);
env.close();
auto sleOffer =
std::make_shared<SLE>(keylet::offer(a2.id(), SeqProxy::rawSequence(10)));
sleOffer->setAccountID(sfAccount, a2);
sleOffer->setFieldAmount(sfTakerPays, a1["USD"](10));
sleOffer->setFieldAmount(sfTakerGets, XRP(1));
sleOffer->setFieldH256(sfDomainID, pd1);
auto sleOffer =
std::make_shared<SLE>(keylet::offer(a2.id(), SeqProxy::rawSequence(10)));
sleOffer->setAccountID(sfAccount, a2);
sleOffer->setFieldAmount(sfTakerPays, a1["USD"](10));
sleOffer->setFieldAmount(sfTakerGets, XRP(1));
sleOffer->setFieldH256(sfDomainID, pd1);
CurrentTransactionRulesGuard const rulesGuard(env.current()->rules());
CurrentTransactionRulesGuard const rulesGuard(env.current()->rules());
ValidPermissionedDEX invariant;
if (afterIsNull)
{
// Defensive path: after is null. Must not fall back to before.
invariant.visitEntry(isDelete, sleOffer, nullptr);
}
else
{
// Normal / real-erase path: after is the offer on pd1.
invariant.visitEntry(isDelete, nullptr, sleOffer);
}
ValidPermissionedDEX invariant;
SLE::const_pointer const before = isDelete ? sleOffer : nullptr;
invariant.visitEntry(InvariantEntry{isDelete, before, sleOffer});
STTx const tx{ttOFFER_CREATE, [&pd2, &a1](STObject& tx) {
tx.setFieldH256(sfDomainID, pd2);
tx.setFieldAmount(sfTakerPays, a1["USD"](10));
tx.setFieldAmount(sfTakerGets, XRP(1));
}};
STTx const tx{ttOFFER_CREATE, [&pd2, &a1](STObject& tx) {
tx.setFieldH256(sfDomainID, pd2);
tx.setFieldAmount(sfTakerPays, a1["USD"](10));
tx.setFieldAmount(sfTakerGets, XRP(1));
}};
test::StreamSink sink{beast::Severity::Warning};
beast::Journal const jlog{sink};
bool const passed =
invariant.finalize(tx, tesSUCCESS, XRPAmount{}, *env.current(), jlog);
BEAST_EXPECT(passed != expectInvariantFailure);
if (expectInvariantFailure)
{
BEAST_EXPECT(sink.messages().str().contains("transaction consumed wrong domains"));
}
else
{
BEAST_EXPECT(sink.messages().str().empty());
}
};
test::StreamSink sink{beast::Severity::Warning};
beast::Journal const jlog{sink};
bool const passed =
invariant.finalize(tx, tesSUCCESS, XRPAmount{}, *env.current(), jlog);
BEAST_EXPECT(passed != expectInvariantFailure);
if (expectInvariantFailure)
{
BEAST_EXPECT(
sink.messages().str().contains("transaction consumed wrong domains"));
}
else
{
BEAST_EXPECT(sink.messages().str().empty());
}
};
auto const pre = all_ - fixCleanup3_4_0;
auto const post = all_;
// after == null: not tracked
check(pre, true, true, false);
check(post, true, true, false);
// after == offer on pd1
// pre-340: domainsOld_ (delete still inserted) → fail
check(pre, false, true, true);
check(pre, true, true);
// post-340: isDelete → only domainsOld_ → pass; !isDelete → domains_ → fail
check(post, false, true, false);
check(post, false, false, true);
check(post, true, false);
check(post, false, true);
}
void
@@ -810,7 +794,8 @@ class InvariantsPermissioned_test : public InvariantsBase
view.rawInsert(makeRootPage(rootDir, directoryQuality + 1));
ValidBookDirectory invariant;
invariant.visitEntry(false, nullptr, makeChildPage(rootDir));
auto const childPage = makeChildPage(rootDir);
invariant.visitEntry(InvariantEntry{false, nullptr, childPage});
test::StreamSink sink{beast::Severity::Warning};
beast::Journal const jlog{sink};
@@ -840,7 +825,7 @@ class InvariantsPermissioned_test : public InvariantsBase
{
// add
ValidBookDirectory invariant;
invariant.visitEntry(false, nullptr, badRoot);
invariant.visitEntry(InvariantEntry{false, nullptr, badRoot});
BEAST_EXPECT(
!invariant.finalize(makeOfferCreateTx(), tesSUCCESS, XRPAmount{}, view, jlog));
@@ -848,7 +833,7 @@ class InvariantsPermissioned_test : public InvariantsBase
{
// modify (without changing the sfRootIndex)
ValidBookDirectory invariant;
invariant.visitEntry(false, badRoot, badRoot);
invariant.visitEntry(InvariantEntry{false, badRoot, badRoot});
BEAST_EXPECT(
invariant.finalize(makeOfferCreateTx(), tesSUCCESS, XRPAmount{}, view, jlog));
@@ -860,7 +845,7 @@ class InvariantsPermissioned_test : public InvariantsBase
childAfter->setFieldH256(sfRootIndex, missingRootDir.key);
ValidBookDirectory invariant;
invariant.visitEntry(false, childBefore, childAfter);
invariant.visitEntry(InvariantEntry{false, childBefore, childAfter});
test::StreamSink missingRootSink{beast::Severity::Warning};
beast::Journal const missingRootJlog{missingRootSink};
@@ -875,7 +860,7 @@ class InvariantsPermissioned_test : public InvariantsBase
BEAST_EXPECT(!view.exists(rootDir));
ValidBookDirectory invariant;
invariant.visitEntry(true, badRoot, badRoot);
invariant.visitEntry(InvariantEntry{true, badRoot, badRoot});
BEAST_EXPECT(
invariant.finalize(makeOfferCreateTx(), tesSUCCESS, XRPAmount{}, view, jlog));
}