Compare commits

...

9 Commits

Author SHA1 Message Date
Vito
fb4c2bcc91 refactor: improve assertion macro 2026-06-04 15:43:24 +02:00
Vito Tumas
86cfde06d9 Update include/xrpl/beast/utility/instrumentation.h
Co-authored-by: xrplf-ai-reviewer[bot] <266832837+xrplf-ai-reviewer[bot]@users.noreply.github.com>
2026-06-04 12:57:32 +02:00
Vito Tumas
81675314cf Merge branch 'develop' into tapanito/cond-assert 2026-06-04 11:33:21 +02:00
Vito Tumas
45ac220e49 Merge branch 'develop' into tapanito/cond-assert 2026-06-02 17:46:55 +02:00
Vito
950f6fd4db address reviewer feedback 2026-06-02 17:46:30 +02:00
Vito
23dc3b2cbb docs: Correct XRPL_ASSERT_IF comment on guard evaluation
The previous wording claimed guard is always evaluated in release builds,
but a side-effect-free guard may be optimized away when the assertion
body is a no-op. Reword to warn callers not to rely on side effects
rather than making an unconditional evaluation guarantee.
2026-06-02 12:58:51 +02:00
Vito
f9f07704d2 refactor: Simplify XRPL_ASSERT_IF to fixed 3-parameter form
The __VA_ARGS__ approach required an MSVC-specific EXPAND workaround.
Switch back to explicit (guard, cond, message) parameters and document
the bare-comma restriction on cond instead.
2026-06-02 12:58:00 +02:00
Vito
5d187be11a fix: Work around MSVC __VA_ARGS__ forwarding bug in XRPL_ASSERT_IF
MSVC's legacy preprocessor treats __VA_ARGS__ as a single token when
it is passed as an argument to another function-like macro, causing
ALWAYS_OR_UNREACHABLE to receive one argument instead of two and
triggering C4003. Wrap the inner XRPL_ASSERT call in an identity macro
(XRPL_ASSERT_IF_EXPAND) that forces a rescan, splitting the tokens back
into separate arguments before ALWAYS_OR_UNREACHABLE sees them.
2026-06-02 12:10:43 +02:00
Vito
cd04055559 refactor: Introduce XRPL_ASSERT_IF for amendment-gated assertions
Add XRPL_ASSERT_IF(guard, ...) to instrumentation.h — a conditional
assertion that fires only when guard is true, using __VA_ARGS__ to
match the XRPL_ASSERT_PARTS variadic convention and avoid bare-comma
pitfalls in cond. guard is always evaluated (even in release builds
where the assertion body is stripped), so it should be side-effect-free.

Replace all amendment-gated XRPL_ASSERT / XRPL_ASSERT_PARTS call sites
in LendingHelpers.cpp and MPTokenHelpers.cpp with the new macro. In
doOverpayment, hoist rules.enabled(fixCleanup3_2_0) to a single bool
to avoid three repeated set lookups, and inline the three intermediate
Number values into a lambda so they are only computed when the guard
fires.
2026-06-02 11:11:11 +02:00
3 changed files with 57 additions and 47 deletions

View File

@@ -11,6 +11,8 @@
// Macros below are copied from antithesis_sdk.h and slightly simplified
// The duplication is because Visual Studio 2019 cannot compile that header
// even with the option -Zc:__cplusplus added.
// NOTE: cond must not contain bare commas outside () or []. Commas inside {}
// are not protected by the preprocessor and would be parsed as extra arguments.
#define ALWAYS(cond, message, ...) assert((message) && (cond))
#define ALWAYS_OR_UNREACHABLE(cond, message) assert((message) && (cond))
#define SOMETIMES(cond, message, ...)
@@ -22,6 +24,8 @@
#define XRPL_ASSERT_PARTS(cond, function, description, ...) \
XRPL_ASSERT(cond, function " : " description)
#define XRPL_ASSERT_IF(guard, cond, message) XRPL_ASSERT(!(guard) || (cond), message)
// How to use the instrumentation macros:
//
// * XRPL_ASSERT if cond must be true but the line might not be reached during
@@ -29,6 +33,14 @@
// * XRPL_ASSERT_PARTS is for convenience, and works like XRPL_ASSERT, but
// splits the message param into "function" and "description", then joins
// them with " : " before passing to XRPL_ASSERT.
// * XRPL_ASSERT_IF(guard, cond, message) asserts the implication
// `guard => cond`: it can only fail when guard is true (e.g. an amendment
// is enabled) and cond is false. Unlike `if (guard) XRPL_ASSERT(...)`, the
// assertion site is always evaluated, so the fuzzer registers it
// unconditionally; cond itself is short-circuited and only evaluated when
// guard is true. NOTE: do not rely on side effects in guard — in release
// builds the assertion body is stripped, and the compiler may optimize away
// a side-effect-free guard entirely.
// * ALWAYS if cond must be true _and_ the line must be reached during fuzzing.
// Same like `assert` in normal use.
// * REACHABLE if the line must be reached during fuzzing

View File

