Skip to content

fix: Pin overpayment principal reduction to exact on-grid value#7360

Open
Tapanito wants to merge 2 commits into
developfrom
tapanito/lending-disable-assertions
Open

fix: Pin overpayment principal reduction to exact on-grid value#7360
Tapanito wants to merge 2 commits into
developfrom
tapanito/lending-disable-assertions

Conversation

@Tapanito
Copy link
Copy Markdown
Collaborator

doOverpayment asserted that trackedPrincipalDelta exactly equals the reduction in stored principal after re-amortization. The assertion was failing because tryOverpayment re-derived the new principal from the new periodic payment via a (P * factor) / factor round-trip, which is not exact in Number arithmetic. A positive residual, combined with Upward rounding, pushed the stored principal one scale-unit high, making the principal drop by delta - 1 unit rather than delta.

Under fixCleanup3_2_0, pin the new principal to the exact on-grid reduction (roundedOldState.principalOutstanding - trackedPrincipalDelta) instead. Both operands are already at loan scale, so the subtraction is exact and requires no further rounding. Interest is re-derived via constructLoanState so the V = P + I + M invariant still holds. The three assertions in doOverpayment that encode this invariant are also gated behind the same amendment, since they only hold once the fix ensures the exact reduction.

Adds testBugOverpaymentPrincipalChange to reproduce the core-dump scenario (principal=100, rate=1, n=3, interval=60, one period + residual overpayment of 0.049999998) and
testLoanSetNearZeroInterestRateSucceeds to verify scheduled payments cover principal at InterestRate=1.

High Level Overview of Change

Context of Change

API Impact

  • Public API: New feature (new methods and/or new fields)
  • Public API: Breaking change (in general, breaking changes should only impact the next api_version)
  • libxrpl change (any change that may affect libxrpl or dependents of libxrpl)
  • Peer protocol change (must be backward compatible or bump the peer protocol version)

doOverpayment asserted that trackedPrincipalDelta exactly equals the
reduction in stored principal after re-amortization. The assertion was
failing because tryOverpayment re-derived the new principal from the
new periodic payment via a (P * factor) / factor round-trip, which is
not exact in Number arithmetic. A positive residual, combined with
Upward rounding, pushed the stored principal one scale-unit high,
making the principal drop by delta - 1 unit rather than delta.

Under fixCleanup3_2_0, pin the new principal to the exact on-grid
reduction (roundedOldState.principalOutstanding - trackedPrincipalDelta)
instead. Both operands are already at loan scale, so the subtraction is
exact and requires no further rounding. Interest is re-derived via
constructLoanState so the V = P + I + M invariant still holds. The
three assertions in doOverpayment that encode this invariant are also
gated behind the same amendment, since they only hold once the fix
ensures the exact reduction.

Adds testBugOverpaymentPrincipalChange to reproduce the core-dump
scenario (principal=100, rate=1, n=3, interval=60, one period +
residual overpayment of 0.049999998) and
testLoanSetNearZeroInterestRateSucceeds to verify scheduled payments
cover principal at InterestRate=1.
@Tapanito Tapanito requested a review from ximinez May 29, 2026 17:33
Copy link
Copy Markdown
Contributor

@xrplf-ai-reviewer xrplf-ai-reviewer Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Had a look at this

Debug return; left in run() at line 8609 short-circuits the entire test suite — see inline.


Review by ReviewBot 🤖

Review by Claude Opus 4.6 · Prompt: V15

Comment thread src/test/app/Loan_test.cpp Outdated
@ximinez ximinez added this to the 3.2.0 milestone May 29, 2026
@ximinez ximinez requested a review from Copilot May 29, 2026 17:41
Copy link
Copy Markdown
Contributor

@xrplf-ai-reviewer xrplf-ai-reviewer Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Gave this a look

One unrelated file deletion flagged inline — confirm cfg/validators-example.txt removal was intentional.


Review by ReviewBot 🤖

Review by Claude Opus 4.6 · Prompt: V15

@Tapanito Tapanito force-pushed the tapanito/lending-disable-assertions branch from 1fc141f to c737fe2 Compare May 29, 2026 17:43
Copy link
Copy Markdown
Contributor

@xrplf-ai-reviewer xrplf-ai-reviewer Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No issues.

Review by Claude Opus 4.6 · Prompt: V15

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Fixes a debug-assertion abort in doOverpayment ("principal change agrees") caused by a (P*factor)/factor round-trip in tryOverpayment that left the re-amortized principal one scale-unit above the exact on-grid target. Under fixCleanup3_2_0, the new principal is now pinned to roundedOldState.principalOutstanding - trackedPrincipalDelta and the three related invariant assertions are gated on the same amendment. Two regression tests are added.

Changes:

  • Pin re-amortized principal to exact on-grid value in tryOverpayment under fixCleanup3_2_0, deriving interest via constructLoanState.
  • Gate three invariant assertions in doOverpayment (principal change agrees, interest paid agrees, principal payment matches) behind fixCleanup3_2_0.
  • Add testBugOverpaymentPrincipalChange and testLoanSetNearZeroInterestRateSucceeds regression tests.
  • Unrelated: deletes cfg/xrpld-example.cfg and cfg/validators-example.txt.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated no comments.

File Description
src/libxrpl/ledger/helpers/LendingHelpers.cpp Pin principal in tryOverpayment and amendment-gate three doOverpayment invariants.
src/test/app/Loan_test.cpp Add regression tests for the overpayment crash and near-zero-rate LoanSet.
cfg/xrpld-example.cfg Deletes example config that is still referenced by install/packaging — appears out of scope.
cfg/validators-example.txt Deletes example validators file still referenced by install/packaging — appears out of scope.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@codecov
Copy link
Copy Markdown

codecov Bot commented May 29, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 82.4%. Comparing base (2f3558c) to head (c737fe2).

Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff            @@
##           develop   #7360     +/-   ##
=========================================
- Coverage     82.4%   82.4%   -0.0%     
=========================================
  Files         1011    1011             
  Lines        76477   76483      +6     
  Branches      7322    7314      -8     
=========================================
  Hits         63005   63005             
- Misses       13472   13478      +6     
Files with missing lines Coverage Δ
src/libxrpl/ledger/helpers/LendingHelpers.cpp 90.7% <100.0%> (+0.1%) ⬆️

... and 7 files with indirect coverage changes

Impacted file tree graph

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@bthomee bthomee requested a review from a1q123456 May 29, 2026 19:04
@kennyzlei kennyzlei requested a review from gregtatcam May 29, 2026 20:11
Copy link
Copy Markdown
Contributor

@a1q123456 a1q123456 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

some minor issues

// pushes the stored principal one scale-unit high and breaks the
// "principal change agrees" check. Pin the principal to the exact value and
// re-derive interest so the V = P + I + M invariant still holds.
if (rules.enabled(fixCleanup3_2_0))
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we're going to rewrite the variable anyway, it'll be nice avoiding calling computeTheoreticalLoanState when fixCleanup3_2_0 is enabled.

}

// Gated together with the principal-change check above (same rationale).
if (rules.enabled(fixCleanup3_2_0))
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This if can be merged with the if block above.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants