Compare commits

...

11 Commits

Author SHA1 Message Date
Mayukha Vadari
fae5366170 Merge branch 'develop' into mvadari/invariant-perf 2026-06-16 13:38:40 -04:00
Mayukha Vadari
4b0a689dcf Merge branch 'develop' into mvadari/invariant-perf 2026-06-11 14:58:29 -04:00
Mayukha Vadari
7a792268a1 fix clang-tidy 2026-06-10 20:30:44 -04:00
Mayukha Vadari
b14c6db8fb use asserts instead of checks 2026-06-10 19:05:28 -04:00
Mayukha Vadari
39158a914a switch to list of valid entry types 2026-06-10 19:02:35 -04:00
Mayukha Vadari
761ff63daa add perf testing harness 2026-06-10 18:36:20 -04:00
Mayukha Vadari
fb6b8f1634 Merge branch 'develop' into mvadari/invariant-perf 2026-06-10 18:04:26 -04:00
Mayukha Vadari
0030320f6a fix clang-tidy 2026-06-10 12:39:42 -04:00
Mayukha Vadari
cb5fb479a0 add static ehck to ensure all invariants are covered 2026-06-09 16:26:35 -04:00
Mayukha Vadari
5b2498bc42 Merge branch 'develop' into mvadari/invariant-perf 2026-06-09 16:07:33 -04:00
Mayukha Vadari
bc0a8b3d8c perf: Only run invariants on the relevant ledger entry types 2026-06-09 16:06:54 -04:00
20 changed files with 1203 additions and 28 deletions

View File

@@ -5,6 +5,7 @@
#include <xrpl/protocol/STAmount.h>
#include <xrpl/protocol/STTx.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/tx/invariants/InvariantEntryTypes.h>
#include <optional>
@@ -20,6 +21,9 @@ class ValidAMM
public:
enum class ZeroAllowed : bool { No = false, Yes = true };
static constexpr auto kRelevantLedgerEntryTypes =
VisitLedgerEntryTypes<ltAMM, ltRIPPLE_STATE, ltACCOUNT_ROOT, ltMPTOKEN>{};
ValidAMM() = default;
void
visitEntry(bool, SLE::const_ref, SLE::const_ref);

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/InvariantEntryTypes.h>
#include <memory>
@@ -17,6 +18,8 @@ class ValidBookDirectory
hash_set<uint256> rootIndexes_;
public:
static constexpr auto kRelevantLedgerEntryTypes = VisitLedgerEntryTypes<ltDIR_NODE>{};
void
visitEntry(bool, std::shared_ptr<SLE const> const&, std::shared_ptr<SLE const> const&);

View File

@@ -6,6 +6,7 @@
#include <xrpl/protocol/STAmount.h>
#include <xrpl/protocol/STTx.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/tx/invariants/InvariantEntryTypes.h>
#include <map>
#include <vector>
@@ -38,6 +39,9 @@ class TransfersNotFrozen
std::map<AccountID, SLE::const_pointer const> possibleIssuers_;
public:
static constexpr auto kRelevantLedgerEntryTypes =
VisitLedgerEntryTypes<ltACCOUNT_ROOT, ltRIPPLE_STATE>{};
void
visitEntry(bool, SLE::const_ref, SLE::const_ref);

View File