@@ -798,44 +798,44 @@ doOverpayment(
// (P * factor) / factor round-trip can leave the new principal one
// scale-unit high, so these equalities do not hold on the pre-amendment
// code path and must be gated to match the fix they verify.
if (rules.enabled(fixCleanup3_2_0))
{
// The valueChange returned by tryOverpayment satisfies
// valueChange = (newInterestDue - oldInterestDue) + untrackedInterest.
// Using the loan-state identity v = p + i + m and the adjacent
// `principal change agrees` assertion (dp = oldP - newP), this
// rearranges into three independently-computable terms:
//
// 1. TVO change beyond what principal repayment alone explains:
// newTVO - (oldTVO - dp)
// 2. Management fee released by re-amortization (positive when
// mfee decreased; zero when managementFeeRate == 0):
// oldMfee - newMfee
// 3. The overpayment's penalty interest part (= untrackedInterest
// for the overpayment path; see computeOverpaymentComponents):
// trackedInterestPart()
[[maybe_unused]] Number const tvoChange = newRoundedLoanState.valueOutstanding -
(totalValueOutstandingProxy - overpaymentComponents.trackedPrincipalDelta);
[[maybe_unused]] Number const managementFeeReleased =
managementFeeOutstandingProxy - newRoundedLoanState.managementFeeDue;
[[maybe_unused]] Number const interestPart = overpaymentComponents.trackedInterestPart();
//
// The valueChange returned by tryOverpayment satisfies
// valueChange = (newInterestDue - oldInterestDue) + untrackedInterest.
// Using the loan-state identity v = p + i + m and the adjacent
// `principal change agrees` assertion (dp = oldP - newP), this
// rearranges into three independently-computable terms:
//
// 1. TVO change beyond what principal repayment alone explains:
// newTVO - (oldTVO - dp)
// 2. Management fee released by re-amortization (positive when
// mfee decreased; zero when managementFeeRate == 0):
// oldMfee - newMfee
// 3. The overpayment's penalty interest part (= untrackedInterest
// for the overpayment path; see computeOverpaymentComponents):
// trackedInterestPart()
bool const fix320Enabled = rules.enabled(fixCleanup3_2_0);
XRPL_ASSERT_IF(
fix320Enabled,
overpaymentComponents.trackedPrincipalDelta ==
principalOutstandingProxy - newRoundedLoanState.principalOutstanding,
"xrpl::detail::doOverpayment : principal change agrees");
XRPL_ASSERT_PARTS(
overpaymentComponents.trackedPrincipalDelta ==
principalOutstandingProxy - newRoundedLoanState.principalOutstanding,
"xrpl::detail::doOverpayment",
"principal change agrees");
XRPL_ASSERT_IF(
fix320Enabled,
[&] {
Number const tvoChange = newRoundedLoanState.valueOutstanding -
(totalValueOutstandingProxy - overpaymentComponents.trackedPrincipalDelta);
Number const managementFeeReleased =
managementFeeOutstandingProxy - newRoundedLoanState.managementFeeDue;
Number const interestPart = overpaymentComponents.trackedInterestPart();
return loanPaymentParts.valueChange == tvoChange + managementFeeReleased + interestPart;
}(),
"xrpl::detail::doOverpayment : interest paid agrees");
XRPL_ASSERT_PARTS(
loanPaymentParts.valueChange == tvoChange + managementFeeReleased + interestPart,
"xrpl::detail::doOverpayment",
"interest paid agrees");
XRPL_ASSERT_PARTS(
overpaymentComponents.trackedPrincipalDelta == loanPaymentParts.principalPaid,
"xrpl::detail::doOverpayment",
"principal payment matches");
}
XRPL_ASSERT_IF(
fix320Enabled,
overpaymentComponents.trackedPrincipalDelta == loanPaymentParts.principalPaid,
"xrpl::detail::doOverpayment : principal payment matches");
// All validations passed, so update the proxy objects (which will
// modify the actual Loan ledger object)
@@ -1326,13 +1326,11 @@ computeOverpaymentComponents(
TenthBips32 const overpaymentFeeRate,
TenthBips16 const managementFeeRate)
{
if (rules.enabled(fixCleanup3_2_0))
{
XRPL_ASSERT(
overpayment > 0 && isRounded(asset, overpayment, loanScale),
"xrpl::detail::computeOverpaymentComponents : valid overpayment "
"amount");
}
XRPL_ASSERT_IF(
rules.enabled(fixCleanup3_2_0),
overpayment > 0 && isRounded(asset, overpayment, loanScale),
"xrpl::detail::computeOverpaymentComponents : valid overpayment "
"amount");
// First, deduct the fixed overpayment fee from the total amount.
// This reduces the effective payment that will be applied to the loan.

View File

@@ -736,10 +736,10 @@ unlockEscrowMPT(
STAmount const& grossAmount,
beast::Journal j)
{
if (!view.rules().enabled(fixTokenEscrowV1))
{
XRPL_ASSERT(netAmount == grossAmount, "xrpl::unlockEscrowMPT : netAmount == grossAmount");
}
XRPL_ASSERT_IF(
!view.rules().enabled(fixTokenEscrowV1),
netAmount == grossAmount,
"xrpl::unlockEscrowMPT : netAmount == grossAmount");
auto const& issuer = netAmount.getIssuer();
auto const& mptIssue = netAmount.get<MPTIssue>();