From 97ca7d57bcedab341e7887cd7950299d2bde9598 Mon Sep 17 00:00:00 2001 From: Vito Tumas <5780819+Tapanito@users.noreply.github.com> Date: Wed, 10 Jun 2026 13:44:57 +0200 Subject: [PATCH 1/2] perf: Dispatch "hasInvalidAmount()" on type tag instead of dynamic_cast (#7402) --- src/libxrpl/protocol/STAmount.cpp | 32 ++++++++++++++++++++++++------- 1 file changed, 25 insertions(+), 7 deletions(-) diff --git a/src/libxrpl/protocol/STAmount.cpp b/src/libxrpl/protocol/STAmount.cpp index 449efc9543..748d00f25a 100644 --- a/src/libxrpl/protocol/STAmount.cpp +++ b/src/libxrpl/protocol/STAmount.cpp @@ -1138,16 +1138,34 @@ hasInvalidAmount(STBase const& field, int depth, beast::Journal j) return true; } - if (auto const amount = dynamic_cast(&field)) - return !isLegalMPT(*amount) || !isLegalNet(*amount); + // Dispatch on the serialized type tag rather than RTTI: this is on the invariant-checking path + // and a dynamic_cast chain over every field of every modified entry is measurably expensive. + // The object-like tags below all denote STObject subclasses (STLedgerEntry, STTx), so the + // downcast is sound; nested fields are only ever plain STI_OBJECT / STI_ARRAY containers. + // safeDowncast keeps a dynamic_cast validity assert in debug builds while compiling to + // static_cast in release. + switch (field.getSType()) + { + case STI_AMOUNT: { + auto const& amount = safeDowncast(field); + return !isLegalMPT(amount) || !isLegalNet(amount); + } - if (auto const object = dynamic_cast(&field)) - return hasInvalidAmount(*object, depth + 1, j); + case STI_OBJECT: + case STI_LEDGERENTRY: + case STI_TRANSACTION: + return hasInvalidAmount(safeDowncast(field), depth + 1, j); - if (auto const array = dynamic_cast(&field)) - return hasInvalidAmount(*array, depth + 1, j); + case STI_ARRAY: + return hasInvalidAmount(safeDowncast(field), depth + 1, j); - return false; + default: { + XRPL_ASSERT( + dynamic_cast(&field) == nullptr, + "xrpl::hasInvalidAmount : unhandled STObject type"); + return false; + } + } } bool From 83cc5df72e9fa2f4d9d91a674908afcd6a50302a Mon Sep 17 00:00:00 2001 From: Vito Tumas <5780819+Tapanito@users.noreply.github.com> Date: Wed, 10 Jun 2026 14:05:53 +0200 Subject: [PATCH 2/2] fix: Disable transaction invariants (#7409) --- src/libxrpl/tx/Transactor.cpp | 26 +++++++++++--------------- 1 file changed, 11 insertions(+), 15 deletions(-) diff --git a/src/libxrpl/tx/Transactor.cpp b/src/libxrpl/tx/Transactor.cpp index 68d3f36916..aa7b81c015 100644 --- a/src/libxrpl/tx/Transactor.cpp +++ b/src/libxrpl/tx/Transactor.cpp @@ -1171,21 +1171,17 @@ Transactor::checkTransactionInvariants(TER result, XRPAmount fee) [[nodiscard]] TER Transactor::checkInvariants(TER result, XRPAmount fee) { - // Transaction invariants first (more specific). These check post-conditions of the specific - // transaction. If these fail, the transaction's core logic is wrong. - auto const txResult = checkTransactionInvariants(result, fee); - - // Protocol invariants second (broader). These check properties that must hold regardless of - // transaction type. - auto const protoResult = ctx_.checkInvariants(result, fee); - - // Fail if either check failed. tef (fatal) takes priority over tec. - if (protoResult == tefINVARIANT_FAILED) - return tefINVARIANT_FAILED; - if (txResult == tecINVARIANT_FAILED || protoResult == tecINVARIANT_FAILED) - return tecINVARIANT_FAILED; - - return result; + /* + * DISABLED for 3.2.0 — Must be re-introduced for 3.3.0 + * + * Transaction invariants are disabled due to a performance regression: + * the two-pass design (transaction-specific invariants + protocol invariants) + * iterates over modified ledger entries twice per transaction. + * + * Until resolved, only protocol invariants are checked (delegated to ctx_). + * This is safe because all transaction invariants in 3.2.0 are no-ops. + */ + return ctx_.checkInvariants(result, fee); } //------------------------------------------------------------------------------ ApplyResult