Compare commits

...

3 Commits

Author SHA1 Message Date
Vito
110587862e fix: disable tx invariants 2026-06-05 18:03:37 +02:00
Vito
f2b226f36a Merge remote-tracking branch 'origin/develop' into tapanito/perf-stamount 2026-06-05 18:03:04 +02:00
Vito
943c92ca1e perf: Dispatch hasInvalidAmount on type tag instead of dynamic_cast
hasInvalidAmount runs on the per-transaction invariant-checking path
(ValidAmounts::finalize) and at preflight, walking every field of every
modified entry. The previous implementation tried a dynamic_cast chain
(STAmount, STObject, STArray) on each field, so every scalar non-amount
field paid three failed RTTI casts.

Dispatch on the serialized type tag via getSType() and a switch instead.
The object-like tags all denote STObject subclasses (STLedgerEntry, STTx),
so the downcast is sound; safeDowncast keeps a dynamic_cast validity
assert in debug builds while compiling to static_cast in release.
2026-06-04 15:27:22 +02:00
2 changed files with 32 additions and 22 deletions

View File

@@ -1250,16 +1250,30 @@ hasInvalidAmount(STBase const& field, int depth, beast::Journal j)
return true;
}
if (auto const amount = dynamic_cast<STAmount const*>(&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<STAmount const&>(field);
return !isLegalMPT(amount) || !isLegalNet(amount);
}
if (auto const object = dynamic_cast<STObject const*>(&field))
return hasInvalidAmount(*object, depth + 1, j);
case STI_OBJECT:
case STI_LEDGERENTRY:
case STI_TRANSACTION:
return hasInvalidAmount(safeDowncast<STObject const&>(field), depth + 1, j);
if (auto const array = dynamic_cast<STArray const*>(&field))
return hasInvalidAmount(*array, depth + 1, j);
case STI_ARRAY:
return hasInvalidAmount(safeDowncast<STArray const&>(field), depth + 1, j);
return false;
default:
return false;
}
}
bool

View File

@@ -1172,21 +1172,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