Skip to content

fix: Parse IOU amounts above INT64_MAX without wrapping - #8190

Open
markneonin wants to merge 1 commit into
XRPLF:developfrom
markneonin:markneonin/fix-iou-json-mantissa-wrap
Open

markneonin wants to merge 1 commit into
XRPLF:developfrom
markneonin:markneonin/fix-iou-json-mantissa-wrap

Conversation

@markneonin

@markneonin markneonin commented Sep 8, 2026

Copy link
Copy Markdown

High Level Overview of Change

An issued currency value whose digit string exceeds INT64_MAX no longer wraps into an unrelated number. Fixes #8188.

LimitAmount.value on a TrustSet before after
"-1000" temBAD_LIMIT unchanged
"1000.0000000000000000" temBAD_LIMIT, limit -844.6744073709552 tesSUCCESS, limit 1000
"-1000.0000000000000000" tesSUCCESS, limit 844.6744073709552 temBAD_LIMIT
"18446744073709551615" limit -1 limit 1844674407370955e4

Context of Change

partsFromString() reports the mantissa as an unsigned 64-bit value with the sign kept apart from it. amountFromJson() passed those parts to the STAmount constructor, whose range check only throws for integral() assets, so for an IOU STAmount::iou() narrowed the mantissa with a static_cast and then negated it, wrapping the value and flipping its sign. What decided this was the digit string, not the number: "-1000" and "-1000.0000000000000000" are the same value, but only the second one has 19 digits.

The fix routes such values through Number, which keeps the mantissa and the sign apart, and lets STAmount::fromNumber() normalize them into the range an IOU can hold. Digits beyond the 16 an IOU keeps are rounded away, as they already are for any other over-precise value — "12345678901234567" has always been accepted as 1234567890123457e1. Number::Unchecked{} rather than Normalized{} keeps the result identical under every MantissaScale. amountFromString() shares the defect and is fixed alongside. A value too large to represent, such as "18446744073709551615e77", now throws std::overflow_error where it previously wrapped to a small number.

No amendment is needed: only JSON parsing changes. Transactions reaching consensus are deserialized from binary, where the IOU mantissa is masked to 54 bits and range-checked before canonicalize() runs. STParsedJSONObject is only constructed from TransactionSign.cpp, Simulate.cpp and the node-local loadLedger path. Same reasoning as #5990.

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)

amountFromJson() and amountFromString() change how they parse a digit string above INT64_MAX. API-CHANGELOG.md is updated under "Bugfixes in 3.4.0".

Test Plan

xrpl.protocol.STAmount, parse json (iou): the same numbers written with and without trailing zeros parse to the same amount in both signs, values spanning the unsigned range keep their sign and round to 16 digits, both sides of the INT64_MAX boundary land on the same value, and "18446744073709551616" is still rejected. xrpl.app.TrustSet: "-1500.0000000000000000" is rejected with temBAD_LIMIT, and a new testcase reads the trust line created by "1500.0000000000000000" back out of the ledger.

Every new assertion fails without the fix. Verified with xrpld --unittest (248 suites, 0 failures), xrpl_tests, and pre-commit.

@github-actions

github-actions Bot commented Sep 8, 2026

Copy link
Copy Markdown

⚠️ This PR contains unsigned commits. To get your PR merged, please sign them. ⚠️

If only the most recent commit is unsigned, you can run:

  1. Amend the commit: git commit --amend --no-edit -n -S
  2. Overwrite the commit: git push --force-with-lease

If multiple commits are unsigned, you can run:

  1. Go into interactive rebase mode: git rebase --interactive HEAD~<NUM_OF_COMMITS>, where NUM_OF_COMMITS is the number of most recent commits that will be available to edit.
  2. Change "pick" to "edit" for the commits you need to sign, and then save and exit.
  3. For each commit, run: git commit --amend --no-edit -n -S
  4. Continue the rebase: git rebase --continue
  5. Overwrite the commit(s): git push --force-with-lease

If you're new to commit signing, there are different ways to set it up:

Sign commits with gpg

Follow the steps below to set up commit signing with gpg:

  1. Generate a GPG key
  2. Add the GPG key to your GitHub account
  3. Configure git to use your GPG key for commit signing
Sign commits with ssh-agent

Follow the steps below to set up commit signing with ssh-agent:

  1. Generate an SSH key and add it to ssh-agent
  2. Add the SSH key to your GitHub account
  3. Configure git to use your SSH key for commit signing
Sign commits with 1Password

You can also sign commits using 1Password, which lets you sign commits with biometrics without the signing key leaving the local 1Password process.
See use 1Password to sign your commits.

Copilot AI left a comment

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.

🔵 Needs a closer look

It changes core amount-parsing behavior across multiple RPC entrypoints and should receive final human review despite strong test coverage.

Pull request overview

Fixes a JSON/string parsing bug in STAmount where issued-currency (IOU) values whose digit strings produce a mantissa above INT64_MAX could wrap during IOU canonicalization (due to narrowing to int64_t), potentially flipping sign and producing an unrelated amount. The change routes those out-of-range mantissas through Number so sign/mantissa stay separated and STAmount::fromNumber() performs the correct IOU-range normalization/rounding.

Changes:

  • Update amountFromJson() and amountFromString() to detect IOU mantissas above INT64_MAX and normalize via Number before constructing STAmount.
  • Add targeted unit tests covering the regression and end-to-end TrustSet behavior for values written with trailing zeros.
  • Document the RPC/API-facing behavior change in API-CHANGELOG.md.
File summaries
File Description
src/libxrpl/protocol/STAmount.cpp Routes large IOU mantissas through Number to prevent wrap/sign-flip during parsing.
src/test/protocol/STAmount_test.cpp Adds coverage for IOU JSON parsing cases around the INT64_MAX boundary and unsigned range.
src/test/app/TrustSet_test.cpp Adds an integration-style test ensuring trailing-zero representations produce correct ledger limits and negative limits are rejected consistently.
API-CHANGELOG.md Notes the parsing bugfix and its observable RPC impact.
Review details
  • Files reviewed: 4/4 changed files
  • Comments generated: 1
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread src/libxrpl/protocol/STAmount.cpp Outdated
partsFromString() reports an unsigned mantissa, but an IOU stores a
signed one, so a digit string above INT64_MAX wrapped into an unrelated
number, and a negative value came back positive. A TrustSet limit of
"-1000" was rejected with temBAD_LIMIT, while the same number written as
"-1000.0000000000000000" was accepted as a limit of 844.6744073709552.

Route those values through Number, which keeps the mantissa and the sign
apart, and let STAmount::fromNumber() normalize them into the range an
IOU can hold. Only JSON parsing changes: transactions deserialized from
binary have their mantissa masked to 54 bits and range-checked, so no
amendment is needed.

Fixes XRPLF#8188
@markneonin
markneonin force-pushed the markneonin/fix-iou-json-mantissa-wrap branch from 9138dae to a8e40f8 Compare September 8, 2026 14:42
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.

IOU amount from JSON wraps above INT64_MAX, and a negative value is accepted (Version: 3.3.0)

2 participants