Conversation
Co-authored-by: Ed Hennis <ed@ripple.com>
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## develop #6632 +/- ##
=========================================
- Coverage 81.5% 81.4% -0.0%
=========================================
Files 999 1006 +7
Lines 74458 74513 +55
Branches 7556 7559 +3
=========================================
+ Hits 60649 60688 +39
- Misses 13809 13825 +16
🚀 New features to boost your workflow:
|
Replace std::optional<int> with plain int, using std::numeric_limits<int>::min() as a sentinel to detect uninitialized state. The optional was unnecessary since every code path that stores a DeltaInfo into deltas_ always sets scale.
There was a problem hiding this comment.
Five issues flagged inline: assert-disabled sentinel propagation, zero-balance trust-line scale edge case, rounding tolerance masking financial discrepancies, dead asset parameter in computeMinScale, and potential round-up at the cover-availability security boundary.
Review by Claude Opus 4.6 · Prompt: V12
…nused asset parameter The function returns the maximum (coarsest) exponent, not the minimum scale. Rename to computeCoarsestScale to accurately reflect behavior. Also remove the unused Asset parameter from the signature.
|
As I was reading this PR, I kept thinking if we can get away from repeated allocation of @ximinez Have you already tried the epsilon based comparison approach? I ran some scenarios on this with AI and asked it to check if the accuracy holds. This is a short summary of my suggestion: When considering using Performance:Each Robustness at rounding boundaries:The a = 5,050,000,000,000,000,499 × 10⁻¹⁷ //→ rounds down to ...000 × 10⁻¹⁴
b = 5,050,000,000,000,000,501 × 10⁻¹⁷ //→ rounds up to ...001 × 10⁻¹⁴
roundToAsset: round(a) ≠ round(b) //→ false invariant violation
epsilon: |a - b| = 2×10⁻¹⁷ < 10⁻¹⁴ //→ correctly equalThis is rare (requires digit 16 at a rounding tie + opposite-direction noise), but interest accrual arithmetic could produce it. One tradeoff — the > comparison: The single magnitude check vaultDeltaAssets > txAmount (deposit must not exceed tx amount) would become Summary:Across all 14 rounded comparisons in finalize (8 equality, 5 sign, 1 magnitude), epsilon is equivalent or more robust for 13, with a negligible theoretical tradeoff on 1. It's also cheaper to compute. |
pratikmankawde
left a comment
There was a problem hiding this comment.
Added a comment about a slightly diff., but performant, approach on implementing this.
| }; | ||
|
|
||
| public: | ||
| struct DeltaInfo final |
There was a problem hiding this comment.
I think it's worth implementing arithmetic operations for this class so that we can simplify the code a lot.
There was a problem hiding this comment.
Sorry, I don't see what you did here?
a1q123456
left a comment
There was a problem hiding this comment.
Left some suggestions. looks good otherwise.
There was a problem hiding this comment.
Took a pass through this
One high-severity issue: the value_or(STAmount::cMaxOffset) fallback at line 1068 contradicts the assert above it and silently disables invariant checks in release builds — see inline comments.
Review by ReviewBot 🤖
Review by Claude Opus 4.6 · Prompt: V12
|
@pratikmankawde , while your suggestion might work, it's out of scope. The current implementation is sufficient for the task at hand. |
epsilon based comparisons may be faster but the errors can accumulate after a couple of arithmetic operations and you'll be in the state where you are not sure if the two numbers are not equal or it was because of accumulated error |
|
|
||
| if (*vaultDeltaAssets >= zero) | ||
| // Get the most coarse scale to round calculations to | ||
| auto const totalDelta = DeltaInfo{ |
There was a problem hiding this comment.
We can probably have something like
DeltaInfo DeltaInfo::diffVault(Vault const& a, Vault const& b)
{
XRPL_ASSERT(a.asset == b.asset, "Vaults have the same asset");
return DeltaInfo{
a.assetTotal - b.assetTotal,
std::max(
scale(a.assetTotal, b.asset),
scale(b.assetTotal, b.asset)
)
};
}
I am not suggesting to use epsilon for operations, I am suggesting it for comparisons. So, we won't be loosing any precision iteratively. @Tapanito The current implementation might solve the problem at hand, but it also introduces some latency. Also. the current solution is not a simple one(or even easy to understand and maintain) to be replaced later. It will take equal, if not more, efforts to update it. My suggestion is to just think about the alternate solution, see if it works and if it does, then maybe prefer that one. Specially if this is not high priority fix for currently planned release. |
| return std::nullopt; | ||
|
|
||
| return it->second * sign; | ||
| return DeltaInfo{it->second.delta * sign, it->second.scale}; |
There was a problem hiding this comment.
This is the only occurrence so it's probably not 100% worth it but I think it looks clearer if we can have:
DeltaInfo operator-(DeltaInfo const& info, std::int8_t sign /* or a bool */)
{
return Deltainfo{info.delta * sign, info.scale}
}
and then this piece of code will become:
return it->second * sign;
| if (*vaultDeltaShares * -1 != *accountDeltaShares) | ||
| // We don't need to round shares, they are integral MPT | ||
| auto const& vaultDeltaShares = *maybeVaultDeltaShares; | ||
| if (vaultDeltaShares.delta * -1 != accountDeltaShares.delta) |
There was a problem hiding this comment.
If you have the operator above, this can be simplified to:
if (vaultDeltaShares * -1 != accountDeltaShares)
| if (*accountDeltaShares <= zero) | ||
| // We don't need to round shares, they are integral MPT | ||
| auto const& accountDeltaShares = *maybeAccDeltaShares; | ||
| if (accountDeltaShares.delta <= zero) |
There was a problem hiding this comment.
Also if you have a comparison operator this can be simplified
There was a problem hiding this comment.
Something like:
bool operator<=>(DeltaInfo const& a, Number const& b)
{
return operator<=>(a.delta, b);
}
There was a problem hiding this comment.
I'm not convinced by operator<=> against Number. Writing accountDeltaShares <= zero instead of accountDeltaShares.delta <= zero saves 6 characters but hides the fact you're comparing only the delta and ignoring scale. For someone reading the invariant logic, .delta makes the intent explicit. Same concern with operator!= between two DeltaInfos — does it compare scale too? That ambiguity seems worse than the verbosity.
Thanks for the detailed analysis @pratikmankawde. Regarding performance invariant checks run once per vault transaction, so the cost of a few On the rounding boundary edge case — it's a real scenario but requires very specific alignment of mantissa digits at a rounding tie. With The tradeoff on magnitude/sign comparisons is actually the more concerning part of the epsilon approach. The invariant checks aren't just equality there are sign checks and Also, even though epsilon would only be used for comparison, the values being compared are already products of arithmetic on We've also simplified the rounding code in this PR (extracted |
…putation The pattern of computing a delta between two Numbers while taking the coarsest scale was repeated 3 times (deposit, withdrawal, clawback). Extract into a static DeltaInfo::makeDelta method.
Summary
Cherry-picked from: #6217
NumberandSTAmountDeltaInfostruct that tracks both the delta value and its scale (exponent), and acomputeMinScalehelper that finds the coarsest scale across all values being comparedscale()utility function inSTAmount.hthat returns the STAmount exponent for a givenNumber/AssetpaircomputeMinScaleand an end-to-end Loan test (testLoanCoverWithdrawAfterInterest) that exercises the rounding fix through a multi-step lending scenarioEdit:
The PR contains one more additional commit: Replace std::optional with plain int, using std::numeric_limits::min() as a sentinel to detect uninitialized state. The optional was unnecessary since every code path that stores a DeltaInfo into deltas_ always sets scale.
Context
When vault invariants compare balance changes across different ledger objects (vault pseudo-account, trust lines, MPTs), the values may have been computed via different arithmetic paths, resulting in
Numbervalues that are mathematically equal but differ at insignificant digits. Previously, invariant checks compared these values directly, which could cause spurious invariant failures — particularly in lending scenarios where interest accrual introduces small rounding discrepancies.The fix rounds all compared values to the coarsest (least precise) scale present among the operands before performing equality/inequality checks, ensuring that insignificant digit differences don't trigger false failures.
High Level Overview of Change
Context of Change
API Impact
libxrplchange (any change that may affectlibxrplor dependents oflibxrpl)