@@ -8,6 +8,7 @@
#include <xrpl/tx/invariants/AMMInvariant.h>
#include <xrpl/tx/invariants/DirectoryInvariant.h>
#include <xrpl/tx/invariants/FreezeInvariant.h>
#include <xrpl/tx/invariants/InvariantEntryTypes.h>
#include <xrpl/tx/invariants/LoanBrokerInvariant.h>
#include <xrpl/tx/invariants/LoanInvariant.h>
#include <xrpl/tx/invariants/MPTInvariant.h>
@@ -107,6 +108,8 @@ public:
class TransactionFeeCheck
{
public:
static constexpr auto kRelevantLedgerEntryTypes = VisitNoLedgerEntryTypes{};
void
visitEntry(bool, SLE::const_ref, SLE::const_ref);
@@ -127,6 +130,8 @@ class XRPNotCreated
std::int64_t drops_ = 0;
public:
static constexpr auto kRelevantLedgerEntryTypes = VisitAllLedgerEntryTypes{};
void
visitEntry(bool, SLE::const_ref, SLE::const_ref);
@@ -147,6 +152,8 @@ class AccountRootsNotDeleted
std::uint32_t accountsDeleted_ = 0;
public:
static constexpr auto kRelevantLedgerEntryTypes = VisitLedgerEntryTypes<ltACCOUNT_ROOT>{};
void
visitEntry(bool, SLE::const_ref, SLE::const_ref);
@@ -174,6 +181,8 @@ class AccountRootsDeletedClean
std::vector<std::pair<SLE::const_pointer, SLE::const_pointer>> accountsDeleted_;
public:
static constexpr auto kRelevantLedgerEntryTypes = VisitLedgerEntryTypes<ltACCOUNT_ROOT>{};
void
visitEntry(bool, SLE::const_ref, SLE::const_ref);
@@ -193,6 +202,8 @@ class XRPBalanceChecks
bool bad_ = false;
public:
static constexpr auto kRelevantLedgerEntryTypes = VisitLedgerEntryTypes<ltACCOUNT_ROOT>{};
void
visitEntry(bool, SLE::const_ref, SLE::const_ref);
@@ -210,6 +221,8 @@ class LedgerEntryTypesMatch
bool invalidTypeAdded_ = false;
public:
static constexpr auto kRelevantLedgerEntryTypes = VisitAllLedgerEntryTypes{};
void
visitEntry(bool, SLE::const_ref, SLE::const_ref);
@@ -228,6 +241,8 @@ class NoXRPTrustLines
bool xrpTrustLine_ = false;
public:
static constexpr auto kRelevantLedgerEntryTypes = VisitLedgerEntryTypes<ltRIPPLE_STATE>{};
void
visitEntry(bool, SLE::const_ref, SLE::const_ref);
@@ -247,6 +262,8 @@ class NoDeepFreezeTrustLinesWithoutFreeze
bool deepFreezeWithoutFreeze_ = false;
public:
static constexpr auto kRelevantLedgerEntryTypes = VisitLedgerEntryTypes<ltRIPPLE_STATE>{};
void
visitEntry(bool, SLE::const_ref, SLE::const_ref);
@@ -266,6 +283,8 @@ class NoBadOffers
bool bad_ = false;
public:
static constexpr auto kRelevantLedgerEntryTypes = VisitLedgerEntryTypes<ltOFFER>{};
void
visitEntry(bool, SLE::const_ref, SLE::const_ref);
@@ -282,6 +301,9 @@ class NoZeroEscrow
bool bad_ = false;
public:
static constexpr auto kRelevantLedgerEntryTypes =
VisitLedgerEntryTypes<ltESCROW, ltMPTOKEN_ISSUANCE, ltMPTOKEN>{};
void
visitEntry(bool, SLE::const_ref, SLE::const_ref);
@@ -302,6 +324,8 @@ class ValidNewAccountRoot
std::uint32_t flags_ = 0;
public:
static constexpr auto kRelevantLedgerEntryTypes = VisitLedgerEntryTypes<ltACCOUNT_ROOT>{};
void
visitEntry(bool, SLE::const_ref, SLE::const_ref);
@@ -323,6 +347,9 @@ class ValidClawback
std::uint32_t mptokensChanged_ = 0;
public:
static constexpr auto kRelevantLedgerEntryTypes =
VisitLedgerEntryTypes<ltRIPPLE_STATE, ltMPTOKEN>{};
void
visitEntry(bool, SLE::const_ref, SLE::const_ref);
@@ -343,6 +370,8 @@ class ValidPseudoAccounts
std::vector<std::string> errors_;
public:
static constexpr auto kRelevantLedgerEntryTypes = VisitLedgerEntryTypes<ltACCOUNT_ROOT>{};
void
visitEntry(bool, SLE::const_ref, SLE::const_ref);
@@ -363,6 +392,8 @@ class NoModifiedUnmodifiableFields
std::set<std::pair<SLE::const_pointer, SLE::const_pointer>> changedEntries_;
public:
static constexpr auto kRelevantLedgerEntryTypes = VisitAllLedgerEntryTypes{};
void
visitEntry(bool, SLE::const_ref, SLE::const_ref);
@@ -378,6 +409,8 @@ class ValidAmounts
std::vector<std::shared_ptr<SLE const>> afterEntries_;
public:
static constexpr auto kRelevantLedgerEntryTypes = VisitAllLedgerEntryTypes{};
void
visitEntry(bool, std::shared_ptr<SLE const> const&, std::shared_ptr<SLE const> const&);

View File

@@ -0,0 +1,50 @@
#pragma once
#include <xrpl/protocol/LedgerFormats.h>
namespace xrpl {
struct VisitAllLedgerEntryTypes
{
static constexpr bool visitsAll = true;
static constexpr bool visitsNone = false;
static constexpr bool empty = false;
template <LedgerEntryType>
static consteval bool
contains()
{
return true;
}
};
struct VisitNoLedgerEntryTypes
{
static constexpr bool visitsAll = false;
static constexpr bool visitsNone = true;
static constexpr bool empty = true;
template <LedgerEntryType>
static consteval bool
contains()
{
return false;
}
};
template <LedgerEntryType... Types>
struct VisitLedgerEntryTypes
{
static constexpr bool visitsAll = false;
static constexpr bool visitsNone = false;
static constexpr bool empty = sizeof...(Types) == 0;
template <LedgerEntryType Type>
static consteval bool
contains()
{
return ((Type == Types) || ...);
}
};
} // namespace xrpl

View File

@@ -5,6 +5,7 @@
#include <xrpl/ledger/ReadView.h>
#include <xrpl/protocol/STTx.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/tx/invariants/InvariantEntryTypes.h>
#include <map>
#include <vector>
@@ -45,6 +46,9 @@ class ValidLoanBroker
goodZeroDirectory(ReadView const& view, SLE::const_ref dir, beast::Journal const& j);
public:
static constexpr auto kRelevantLedgerEntryTypes =
VisitLedgerEntryTypes<ltLOAN_BROKER, ltACCOUNT_ROOT, ltRIPPLE_STATE, ltMPTOKEN>{};
void
visitEntry(bool, SLE::const_ref, SLE::const_ref);

View File

@@ -4,6 +4,7 @@
#include <xrpl/ledger/ReadView.h>
#include <xrpl/protocol/STTx.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/tx/invariants/InvariantEntryTypes.h>
#include <vector>
@@ -22,6 +23,8 @@ class ValidLoan
std::vector<std::pair<SLE::const_pointer, SLE::const_pointer>> loans_;
public:
static constexpr auto kRelevantLedgerEntryTypes = VisitLedgerEntryTypes<ltLOAN>{};
void
visitEntry(bool, SLE::const_ref, SLE::const_ref);

View File

@@ -5,6 +5,7 @@
#include <xrpl/protocol/STLedgerEntry.h>
#include <xrpl/protocol/STTx.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/tx/invariants/InvariantEntryTypes.h>
#include <cstdint>
#include <memory>
@@ -36,6 +37,9 @@ class ValidMPTIssuance
std::vector<std::shared_ptr<SLE const>> deletedHoldings_;
public:
static constexpr auto kRelevantLedgerEntryTypes =
VisitLedgerEntryTypes<ltMPTOKEN_ISSUANCE, ltMPTOKEN, ltRIPPLE_STATE>{};
void
visitEntry(bool, SLE::const_ref, SLE::const_ref);
@@ -64,6 +68,9 @@ class ValidMPTPayment
hash_map<uint192, MPTData> data_;
public:
static constexpr auto kRelevantLedgerEntryTypes =
VisitLedgerEntryTypes<ltMPTOKEN_ISSUANCE, ltMPTOKEN>{};
void
visitEntry(bool, SLE::const_ref, SLE::const_ref);
@@ -85,6 +92,8 @@ class ValidMPTTransfer
hash_map<uint256, bool> deletedAuthorized_;
public:
static constexpr auto kRelevantLedgerEntryTypes = VisitLedgerEntryTypes<ltMPTOKEN>{};
void
visitEntry(bool, std::shared_ptr<SLE const> const&, std::shared_ptr<SLE const> const&);

View File

@@ -5,6 +5,7 @@
#include <xrpl/ledger/ReadView.h>
#include <xrpl/protocol/STTx.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/tx/invariants/InvariantEntryTypes.h>
#include <cstdint>
@@ -32,6 +33,8 @@ class ValidNFTokenPage
bool deletedLink_ = false;
public:
static constexpr auto kRelevantLedgerEntryTypes = VisitLedgerEntryTypes<ltNFTOKEN_PAGE>{};
void
visitEntry(bool, SLE::const_ref, SLE::const_ref);
@@ -60,6 +63,8 @@ class NFTokenCountTracking
std::uint32_t afterBurnedTotal_ = 0;
public:
static constexpr auto kRelevantLedgerEntryTypes = VisitLedgerEntryTypes<ltACCOUNT_ROOT>{};
void
visitEntry(bool, SLE::const_ref, SLE::const_ref);

View File

@@ -5,6 +5,7 @@
#include <xrpl/ledger/ReadView.h>
#include <xrpl/protocol/STTx.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/tx/invariants/InvariantEntryTypes.h>
namespace xrpl {
@@ -17,6 +18,8 @@ class ValidPermissionedDEX
hash_set<uint256> domains_;
public:
static constexpr auto kRelevantLedgerEntryTypes = VisitLedgerEntryTypes<ltDIR_NODE, ltOFFER>{};
void
visitEntry(bool, SLE::const_ref, SLE::const_ref);

View File

@@ -4,6 +4,7 @@
#include <xrpl/ledger/ReadView.h>
#include <xrpl/protocol/STTx.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/tx/invariants/InvariantEntryTypes.h>
#include <vector>
@@ -31,6 +32,9 @@ class ValidPermissionedDomain
std::vector<SleStatus> sleStatus_;
public:
static constexpr auto kRelevantLedgerEntryTypes =
VisitLedgerEntryTypes<ltPERMISSIONED_DOMAIN>{};
void
visitEntry(bool, SLE::const_ref, SLE::const_ref);

View File

@@ -9,6 +9,7 @@
#include <xrpl/protocol/STTx.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol/XRPAmount.h>
#include <xrpl/tx/invariants/InvariantEntryTypes.h>
#include <optional>
#include <unordered_map>
@@ -153,6 +154,13 @@ public:
[[nodiscard]] static std::int32_t
computeCoarsestScale(std::vector<DeltaInfo> const& numbers);
static constexpr auto kRelevantLedgerEntryTypes = VisitLedgerEntryTypes<
ltVAULT,
ltMPTOKEN_ISSUANCE,
ltMPTOKEN,
ltACCOUNT_ROOT,
ltRIPPLE_STATE>{};
void
visitEntry(bool, SLE::const_ref, SLE::const_ref);

View File

@@ -8,22 +8,163 @@
#include <xrpl/json/to_string.h>
#include <xrpl/ledger/ApplyView.h>
#include <xrpl/ledger/OpenView.h>
#include <xrpl/protocol/LedgerFormats.h>
#include <xrpl/protocol/STTx.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol/TxMeta.h>
#include <xrpl/protocol/XRPAmount.h>
#include <xrpl/tx/invariants/InvariantCheck.h>
#include <algorithm>
#include <array>
#include <cstddef>
#include <exception>
#include <functional>
#include <optional>
#include <tuple>
#include <type_traits>
#include <utility>
namespace xrpl {
namespace {
template <class Checker>
using RelevantLedgerEntryTypes = std::remove_cvref_t<decltype(Checker::kRelevantLedgerEntryTypes)>;
template <class Checker>
concept HasRelevantLedgerEntryTypes = requires { Checker::kRelevantLedgerEntryTypes; };
template <class Checker>
consteval bool
hasInvariantVisitRoute()
{
if constexpr (HasRelevantLedgerEntryTypes<Checker>)
{
using Types = RelevantLedgerEntryTypes<Checker>;
return Types::visitsAll || Types::visitsNone || !Types::empty;
}
return false;
}
template <std::size_t... Is>
consteval bool
allInvariantChecksHaveVisitRoutes(std::index_sequence<Is...>)
{
return (hasInvariantVisitRoute<std::tuple_element_t<Is, InvariantChecks>>() && ...);
}
static_assert(
allInvariantChecksHaveVisitRoutes(
std::make_index_sequence<std::tuple_size_v<InvariantChecks>>{}),
"Every invariant check must declare ledger-entry visit routing.");
template <class Checker>
void
visitAllEntryInvariantCheck(
InvariantChecks& checkers,
bool isDelete,
SLE::const_ref before,
SLE::const_ref after)
{
if constexpr (RelevantLedgerEntryTypes<Checker>::visitsAll)
std::get<Checker>(checkers).visitEntry(isDelete, before, after);
}
template <std::size_t... Is>
void
visitAllEntryInvariantChecks(
InvariantChecks& checkers,
bool isDelete,
SLE::const_ref before,
SLE::const_ref after,
std::index_sequence<Is...>)
{
(...,
visitAllEntryInvariantCheck<std::tuple_element_t<Is, InvariantChecks>>(
checkers, isDelete, before, after));
}
[[nodiscard]] std::optional<LedgerEntryType>
entryTypeForMappedInvariants(SLE::const_ref before, SLE::const_ref after)
{
if (before && after && before->getType() != after->getType())
{
// LedgerEntryTypesMatch runs as an all-entry invariant and reports this.
return std::nullopt;
}
if (after)
return after->getType();
if (before)
return before->getType();
return std::nullopt;
}
template <LedgerEntryType Type, class Checker>
void
visitLedgerTypeInvariantCheck(
InvariantChecks& checkers,
bool isDelete,
SLE::const_ref before,
SLE::const_ref after)
{
using Types = RelevantLedgerEntryTypes<Checker>;
if constexpr (!Types::visitsAll && !Types::visitsNone && Types::template contains<Type>())
{
std::get<Checker>(checkers).visitEntry(isDelete, before, after);
}
}
template <LedgerEntryType Type, std::size_t... Is>
void
visitLedgerTypeInvariantChecks(
InvariantChecks& checkers,
bool isDelete,
SLE::const_ref before,
SLE::const_ref after,
std::index_sequence<Is...>)
{
(...,
visitLedgerTypeInvariantCheck<Type, std::tuple_element_t<Is, InvariantChecks>>(
checkers, isDelete, before, after));
}
using LedgerTypeInvariantVisitor = void (*)(InvariantChecks&, bool, SLE::const_ref, SLE::const_ref);
template <LedgerEntryType Type>
void
visitMappedLedgerTypeInvariantChecks(
InvariantChecks& checkers,
bool isDelete,
SLE::const_ref before,
SLE::const_ref after)
{
visitLedgerTypeInvariantChecks<Type>(
checkers,
isDelete,
before,
after,
std::make_index_sequence<std::tuple_size_v<InvariantChecks>>{});
}
#pragma push_macro("LEDGER_ENTRY")
#undef LEDGER_ENTRY
#define LEDGER_ENTRY(tag, ...) \
std::pair<LedgerEntryType, LedgerTypeInvariantVisitor>{ \
tag, &visitMappedLedgerTypeInvariantChecks<tag>},
constexpr auto kLedgerTypeInvariantVisitors =
std::to_array<std::pair<LedgerEntryType, LedgerTypeInvariantVisitor>>({
#include <xrpl/protocol/detail/ledger_entries.macro>
});
#undef LEDGER_ENTRY
#pragma pop_macro("LEDGER_ENTRY")
} // namespace
ApplyContext::ApplyContext(
ServiceRegistry& registry,
OpenView& base,
@@ -99,11 +240,25 @@ ApplyContext::checkInvariantsHelper(
auto checkers = getInvariantChecks();
// call each check's per-entry method
visit(
[&checkers](
uint256 const& index, bool isDelete, SLE::const_ref before, SLE::const_ref after) {
(..., std::get<Is>(checkers).visitEntry(isDelete, before, after));
});
visit([&checkers](
uint256 const&, bool isDelete, SLE::const_ref before, SLE::const_ref after) {
visitAllEntryInvariantChecks(
checkers,
isDelete,
before,
after,
std::make_index_sequence<std::tuple_size_v<InvariantChecks>>{});
if (auto const type = entryTypeForMappedInvariants(before, after))
{
auto const iter = std::find_if(
kLedgerTypeInvariantVisitors.cbegin(),
kLedgerTypeInvariantVisitors.cend(),
[type](auto const& visitor) { return visitor.first == *type; });
if (iter != kLedgerTypeInvariantVisitors.cend())
iter->second(checkers, isDelete, before, after);
}
});
// Note: do not replace this logic with a `...&&` fold expression.
// The fold expression will only run until the first check fails (it

View File

@@ -2,6 +2,7 @@
#include <xrpl/basics/Log.h>
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/beast/utility/instrumentation.h>
#include <xrpl/ledger/ReadView.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Indexes.h>
@@ -51,7 +52,11 @@ ValidBookDirectory::visitEntry(
// 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)
XRPL_ASSERT(
(!before || before->getType() == ltDIR_NODE) && (!after || after->getType() == ltDIR_NODE),
"xrpl::ValidBookDirectory::visitEntry : directory node input");
if (badBookDirectory_ || isDelete || !after)
return;
auto const rootIndex = after->getFieldH256(sfRootIndex);

View File

@@ -188,6 +188,11 @@ XRPNotCreated::finalize(
void
XRPBalanceChecks::visitEntry(bool, SLE::const_ref before, SLE::const_ref after)
{
XRPL_ASSERT(
(!before || before->getType() == ltACCOUNT_ROOT) &&
(!after || after->getType() == ltACCOUNT_ROOT),
"xrpl::XRPBalanceChecks::visitEntry : account root input");
auto isBad = [](STAmount const& balance) {
if (!balance.native())
return true;
@@ -206,10 +211,10 @@ XRPBalanceChecks::visitEntry(bool, SLE::const_ref before, SLE::const_ref after)
return false;
};
if (before && before->getType() == ltACCOUNT_ROOT)
if (before)
bad_ |= isBad((*before)[sfBalance]);
if (after && after->getType() == ltACCOUNT_ROOT)
if (after)
bad_ |= isBad((*after)[sfBalance]);
}
@@ -235,6 +240,10 @@ XRPBalanceChecks::finalize(
void
NoBadOffers::visitEntry(bool isDelete, SLE::const_ref before, SLE::const_ref after)
{
XRPL_ASSERT(
(!before || before->getType() == ltOFFER) && (!after || after->getType() == ltOFFER),
"xrpl::NoBadOffers::visitEntry : offer input");
auto isBad = [](STAmount const& pays, STAmount const& gets) {
// An offer should never be negative
if (pays < beast::kZero)
@@ -247,10 +256,10 @@ NoBadOffers::visitEntry(bool isDelete, SLE::const_ref before, SLE::const_ref aft
return pays.native() && gets.native();
};
if (before && before->getType() == ltOFFER)
if (before)
bad_ |= isBad((*before)[sfTakerPays], (*before)[sfTakerGets]);
if (after && after->getType() == ltOFFER)
if (after)
bad_ |= isBad((*after)[sfTakerPays], (*after)[sfTakerGets]);
}
@@ -276,6 +285,14 @@ NoBadOffers::finalize(
void
NoZeroEscrow::visitEntry(bool isDelete, SLE::const_ref before, SLE::const_ref after)
{
auto const validType = [](SLE::const_ref sle) {
return !sle || sle->getType() == ltESCROW || sle->getType() == ltMPTOKEN_ISSUANCE ||
sle->getType() == ltMPTOKEN;
};
XRPL_ASSERT(
validType(before) && validType(after),
"xrpl::NoZeroEscrow::visitEntry : escrow or mpt input");
auto isBad = [](STAmount const& amount) {
// XRP case
if (amount.native())
@@ -380,7 +397,11 @@ NoZeroEscrow::finalize(
void
AccountRootsNotDeleted::visitEntry(bool isDelete, SLE::const_ref before, SLE::const_ref)
{
if (isDelete && before && before->getType() == ltACCOUNT_ROOT)
XRPL_ASSERT(
!before || before->getType() == ltACCOUNT_ROOT,
"xrpl::AccountRootsNotDeleted::visitEntry : account root input");
if (isDelete && before)
accountsDeleted_++;
}
@@ -430,7 +451,12 @@ AccountRootsNotDeleted::finalize(
void
AccountRootsDeletedClean::visitEntry(bool isDelete, SLE::const_ref before, SLE::const_ref after)
{
if (isDelete && before && before->getType() == ltACCOUNT_ROOT)
XRPL_ASSERT(
(!before || before->getType() == ltACCOUNT_ROOT) &&
(!after || after->getType() == ltACCOUNT_ROOT),
"xrpl::AccountRootsDeletedClean::visitEntry : account root input");
if (isDelete && before)
accountsDeleted_.emplace_back(before, after);
}
@@ -601,9 +627,13 @@ LedgerEntryTypesMatch::finalize(
void
NoXRPTrustLines::visitEntry(bool, SLE::const_ref, SLE::const_ref after)
{
XRPL_ASSERT(
!after || after->getType() == ltRIPPLE_STATE,
"xrpl::NoXRPTrustLines::visitEntry : ripple state input");
bool const overwriteFixEnabled = isFeatureEnabled(fixCleanup3_1_3, true);
if (after && after->getType() == ltRIPPLE_STATE)
if (after)
{
// checking the issue directly here instead of
// relying on .native() just in case native somehow
@@ -641,7 +671,11 @@ NoXRPTrustLines::finalize(
void
NoDeepFreezeTrustLinesWithoutFreeze::visitEntry(bool, SLE::const_ref, SLE::const_ref after)
{
if (after && after->getType() == ltRIPPLE_STATE)
XRPL_ASSERT(
!after || after->getType() == ltRIPPLE_STATE,
"xrpl::NoDeepFreezeTrustLinesWithoutFreeze::visitEntry : ripple state input");
if (after)
{
bool const overwriteFixEnabled = isFeatureEnabled(fixCleanup3_1_3, true);
@@ -684,7 +718,12 @@ NoDeepFreezeTrustLinesWithoutFreeze::finalize(
void
ValidNewAccountRoot::visitEntry(bool, SLE::const_ref before, SLE::const_ref after)
{
if (!before && after->getType() == ltACCOUNT_ROOT)
XRPL_ASSERT(
(!before || before->getType() == ltACCOUNT_ROOT) &&
(!after || after->getType() == ltACCOUNT_ROOT),
"xrpl::ValidNewAccountRoot::visitEntry : account root input");
if (!before && after)
{
accountsCreated_++;
accountSeq_ = (*after)[sfSequence];
@@ -758,6 +797,10 @@ ValidNewAccountRoot::finalize(
void
ValidClawback::visitEntry(bool, SLE::const_ref before, SLE::const_ref)
{
XRPL_ASSERT(
!before || before->getType() == ltRIPPLE_STATE || before->getType() == ltMPTOKEN,
"xrpl::ValidClawback::visitEntry : ripple state or mptoken input");
if (before && before->getType() == ltRIPPLE_STATE)
trustlinesChanged_++;
@@ -843,13 +886,18 @@ ValidClawback::finalize(
void
ValidPseudoAccounts::visitEntry(bool isDelete, SLE::const_ref before, SLE::const_ref after)
{
XRPL_ASSERT(
(!before || before->getType() == ltACCOUNT_ROOT) &&
(!after || after->getType() == ltACCOUNT_ROOT),
"xrpl::ValidPseudoAccounts::visitEntry : account root input");
if (isDelete)
{
// Deletion is ignored
return;
}
if (after && after->getType() == ltACCOUNT_ROOT)
if (after)
{
bool const isPseudo = [&]() {
// isPseudoAccount checks that any of the pseudo-account fields are

View File

@@ -3,6 +3,7 @@
#include <xrpl/basics/Log.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/protocol/LedgerFormats.h>
#include <xrpl/protocol/SField.h>
@@ -17,10 +18,12 @@ namespace xrpl {
void
ValidLoan::visitEntry(bool isDelete, SLE::const_ref before, SLE::const_ref after)
{
if (after && after->getType() == ltLOAN)
{
XRPL_ASSERT(
(!before || before->getType() == ltLOAN) && (!after || after->getType() == ltLOAN),
"xrpl::ValidLoan::visitEntry : loan input");
if (after)
loans_.emplace_back(before, after);
}
}
bool

View File

@@ -4,6 +4,7 @@
#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/NFTokenHelpers.h>
#include <xrpl/protocol/Feature.h>
@@ -30,9 +31,10 @@ ValidNFTokenPage::visitEntry(bool isDelete, SLE::const_ref before, SLE::const_re
static constexpr uint256 const& kPageBits = nft::kPageMask;
static constexpr uint256 kAccountBits = ~kPageBits;
if ((before && before->getType() != ltNFTOKEN_PAGE) ||
(after && after->getType() != ltNFTOKEN_PAGE))
return;
XRPL_ASSERT(
(!before || before->getType() == ltNFTOKEN_PAGE) &&
(!after || after->getType() == ltNFTOKEN_PAGE),
"xrpl::ValidNFTokenPage::visitEntry : nftoken page input");
auto check = [this, isDelete](SLE::const_ref sle) {
uint256 const account = sle->key() & kAccountBits;
@@ -185,13 +187,18 @@ ValidNFTokenPage::finalize(
void
NFTokenCountTracking::visitEntry(bool, SLE::const_ref before, SLE::const_ref after)
{
if (before && before->getType() == ltACCOUNT_ROOT)
XRPL_ASSERT(
(!before || before->getType() == ltACCOUNT_ROOT) &&
(!after || after->getType() == ltACCOUNT_ROOT),
"xrpl::NFTokenCountTracking::visitEntry : account root input");
if (before)
{
beforeMintedTotal_ += (*before)[~sfMintedNFTokens].value_or(0);
beforeBurnedTotal_ += (*before)[~sfBurnedNFTokens].value_or(0);
}
if (after && after->getType() == ltACCOUNT_ROOT)
if (after)
{
afterMintedTotal_ += (*after)[~sfMintedNFTokens].value_or(0);
afterBurnedTotal_ += (*after)[~sfBurnedNFTokens].value_or(0);

View File

@@ -2,6 +2,7 @@
#include <xrpl/basics/Log.h>
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/beast/utility/instrumentation.h>
#include <xrpl/ledger/ReadView.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Indexes.h>
@@ -19,6 +20,13 @@ namespace xrpl {
void
ValidPermissionedDEX::visitEntry(bool isDelete, SLE::const_ref before, SLE::const_ref after)
{
auto const validType = [](SLE::const_ref sle) {
return !sle || sle->getType() == ltDIR_NODE || sle->getType() == ltOFFER;
};
XRPL_ASSERT(
validType(before) && validType(after),
"xrpl::ValidPermissionedDEX::visitEntry : directory node or offer input");
if (after && after->getType() == ltDIR_NODE)
{
if (after->isFieldPresent(sfDomainID))

View File

@@ -2,6 +2,7 @@
#include <xrpl/basics/Log.h>
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/beast/utility/instrumentation.h>
#include <xrpl/ledger/ReadView.h>
#include <xrpl/ledger/helpers/CredentialHelpers.h>
#include <xrpl/protocol/Feature.h>
@@ -22,10 +23,10 @@ namespace xrpl {
void
ValidPermissionedDomain::visitEntry(bool isDel, SLE::const_ref before, SLE::const_ref after)
{
if (before && before->getType() != ltPERMISSIONED_DOMAIN)
return;
if (after && after->getType() != ltPERMISSIONED_DOMAIN)
return;
XRPL_ASSERT(
(!before || before->getType() == ltPERMISSIONED_DOMAIN) &&
(!after || after->getType() == ltPERMISSIONED_DOMAIN),
"xrpl::ValidPermissionedDomain::visitEntry : permissioned domain input");
auto check = [isDel](std::vector<SleStatus>& sleStatus, SLE::const_ref sle) {
auto const& credentials = sle->getFieldArray(sfAcceptedCredentials);

View File

@@ -0,0 +1,818 @@
#include <test/jtx/Account.h>
#include <test/jtx/Env.h>
#include <test/jtx/amount.h>
#include <test/jtx/pay.h>
#include <test/unit_test/SuiteJournal.h>
#include <xrpld/app/main/Application.h>
#include <xrpl/basics/Number.h>
#include <xrpl/beast/unit_test/suite.h>
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/beast/utility/instrumentation.h>
#include <xrpl/ledger/ApplyView.h>
#include <xrpl/ledger/OpenView.h>
#include <xrpl/ledger/ReadView.h>
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/InnerObjectFormats.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/STAmount.h>
#include <xrpl/protocol/STArray.h>
#include <xrpl/protocol/STIssue.h>
#include <xrpl/protocol/STLedgerEntry.h>
#include <xrpl/protocol/STNumber.h>
#include <xrpl/protocol/STObject.h>
#include <xrpl/protocol/STTx.h>
#include <xrpl/protocol/STXChainBridge.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol/TxFormats.h>
#include <xrpl/protocol/UintTypes.h>
#include <xrpl/protocol/XRPAmount.h>
#include <xrpl/tx/ApplyContext.h>
#include <algorithm>
#include <array>
#include <cassert>
#include <chrono>
#include <cmath>
#include <cstddef>
#include <cstdint>
#include <exception>
#include <iostream>
#include <memory>
#include <string>
#include <string_view>
#include <utility>
#include <vector>
namespace xrpl::test {
namespace {
std::ostream&
operator<<(std::ostream& os, std::chrono::nanoseconds ns)
{
return os << ns.count() << "ns";
}
FeatureBitset
perfAmendments()
{
return test::jtx::testableAmendments() | fixCleanup3_1_3 | fixCleanup3_2_0;
}
STTx
makeTx(TxType type)
{
return STTx{type, [](STObject&) {}};
}
std::vector<jtx::Account>
makeAccounts(std::size_t const count)
{
std::vector<jtx::Account> accounts;
accounts.reserve(count);
for (std::size_t i = 0; i < count; ++i)
accounts.emplace_back("perf" + std::to_string(i));
return accounts;
}
using LedgerTypes = std::array<LedgerEntryType, 30>;
LedgerTypes constexpr kConcreteLedgerTypes = {
ltNFTOKEN_OFFER,
ltCHECK,
ltDID,
ltNEGATIVE_UNL,
ltNFTOKEN_PAGE,
ltSIGNER_LIST,
ltTICKET,
ltACCOUNT_ROOT,
ltDIR_NODE,
ltAMENDMENTS,
ltLEDGER_HASHES,
ltBRIDGE,
ltOFFER,
ltDEPOSIT_PREAUTH,
ltXCHAIN_OWNED_CLAIM_ID,
ltRIPPLE_STATE,
ltFEE_SETTINGS,
ltXCHAIN_OWNED_CREATE_ACCOUNT_CLAIM_ID,
ltESCROW,
ltPAYCHAN,
ltAMM,
ltMPTOKEN_ISSUANCE,
ltMPTOKEN,
ltORACLE,
ltCREDENTIAL,
ltPERMISSIONED_DOMAIN,
ltDELEGATE,
ltVAULT,
ltLOAN_BROKER,
ltLOAN};
bool
needsOwnContext(LedgerEntryType const type)
{
return type == ltPERMISSIONED_DOMAIN || type == ltVAULT;
}
AccountID
account(std::uint32_t const seed)
{
return AccountID(seed);
}
Issue
usdIssue(std::uint32_t const seed = 1)
{
return Issue{toCurrency("USD"), account(100'000 + seed)};
}
STAmount
usd(std::uint32_t const value, std::uint32_t const seed = 1)
{
return STAmount{usdIssue(seed), value};
}
STNumber
stNumber(SField const& field, int const value)
{
return STNumber{field, Number{value}};
}
MPTID
mptID(std::uint32_t const seed)
{
return makeMptID(seed + 1, account(200'000 + seed));
}
STXChainBridge
xchainBridge(std::uint32_t const seed)
{
auto const lockingDoor = account(210'000 + seed);
auto const issuingDoor = account(220'000 + seed);
return STXChainBridge{
lockingDoor,
Issue{toCurrency("USD"), lockingDoor},
issuingDoor,
Issue{toCurrency("USD"), issuingDoor}};
}
void
setSyntheticAccountRoot(SLE::ref sle, AccountID const& id)
{
sle->setAccountID(sfAccount, id);
sle->setFieldU32(sfSequence, 1);
sle->setFieldAmount(sfBalance, STAmount{XRPAmount{}});
sle->setFieldU32(sfOwnerCount, 0);
}
STArray
credentialRules(AccountID const& issuer, std::uint32_t const seed)
{
STArray credentials(sfAcceptedCredentials, 1);
auto cred = STObject::makeInnerObject(sfCredential);
cred.setAccountID(sfIssuer, issuer);
auto const type = "cred_type" + std::to_string(seed);
cred.setFieldVL(sfCredentialType, Slice(type.data(), type.size()));
credentials.pushBack(std::move(cred));
return credentials;
}
STArray
nftokens(std::uint32_t const seed)
{
auto const* format = InnerObjectFormats::getInstance().findSOTemplateBySField(sfNFToken);
XRPL_ASSERT(format, "InvariantPerf_test::nftokens : NFToken inner format");
STArray tokens(sfNFTokens, 1);
auto const tokenID = uint256{(seed * 4) + 1};
STObject token{
*format, sfNFToken, [&](STObject& object) { object.setFieldH256(sfNFTokenID, tokenID); }};
tokens.pushBack(std::move(token));
return tokens;
}
Keylet
syntheticKey(LedgerEntryType const type, std::uint32_t const seed)
{
auto const id = account(10'000 + seed);
switch (type)
{
case ltACCOUNT_ROOT:
return keylet::account(id);
case ltCHECK:
return keylet::check(id, seed + 1);
case ltDID:
return keylet::did(id);
case ltNEGATIVE_UNL:
return {ltNEGATIVE_UNL, uint256{300'000 + seed}};
case ltNFTOKEN_PAGE:
return {ltNFTOKEN_PAGE, uint256{(seed * 4) + 2}};
case ltSIGNER_LIST:
return keylet::signers(id);
case ltTICKET:
return keylet::kTicket(id, seed + 1);
case ltDIR_NODE:
return keylet::ownerDir(id);
case ltAMENDMENTS:
return {ltAMENDMENTS, uint256{310'000 + seed}};
case ltLEDGER_HASHES:
return {ltLEDGER_HASHES, uint256{330'000 + seed}};
case ltBRIDGE:
return keylet::bridge(xchainBridge(seed), STXChainBridge::ChainType::Locking);
case ltOFFER:
return keylet::offer(id, seed + 1);
case ltDEPOSIT_PREAUTH:
return keylet::depositPreauth(id, account(20'000 + seed));
case ltXCHAIN_OWNED_CLAIM_ID:
return keylet::xChainClaimID(xchainBridge(seed), seed + 1);
case ltRIPPLE_STATE:
return keylet::line(id, usdIssue(seed));
case ltFEE_SETTINGS:
return {ltFEE_SETTINGS, uint256{320'000 + seed}};
case ltXCHAIN_OWNED_CREATE_ACCOUNT_CLAIM_ID:
return keylet::xChainCreateAccountClaimID(xchainBridge(seed), seed + 1);
case ltESCROW:
return keylet::escrow(id, seed + 1);
case ltPAYCHAN:
return keylet::payChan(id, account(30'000 + seed), seed + 1);
case ltAMM:
return keylet::amm(uint256{seed + 1});
case ltNFTOKEN_OFFER:
return keylet::nftoffer(id, seed + 1);
case ltMPTOKEN_ISSUANCE:
return keylet::mptIssuance(mptID(seed));
case ltMPTOKEN:
return keylet::mptoken(mptID(seed), id);
case ltORACLE:
return keylet::oracle(id, seed + 1);
case ltCREDENTIAL: {
auto const type = "cred_type" + std::to_string(seed);
return keylet::credential(id, account(40'000 + seed), Slice(type.data(), type.size()));
}
case ltPERMISSIONED_DOMAIN:
return keylet::permissionedDomain(id, seed + 1);
case ltDELEGATE:
return keylet::delegate(id, account(50'000 + seed));
case ltVAULT:
return keylet::vault(id, seed + 1);
case ltLOAN_BROKER:
return keylet::loanbroker(id, seed + 1);
case ltLOAN:
return keylet::loan(uint256{60'000 + seed}, seed + 1);
default:
return {type, uint256{seed + 1}};
}
}
void
populateVault(SLE::ref sle, std::uint32_t const seed)
{
auto const owner = account(70'000 + seed);
auto const pseudo = account(80'000 + seed);
sle->setFieldU32(sfSequence, seed + 1);
sle->setAccountID(sfOwner, owner);
sle->setAccountID(sfAccount, pseudo);
sle->setFieldIssue(sfAsset, STIssue{sfAsset, xrpIssue()});
sle->setFieldNumber(sfAssetsTotal, stNumber(sfAssetsTotal, 0));
sle->setFieldNumber(sfAssetsAvailable, stNumber(sfAssetsAvailable, 0));
sle->setFieldNumber(sfAssetsMaximum, stNumber(sfAssetsMaximum, 0));
sle->setFieldNumber(sfLossUnrealized, stNumber(sfLossUnrealized, 0));
sle->setFieldH192(sfShareMPTID, mptID(90'000 + seed));
sle->setFieldU8(sfWithdrawalPolicy, kVaultStrategyFirstComeFirstServe);
}
SLE::pointer
makeSyntheticSle(LedgerEntryType const type, std::uint32_t const seed)
{
auto sle = std::make_shared<SLE>(syntheticKey(type, seed));
auto const id = account(10'000 + seed);
auto const other = account(20'000 + seed);
switch (type)
{
case ltNFTOKEN_OFFER:
sle->setAccountID(sfOwner, id);
sle->setFieldH256(sfNFTokenID, uint256{seed + 1});
sle->setFieldAmount(sfAmount, STAmount{XRPAmount{1}});
break;
case ltCHECK:
sle->setAccountID(sfAccount, id);
sle->setAccountID(sfDestination, other);
sle->setFieldAmount(sfSendMax, usd(1, seed));
sle->setFieldU32(sfSequence, seed + 1);
break;
case ltDID:
sle->setAccountID(sfAccount, id);
break;
case ltNFTOKEN_PAGE:
sle->setFieldArray(sfNFTokens, nftokens(seed));
break;
case ltTICKET:
sle->setAccountID(sfAccount, id);
sle->setFieldU32(sfTicketSequence, seed + 1);
break;
case ltACCOUNT_ROOT:
setSyntheticAccountRoot(sle, id);
break;
case ltDIR_NODE:
sle->setFieldH256(sfRootIndex, sle->key());
break;
case ltBRIDGE:
sle->at(sfXChainBridge) = xchainBridge(seed);
sle->setAccountID(sfAccount, id);
sle->setFieldAmount(sfSignatureReward, STAmount{XRPAmount{1}});
sle->setFieldU64(sfXChainClaimID, 0);
sle->setFieldU64(sfXChainAccountCreateCount, 0);
sle->setFieldU64(sfXChainAccountClaimCount, 0);
break;
case ltOFFER:
sle->setAccountID(sfAccount, id);
sle->setFieldU32(sfSequence, seed + 1);
sle->setFieldAmount(sfTakerPays, usd(1, seed));
sle->setFieldAmount(sfTakerGets, STAmount{XRPAmount{1}});
sle->setFieldH256(sfBookDirectory, uint256{seed + 1});
break;
case ltDEPOSIT_PREAUTH:
sle->setAccountID(sfAccount, id);
sle->setAccountID(sfAuthorize, other);
break;
case ltXCHAIN_OWNED_CLAIM_ID:
sle->setAccountID(sfAccount, id);
sle->at(sfXChainBridge) = xchainBridge(seed);
sle->setFieldU64(sfXChainClaimID, seed + 1);
sle->setAccountID(sfOtherChainSource, other);
sle->setFieldAmount(sfSignatureReward, STAmount{XRPAmount{1}});
break;
case ltRIPPLE_STATE:
sle->setFieldAmount(sfBalance, usd(0, seed));
sle->setFieldAmount(sfLowLimit, usd(0, seed));
sle->setFieldAmount(sfHighLimit, usd(0, seed));
break;
case ltXCHAIN_OWNED_CREATE_ACCOUNT_CLAIM_ID:
sle->setAccountID(sfAccount, id);
sle->at(sfXChainBridge) = xchainBridge(seed);
sle->setFieldU64(sfXChainAccountCreateCount, seed + 1);
break;
case ltESCROW:
sle->setAccountID(sfAccount, id);
sle->setAccountID(sfDestination, other);
sle->setFieldU32(sfSequence, seed + 1);
sle->setFieldAmount(sfAmount, STAmount{XRPAmount{1}});
break;
case ltPAYCHAN:
sle->setAccountID(sfAccount, id);
sle->setAccountID(sfDestination, other);
sle->setFieldU32(sfSequence, seed + 1);
sle->setFieldAmount(sfAmount, STAmount{XRPAmount{1}});
sle->setFieldAmount(sfBalance, STAmount{XRPAmount{}});
sle->setFieldU32(sfSettleDelay, 1);
break;
case ltAMM:
sle->setAccountID(sfAccount, id);
sle->setFieldAmount(sfLPTokenBalance, STAmount{usdIssue(seed), 1});
sle->setFieldIssue(sfAsset, STIssue{sfAsset, xrpIssue()});
sle->setFieldIssue(sfAsset2, STIssue{sfAsset2, usdIssue(seed)});
break;
case ltMPTOKEN_ISSUANCE:
sle->setAccountID(sfIssuer, account(200'000 + seed));
sle->setFieldU32(sfSequence, seed + 1);
sle->setFieldU64(sfOutstandingAmount, 0);
break;
case ltMPTOKEN:
sle->setAccountID(sfAccount, id);
sle->setFieldH192(sfMPTokenIssuanceID, mptID(seed));
sle->setFieldU64(sfMPTAmount, 0);
break;
case ltORACLE:
sle->setAccountID(sfOwner, id);
sle->setFieldVL(sfProvider, Slice("provider", 8));
sle->setFieldVL(sfAssetClass, Slice("currency", 8));
sle->setFieldU32(sfLastUpdateTime, 1);
break;
case ltCREDENTIAL:
sle->setAccountID(sfSubject, id);
sle->setAccountID(sfIssuer, account(40'000 + seed));
sle->setFieldVL(sfCredentialType, Slice("cred_type", 9));
break;
case ltPERMISSIONED_DOMAIN:
sle->setAccountID(sfOwner, id);
sle->setFieldU32(sfSequence, seed + 1);
sle->setFieldArray(sfAcceptedCredentials, credentialRules(other, seed));
break;
case ltDELEGATE:
sle->setAccountID(sfAccount, id);
sle->setAccountID(sfAuthorize, other);
break;
case ltVAULT:
populateVault(sle, seed);
break;
case ltLOAN_BROKER:
sle->setFieldU32(sfSequence, seed + 1);
sle->setFieldH256(sfVaultID, syntheticKey(ltVAULT, 110'000 + seed).key);
sle->setAccountID(sfAccount, account(120'000 + seed));
sle->setAccountID(sfOwner, id);
sle->setFieldU32(sfLoanSequence, 1);
sle->setFieldNumber(sfDebtTotal, stNumber(sfDebtTotal, 0));
sle->setFieldNumber(sfCoverAvailable, stNumber(sfCoverAvailable, 0));
break;
case ltLOAN:
sle->setFieldH256(sfLoanBrokerID, uint256{60'000 + seed});
sle->setFieldU32(sfLoanSequence, seed + 1);
sle->setAccountID(sfBorrower, id);
sle->setFieldU32(sfStartDate, 1);
sle->setFieldU32(sfPaymentInterval, 1);
sle->setFieldU32(sfPaymentRemaining, 0);
sle->setFieldNumber(sfPeriodicPayment, stNumber(sfPeriodicPayment, 1));
sle->setFieldNumber(sfPrincipalOutstanding, stNumber(sfPrincipalOutstanding, 0));
sle->setFieldNumber(sfTotalValueOutstanding, stNumber(sfTotalValueOutstanding, 0));
sle->setFieldNumber(
sfManagementFeeOutstanding, stNumber(sfManagementFeeOutstanding, 0));
break;
default:
break;
}
return sle;
}
void
seedSyntheticDependencies(OpenView& ov, LedgerEntryType const type, std::uint32_t const seed)
{
if (type == ltMPTOKEN)
{
auto issuance = makeSyntheticSle(ltMPTOKEN_ISSUANCE, seed);
ov.rawInsert(issuance);
}
if (type == ltVAULT)
{
auto const vault = makeSyntheticSle(ltVAULT, seed);
MPTID const shareID = vault->getFieldH192(sfShareMPTID);
auto issuance = std::make_shared<SLE>(keylet::mptIssuance(shareID));
auto const issuer = getMPTIssuer(shareID);
issuance->setAccountID(sfIssuer, issuer);
issuance->setFieldU32(sfSequence, seed + 90'001);
issuance->setFieldU64(sfOutstandingAmount, 0);
ov.rawInsert(issuance);
return;
}
if (type == ltLOAN_BROKER)
{
auto vault = makeSyntheticSle(ltVAULT, 110'000 + seed);
ov.rawInsert(vault);
}
}
} // namespace
class InvariantPerf_test : public beast::unit_test::Suite
{
using Clock = std::chrono::steady_clock;
struct Timing
{
Clock::duration mean;
Clock::duration stdDev;
std::size_t samples;
};
template <class F>
Timing
time(std::size_t n, F&& f)
{
assert(n > 0);
double sum = 0;
double sumSquared = 0;
std::size_t samples = 0;
while (samples < n)
{
std::array<long, 100> batch = {};
for (auto& sample : batch)
{
auto const start = Clock::now();
f();
sample = (Clock::now() - start).count();
}
std::ranges::sort(batch);
for (std::size_t i = 35; i < 65 && samples < n; ++i)
{
++samples;
sum += batch[i];
sumSquared += batch[i] * batch[i];
}
}
double const mean = sum / samples;
double const variance = (sumSquared / samples) - (mean * mean);
return {
.mean = Clock::duration{static_cast<Clock::rep>(mean)},
.stdDev = Clock::duration{static_cast<Clock::rep>(std::sqrt(std::max(0.0, variance)))},
.samples = samples};
}
template <class SetupEnv, class SetupContext>
void
report(
std::string_view name,
std::size_t const changeCount,
TxType const txType,
SetupEnv&& setupEnv,
SetupContext&& setupContext)
{
using namespace test::jtx;
testcase(std::string{name});
Env env{*this, perfAmendments()};
setupEnv(env);
env.close();
OpenView ov{*env.current()};
auto tx = makeTx(txType);
test::StreamSink sink{beast::Severity::Warning};
beast::Journal const journal{sink};
ApplyContext ac{
env.app(), ov, tx, tesSUCCESS, env.current()->fees().base, TapNone, journal};
CurrentTransactionRulesGuard const rulesGuard(ov.rules());
setupContext(ac);
BEAST_EXPECT(ac.size() == changeCount);
BEAST_EXPECT(ac.checkInvariants(tesSUCCESS, XRPAmount{}) == tesSUCCESS);
std::size_t attempts = 0;
std::size_t passes = 0;
auto const result = time(3'000, [&] {
++attempts;
if (ac.checkInvariants(tesSUCCESS, XRPAmount{}) == tesSUCCESS)
++passes;
});
std::cout << "InvariantPerf " << name << " changes=" << changeCount
<< " mean=" << std::chrono::duration_cast<std::chrono::nanoseconds>(result.mean)
<< " stdDev="
<< std::chrono::duration_cast<std::chrono::nanoseconds>(result.stdDev)
<< " N=" << result.samples << '\n';
BEAST_EXPECT(passes == attempts);
}
struct BenchContext
{
std::unique_ptr<OpenView> view;
STTx tx;
test::StreamSink sink;
beast::Journal journal;
std::unique_ptr<ApplyContext> applyContext;
BenchContext(
Application& app,
std::shared_ptr<ReadView const> const& base,
TxType const txType)
: view{std::make_unique<OpenView>(base.get(), base)}
, tx{makeTx(txType)}
, sink{beast::Severity::Warning}
, journal{sink}
, applyContext{std::make_unique<
ApplyContext>(app, *view, tx, tesSUCCESS, base->fees().base, TapNone, journal)}
{
}
};
void
seedAndStage(
BenchContext& context,
LedgerEntryType const type,
std::size_t const changeCount,
std::uint32_t const seedBase)
{
for (std::size_t i = 0; i < changeCount; ++i)
{
auto const seed = seedBase + static_cast<std::uint32_t>(i);
try
{
seedSyntheticDependencies(*context.view, type, seed);
auto sle = makeSyntheticSle(type, seed);
auto const key = sle->key();
context.view->rawInsert(sle);
auto const staged = context.applyContext->view().peek(keylet::unchecked(key));
if (!BEAST_EXPECT(staged))
continue;
staged->setFieldU32(sfFlags, staged->getFlags() ^ 0x4000'0000);
context.applyContext->view().update(staged);
}
catch (std::exception const& e)
{
fail(
"synthetic ledger entry setup failed for type " +
std::to_string(static_cast<std::uint16_t>(type)) + " seed " +
std::to_string(seed) + ": " + e.what());
throw;
}
}
}
static bool
check(BenchContext& context)
{
return context.applyContext->checkInvariants(tesSUCCESS, XRPAmount{}) == tesSUCCESS;
}
void
reportAllLedgerEntryTypes(std::size_t const changeCount)
{
using namespace test::jtx;
testcase("all ledger entry types");
Env env{*this, perfAmendments()};
env.close();
CurrentTransactionRulesGuard const rulesGuard(env.current()->rules());
std::vector<std::unique_ptr<BenchContext>> contexts;
contexts.reserve(1 + (2 * changeCount));
auto generic = std::make_unique<BenchContext>(env.app(), env.current(), ttACCOUNT_SET);
std::size_t expectedChanges = 0;
std::uint32_t seedBase = 1;
for (auto const type : kConcreteLedgerTypes)
{
if (needsOwnContext(type))
continue;
seedAndStage(*generic, type, changeCount, seedBase);
expectedChanges += changeCount;
seedBase += 10'000;
}
contexts.push_back(std::move(generic));
for (auto const type : {ltPERMISSIONED_DOMAIN, ltVAULT})
{
for (std::size_t i = 0; i < changeCount; ++i)
{
auto context = std::make_unique<BenchContext>(
env.app(),
env.current(),
type == ltPERMISSIONED_DOMAIN ? ttPERMISSIONED_DOMAIN_SET : ttVAULT_SET);
seedAndStage(*context, type, 1, seedBase + static_cast<std::uint32_t>(i));
++expectedChanges;
contexts.push_back(std::move(context));
}
seedBase += 10'000;
}
std::size_t totalChanges = 0;
for (auto const& context : contexts)
{
totalChanges += context->applyContext->size();
BEAST_EXPECT(check(*context));
}
BEAST_EXPECT(totalChanges == expectedChanges);
BEAST_EXPECT(expectedChanges == changeCount * kConcreteLedgerTypes.size());
std::size_t attempts = 0;
std::size_t passes = 0;
auto const result = time(300, [&] {
++attempts;
bool ok = true;
for (auto const& context : contexts)
ok = check(*context) && ok;
if (ok)
++passes;
});
std::cout << "InvariantPerf all ledger entry types changes_per_type=" << changeCount
<< " total_changes=" << totalChanges
<< " mean=" << std::chrono::duration_cast<std::chrono::nanoseconds>(result.mean)
<< " stdDev="
<< std::chrono::duration_cast<std::chrono::nanoseconds>(result.stdDev)
<< " N=" << result.samples << '\n';
BEAST_EXPECT(passes == attempts);
}
void
reportAccountRootUpdates(std::size_t const changeCount)
{
auto accounts = makeAccounts(changeCount);
report(
"account root updates",
changeCount,
ttACCOUNT_SET,
[&](jtx::Env& env) {
for (auto const& account : accounts)
env.fund(jtx::XRP(1'000), account);
},
[&](ApplyContext& ac) {
for (auto const& account : accounts)
{
auto const sle = ac.view().peek(keylet::account(account.id()));
if (!BEAST_EXPECT(sle))
continue;
sle->at(sfSequence) += 1;
ac.view().update(sle);
}
});
}
void
reportTrustLineUpdates(std::size_t const changeCount)
{
using namespace test::jtx;
Account const gateway{"gateway"};
auto const usd = gateway["USD"];
auto accounts = makeAccounts(changeCount);
report(
"trust line updates",
changeCount,
ttPAYMENT,
[&](Env& env) {
env.fund(XRP(1'000'000), gateway);
for (auto const& account : accounts)
{
env.fund(XRP(1'000), account);
env.trust(usd(1'000), account);
}
},
[&](ApplyContext& ac) {
for (std::size_t i = 0; i < accounts.size(); ++i)
{
auto const sle = ac.view().peek(keylet::line(accounts[i], usd));
if (!BEAST_EXPECT(sle))
continue;
sle->setFieldAmount(sfBalance, usd(static_cast<std::int64_t>(i + 1)));
ac.view().update(sle);
}
});
}
void
reportOfferCreates(std::size_t const changeCount)
{
using namespace test::jtx;
Account const trader{"trader"};
Account const gateway{"gateway"};
auto const usd = gateway["USD"];
report(
"offer creates",
changeCount,
ttOFFER_CREATE,
[&](Env& env) {
env.fund(XRP(1'000'000), gateway, trader);
env.trust(usd(1'000'000), trader);
env(pay(gateway, trader, usd(1'000'000)));
},
[&](ApplyContext& ac) {
for (std::size_t i = 0; i < changeCount; ++i)
{
auto const seq = static_cast<std::uint32_t>(1'000'000 + i);
auto sle = std::make_shared<SLE>(keylet::offer(trader.id(), seq));
sle->setAccountID(sfAccount, trader.id());
sle->setFieldU32(sfSequence, seq);
sle->setFieldAmount(sfTakerPays, usd(1));
sle->setFieldAmount(sfTakerGets, XRP(1));
ac.view().insert(sle);
}
});
}
public:
void
run() override
{
for (auto const changeCount : {std::size_t{1}, std::size_t{16}, std::size_t{64}})
{
reportAccountRootUpdates(changeCount);
reportTrustLineUpdates(changeCount);
reportOfferCreates(changeCount);
reportAllLedgerEntryTypes(changeCount);
}
}
};
BEAST_DEFINE_TESTSUITE_MANUAL(InvariantPerf, app, xrpl);
} // namespace xrpl::test