mirror of
https://github.com/XRPLF/rippled.git
synced 2026-10-21 19:18:22 +00:00
Compare commits
15
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b515af4d4e | ||
|
|
cf495fde86 | ||
|
|
00bf88afad | ||
|
|
f45dab46d1 | ||
|
|
b87ad0f08e | ||
|
|
50c9e6335e | ||
|
|
78709c1d09 | ||
|
|
b67266aad4 | ||
|
|
79a8b4849e | ||
|
|
49410ad867 | ||
|
|
3689dca688 | ||
|
|
671a00b1ac | ||
|
|
9096ec03a7 | ||
|
|
5fec493960 | ||
|
|
a6a4fff2aa |
No files matched your search
@@ -103,7 +103,6 @@ words:
|
||||
- dearmor
|
||||
- decryptor
|
||||
- dedented
|
||||
- dedup
|
||||
- deleteme
|
||||
- demultiplexer
|
||||
- deserializaton
|
||||
@@ -345,7 +344,6 @@ words:
|
||||
- unambiguity
|
||||
- unauthorizes
|
||||
- unauthorizing
|
||||
- undeserializable
|
||||
- unergonomic
|
||||
- unfetched
|
||||
- unfindable
|
||||
|
||||
@@ -36,18 +36,3 @@ updates:
|
||||
rust-dependencies:
|
||||
patterns:
|
||||
- "*"
|
||||
|
||||
- package-ecosystem: uv
|
||||
directory: /cmake/scripts/codegen
|
||||
schedule:
|
||||
interval: weekly
|
||||
day: monday
|
||||
time: "04:00"
|
||||
timezone: Etc/GMT
|
||||
commit-message:
|
||||
prefix: "chore: [DEPENDABOT] "
|
||||
target-branch: develop
|
||||
groups:
|
||||
codegen-dependencies:
|
||||
patterns:
|
||||
- "*"
|
||||
@@ -62,7 +62,6 @@ libxrpl.tx > xrpl.protocol
|
||||
libxrpl.tx > xrpl.server
|
||||
libxrpl.tx > xrpl.tx
|
||||
test.app > test.jtx
|
||||
test.app > tests.libxrpl
|
||||
test.app > test.unit_test
|
||||
test.app > xrpl.basics
|
||||
test.app > xrpl.config
|
||||
@@ -175,8 +174,10 @@ test.server > xrpl.basics
|
||||
test.server > xrpl.config
|
||||
test.server > xrpld.app
|
||||
test.server > xrpld.core
|
||||
test.server > xrpld.rpc
|
||||
test.server > xrpl.json
|
||||
test.server > xrpl.protocol
|
||||
test.server > xrpl.resource
|
||||
test.server > xrpl.server
|
||||
test.unit_test > xrpl.basics
|
||||
test.unit_test > xrpl.protocol
|
||||
|
||||
@@ -123,19 +123,13 @@ jobs:
|
||||
|
||||
- name: Fix style
|
||||
if: ${{ steps.files_changed.outcome != 'success' }}
|
||||
# pre-commit installs every hook's environment, even for hooks that match
|
||||
# no files, and node-based hooks need libatomic, which this image lacks.
|
||||
# Exit code 1 just means hooks modified files or found issues.
|
||||
env:
|
||||
SKIP: prettier,cspell
|
||||
run: |
|
||||
git add --update
|
||||
pre-commit run || [ $? -eq 1 ]
|
||||
pre-commit run --all-files || true
|
||||
|
||||
- name: Generate git diff
|
||||
if: ${{ steps.files_changed.outcome != 'success' }}
|
||||
run: |
|
||||
git diff HEAD | tee "${DIFF_FILE}"
|
||||
git diff | tee "${DIFF_FILE}"
|
||||
|
||||
- name: Upload clang-tidy diff output
|
||||
if: ${{ github.event.repository.visibility == 'public' && steps.files_changed.outcome != 'success' }}
|
||||
|
||||
@@ -95,15 +95,11 @@ jobs:
|
||||
apt-get update -qq
|
||||
apt-get install -y "./${PACKAGE}"
|
||||
|
||||
# Rocky Linux and AlmaLinux pick a mirror from a mirrorlist, and now and
|
||||
# then get one that serves the repository metadata at a few hundred kB/s,
|
||||
# which alone outlasts the job's timeout. dnf only abandons a mirror below
|
||||
# 1000 B/s for 30 s, so raise that floor to fail over to the next one fast.
|
||||
- name: Install the RPM
|
||||
if: ${{ inputs.package_type == 'rpm' }}
|
||||
env:
|
||||
PACKAGE: ${{ steps.find.outputs.package }}
|
||||
run: dnf install -y --setopt=minrate=1M --setopt=timeout=10 "./${PACKAGE}"
|
||||
run: dnf install -y "./${PACKAGE}"
|
||||
|
||||
- name: Run xrpld
|
||||
run: xrpld --version
|
||||
|
||||
+24
-2
@@ -22,6 +22,30 @@ API version 2 is available in `xrpld` version 2.0.0 and later. See [API-VERSION-
|
||||
|
||||
This version is supported by all `xrpld` versions. For WebSocket and HTTP JSON-RPC requests, it is currently the default API version used when no `api_version` is specified.
|
||||
|
||||
## Unreleased
|
||||
|
||||
### Breaking changes
|
||||
|
||||
- The `ripplerpc` request field, which selects the shape of the JSON-RPC reply envelope, is now validated, and a value that is not exactly `"1.0"`, `"2.0"` or `"3.0"` is rejected. This reaches the JSON-RPC transport at every API version. It does not reach a WebSocket session, which reads the field only to echo it back. A request sending `"2"`, `" 2.0"`, `"2.00"`, `"02.0"`, `"2.0.0"`, `"10.0"` or any other text, such as `"abc"` or `"x2"`, gets a working reply today. After this ships, such a request sent alone gets HTTP 400 with `ripplerpc is not a supported version`, charged as a malformed request, and such an entry of a `"method": "batch"` body gets that error in its own reply while the batch answers 200. A client that spells the version loosely must therefore be corrected to one of the three exact values, or omit the field. Previously the value was compared as a string, which both accepted values that name no version and ordered multi-digit versions incorrectly: `"abc"` and `"x2"` sorted above `"3.0"` and so selected version 3, and `"10.0"` sorted below `"2.0"` and so selected version 1. Requests that send one of the three supported values, or omit the field, are unaffected.
|
||||
|
||||
### Additions
|
||||
|
||||
- A JSON-RPC (HTTP) request may send its parameters as an object, `"params": {"account": "r..."}`, as well as the array of one object that was already accepted. This reaches every API version: such a request was previously rejected with HTTP 400 and `params unparsable`, and is now served. A request that already sends the array form is unaffected. An entry of a `"method": "batch"` request that carries a by-name `params` object is read for `api_version` and credentials from that object. Previously such an entry had its `api_version` read from its top level and its credentials ignored, since credentials were read only from an array-form `params`.
|
||||
|
||||
### Bugfixes
|
||||
|
||||
- A request echoed back in an error reply now has every credential-bearing field masked: `admin_password`, `admin_user`, `passphrase`, `password`, `secret`, `seed`, `seed_hex`, `url_password`, `url_username` and `username`. Nesting no longer matters, so a credential inside `params` is masked too. The same masking is applied to every request and reply written to the log, and it covers six further names that only a reply carries: `master_key`, `master_seed`, `master_seed_hex`, `validation_key`, `validation_private_key` and `validation_seed`, which is how `wallet_propose` and `validation_create` used to write a live private key to the log. A request or reply written to the log is truncated at 10,000 characters. An `[rpc_startup]` line that does not parse is reported by its position in the section and the parser's message, never by its text, and no command runs for it.
|
||||
- The command line client no longer prints a credential the operator did not type. A failing command echoes the request it built under `request_sent`, which carries the `admin_password` the client copies out of `[port_rpc]` in the config, so `./xrpld account_info rBogus` printed that password to stdout and into any captured output. `request_sent` is now masked. The `rpc` member beside it, which echoes the arguments as they were typed, is unchanged. The command line client also no longer writes an unparsed `json` or `ripple_path_find` argument to its trace log before parsing it, where a `secret` inside that argument could not be masked; it logs the parsed request instead, masked. The reply it receives is logged the same way, parsed and masked, where the raw body was written before, a `validation_create` answer included.
|
||||
- A WebSocket frame that does not parse, or exceeds the request size limit, is answered `{"type": "error", "error": "jsonInvalid", "size": <bytes>}`. The frame's body is reported by size rather than echoed back in a `value` member, since a body that does not parse has no fields to mask. A client that read `value` gets `size` instead.
|
||||
- Four error codes that named no HTTP status of their own, and so answered 200 on a reply reporting an error, now name one: `actMalformed`, `alreadyMultisig` and `alreadySingleSig` answer 400, and `actNotFound` answers 404. **No shipped envelope reports these four.** A request sending `ripplerpc: "3.0"` still receives 200 for all four, as it always has, so `account_info` on a malformed account or one the ledger does not hold answers 200 exactly as before.
|
||||
- `submit`, `simulate`, `transaction_entry`, `ledger_entry` and `ledger_accept`: Errors from these methods now include `error_code` and `error_message` alongside the `error` token, as every other method already did. Each error now answers the status its code names: 400 for a malformed request, 404 for `transactionNotFound`, 500 for an internal failure, and 501 for `notYetImplemented` and `notStandAlone`. That status change reaches only a request sending `ripplerpc: "3.0"`, which is the envelope that derives the status from the error. With `ripplerpc` `"1.0"` the status stays 200 and the two new members appear beside `error`; with `"2.0"` the status stays 200, `error_code` appears, and the `code` and `message` members carry the code and the message rather than null, since that envelope copies them from `error_code` and `error_message` and drops `error_message`.
|
||||
- A reply reporting HTTP 402 or 502 now carries a status line. Those two statuses named no case in the switch that writes one, so such a reply began with a header instead and did not parse as an HTTP response at all. Both are reachable at any API version with `ripplerpc: "3.0"`, which derives the status from the error code: 402 through `highFee` from `sign`, `sign_for` or `submit` with a low `fee_mult_max`, and 502 through `dbDeserialization` from `tx`. The eleven statuses that already named a case report the same phrase they always have.
|
||||
- An error reply to a request sending `ripplerpc: "2.0"` or `"3.0"` no longer carries a stray `"error_message": null` beside the error it reports. The member appeared only when the `Server` log partition was set to debug or lower, because the log statement read `error_message` after the reply had renamed it to `message`, and reading it put it back as null. So the reply a client received depended on the server's log level, and the log line itself printed an empty message. Both are fixed.
|
||||
- A body the server rejects before it reads a request out of it now says which of four things was wrong. A body over the size limit answers `Request is too large`. A body that parses to `{}`, `[]` or `null` answers `Request is empty`. A body that parses to a non-empty array answers `Request is not a JSON object`; that is the only other document the parser accepts at the top level. All three previously answered `Unable to parse request: ` with nothing after the colon, the parser having recorded no error for them. Any other body does not parse, which includes one that is only whitespace and one whose top-level value is a string, number or boolean; it answers `Unable to parse request: ` followed by the parser's own reason, as it did before. The status is 400 for all four, as before, and this reaches every API version.
|
||||
- `batch`: An entry that is not identified through a secure gateway no longer clears the connection's `X-User` and forwarded-for values for the entries after it, so every entry of one body reports the role and username it would have reported on its own.
|
||||
- `batch`: Every entry of a `"method": "batch"` request is now charged against the sender's resource allowance, including one rejected before it reaches a handler, and the request stops at the first entry the connection is too loaded to serve. A reply array can therefore be shorter than the request array, and its last element depends on where the batch stopped. An entry that is not a JSON object, or names an API version the server does not serve, is charged before its role is known and answered with its own rejection; if that charge took the connection over the drop threshold, the batch stops there and that rejection is the last answer. Every other entry met over the threshold is answered `Server is overloaded` and nothing follows. A client should read any reply array shorter than its request as a connection over the drop threshold, whatever the last element says. A well-behaved client is unaffected; one that sends thousands of entries in a single body no longer gets every one of them answered.
|
||||
- A body the server rejects before it reads a request out of it is now charged against the sender's resource allowance, as a malformed request already was. Five conditions were free: a body over the size limit, one that does not parse, one carrying no document, one that is not a JSON object, and a `"method": "batch"` naming no entry array. The answer to each is unchanged. A well-behaved client is unaffected; one that repeats such a body exhausts its allowance and is refused the next request a handler would have served. A WebSocket frame that does not parse, or exceeds the request size limit, is charged the same way, and a connection that sends only such frames is closed once it crosses the drop threshold.
|
||||
|
||||
## XRP Ledger server version 3.5.0
|
||||
|
||||
Version 3.5.0 is not yet released.
|
||||
@@ -35,8 +59,6 @@ Version 3.5.0 is not yet released.
|
||||
|
||||
- `channel_authorize`: The `channel_id` field now returns an `invalidParams` error if the value is not a string. [#7582](https://github.com/XRPLF/rippled/pull/7582)
|
||||
- `channel_verify`: The `channel_id` and `signature` fields now return an `invalidParams` error if the value is not a string. [#7582](https://github.com/XRPLF/rippled/pull/7582)
|
||||
- `account_currencies`, `account_nfts`, `account_offers`: A request with more than one problem may now report a different one of them. An invalid `ledger_hash` or `ledger_index` is reported before an invalid or missing `account`. An invalid `limit` or `marker` is reported before `lgrNotFound` or `actNotFound`, and for `account_offers` also before `actMalformed`. A request with a single problem returns the same error as before. [#8561](https://github.com/XRPLF/rippled/pull/8561)
|
||||
- `deposit_authorized`: A `credentials` entry that is all zeros (for example `"0"` or 64 `0` characters) now returns a `badCredentials` error instead of looking up a credential that can never exist.
|
||||
|
||||
### Bugfixes in 3.5.0
|
||||
|
||||
|
||||
@@ -2,12 +2,33 @@
|
||||
| ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| These instructions assume you have a C++ development environment ready with Git, Python, Conan, CMake, and a C++ compiler. For help setting one up on Linux, macOS, or Windows, [see this guide](./docs/build/environment.md).<br><br>These instructions also assume a basic familiarity with Conan and CMake. If you are unfamiliar with Conan, you can read our [crash course](./docs/build/conan.md) or the official [Getting Started][conan-getting-started] walkthrough. |
|
||||
|
||||
## Supported platforms
|
||||
## Minimum Requirements
|
||||
|
||||
Building and testing are supported on Linux, macOS, and (best effort) Windows;
|
||||
see [Supported development platforms](./docs/build/environment.md#supported-development-platforms).
|
||||
Running `xrpld` is officially supported on Linux only;
|
||||
see [Requirements](./docs/install.md#requirements).
|
||||
For the hardware needed to run a node, see
|
||||
[System Requirements](https://xrpl.org/system-requirements.html).
|
||||
|
||||
For the software needed to build xrpld, see the
|
||||
[environment setup guide](./docs/build/environment.md).
|
||||
|
||||
## Operating Systems
|
||||
|
||||
### Linux
|
||||
|
||||
The Ubuntu Linux distribution has received the highest level of quality
|
||||
assurance, testing, and support. We also support Red Hat and use Debian
|
||||
internally.
|
||||
Our Linux CI tooling is distro-independent and uses a Nix-based environment, so it should be possible to build on other Linux distributions as well, although we have not tested them.
|
||||
|
||||
### macOS
|
||||
|
||||
Many `xrpld` engineers use macOS for development.
|
||||
The minimum supported version is macOS 15 (Sequoia).
|
||||
CI testing is done in macOS 26 (Tahoe), but the build defaults `CMAKE_OSX_DEPLOYMENT_TARGET` to 15.
|
||||
|
||||
### Windows
|
||||
|
||||
Windows is used by some engineers for development only, and is not recommended
|
||||
for production use.
|
||||
|
||||
## Steps
|
||||
|
||||
|
||||
+1
-1
@@ -42,7 +42,7 @@ if(is_gcc)
|
||||
# GCC-specific fixes
|
||||
add_compile_options(-Wno-unknown-pragmas -Wno-subobject-linkage)
|
||||
# -Wno-subobject-linkage can be removed when we upgrade GCC version to at least 13.3
|
||||
elseif(is_any_clang)
|
||||
elseif(is_clang)
|
||||
# Clang-specific fixes
|
||||
add_compile_options(-Wno-unknown-warning-option) # Ignore unknown warning options
|
||||
elseif(is_msvc)
|
||||
|
||||
+1
-1
@@ -332,7 +332,7 @@ This ensures that configuration changes don't introduce new warnings across the
|
||||
|
||||
### Installing clang-tidy
|
||||
|
||||
The [Nix development shell](./docs/build/nix.md) provides `clang-tidy` at the [tested version](./docs/build/environment.md#tested-compiler-versions).
|
||||
See the [environment setup guide](./docs/build/environment.md#clang-tidy) for how to get clang-tidy.
|
||||
|
||||
### Running clang-tidy locally
|
||||
|
||||
|
||||
+10
-16
@@ -34,7 +34,7 @@ so tools can filter them and protection rules can target `release/*` and `stagin
|
||||
- **Branches only move forward.**
|
||||
`develop`, `staging/X.Y.x`, and `release/X.Y.x` are never rewritten.
|
||||
- **Cherry-picking only goes from `develop` to a staging branch**,
|
||||
for fixes that must get into a release after the [code freeze](#release-candidates)
|
||||
for fixes that must get into a release after the code freeze
|
||||
(see [Emergency Fixes From `develop`](#emergency-fixes-from-develop)).
|
||||
- **Security fixes are prepared privately and published with the release that contains them**,
|
||||
so vulnerabilities are not disclosed prematurely.
|
||||
@@ -49,7 +49,6 @@ develop staging/X.Y.x & release/X.Y.x
|
||||
│
|
||||
├── Tag: X.Y.0-b1
|
||||
├── Tag: X.Y.0-bN
|
||||
│ (2 weeks before rc1: freeze on large changes starts)
|
||||
├── Tag: X.Y.0-rc1 ───────────────┐ (branches created)
|
||||
│ │
|
||||
│ (development continues) ├── Fixes
|
||||
@@ -76,20 +75,16 @@ develop staging/X.Y.x & release/X.Y.x
|
||||
3. **Branches:** `staging/X.Y.x` and `release/X.Y.x` are created from `develop`
|
||||
at the commit tagged `X.Y.0-rc1`.
|
||||
The first RC also kicks off the QE process.
|
||||
|
||||
### Freeze on Large Changes
|
||||
|
||||
From two weeks before `X.Y.0-rc1` until `X.Y.0` is [merged back](#merging-back-into-develop),
|
||||
large changes (e.g., big refactors, moving or renaming many files, mass reformatting)
|
||||
are not merged into `develop`.
|
||||
4. **Code freeze:** No new features or unrelated changes are pulled from `develop`
|
||||
into `staging/X.Y.x` or `release/X.Y.x`.
|
||||
Only critical stabilization fixes go into the line.
|
||||
5. **No large changes on `develop`:** Until `X.Y.0` is [merged back](#merging-back-into-develop),
|
||||
large changes (e.g., big refactors, moving or renaming many files, mass reformatting)
|
||||
are not merged into `develop`,
|
||||
so that fixes on the line and the merge back don't run into conflicts.
|
||||
|
||||
### Release Candidates
|
||||
|
||||
After branching, the line is in code freeze:
|
||||
no new features or unrelated changes are pulled from `develop`
|
||||
into `staging/X.Y.x` or `release/X.Y.x`.
|
||||
Only critical stabilization fixes go into the line.
|
||||
|
||||
1. Fixes are developed against `staging/X.Y.x`.
|
||||
2. When ready, a new RC is created on `staging/X.Y.x`,
|
||||
and `release/X.Y.x` is fast-forwarded to it.
|
||||
@@ -111,8 +106,7 @@ one right after the other.
|
||||
2. `release/X.Y.x` is fast-forwarded to `staging/X.Y.x`,
|
||||
and the release is tagged on it.
|
||||
3. The release is immediately [merged back into `develop`](#merging-back-into-develop).
|
||||
For a major-minor release, this lifts the [freeze on large changes](#freeze-on-large-changes)
|
||||
in `develop`.
|
||||
For a major-minor release, this lifts the freeze on large changes in `develop`.
|
||||
|
||||
### Merging Back Into `develop`
|
||||
|
||||
@@ -133,7 +127,7 @@ After the merge, `git log develop..<tag>` must be empty.
|
||||
### Emergency Fixes From `develop`
|
||||
|
||||
If a commit was merged to `develop`
|
||||
and needs to be included in a release after the [code freeze](#release-candidates):
|
||||
and needs to be included in a release after the code freeze:
|
||||
|
||||
1. Create a PR that cherry-picks the commit onto `staging/X.Y.x`.
|
||||
2. Leave `develop` as it is, with no reverts.
|
||||
|
||||
@@ -1360,39 +1360,6 @@
|
||||
# Example:
|
||||
# owner_reserve = 200000 # 0.2 XRP
|
||||
#
|
||||
# gas_limit = <gas>
|
||||
#
|
||||
# The gas limit is the maximum amount of gas that can be
|
||||
# consumed by a single transaction. The gas limit is used to prevent
|
||||
# transactions from consuming too many resources.
|
||||
#
|
||||
# If this parameter is unspecified, xrpld will use an internal
|
||||
# default. Don't change this without understanding the consequences.
|
||||
#
|
||||
# Example:
|
||||
# gas_limit = 1000000 # 1 million gas
|
||||
#
|
||||
# bytecode_size_limit = <bytes>
|
||||
#
|
||||
# The bytecode size limit is the maximum size of a WASM extension in
|
||||
# bytes. The size limit is used to prevent extensions from consuming
|
||||
# too many resources.
|
||||
#
|
||||
# If this parameter is unspecified, xrpld will use an internal
|
||||
# default. Don't change this without understanding the consequences.
|
||||
#
|
||||
# Example:
|
||||
# bytecode_size_limit = 100000 # 100 kb
|
||||
#
|
||||
# gas_price = <micro-drops>
|
||||
#
|
||||
# The gas price is the conversion between WASM gas and its price in drops.
|
||||
#
|
||||
# If this parameter is unspecified, xrpld will use an internal
|
||||
# default. Don't change this without understanding the consequences.
|
||||
#
|
||||
# Example:
|
||||
# gas_price = 1000000 # 1 drop per gas
|
||||
#-------------------------------------------------------------------------------
|
||||
#
|
||||
# 9. Misc Settings
|
||||
|
||||
@@ -9,21 +9,13 @@ include_guard(GLOBAL)
|
||||
# --------------------------------------------------------------------
|
||||
# Compiler detection (C++)
|
||||
# --------------------------------------------------------------------
|
||||
# is_any_clang is true for both LLVM Clang and AppleClang. Their version numbers
|
||||
# and feature sets differ, so use is_llvm_clang or is_apple_clang when it matters.
|
||||
set(is_any_clang FALSE)
|
||||
set(is_llvm_clang FALSE)
|
||||
set(is_apple_clang FALSE)
|
||||
set(is_clang FALSE)
|
||||
set(is_gcc FALSE)
|
||||
set(is_msvc FALSE)
|
||||
set(is_xcode FALSE)
|
||||
|
||||
if(CMAKE_CXX_COMPILER_ID STREQUAL "Clang")
|
||||
set(is_any_clang TRUE)
|
||||
set(is_llvm_clang TRUE)
|
||||
elseif(CMAKE_CXX_COMPILER_ID STREQUAL "AppleClang")
|
||||
set(is_any_clang TRUE)
|
||||
set(is_apple_clang TRUE)
|
||||
if(CMAKE_CXX_COMPILER_ID MATCHES ".*Clang") # Clang or AppleClang
|
||||
set(is_clang TRUE)
|
||||
elseif(CMAKE_CXX_COMPILER_ID STREQUAL "GNU")
|
||||
set(is_gcc TRUE)
|
||||
elseif(MSVC)
|
||||
|
||||
@@ -130,7 +130,7 @@ else()
|
||||
INTERFACE
|
||||
-Wall
|
||||
-Wdeprecated
|
||||
$<$<BOOL:${is_any_clang}>:-Wno-deprecated-declarations>
|
||||
$<$<BOOL:${is_clang}>:-Wno-deprecated-declarations>
|
||||
$<$<BOOL:${wextra}>:-Wextra
|
||||
-Wno-unused-parameter>
|
||||
$<$<BOOL:${werr}>:-Werror>
|
||||
@@ -177,7 +177,7 @@ else()
|
||||
if(
|
||||
is_nix_compiler
|
||||
AND is_linux
|
||||
AND is_any_clang
|
||||
AND is_clang
|
||||
AND CMAKE_CXX_FLAGS MATCHES "stdlib=libstdc"
|
||||
)
|
||||
string(
|
||||
@@ -204,10 +204,7 @@ if(voidstar)
|
||||
"Antithesis instrumentation requires Linux, aborting..."
|
||||
)
|
||||
elseif(
|
||||
NOT (
|
||||
is_llvm_clang
|
||||
AND CMAKE_CXX_COMPILER_VERSION VERSION_GREATER_EQUAL 16.0
|
||||
)
|
||||
NOT (is_clang AND CMAKE_CXX_COMPILER_VERSION VERSION_GREATER_EQUAL 16.0)
|
||||
)
|
||||
message(
|
||||
FATAL_ERROR
|
||||
|
||||
@@ -296,13 +296,7 @@ if(xrpld)
|
||||
OUT_VAR rpcspec_instantiations
|
||||
VALUE_TYPE "::json::Value"
|
||||
VIEW_HEADER "xrpld/rpc/detail/JsonObjectView.hpp"
|
||||
HANDLERS
|
||||
account_currencies
|
||||
account_nfts
|
||||
account_offers
|
||||
book_changes
|
||||
ledger
|
||||
transaction_entry
|
||||
HANDLERS book_changes ledger transaction_entry
|
||||
)
|
||||
target_sources(xrpld PRIVATE ${rpcspec_instantiations})
|
||||
|
||||
|
||||
@@ -58,7 +58,7 @@ target_link_options(common INTERFACE ${sanitizers_linker_flags})
|
||||
# This module appends -fsanitize-ignorelist=<path> for Clang builds.
|
||||
# The ignorelist path contains CMAKE_SOURCE_DIR, so it must be set here, rather than in the Conan profile.
|
||||
# GCC does not support -fsanitize-ignorelist.
|
||||
if(is_any_clang)
|
||||
if(is_clang)
|
||||
set(ignorelist_path
|
||||
"${CMAKE_SOURCE_DIR}/sanitizers/suppressions/sanitizer-ignorelist.txt"
|
||||
)
|
||||
|
||||
@@ -23,8 +23,11 @@ if(NOT is_multiconfig)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
if(is_llvm_clang)
|
||||
if(CMAKE_CXX_COMPILER_VERSION VERSION_LESS 16.0)
|
||||
if(is_clang) # both Clang and AppleClang
|
||||
if(
|
||||
"${CMAKE_CXX_COMPILER_ID}" STREQUAL "Clang"
|
||||
AND CMAKE_CXX_COMPILER_VERSION VERSION_LESS 16.0
|
||||
)
|
||||
message(FATAL_ERROR "This project requires clang 16 or later")
|
||||
endif()
|
||||
elseif(is_gcc)
|
||||
|
||||
@@ -62,11 +62,11 @@ if(unity)
|
||||
set(CMAKE_UNITY_BUILD ON CACHE BOOL "Do a unity build")
|
||||
endif()
|
||||
|
||||
if(is_llvm_clang AND is_linux)
|
||||
if(is_clang AND is_linux)
|
||||
option(voidstar "Enable Antithesis instrumentation." OFF)
|
||||
endif()
|
||||
|
||||
if(is_gcc OR is_any_clang)
|
||||
if(is_gcc OR is_clang)
|
||||
include(ProcessorCount)
|
||||
ProcessorCount(PROCESSOR_COUNT)
|
||||
|
||||
@@ -125,7 +125,7 @@ else()
|
||||
set(use_mold OFF CACHE BOOL "mold linker, linux only" FORCE)
|
||||
endif()
|
||||
|
||||
if(is_any_clang)
|
||||
if(is_clang)
|
||||
option(use_lld "enables detection of lld linker" ON)
|
||||
else()
|
||||
set(use_lld OFF CACHE BOOL "try lld linker, clang only" FORCE)
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
pcpp>=1.30
|
||||
|
||||
# Parser combinator library - used to parse the macro DSL
|
||||
pyparsing>=3.3.3
|
||||
pyparsing>=3.0.0
|
||||
|
||||
# Template engine - used to generate C++ code from templates
|
||||
Mako>=1.4.2
|
||||
Mako>=1.2.2
|
||||
@@ -1,8 +1,8 @@
|
||||
# This file was autogenerated by uv via the following command:
|
||||
# uv pip compile requirements.in --generate-hashes --output-file requirements.txt
|
||||
mako==1.4.3 \
|
||||
--hash=sha256:723296007c870bfd6b3f0c3230dba7198096e5269297ebf5e4eff9e7ffa39d4f \
|
||||
--hash=sha256:cd6537fe88d5fec315c55c2f8529bc4ce7a9a352ad7db3eeaa6a66e2dd4ec37a
|
||||
mako==1.3.12 \
|
||||
--hash=sha256:8f61569480282dbf557145ce441e4ba888be453c30989f879f0d652e39f53ea9 \
|
||||
--hash=sha256:9f778e93289bd410bb35daadeb4fc66d95a746f0b75777b942088b7fd7af550a
|
||||
# via -r requirements.in
|
||||
markupsafe==3.0.3 \
|
||||
--hash=sha256:0303439a41979d9e74d18ff5e2dd8c43ed6c6001fd40e5bf2e43f7bd9bbc523f \
|
||||
@@ -99,7 +99,7 @@ pcpp==1.30 \
|
||||
--hash=sha256:05fe08292b6da57f385001c891a87f40d6aa7f46787b03e8ba326d20a3297c6e \
|
||||
--hash=sha256:5af9fbce55f136d7931ae915fae03c34030a3b36c496e72d9636cedc8e2543a1
|
||||
# via -r requirements.in
|
||||
pyparsing==3.3.3 \
|
||||
--hash=sha256:928ae7e20211f3b6f3915a72f06a0cfd29ab9d24279dd6346b6b1a7146397d36 \
|
||||
--hash=sha256:ece8c00a69cf01b45d0b1dedabb469c90d8caf996d4fda40f147627a122849a4
|
||||
pyparsing==3.3.2 \
|
||||
--hash=sha256:850ba148bd908d7e2411587e247a1e4f0327839c40e2e5e6d05a007ecc69911d \
|
||||
--hash=sha256:c777f4d763f140633dcb6d8a3eda953bf7a214dc4eff598413c070bcdc117cbc
|
||||
# via -r requirements.in
|
||||
+1
-1
@@ -3,7 +3,7 @@
|
||||
"requires": [
|
||||
"zlib/1.3.2#1cb806da49011867778ffb6ac7190fcb%1782392402.122708",
|
||||
"xxhash/0.8.3#681d36a0a6111fc56e5e45ea182c19cc%1782392402.420688",
|
||||
"xrpl-rpc-spec/0.1.23#56e8946f5b3fb949a3732e1a51135ca2%1790977482.197086",
|
||||
"xrpl-rpc-spec/0.1.21#d536f87a2ae7d313452746cfa3ac4404%1790869005.122975",
|
||||
"sqlite3/3.53.0#324ada52333108388a9a6108bfa96734%1782392403.185447",
|
||||
"soci/4.0.3#e726491a03468795453f7c83fc924a96%1782392402.679521",
|
||||
"snappy/1.1.10#968fef506ff261592ec30c574d4a7809%1782307151.633168",
|
||||
|
||||
+1
-1
@@ -40,7 +40,7 @@ class Xrpl(ConanFile):
|
||||
"nudb/2.0.9",
|
||||
"openssl/3.6.3",
|
||||
"soci/4.0.3",
|
||||
"xrpl-rpc-spec/0.1.23",
|
||||
"xrpl-rpc-spec/0.1.21",
|
||||
"zlib/1.3.2",
|
||||
]
|
||||
|
||||
|
||||
Vendored
+77
-32
@@ -4,18 +4,6 @@ This document explains how to set one up.
|
||||
|
||||
[BUILD.md]: ../../BUILD.md
|
||||
|
||||
## Supported development platforms
|
||||
|
||||
- **Linux**: the [Nix development shell](./nix.md),
|
||||
which provides the same toolchain CI uses on any distribution.
|
||||
- **macOS** 15 (Sequoia) or newer: the [Nix development shell](./nix.md),
|
||||
or the system [Apple Clang](#macos-apple-clang).
|
||||
CI runs on macOS 26 (Tahoe), but the build defaults `CMAKE_OSX_DEPLOYMENT_TARGET` to 15.
|
||||
- **Windows**: best effort, see [Windows](#windows).
|
||||
|
||||
For the platforms supported for running `xrpld`,
|
||||
see [Requirements](../install.md#requirements).
|
||||
|
||||
## Tested compiler versions
|
||||
|
||||
`xrpld` is built in the **C++23** dialect by default, so your toolchain has to
|
||||
@@ -47,16 +35,20 @@ compiler building. Treat support for anything outside the table as best-effort.
|
||||
|
||||
## Required tools
|
||||
|
||||
Besides a compiler, building `xrpld` requires
|
||||
[Git](https://git-scm.com/downloads),
|
||||
[Python](https://www.python.org/downloads/),
|
||||
[Conan](https://conan.io/downloads.html),
|
||||
and [CMake](https://cmake.org/download/).
|
||||
Building with `-Drust=ON` additionally requires a Rust toolchain,
|
||||
see [Rust](#rust).
|
||||
Besides a compiler, building `xrpld` requires:
|
||||
|
||||
| Tool | Minimum version |
|
||||
| ------------------------------------------- | --------------- |
|
||||
| [Git](https://git-scm.com/downloads) | any recent |
|
||||
| [Python](https://www.python.org/downloads/) | 3.11 |
|
||||
| [Conan](https://conan.io/downloads.html) | 2.17 |
|
||||
| [CMake](https://cmake.org/download/) | 3.16 |
|
||||
|
||||
On Linux and macOS, the [Nix development shell](./nix.md) provides all of them
|
||||
at the versions CI uses, recorded in [`nix/check-tools/`](../../nix/check-tools).
|
||||
(see below). On Windows they have to be installed manually.
|
||||
|
||||
Building with `-Drust=ON` additionally requires a Rust toolchain, see
|
||||
[Rust](#rust). A default build does not, so it is not in the table above.
|
||||
|
||||
Once they are in place, verify that everything is installed and runnable with:
|
||||
|
||||
@@ -66,19 +58,60 @@ Once they are in place, verify that everything is installed and runnable with:
|
||||
|
||||
## Linux and macOS
|
||||
|
||||
Use the [Nix development shell](./nix.md):
|
||||
it provides the compiler and every tool CI uses,
|
||||
without installing anything system-wide.
|
||||
The **recommended way** to get a development environment on Linux and macOS is
|
||||
the Nix development shell. It provides the exact tooling used in CI — `git`,
|
||||
`python`, `conan`, `cmake`, `clang-tidy`, `clang-format`, and everything else —
|
||||
with a single command and without installing anything system-wide:
|
||||
|
||||
### macOS: Apple Clang
|
||||
```bash
|
||||
nix --experimental-features 'nix-command flakes' develop
|
||||
```
|
||||
|
||||
You can also build with the system Apple Clang,
|
||||
installing the other [required tools](#required-tools) any way you like
|
||||
(or getting them from `nix develop .#apple-clang`).
|
||||
Its version is whatever your installed Xcode (or Command Line Tools) provides,
|
||||
so check that `clang --version` is at least the [tested one](#tested-compiler-versions).
|
||||
If several Xcode versions are installed,
|
||||
select one with `sudo xcode-select -s <path>` or the `DEVELOPER_DIR` environment variable.
|
||||
On **Linux**, Nix also provides the compiler (GCC); on **macOS**, it provides
|
||||
Clang. If you instead opt to use your system-wide Apple Clang (via
|
||||
`nix develop .#apple-clang`), you need to manage its version yourself (see
|
||||
below).
|
||||
|
||||
See [Using the Nix development shell](./nix.md) for installation and usage
|
||||
details, including how to select a different compiler and why we recommend Nix
|
||||
over a hand-maintained environment.
|
||||
|
||||
### macOS: managing the Apple Clang version
|
||||
|
||||
If you use your system-wide Apple Clang on macOS (via `nix develop .#apple-clang`),
|
||||
the compiler version is whatever your installed Xcode (or Command Line Tools)
|
||||
provides. The following command should return a version greater than or equal to
|
||||
the [tested one](#tested-compiler-versions):
|
||||
|
||||
```bash
|
||||
clang --version
|
||||
```
|
||||
|
||||
If you develop other applications using Xcode, you might be consistently
|
||||
updating to the newest version of Apple Clang, which will likely cause issues
|
||||
building xrpld. You may want to install and pin a specific version of Xcode:
|
||||
|
||||
1. **Download Xcode**
|
||||
- Visit [Apple Developer Downloads](https://developer.apple.com/download/more/)
|
||||
- Sign in with your Apple Developer account
|
||||
- Search for an Xcode version that includes the expected Apple Clang version
|
||||
- Download the `.xip` file
|
||||
|
||||
2. **Install and configure Xcode**
|
||||
|
||||
```bash
|
||||
# Extract the .xip file and rename for version management
|
||||
# Example: Xcode_16.2.app
|
||||
|
||||
# Move to Applications directory
|
||||
sudo mv Xcode_16.2.app /Applications/
|
||||
|
||||
# Set as default toolchain (persistent)
|
||||
sudo xcode-select -s /Applications/Xcode_16.2.app/Contents/Developer
|
||||
|
||||
# Set as environment variable (temporary)
|
||||
export DEVELOPER_DIR=/Applications/Xcode_16.2.app/Contents/Developer
|
||||
```
|
||||
|
||||
## Windows
|
||||
|
||||
@@ -90,7 +123,8 @@ manually:
|
||||
"x64 Native Tools Command Prompt". CI configures CMake with the
|
||||
`Visual Studio 18 2026` generator.
|
||||
- [Git for Windows](https://git-scm.com/download/win)
|
||||
- recent releases of Python, Conan, and CMake
|
||||
- Python, Conan, and CMake, at the versions listed in
|
||||
[Required tools](#required-tools).
|
||||
- a [Rust toolchain](https://rustup.rs) — only needed to build with
|
||||
`-Drust=ON`, see [Rust](#rust)
|
||||
|
||||
@@ -111,5 +145,16 @@ version.
|
||||
Everything else the Rust build needs on the CMake side comes from Conan along
|
||||
with the rest of the dependencies, so there is nothing further to install.
|
||||
|
||||
## Clang-tidy
|
||||
|
||||
`clang-tidy` is required to run static analysis checks locally (see
|
||||
[CONTRIBUTING.md](../../CONTRIBUTING.md)). It is not required to build the
|
||||
project. The version this project uses is listed in
|
||||
[Tested compiler versions](#tested-compiler-versions).
|
||||
|
||||
On Linux and macOS, the [Nix development shell](./nix.md) provides that exact
|
||||
version out of the box — run it via `run-clang-tidy`. No separate installation
|
||||
is needed.
|
||||
|
||||
[cpp23-support]: https://en.cppreference.com/w/cpp/compiler_support/23
|
||||
[packages.nix]: ../../nix/packages.nix
|
||||
Vendored
+9
-2
@@ -9,7 +9,9 @@ This guide explains how to use Nix to set up a reproducible development environm
|
||||
- **Reproducible environment**: Everyone gets the same versions of tools and compilers
|
||||
- **Matches CI**: The Linux CI runs in Docker images built from this exact Nix environment, and CI builds some macOS configurations in it as well
|
||||
- **No system pollution**: Dependencies are isolated and don't affect your system packages
|
||||
- **Consistent compilers**: The GCC and Clang shells use the same versions as CI
|
||||
- **Quick setup**: Get started with a single command
|
||||
- **Works on Linux and macOS**: Consistent experience across platforms
|
||||
|
||||
## Install Nix
|
||||
|
||||
@@ -25,8 +27,13 @@ From the root of the xrpld repository, enter the default development shell:
|
||||
nix --experimental-features 'nix-command flakes' develop
|
||||
```
|
||||
|
||||
This downloads and sets up all required development tools (CMake, Ninja, Conan, etc.)
|
||||
and the compiler for your platform (see [Platform notes](#platform-notes)).
|
||||
This will:
|
||||
|
||||
- Download and set up all required development tools (CMake, Ninja, Conan, etc.)
|
||||
- Configure the appropriate compiler for your platform:
|
||||
- **Linux**: GCC (provided by Nix)
|
||||
- **macOS**: Clang (provided by Nix)
|
||||
|
||||
The first time you run this command, it will take a few minutes to download and build the environment. Subsequent runs will be much faster.
|
||||
|
||||
### Platform notes
|
||||
|
||||
@@ -10,19 +10,6 @@ and DNF on Red Hat-based distributions such as RHEL, AlmaLinux, and Rocky Linux,
|
||||
where `yum` is a symlink to `dnf`.
|
||||
To build from source instead, see [BUILD.md](../BUILD.md).
|
||||
|
||||
## Requirements
|
||||
|
||||
Linux is the only operating system officially supported for running `xrpld`;
|
||||
macOS and Windows builds are for development only.
|
||||
|
||||
Any x86-64 Linux distribution with APT or DNF and glibc 2.31 or newer should work
|
||||
(see [How libc is handled](../nix/docker/README.md#how-libc-is-handled)).
|
||||
CI tests the packages on Debian 11–13, Ubuntu 20.04–26.04,
|
||||
RHEL (UBI) 9–10, AlmaLinux 9–10, and Rocky Linux 9–10.
|
||||
|
||||
For the hardware needed to run a node,
|
||||
see [System Requirements](https://xrpl.org/system-requirements.html).
|
||||
|
||||
## Release channels
|
||||
|
||||
Packages are published to four channels:
|
||||
|
||||
@@ -39,31 +39,6 @@ checkedSub(std::int64_t a, std::int64_t b) noexcept
|
||||
return a - b;
|
||||
}
|
||||
|
||||
/**
|
||||
* Multiply two signed 64-bit integers, returning std::nullopt when the exact
|
||||
* mathematical product is not representable in std::int64_t.
|
||||
*/
|
||||
[[nodiscard]] constexpr std::optional<std::int64_t>
|
||||
checkedMul(std::int64_t a, std::int64_t b) noexcept
|
||||
{
|
||||
using L = std::numeric_limits<std::int64_t>;
|
||||
|
||||
if (a == 0 || b == 0)
|
||||
return 0;
|
||||
|
||||
if (a > 0)
|
||||
{
|
||||
if (b > 0 ? a > L::max() / b : b < L::min() / a)
|
||||
return std::nullopt;
|
||||
}
|
||||
else if (b > 0 ? a < L::min() / b : b < L::max() / a)
|
||||
{
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
return a * b;
|
||||
}
|
||||
|
||||
static_assert(checkedAdd(0, 0) == 0);
|
||||
static_assert(checkedAdd(1, -1) == 0);
|
||||
static_assert(checkedAdd(-5, 2) == -3);
|
||||
@@ -106,53 +81,6 @@ static_assert(
|
||||
!checkedSub(std::numeric_limits<std::int64_t>::min(), std::numeric_limits<std::int64_t>::max())
|
||||
.has_value());
|
||||
|
||||
static_assert(checkedMul(0, std::numeric_limits<std::int64_t>::min()) == 0);
|
||||
static_assert(checkedMul(std::numeric_limits<std::int64_t>::min(), 0) == 0);
|
||||
static_assert(checkedMul(-1, -1) == 1);
|
||||
// a > 0, b > 0
|
||||
static_assert(
|
||||
checkedMul(std::numeric_limits<std::int64_t>::max(), 1) ==
|
||||
std::numeric_limits<std::int64_t>::max());
|
||||
static_assert(
|
||||
checkedMul(std::numeric_limits<std::int64_t>::max() / 2, 2) ==
|
||||
std::numeric_limits<std::int64_t>::max() - 1);
|
||||
static_assert(!checkedMul((std::numeric_limits<std::int64_t>::max() / 2) + 1, 2).has_value());
|
||||
static_assert(!checkedMul(2, (std::numeric_limits<std::int64_t>::max() / 2) + 1).has_value());
|
||||
// a > 0, b < 0
|
||||
static_assert(
|
||||
checkedMul(std::numeric_limits<std::int64_t>::max(), -1) ==
|
||||
-std::numeric_limits<std::int64_t>::max());
|
||||
static_assert(
|
||||
checkedMul((std::numeric_limits<std::int64_t>::max() / 2) + 1, -2) ==
|
||||
std::numeric_limits<std::int64_t>::min());
|
||||
static_assert(!checkedMul((std::numeric_limits<std::int64_t>::max() / 2) + 2, -2).has_value());
|
||||
static_assert(
|
||||
checkedMul(1, std::numeric_limits<std::int64_t>::min()) ==
|
||||
std::numeric_limits<std::int64_t>::min());
|
||||
static_assert(
|
||||
checkedMul(2, std::numeric_limits<std::int64_t>::min() / 2) ==
|
||||
std::numeric_limits<std::int64_t>::min());
|
||||
static_assert(!checkedMul(2, (std::numeric_limits<std::int64_t>::min() / 2) - 1).has_value());
|
||||
// a < 0, b > 0
|
||||
static_assert(
|
||||
checkedMul(std::numeric_limits<std::int64_t>::min(), 1) ==
|
||||
std::numeric_limits<std::int64_t>::min());
|
||||
static_assert(
|
||||
checkedMul(std::numeric_limits<std::int64_t>::min() / 2, 2) ==
|
||||
std::numeric_limits<std::int64_t>::min());
|
||||
static_assert(!checkedMul((std::numeric_limits<std::int64_t>::min() / 2) - 1, 2).has_value());
|
||||
static_assert(!checkedMul(std::numeric_limits<std::int64_t>::min(), 2).has_value());
|
||||
// a < 0, b < 0
|
||||
static_assert(
|
||||
checkedMul(-(std::numeric_limits<std::int64_t>::max() / 2), -2) ==
|
||||
std::numeric_limits<std::int64_t>::max() - 1);
|
||||
static_assert(!checkedMul(std::numeric_limits<std::int64_t>::min() / 2, -2).has_value());
|
||||
static_assert(
|
||||
checkedMul(-1, -std::numeric_limits<std::int64_t>::max()) ==
|
||||
std::numeric_limits<std::int64_t>::max());
|
||||
static_assert(!checkedMul(std::numeric_limits<std::int64_t>::min(), -1).has_value());
|
||||
static_assert(!checkedMul(-1, std::numeric_limits<std::int64_t>::min()).has_value());
|
||||
|
||||
/**
|
||||
* Calculate one number divided by another number in percentage.
|
||||
* The result is rounded up to the next integer, and capped in the range [0,100]
|
||||
|
||||
@@ -153,24 +153,6 @@ private:
|
||||
Policy policy,
|
||||
Callback&& replaceCallback = nullptr);
|
||||
|
||||
/**
|
||||
* canonicalizeImpl variant for callers that already hold `mutex_`.
|
||||
*
|
||||
* The unnamed `scoped_lock<MutexType> const&` parameter is proof of
|
||||
* ownership; this overload does not acquire the lock. Use it from other
|
||||
* methods (e.g. fetchAndModify) that need to compose canonicalize with
|
||||
* additional work under a single critical section, without relying on
|
||||
* recursive-mutex re-entry.
|
||||
*/
|
||||
template <class Policy, class Callback = std::nullptr_t>
|
||||
bool
|
||||
canonicalizeImpl(
|
||||
std::scoped_lock<MutexType> const&,
|
||||
key_type const& key,
|
||||
CanonicalizeClientPointerType<Policy> data,
|
||||
Policy policy,
|
||||
Callback&& replaceCallback = nullptr);
|
||||
|
||||
public:
|
||||
/**
|
||||
* Replace aliased objects with originals.
|
||||
@@ -257,6 +239,9 @@ public:
|
||||
bool
|
||||
retrieve(key_type const& key, T& data);
|
||||
|
||||
MutexType&
|
||||
peekMutex();
|
||||
|
||||
std::vector<key_type>
|
||||
getKeys() const;
|
||||
|
||||
@@ -278,22 +263,6 @@ public:
|
||||
fetch(key_type const& digest, Handler const& h);
|
||||
// End CachedSLEs functions.
|
||||
|
||||
/**
|
||||
* Fetch or create an entry and execute a callback while holding the lock.
|
||||
*
|
||||
* The entry for the given key is fetched from the cache or created if it
|
||||
* doesn't exist. The callback is then invoked with a reference to the
|
||||
* entry while the cache mutex is still held, allowing safe modification
|
||||
* of the cached object.
|
||||
*
|
||||
* @param key The key to fetch or create
|
||||
* @param callback Function to call with the entry under lock.
|
||||
* Signature: void(T&)
|
||||
*/
|
||||
template <class Callback>
|
||||
void
|
||||
fetchAndModify(key_type const& key, Callback&& callback);
|
||||
|
||||
private:
|
||||
SharedPointerType
|
||||
initialFetch(key_type const& key, std::scoped_lock<MutexType> const& l);
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
#include <xrpl/basics/Log.h> // IWYU pragma: keep
|
||||
#include <xrpl/basics/TaggedCache.h>
|
||||
#include <xrpl/basics/scope.h>
|
||||
#include <xrpl/beast/utility/instrumentation.h>
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
@@ -336,29 +335,6 @@ template <class Policy, class Callback>
|
||||
inline bool
|
||||
TaggedCache<Key, T, IsKeyCache, SharedWeakUnionPointer, SharedPointerType, Hash, KeyEqual, Mutex>::
|
||||
canonicalizeImpl(
|
||||
key_type const& key,
|
||||
CanonicalizeClientPointerType<Policy> data,
|
||||
Policy policy,
|
||||
Callback&& replaceCallback)
|
||||
{
|
||||
std::scoped_lock<MutexType> const lock(mutex_);
|
||||
return canonicalizeImpl(lock, key, data, policy, std::forward<Callback>(replaceCallback));
|
||||
}
|
||||
|
||||
template <
|
||||
class Key,
|
||||
class T,
|
||||
bool IsKeyCache,
|
||||
class SharedWeakUnionPointer,
|
||||
class SharedPointerType,
|
||||
class Hash,
|
||||
class KeyEqual,
|
||||
class Mutex>
|
||||
template <class Policy, class Callback>
|
||||
inline bool
|
||||
TaggedCache<Key, T, IsKeyCache, SharedWeakUnionPointer, SharedPointerType, Hash, KeyEqual, Mutex>::
|
||||
canonicalizeImpl(
|
||||
std::scoped_lock<MutexType> const&,
|
||||
key_type const& key,
|
||||
CanonicalizeClientPointerType<Policy> data,
|
||||
[[maybe_unused]] Policy policy,
|
||||
@@ -378,6 +354,8 @@ TaggedCache<Key, T, IsKeyCache, SharedWeakUnionPointer, SharedPointerType, Hash,
|
||||
// passing a const argument is a compile error.
|
||||
constexpr bool replaceCached = std::is_same_v<Policy, detail::ReplaceCached>;
|
||||
|
||||
std::scoped_lock const lock(mutex_);
|
||||
|
||||
auto const cit = cache_.find(key);
|
||||
|
||||
if (cit == cache_.end())
|
||||
@@ -594,6 +572,22 @@ TaggedCache<Key, T, IsKeyCache, SharedWeakUnionPointer, SharedPointerType, Hash,
|
||||
return true;
|
||||
}
|
||||
|
||||
template <
|
||||
class Key,
|
||||
class T,
|
||||
bool IsKeyCache,
|
||||
class SharedWeakUnionPointer,
|
||||
class SharedPointerType,
|
||||
class Hash,
|
||||
class KeyEqual,
|
||||
class Mutex>
|
||||
inline auto
|
||||
TaggedCache<Key, T, IsKeyCache, SharedWeakUnionPointer, SharedPointerType, Hash, KeyEqual, Mutex>::
|
||||
peekMutex() -> MutexType&
|
||||
{
|
||||
return mutex_;
|
||||
}
|
||||
|
||||
template <
|
||||
class Key,
|
||||
class T,
|
||||
@@ -706,49 +700,6 @@ TaggedCache<Key, T, IsKeyCache, SharedWeakUnionPointer, SharedPointerType, Hash,
|
||||
}
|
||||
// End CachedSLEs functions.
|
||||
|
||||
template <
|
||||
class Key,
|
||||
class T,
|
||||
bool IsKeyCache,
|
||||
class SharedWeakUnionPointer,
|
||||
class SharedPointerType,
|
||||
class Hash,
|
||||
class KeyEqual,
|
||||
class Mutex>
|
||||
template <class Callback>
|
||||
inline void
|
||||
TaggedCache<Key, T, IsKeyCache, SharedWeakUnionPointer, SharedPointerType, Hash, KeyEqual, Mutex>::
|
||||
fetchAndModify(key_type const& key, Callback&& callback)
|
||||
{
|
||||
static_assert(
|
||||
!IsKeyCache, "fetchAndModify is only supported for value caches, not key-only caches");
|
||||
static_assert(
|
||||
std::is_same_v<std::shared_ptr<T>, SharedPointerType> ||
|
||||
std::is_same_v<intr_ptr::SharedPtr<T>, SharedPointerType>,
|
||||
"fetchAndModify requires std::shared_ptr<T> or intr_ptr::SharedPtr<T>");
|
||||
|
||||
std::scoped_lock<MutexType> const lock(mutex_);
|
||||
|
||||
// Same pointer-type dispatch as insert(): the placeholder must have exactly
|
||||
// SharedPointerType so canonicalizeImpl can write the cached pointer back.
|
||||
SharedPointerType entry = [] {
|
||||
if constexpr (std::is_same_v<std::shared_ptr<T>, SharedPointerType>)
|
||||
{
|
||||
return std::make_shared<T>();
|
||||
}
|
||||
else
|
||||
{
|
||||
return intr_ptr::makeShared<T>();
|
||||
}
|
||||
}();
|
||||
canonicalizeImpl(lock, key, entry, detail::ReplaceClient{});
|
||||
|
||||
ALWAYS(
|
||||
entry != nullptr, "xrpl::TaggedCache::fetchAndModify : entry present after canonicalize");
|
||||
|
||||
callback(*entry);
|
||||
}
|
||||
|
||||
template <
|
||||
class Key,
|
||||
class T,
|
||||
|
||||
@@ -94,7 +94,6 @@ struct Keys
|
||||
static constexpr auto kBbtOptions = "bbt_options";
|
||||
static constexpr auto kBgThreads = "bg_threads";
|
||||
static constexpr auto kBlockSize = "block_size";
|
||||
static constexpr auto kBytecodeSizeLimit = "bytecode_size_limit";
|
||||
static constexpr auto kCacheAge = "cache_age";
|
||||
static constexpr auto kCacheMb = "cache_mb";
|
||||
static constexpr auto kCacheSize = "cache_size";
|
||||
@@ -109,8 +108,6 @@ struct Keys
|
||||
static constexpr auto kFileSizeMult = "file_size_mult";
|
||||
static constexpr auto kFilterBits = "filter_bits";
|
||||
static constexpr auto kFilterFull = "filter_full";
|
||||
static constexpr auto kGasLimit = "gas_limit";
|
||||
static constexpr auto kGasPrice = "gas_price";
|
||||
static constexpr auto kHardSet = "hard_set";
|
||||
static constexpr auto kHighThreads = "high_threads";
|
||||
static constexpr auto kHoldTime = "hold_time";
|
||||
|
||||
@@ -3,11 +3,13 @@
|
||||
#include <xrpl/basics/Number.h>
|
||||
#include <xrpl/json/json_forwards.h>
|
||||
|
||||
#include <concepts>
|
||||
#include <cstring>
|
||||
#include <iterator>
|
||||
#include <limits>
|
||||
#include <map>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
/**
|
||||
@@ -198,6 +200,15 @@ public:
|
||||
*/
|
||||
Value(StaticString const& value);
|
||||
Value(std::string const& value);
|
||||
/**
|
||||
* @brief Constructs a value from a string view.
|
||||
*
|
||||
* The characters are copied, so the view need not outlive the call and need
|
||||
* not be NUL-terminated.
|
||||
*
|
||||
* @param value The characters to copy.
|
||||
*/
|
||||
Value(std::string_view value);
|
||||
Value(bool value);
|
||||
Value(Value const& other);
|
||||
~Value();
|
||||
@@ -472,6 +483,32 @@ toJson(xrpl::Number const& number)
|
||||
bool
|
||||
operator==(Value const&, Value const&);
|
||||
|
||||
/**
|
||||
* Compares a value with a string view, reading the value's characters in place
|
||||
* rather than building a Value from the view.
|
||||
*
|
||||
* Constrained to the exact type: a string literal converts equally well to a
|
||||
* view and to a Value, so a plain overload makes `value == "literal"`
|
||||
* ambiguous.
|
||||
*
|
||||
* @param x The value to compare.
|
||||
* @param y The characters to compare it against.
|
||||
* @return Whether `x` is a string whose characters up to its first NUL are
|
||||
* exactly the characters of `y`.
|
||||
*/
|
||||
template <class T>
|
||||
requires std::same_as<T, std::string_view>
|
||||
bool
|
||||
operator==(Value const& x, T y)
|
||||
{
|
||||
if (!x.isString())
|
||||
return false;
|
||||
|
||||
// A string `Value` can hold a null pointer, which names no characters, so it equals no view.
|
||||
char const* const s = x.asCString();
|
||||
return s != nullptr && std::string_view{s} == y;
|
||||
}
|
||||
|
||||
bool
|
||||
operator<(Value const&, Value const&);
|
||||
|
||||
|
||||
@@ -257,42 +257,13 @@ public:
|
||||
header_.validated = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark this ledger as accepted and attempt to make it immutable.
|
||||
*
|
||||
* The close-time fields are recorded first, since the ledger hash covers
|
||||
* them.
|
||||
*
|
||||
* @param closeTime The consensus-agreed close time.
|
||||
* @param closeResolution The close time resolution.
|
||||
* @param correctCloseTime Whether consensus agreed on the close time. If
|
||||
* false, kSLcfNoConsensusTime is recorded in closeFlags instead.
|
||||
* @return What setImmutable() returned.
|
||||
*/
|
||||
[[nodiscard]] bool
|
||||
void
|
||||
setAccepted(
|
||||
NetClock::time_point closeTime,
|
||||
NetClock::duration closeResolution,
|
||||
bool correctCloseTime);
|
||||
|
||||
/**
|
||||
* Mark this ledger as immutable, so it can no longer be modified.
|
||||
*
|
||||
* A ledger built or loaded locally cannot have an invalid map, since only
|
||||
* a map syncing against hashes from outside can be proven impossible (see
|
||||
* SHAMap::addKnownNode), so a caller on such a path may treat a false
|
||||
* return as a broken internal invariant. A caller assembling a ledger from
|
||||
* peer data may not: for it, false is an outcome a peer can produce.
|
||||
*
|
||||
* @param rehash Whether to recompute the ledger hash from the header
|
||||
* fields. The transaction and account hashes are recomputed from the
|
||||
* maps too, but only the first time.
|
||||
* @return false if either map is Invalid, leaving the immutable flag as it
|
||||
* was and the header untouched. A map invalidated partway through can
|
||||
* still leave the other one immutable, so a false return means the
|
||||
* ledger must be discarded rather than retried.
|
||||
*/
|
||||
[[nodiscard]] bool
|
||||
void
|
||||
setImmutable(bool rehash = true);
|
||||
|
||||
bool
|
||||
@@ -301,28 +272,23 @@ public:
|
||||
return immutable_;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Whether both maps can still be the maps the header names. See
|
||||
* SHAMap::isValid().
|
||||
*/
|
||||
[[nodiscard]] bool
|
||||
mapsValid() const
|
||||
{
|
||||
return txMap_.isValid() && stateMap_.isValid();
|
||||
}
|
||||
/* Mark this ledger as "should be full".
|
||||
|
||||
/**
|
||||
* Mark this ledger as "should be full", indicating that the local server
|
||||
* wants all the corresponding nodes in durable storage.
|
||||
*
|
||||
* Const because it reflects metadata, not data this ledger shares with
|
||||
* other nodes on the network.
|
||||
*/
|
||||
"Full" is metadata property of the ledger, it indicates
|
||||
that the local server wants all the corresponding nodes
|
||||
in durable storage.
|
||||
|
||||
This is marked `const` because it reflects metadata
|
||||
and not data that is in common with other nodes on the
|
||||
network.
|
||||
*/
|
||||
void
|
||||
setFull() const
|
||||
{
|
||||
txMap_.setFull(header_.seq);
|
||||
stateMap_.setFull(header_.seq);
|
||||
txMap_.setFull();
|
||||
txMap_.setLedgerSeq(header_.seq);
|
||||
stateMap_.setFull();
|
||||
stateMap_.setLedgerSeq(header_.seq);
|
||||
}
|
||||
|
||||
void
|
||||
@@ -452,22 +418,6 @@ private:
|
||||
static std::pair<std::shared_ptr<STTx const>, std::shared_ptr<STObject const>>
|
||||
deserializeTxPlusMeta(SHAMapItem const& item);
|
||||
|
||||
/**
|
||||
* Make both maps immutable.
|
||||
*
|
||||
* Each map is settled on its own, so neither is left mid-sync because the
|
||||
* other refused.
|
||||
*
|
||||
* @return Whether both maps are immutable.
|
||||
*/
|
||||
[[nodiscard]] bool
|
||||
setMapsImmutable()
|
||||
{
|
||||
bool const txImmutable = txMap_.setImmutable();
|
||||
bool const stateImmutable = stateMap_.setImmutable();
|
||||
return txImmutable && stateImmutable;
|
||||
}
|
||||
|
||||
bool immutable_;
|
||||
|
||||
// A SHAMap containing the transactions associated with this ledger.
|
||||
|
||||
@@ -265,13 +265,6 @@ canWithdraw(
|
||||
[[nodiscard]] TER
|
||||
canWithdraw(ReadView const& view, STTx const& tx);
|
||||
|
||||
/**
|
||||
* Pays out a withdrawal from a vault or loan broker pseudo-account.
|
||||
*
|
||||
* `sourceAmount` leaves `sourceAcct` and `destinationAmount` reaches `dstAcct`.
|
||||
* The two differ only when the withdrawal carries a transfer fee; the
|
||||
* difference is the fee and is settled through the issuer.
|
||||
*/
|
||||
[[nodiscard]] TER
|
||||
doWithdraw(
|
||||
ApplyViewContext ctx,
|
||||
@@ -279,8 +272,7 @@ doWithdraw(
|
||||
AccountID const& dstAcct,
|
||||
AccountID const& sourceAcct,
|
||||
XRPAmount priorBalance,
|
||||
STAmount const& destinationAmount,
|
||||
STAmount const& sourceAmount,
|
||||
STAmount const& amount,
|
||||
beast::Journal j);
|
||||
|
||||
/**
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
#include <xrpl/basics/Log.h>
|
||||
#include <xrpl/basics/Number.h>
|
||||
#include <xrpl/basics/contract.h>
|
||||
#include <xrpl/beast/utility/Journal.h>
|
||||
#include <xrpl/beast/utility/Zero.h>
|
||||
#include <xrpl/ledger/ApplyView.h>
|
||||
@@ -28,6 +29,7 @@
|
||||
#include <expected>
|
||||
#include <functional>
|
||||
#include <optional>
|
||||
#include <stdexcept>
|
||||
#include <tuple>
|
||||
#include <utility>
|
||||
|
||||
@@ -311,11 +313,11 @@ getAMMOfferStartWithTakerPays(
|
||||
* is equal to LOB quality (in this case AMM offer quality is
|
||||
* better than LOB quality) or AMM offer is equal to LOB quality
|
||||
* (in this case SPQ is better than LOB quality).
|
||||
* The economically coarser integral offer side is calculated first. The
|
||||
* result is rounded down, which makes the offer quality better. Calculating
|
||||
* takerPays first instead would round down takerGets when that is the coarser
|
||||
* side, giving worse quality than LOB quality, and the offer might fail to
|
||||
* generate.
|
||||
* Pre-amendment code calculates takerPays first. If takerGets is the
|
||||
* economically coarser integral side, it is rounded down, which results in
|
||||
* worse offer quality than LOB quality, and the offer might fail to generate.
|
||||
* Post-amendment code calculates the economically coarser integral offer side
|
||||
* first. The result is rounded down, which makes the offer quality better.
|
||||
* It might not be possible to match either SPQ or AMM offer to LOB
|
||||
* quality. This generally happens at higher fees.
|
||||
* @param pool AMM pool balances
|
||||
@@ -329,8 +331,69 @@ changeSpotPriceQuality(
|
||||
TAmounts<TIn, TOut> const& pool,
|
||||
Quality const& quality,
|
||||
std::uint16_t tfee,
|
||||
Rules const& rules,
|
||||
beast::Journal j)
|
||||
{
|
||||
if (!rules.enabled(fixAMMv1_1))
|
||||
{
|
||||
// Finds takerPays (i) and takerGets (o) such that given pool
|
||||
// composition poolGets(I) and poolPays(O): (O - o) / (I + i) = quality.
|
||||
// Where takerGets is calculated as the swapAssetIn (see below).
|
||||
// The above equation produces the quadratic equation:
|
||||
// i^2*(1-fee) + i*I*(2-fee) + I^2 - I*O/quality,
|
||||
// which is solved for i, and o is found with swapAssetIn().
|
||||
auto const f = feeMult(tfee); // 1 - fee
|
||||
auto const& a = f;
|
||||
auto const b = pool.in * (1 + f);
|
||||
Number const c = pool.in * pool.in - pool.in * pool.out * quality.rate();
|
||||
auto const res = b * b - 4 * a * c;
|
||||
if (res < 0)
|
||||
{
|
||||
return std::nullopt; // LCOV_EXCL_LINE
|
||||
}
|
||||
if (auto const nTakerPaysPropose = (-b + root2(res)) / (2 * a); nTakerPaysPropose > 0)
|
||||
{
|
||||
auto const nTakerPays = [&] {
|
||||
// The fee might make the AMM offer quality less than CLOB
|
||||
// quality. Therefore, AMM offer has to satisfy this constraint:
|
||||
// o / i >= q. Substituting o with swapAssetIn() gives: i <= O /
|
||||
// q - I / (1 - fee).
|
||||
auto const nTakerPaysConstraint = pool.out * quality.rate() - pool.in / f;
|
||||
if (nTakerPaysPropose > nTakerPaysConstraint)
|
||||
return nTakerPaysConstraint;
|
||||
return nTakerPaysPropose;
|
||||
}();
|
||||
if (nTakerPays <= 0)
|
||||
{
|
||||
JLOG(j.trace()) << "changeSpotPriceQuality calc failed: " << to_string(pool.in)
|
||||
<< " " << to_string(pool.out) << " " << quality << " " << tfee;
|
||||
return std::nullopt;
|
||||
}
|
||||
auto const takerPays =
|
||||
toAmount<TIn>(getAsset(pool.in), nTakerPays, Number::RoundingMode::Upward);
|
||||
// should not fail
|
||||
auto amounts = TAmounts<TIn, TOut>{takerPays, swapAssetIn(pool, takerPays, tfee)};
|
||||
if (Quality{amounts} < quality &&
|
||||
!withinRelativeDistance(Quality{amounts}, quality, Number(1, -7)))
|
||||
{
|
||||
JLOG(j.error()) << "changeSpotPriceQuality failed: " << to_string(pool.in) << " "
|
||||
<< to_string(pool.out) << " "
|
||||
<< " " << quality << " " << tfee << " " << to_string(amounts.in)
|
||||
<< " " << to_string(amounts.out);
|
||||
Throw<std::runtime_error>("changeSpotPriceQuality failed");
|
||||
}
|
||||
|
||||
JLOG(j.trace()) << "changeSpotPriceQuality succeeded: " << to_string(pool.in) << " "
|
||||
<< to_string(pool.out) << " "
|
||||
<< " " << quality << " " << tfee << " " << to_string(amounts.in) << " "
|
||||
<< to_string(amounts.out);
|
||||
return amounts;
|
||||
}
|
||||
JLOG(j.trace()) << "changeSpotPriceQuality calc failed: " << to_string(pool.in) << " "
|
||||
<< to_string(pool.out) << " " << quality << " " << tfee;
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
auto amounts = [&] {
|
||||
bool const inIntegral = getAsset(pool.in).integral();
|
||||
bool const outIntegral = getAsset(pool.out).integral();
|
||||
@@ -396,49 +459,57 @@ template <typename TIn, typename TOut>
|
||||
TOut
|
||||
swapAssetIn(TAmounts<TIn, TOut> const& pool, TIn const& assetIn, std::uint16_t tfee)
|
||||
{
|
||||
// set rounding to always favor the amm. Clip to zero.
|
||||
// calculate:
|
||||
// pool.out -
|
||||
// (pool.in * pool.out) / (pool.in + assetIn * feeMult(tfee)),
|
||||
// and explicitly set the rounding modes
|
||||
// Favoring the amm means we should:
|
||||
// minimize:
|
||||
// pool.out -
|
||||
// (pool.in * pool.out) / (pool.in + assetIn * feeMult(tfee)),
|
||||
// maximize:
|
||||
// (pool.in * pool.out) / (pool.in + assetIn * feeMult(tfee)),
|
||||
// (pool.in * pool.out)
|
||||
// minimize:
|
||||
// (pool.in + assetIn * feeMult(tfee)),
|
||||
// minimize:
|
||||
// assetIn * feeMult(tfee)
|
||||
// feeMult is: (1-fee), fee is tfee/100000
|
||||
// minimize:
|
||||
// 1-fee
|
||||
// maximize:
|
||||
// fee
|
||||
SaveNumberRoundMode const _{Number::getround()};
|
||||
if (auto const& rules = getCurrentTransactionRules(); rules && rules->enabled(fixAMMv1_1))
|
||||
{
|
||||
// set rounding to always favor the amm. Clip to zero.
|
||||
// calculate:
|
||||
// pool.out -
|
||||
// (pool.in * pool.out) / (pool.in + assetIn * feeMult(tfee)),
|
||||
// and explicitly set the rounding modes
|
||||
// Favoring the amm means we should:
|
||||
// minimize:
|
||||
// pool.out -
|
||||
// (pool.in * pool.out) / (pool.in + assetIn * feeMult(tfee)),
|
||||
// maximize:
|
||||
// (pool.in * pool.out) / (pool.in + assetIn * feeMult(tfee)),
|
||||
// (pool.in * pool.out)
|
||||
// minimize:
|
||||
// (pool.in + assetIn * feeMult(tfee)),
|
||||
// minimize:
|
||||
// assetIn * feeMult(tfee)
|
||||
// feeMult is: (1-fee), fee is tfee/100000
|
||||
// minimize:
|
||||
// 1-fee
|
||||
// maximize:
|
||||
// fee
|
||||
SaveNumberRoundMode const _{Number::getround()};
|
||||
|
||||
Number::setround(Number::RoundingMode::Upward);
|
||||
auto const numerator = pool.in * pool.out;
|
||||
auto const fee = getFee(tfee);
|
||||
Number::setround(Number::RoundingMode::Upward);
|
||||
auto const numerator = pool.in * pool.out;
|
||||
auto const fee = getFee(tfee);
|
||||
|
||||
Number::setround(Number::RoundingMode::Downward);
|
||||
auto const denom = pool.in + assetIn * (1 - fee);
|
||||
Number::setround(Number::RoundingMode::Downward);
|
||||
auto const denom = pool.in + assetIn * (1 - fee);
|
||||
|
||||
if (denom.signum() <= 0)
|
||||
return toAmount<TOut>(getAsset(pool.out), 0);
|
||||
if (denom.signum() <= 0)
|
||||
return toAmount<TOut>(getAsset(pool.out), 0);
|
||||
|
||||
Number::setround(Number::RoundingMode::Upward);
|
||||
auto const ratio = numerator / denom;
|
||||
Number::setround(Number::RoundingMode::Upward);
|
||||
auto const ratio = numerator / denom;
|
||||
|
||||
Number::setround(Number::RoundingMode::Downward);
|
||||
auto const swapOut = pool.out - ratio;
|
||||
Number::setround(Number::RoundingMode::Downward);
|
||||
auto const swapOut = pool.out - ratio;
|
||||
|
||||
if (swapOut.signum() < 0)
|
||||
return toAmount<TOut>(getAsset(pool.out), 0);
|
||||
if (swapOut.signum() < 0)
|
||||
return toAmount<TOut>(getAsset(pool.out), 0);
|
||||
|
||||
return toAmount<TOut>(getAsset(pool.out), swapOut, Number::RoundingMode::Downward);
|
||||
return toAmount<TOut>(getAsset(pool.out), swapOut, Number::RoundingMode::Downward);
|
||||
}
|
||||
|
||||
return toAmount<TOut>(
|
||||
getAsset(pool.out),
|
||||
pool.out - (pool.in * pool.out) / (pool.in + assetIn * feeMult(tfee)),
|
||||
Number::RoundingMode::Downward);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -455,49 +526,57 @@ template <typename TIn, typename TOut>
|
||||
TIn
|
||||
swapAssetOut(TAmounts<TIn, TOut> const& pool, TOut const& assetOut, std::uint16_t tfee)
|
||||
{
|
||||
// set rounding to always favor the amm. Clip to zero.
|
||||
// calculate:
|
||||
// ((pool.in * pool.out) / (pool.out - assetOut) - pool.in) /
|
||||
// (1-tfee/100000)
|
||||
// maximize:
|
||||
// ((pool.in * pool.out) / (pool.out - assetOut) - pool.in)
|
||||
// maximize:
|
||||
// (pool.in * pool.out) / (pool.out - assetOut)
|
||||
// maximize:
|
||||
// (pool.in * pool.out)
|
||||
// minimize
|
||||
// (pool.out - assetOut)
|
||||
// minimize:
|
||||
// (1-tfee/100000)
|
||||
// maximize:
|
||||
// tfee/100000
|
||||
|
||||
SaveNumberRoundMode const _{Number::getround()};
|
||||
|
||||
Number::setround(Number::RoundingMode::Upward);
|
||||
auto const numerator = pool.in * pool.out;
|
||||
|
||||
Number::setround(Number::RoundingMode::Downward);
|
||||
auto const denom = pool.out - assetOut;
|
||||
if (denom.signum() <= 0)
|
||||
if (auto const& rules = getCurrentTransactionRules(); rules && rules->enabled(fixAMMv1_1))
|
||||
{
|
||||
return toMaxAmount<TIn>(getAsset(pool.in));
|
||||
// set rounding to always favor the amm. Clip to zero.
|
||||
// calculate:
|
||||
// ((pool.in * pool.out) / (pool.out - assetOut) - pool.in) /
|
||||
// (1-tfee/100000)
|
||||
// maximize:
|
||||
// ((pool.in * pool.out) / (pool.out - assetOut) - pool.in)
|
||||
// maximize:
|
||||
// (pool.in * pool.out) / (pool.out - assetOut)
|
||||
// maximize:
|
||||
// (pool.in * pool.out)
|
||||
// minimize
|
||||
// (pool.out - assetOut)
|
||||
// minimize:
|
||||
// (1-tfee/100000)
|
||||
// maximize:
|
||||
// tfee/100000
|
||||
|
||||
SaveNumberRoundMode const _{Number::getround()};
|
||||
|
||||
Number::setround(Number::RoundingMode::Upward);
|
||||
auto const numerator = pool.in * pool.out;
|
||||
|
||||
Number::setround(Number::RoundingMode::Downward);
|
||||
auto const denom = pool.out - assetOut;
|
||||
if (denom.signum() <= 0)
|
||||
{
|
||||
return toMaxAmount<TIn>(getAsset(pool.in));
|
||||
}
|
||||
|
||||
Number::setround(Number::RoundingMode::Upward);
|
||||
auto const ratio = numerator / denom;
|
||||
auto const numerator2 = ratio - pool.in;
|
||||
auto const fee = getFee(tfee);
|
||||
|
||||
Number::setround(Number::RoundingMode::Downward);
|
||||
auto const feeMult = 1 - fee;
|
||||
|
||||
Number::setround(Number::RoundingMode::Upward);
|
||||
auto const swapIn = numerator2 / feeMult;
|
||||
if (swapIn.signum() < 0)
|
||||
return toAmount<TIn>(getAsset(pool.in), 0);
|
||||
|
||||
return toAmount<TIn>(getAsset(pool.in), swapIn, Number::RoundingMode::Upward);
|
||||
}
|
||||
|
||||
Number::setround(Number::RoundingMode::Upward);
|
||||
auto const ratio = numerator / denom;
|
||||
auto const numerator2 = ratio - pool.in;
|
||||
auto const fee = getFee(tfee);
|
||||
|
||||
Number::setround(Number::RoundingMode::Downward);
|
||||
auto const feeMult = 1 - fee;
|
||||
|
||||
Number::setround(Number::RoundingMode::Upward);
|
||||
auto const swapIn = numerator2 / feeMult;
|
||||
if (swapIn.signum() < 0)
|
||||
return toAmount<TIn>(getAsset(pool.in), 0);
|
||||
|
||||
return toAmount<TIn>(getAsset(pool.in), swapIn, Number::RoundingMode::Upward);
|
||||
return toAmount<TIn>(
|
||||
getAsset(pool.in),
|
||||
((pool.in * pool.out) / (pool.out - assetOut) - pool.in) / feeMult(tfee),
|
||||
Number::RoundingMode::Upward);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -7,10 +7,10 @@
|
||||
#include <xrpl/beast/utility/instrumentation.h>
|
||||
#include <xrpl/ledger/ApplyView.h>
|
||||
#include <xrpl/ledger/ReadView.h>
|
||||
#include <xrpl/ledger/helpers/VaultHelpers.h>
|
||||
#include <xrpl/protocol/AccountID.h>
|
||||
#include <xrpl/protocol/Asset.h>
|
||||
#include <xrpl/protocol/LedgerFormats.h> // IWYU pragma: keep
|
||||
#include <xrpl/protocol/Protocol.h>
|
||||
#include <xrpl/protocol/Rules.h>
|
||||
#include <xrpl/protocol/SField.h>
|
||||
#include <xrpl/protocol/STAmount.h>
|
||||
@@ -31,9 +31,9 @@ namespace xrpl {
|
||||
/**
|
||||
* Broker cover preclaim precision guard (fixCleanup3_2_0).
|
||||
*
|
||||
* Prevents a silent sub-ULP no-op where a deposit, withdrawal, or clawback
|
||||
* amount is so small that it rounds to zero at sfCoverAvailable's scale.
|
||||
* Without this guard, both the pseudo trust-line and sfCoverAvailable would
|
||||
* Prevents a "silent sub-ULP no-op" where a deposit, withdrawal, or clawback
|
||||
* amount is so small that it rounds to zero at `sfCoverAvailable`'s scale.
|
||||
* Without this guard, both the pseudo trust-line and `sfCoverAvailable` would
|
||||
* identically absorb the rounded zero, resulting in a successful transaction
|
||||
* (tesSUCCESS) where no funds actually moved.
|
||||
*
|
||||
@@ -44,8 +44,8 @@ namespace xrpl {
|
||||
* @param j Journal for logging.
|
||||
* @param logPrefix Transactor name for log diagnostics.
|
||||
*
|
||||
* @return tecPRECISION_LOSS if the request rounds to zero at cover scale.
|
||||
* tesSUCCESS if the amendment is disabled or the request is safely supra-ULP.
|
||||
* @return `tecPRECISION_LOSS` if the request rounds to zero at cover scale.
|
||||
* `tesSUCCESS` if the amendment is disabled or the request is safely supra-ULP.
|
||||
*/
|
||||
[[nodiscard]] TER
|
||||
canApplyToBrokerCover(
|
||||
@@ -56,82 +56,6 @@ canApplyToBrokerCover(
|
||||
beast::Journal j,
|
||||
std::string_view logPrefix);
|
||||
|
||||
namespace detail {
|
||||
|
||||
/**
|
||||
* Return a LoanBroker's posterior live cover exponent after applying an
|
||||
* unrounded delta.
|
||||
*/
|
||||
[[nodiscard]] int
|
||||
getPosteriorBrokerCoverScale(SLE::ConstRef vault, SLE::ConstRef broker, Number const& delta);
|
||||
|
||||
/**
|
||||
* Round a cover outflow delta (cover withdraw, cover clawback, the
|
||||
* broker-side decrease of a default's cover credit) at the LoanBroker's
|
||||
* posterior live exponent. Safe to round the delta directly for an outflow:
|
||||
* amount on the posterior grid of CoverAvailable always leaves CoverAvailable
|
||||
* minus amount representable, since the posterior grid is exactly the grid
|
||||
* CoverAvailable itself will canonicalize to after the subtraction.
|
||||
*/
|
||||
[[nodiscard]] STAmount
|
||||
roundToPosteriorBrokerCoverScale(
|
||||
SLE::ConstRef vault,
|
||||
SLE::ConstRef broker,
|
||||
STAmount const& delta,
|
||||
Number::RoundingMode roundingMode);
|
||||
|
||||
} // namespace detail
|
||||
|
||||
/**
|
||||
* Round a cover debit (cover withdraw, cover clawback, the broker-side
|
||||
* decrease of a default's cover credit): amount is the non-negative
|
||||
* magnitude leaving CoverAvailable. Negates so the posterior is computed for
|
||||
* CoverAvailable minus amount, then negates the result back to a
|
||||
* non-negative magnitude, per roundToPosteriorBrokerCoverScale's outflow
|
||||
* rationale above. Magnitude in, magnitude out -- unlike the vault-side
|
||||
* outflow dispatcher (clampVaultOutflow), which takes and returns a negative
|
||||
* delta; callers on each side already hold the value in that side's native
|
||||
* form.
|
||||
*/
|
||||
[[nodiscard]] STAmount
|
||||
debitToPosteriorBrokerCoverScale(
|
||||
SLE::ConstRef vault,
|
||||
SLE::ConstRef broker,
|
||||
STAmount const& amount,
|
||||
Number::RoundingMode roundingMode);
|
||||
|
||||
/**
|
||||
* Round a cover inflow (cover deposit, LoanPay's fee redirect into cover)
|
||||
* into CoverAvailable by flooring the sum rather than the delta:
|
||||
* credit = floor16(CoverAvailable + raw) - CoverAvailable, never finer
|
||||
* than the Vault's base exponent (-Scale). See
|
||||
* creditToPosteriorAvailableScale's doc for why the sum, not the delta,
|
||||
* must be floored; the same reasoning applies to CoverAvailable.
|
||||
*/
|
||||
[[nodiscard]] STAmount
|
||||
creditToPosteriorBrokerCoverScale(
|
||||
SLE::ConstRef vault,
|
||||
SLE::ConstRef broker,
|
||||
Number const& raw,
|
||||
Number::RoundingMode roundingMode);
|
||||
|
||||
/**
|
||||
* Check whether amount is an admissible optional cover inflow.
|
||||
*
|
||||
* Legacy and CashBasis Vaults always succeed. A LoanBroker attached to a
|
||||
* FixedPrecision Vault must remain at the Vault's base scale after applying
|
||||
* the rounded amount, and its posterior CoverAvailable must stay within the
|
||||
* Open zone.
|
||||
*
|
||||
* LoanPay's fee redirect into cover is a mandatory inflow and does not call
|
||||
* this check, so it can push CoverAvailable past the Open zone limit this
|
||||
* check enforces. Once that happens, every subsequent LoanBrokerCoverDeposit
|
||||
* returns tecLIMIT_EXCEEDED until the owner withdraws cover back under the
|
||||
* limit; LoanBrokerCoverWithdraw and LoanBrokerCoverClawback are unaffected.
|
||||
*/
|
||||
[[nodiscard]] TER
|
||||
checkOptionalBrokerCoverInflow(SLE::ConstRef vault, SLE::ConstRef broker, STAmount const& amount);
|
||||
|
||||
// Lending protocol has dependencies, so capture them here.
|
||||
bool
|
||||
checkLendingProtocolDependencies(Rules const& rules, STTx const& tx);
|
||||
@@ -140,13 +64,13 @@ checkLendingProtocolDependencies(Rules const& rules, STTx const& tx);
|
||||
* The accounts and asset that LoanManage::defaultLoan's fixCleanup3_4_0
|
||||
* freeze/lock exemption applies to.
|
||||
*
|
||||
* defaultLoan moves funds from the LoanBroker pseudo-account to the Vault
|
||||
* pseudo-account via accountSend. Since neither is the vault asset's
|
||||
* `defaultLoan` moves funds from the LoanBroker pseudo-account to the Vault
|
||||
* pseudo-account via `accountSend`. Since neither is the vault asset's
|
||||
* issuer, this is a third-party transfer that transits through the issuer in
|
||||
* two hops (broker -> issuer, issuer -> vault; see
|
||||
* directSendNoLimitIOU/directSendNoLimitMPT), so the exemption must cover
|
||||
* `directSendNoLimitIOU`/`directSendNoLimitMPT`), so the exemption must cover
|
||||
* both the issuer/broker and issuer/vault pairs, not a direct broker/vault
|
||||
* pair. asset scopes it further to the vault's own currency/MPT issuance,
|
||||
* pair. `asset` scopes it further to the vault's own currency/MPT issuance,
|
||||
* so an unrelated one the same accounts happen to hold is still protected.
|
||||
*/
|
||||
struct LoanDefaultFreezeExemptAccounts
|
||||
@@ -164,10 +88,10 @@ struct LoanDefaultFreezeExemptAccounts
|
||||
* @param view Ledger view used to resolve the Loan -> LoanBroker -> Vault
|
||||
* chain.
|
||||
* @param tx The transaction under invariant review.
|
||||
* @return The exempt accounts and asset if tx is a ttLOAN_MANAGE
|
||||
* transaction with the tfLoanDefault flag set, fixCleanup3_4_0 is
|
||||
* @return The exempt accounts and asset if `tx` is a `ttLOAN_MANAGE`
|
||||
* transaction with the `tfLoanDefault` flag set, `fixCleanup3_4_0` is
|
||||
* enabled, and the loan/broker/vault objects it references can all be
|
||||
* resolved; std::nullopt otherwise.
|
||||
* resolved; `std::nullopt` otherwise.
|
||||
*/
|
||||
[[nodiscard]] std::optional<LoanDefaultFreezeExemptAccounts>
|
||||
getLoanDefaultFreezeExemptAccounts(ReadView const& view, STTx const& tx);
|
||||
@@ -330,38 +254,28 @@ adjustImpreciseNumber(
|
||||
value = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply a signed delta to a LoanBroker's DebtTotal: on FixedPrecision Vaults,
|
||||
* add the exact delta (DebtTotal moves by exactly the same amount as the
|
||||
* Vault's AssetsDeployed, never re-rounded); on Legacy/CashBasis Vaults,
|
||||
* round through adjustImpreciseNumber at vaultScale. Shared by LoanSet
|
||||
* (origination) and LoanPay (payment, where delta is negative).
|
||||
*
|
||||
* @param brokerSle The LoanBroker whose DebtTotal is adjusted.
|
||||
* @param vaultSle The LoanBroker's Vault.
|
||||
* @param delta The signed change to DebtTotal.
|
||||
* @param vaultScale The Vault scale the caller captured before changing
|
||||
* AssetsTotal. Legacy/CashBasis round DebtTotal at that scale, not at
|
||||
* the scale after this transaction's AssetsTotal change. Unused on
|
||||
* FixedPrecision Vaults.
|
||||
*/
|
||||
void
|
||||
adjustBrokerDebtTotal(
|
||||
SLE::Ref brokerSle,
|
||||
SLE::ConstRef vaultSle,
|
||||
Number const& delta,
|
||||
int vaultScale);
|
||||
inline int
|
||||
getAssetsTotalScale(SLE::ConstRef vaultSle)
|
||||
{
|
||||
if (!vaultSle)
|
||||
return Number::kMinExponent - 1; // LCOV_EXCL_LINE
|
||||
return scale(vaultSle->at(sfAssetsTotal), vaultSle->at(sfAsset));
|
||||
}
|
||||
|
||||
/**
|
||||
* Minimum required broker cover, rounded up.
|
||||
*
|
||||
* DebtTotal is a broker-level aggregate, never rounded at an individual
|
||||
* loan's scale. Legacy and CashBasis Vaults round at the live AssetsTotal
|
||||
* exponent. FixedPrecision Vaults round at the Vault's base exponent
|
||||
* (-Scale, or 0 for integral assets).
|
||||
*/
|
||||
Number
|
||||
minimumBrokerCover(Number const& debtTotal, TenthBips32 coverRateMinimum, SLE::ConstRef vaultSle);
|
||||
// Compute the minimum required broker cover, rounded consistently.
|
||||
// DebtTotal is a broker-level aggregate maintained at vault scale, so the
|
||||
// rounding must also use vault scale — never an individual loan's scale.
|
||||
inline Number
|
||||
minimumBrokerCover(Number const& debtTotal, TenthBips32 coverRateMinimum, SLE::ConstRef vaultSle)
|
||||
{
|
||||
XRPL_ASSERT(
|
||||
vaultSle && vaultSle->getType() == ltVAULT, "xrpl::minimumBrokerCover : valid Vault sle");
|
||||
NumberRoundModeGuard const mg(Number::RoundingMode::Upward);
|
||||
return roundToAsset(
|
||||
vaultSle->at(sfAsset),
|
||||
tenthBipsOfValue(debtTotal, coverRateMinimum),
|
||||
getAssetsTotalScale(vaultSle));
|
||||
}
|
||||
|
||||
TER
|
||||
checkLoanGuards(
|
||||
@@ -465,10 +379,10 @@ loanPaymentDeltas(LoanPaymentParts const& parts);
|
||||
|
||||
} // namespace cash_basis
|
||||
|
||||
// Public dispatchers: pick cash_basis:: if the Vault's LEVersion
|
||||
// (VaultHelpers::getVaultVersion) is CashBasis or later (CashBasis or
|
||||
// FixedPrecision), else instant_recognition::. These are the only entry
|
||||
// points transactors call.
|
||||
// Public dispatchers: pick cash_basis:: if featureLendingProtocolV1_1 is
|
||||
// enabled AND the Vault's LEVersion (VaultHelpers::getVaultVersion) is
|
||||
// VaultVersion::CashBasis, else instant_recognition::. These are the only entry points
|
||||
// transactors call.
|
||||
AccountingDeltas
|
||||
loanOriginationDeltas(
|
||||
SLE::ConstRef vaultSle,
|
||||
|
||||
@@ -312,22 +312,12 @@ checkCreateMPT(
|
||||
//
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* MaximumAmount doesn't exceed 2**63-1
|
||||
*/
|
||||
// MaximumAmount doesn't exceed 2**63-1
|
||||
std::int64_t
|
||||
maxMPTAmount(SLE const& sleIssuance);
|
||||
|
||||
/**
|
||||
* std::nullopt if the issuance doesn't exist.
|
||||
*/
|
||||
std::optional<std::int64_t>
|
||||
maxMPTAmount(ReadView const& view, MPTID const& mptID);
|
||||
|
||||
/**
|
||||
* OutstandingAmount may overflow and available amount might be negative.
|
||||
* But available amount is always <= |MaximumAmount - OutstandingAmount|.
|
||||
*/
|
||||
// OutstandingAmount may overflow and available amount might be negative.
|
||||
// But available amount is always <= |MaximumAmount - OutstandingAmount|.
|
||||
std::int64_t
|
||||
availableMPTAmount(SLE const& sleIssuance);
|
||||
|
||||
@@ -341,12 +331,7 @@ availableMPTAmount(ReadView const& view, MPTID const& mptID);
|
||||
* directSendNoFee transactions that bypass the payment engine.
|
||||
* 2. **accountSend & Payment Engine (Overflow: Yes):** A temporary overflow
|
||||
* check when `OutstandingAmount > UINT64_MAX`. This higher threshold is used
|
||||
* for `accountSend` and payments processed via the payment engine, whose
|
||||
* reverse pass may issue before it redeems.
|
||||
*
|
||||
* In both cases a single `sendAmount > MaximumAmount` is rejected. Such a send
|
||||
* can never settle: redemptions within the transaction are bounded by the
|
||||
* outstanding amount, so the final OutstandingAmount would exceed the cap.
|
||||
* for `accountSend` and payments processed via the payment engine.
|
||||
*/
|
||||
bool
|
||||
isMPTOverflow(
|
||||
@@ -368,12 +353,6 @@ issuerFundsToSelfIssue(ReadView const& view, MPTIssue const& issue);
|
||||
/**
|
||||
* Facilitate tracking of MPT sold by an issuer owning MPT sell offer.
|
||||
* See ApplyView::issuerSelfDebitHookMPT().
|
||||
*
|
||||
* `amount` may be 0. BookStep removes an offer whose fee-adjusted funded
|
||||
* output is 0, but other clips (for example MaximumAmount via limitStepIn)
|
||||
* can still consume a zero slice of a funded remainder. The hook must
|
||||
* accept 0: first call still snapshots origBalance; a zero debit adds
|
||||
* nothing to selfDebit.
|
||||
*/
|
||||
void
|
||||
issuerSelfDebitHookMPT(ApplyView& view, MPTIssue const& issue, std::uint64_t amount);
|
||||
|
||||
@@ -1,17 +1,10 @@
|
||||
#pragma once
|
||||
|
||||
#include <xrpl/beast/utility/Journal.h>
|
||||
#include <xrpl/ledger/ApplyView.h>
|
||||
#include <xrpl/ledger/ReadView.h>
|
||||
#include <xrpl/protocol/SField.h>
|
||||
#include <xrpl/protocol/STLedgerEntry.h>
|
||||
#include <xrpl/protocol/STObject.h>
|
||||
#include <xrpl/protocol/STTx.h>
|
||||
#include <xrpl/protocol/TER.h>
|
||||
#include <xrpl/protocol/TxFormats.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <optional>
|
||||
|
||||
namespace xrpl::proposal {
|
||||
|
||||
@@ -38,21 +31,30 @@ proposalOwnerCount(STObject const& proposedTx)
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the proposed transaction is independently submittable through the
|
||||
* ordinary multi-sign path: not a proposal transaction itself, not a
|
||||
* pseudo-transaction, not itself flagged as someone else's inner batch
|
||||
* transaction, and — if it is a Batch — every one of its own inner
|
||||
* transactions carries a TransactionType and is neither a proposal, a
|
||||
* pseudo-transaction, nor a nested Batch. A Batch inner transaction cannot
|
||||
* lack a TransactionType, be pseudo, or be a nested Batch (STTx construction
|
||||
* rejects a typeless inner and an inner Batch, and preflight0 rejects the
|
||||
* pseudo/tfInnerBatchTxn combination), but those guards live outside this
|
||||
* feature, so they are checked again here rather than relied upon.
|
||||
* Whether the proposed transaction is itself a proposal transaction, which
|
||||
* would nest one proposal inside another.
|
||||
*
|
||||
* TODO: cover ttTRANSACTION_PROPOSAL_SIGN once that transaction exists.
|
||||
* TODO: cover ttTRANSACTION_PROPOSAL_SIGN and ttTRANSACTION_PROPOSAL_CANCEL
|
||||
* once those transactions exist.
|
||||
*/
|
||||
inline bool
|
||||
isProposalTx(STObject const& proposedTx)
|
||||
{
|
||||
return proposedTx.getFieldU16(sfTransactionType) == ttTRANSACTION_PROPOSAL_CREATE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the proposed transaction is independently submittable through the
|
||||
* ordinary multi-sign path: not a nested proposal, not a pseudo-transaction,
|
||||
* not itself flagged as someone else's inner batch transaction, and — if it
|
||||
* is a Batch — none of its own inner transactions is a nested proposal or a
|
||||
* pseudo-transaction either. A Batch inner transaction cannot itself be
|
||||
* pseudo (preflight0 rejects the pseudo/tfInnerBatchTxn combination
|
||||
* generically), but that guard lives outside this feature, so it is checked
|
||||
* again here rather than relied upon.
|
||||
*/
|
||||
bool
|
||||
isValidProposalTxnType(STObject const& proposedTx);
|
||||
isValidProposal(STObject const& proposedTx);
|
||||
|
||||
/**
|
||||
* Whether the proposed transaction carries any signature field.
|
||||
@@ -82,93 +84,4 @@ hasEmptySigningPubKey(STObject const& proposedTx)
|
||||
proposedTx.getFieldVL(sfSigningPubKey).empty();
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the proposal is terminal (On-Chain Cosigner spec §4.5). A terminal proposal
|
||||
* stops accepting signatures and anyone may delete it. An already signed
|
||||
* payload can still execute after Expiration, because the Ticket reservation
|
||||
* compares payloads only; a passed LastLedgerSequence does stop it.
|
||||
*
|
||||
* A proposal is terminal when either:
|
||||
* - its Expiration has passed (the parent ledger closed at or after it), or
|
||||
* - the proposed transaction carries a LastLedgerSequence and the current
|
||||
* ledger sequence is strictly greater than it. This mirrors the ordinary
|
||||
* transactor's tefMAX_LEDGER rule: at view.seq() == LastLedgerSequence the
|
||||
* payload can still be submitted in the current ledger, so the proposal is
|
||||
* still live.
|
||||
*
|
||||
* @param view The ledger the deciding transaction is being applied to.
|
||||
* @param expiration The proposal's Expiration field.
|
||||
* @param proposedTx The proposal's ProposedTransaction field.
|
||||
*/
|
||||
bool
|
||||
isTerminal(
|
||||
ReadView const& view,
|
||||
std::optional<std::uint32_t> expiration,
|
||||
STObject const& proposedTx);
|
||||
|
||||
/**
|
||||
* Delete a TransactionProposal ledger entry.
|
||||
*
|
||||
* Removes the entry from its Owner's directory, releases the reserve the
|
||||
* proposal holds against the Owner, and erases the entry. Shared by every
|
||||
* deletion path the spec defines (On-Chain Cosigner spec §4.5): automatic cleanup when the
|
||||
* proposed transaction's TicketSequence is consumed, and the
|
||||
* TransactionProposalCancel / TransactionProposalSign cleanup paths once
|
||||
* those transactions exist.
|
||||
*
|
||||
* The release goes through decreaseOwnerCountForObject, so it follows the
|
||||
* sfSponsor an entry created with reserve sponsorship carries: the reserve is
|
||||
* returned to whoever was holding it, the Owner or that sponsor.
|
||||
*
|
||||
* @param view The apply view for making changes
|
||||
* @param sleProposal The TransactionProposal ledger entry to delete
|
||||
* @param j Journal for logging
|
||||
* @return tesSUCCESS, tecINTERNAL for an invalid entry, or tefBAD_LEDGER if
|
||||
* the ledger contradicts the entry
|
||||
*/
|
||||
TER
|
||||
deleteProposal(ApplyView& view, SLE::pointer const& sleProposal, beast::Journal j);
|
||||
|
||||
/**
|
||||
* Whether tx carries the same payload as a proposal's stored
|
||||
* ProposedTransaction (On-Chain Cosigner spec §4.2.1).
|
||||
*
|
||||
* The payload is the signing payload: the fields the collected signatures
|
||||
* commit to. Compared is every field except those flagged kNotSigning
|
||||
* (TxnSignature, Signers, BatchSigners, CounterpartySignature,
|
||||
* SponsorSignature, Signature, MasterSignature) and SigningPubKey, which is
|
||||
* stored empty and filled only if the target signs with its own key. So the
|
||||
* completed transaction matches no matter which mix of collected or
|
||||
* off-ledger signatures it carries, and any change to a non-signature field
|
||||
* results in a false-return value.
|
||||
*
|
||||
* Signature (PaymentChannelClaim and the XChain attestation transactions)
|
||||
* is deliberately not compared: it is outside the account's own TxnSignature
|
||||
* even for an ordinary submission, so a proposal cannot bind it more tightly
|
||||
* than the signatures it collects do, and those transactors still validate a
|
||||
* substituted value exactly as they would for an ordinary submission.
|
||||
* MasterSignature appears in no transaction format.
|
||||
*
|
||||
* @param proposedTx The proposal's ProposedTransaction field.
|
||||
* @param tx The transaction to compare, typically an STTx.
|
||||
*/
|
||||
bool
|
||||
payloadMatches(STObject const& proposedTx, STObject const& tx);
|
||||
|
||||
/**
|
||||
* Whether tx may consume its Ticket.
|
||||
*
|
||||
* While a proposal keyed to the Ticket exists, the target's Ticket is
|
||||
* reserved: only the proposal's own proposed transaction may spend it
|
||||
* (On-Chain Cosigner spec §4.2.1), so that unrelated target-account activity cannot
|
||||
* invalidate the proposal while signatures are being collected.
|
||||
* TransactionProposalCancel is not allowed through: it deletes the
|
||||
* reservation without consuming the ticket.
|
||||
*
|
||||
* @param view The ledger containing the Ticket and any reservation.
|
||||
* @param tx The transaction attempting to consume that ticket.
|
||||
*/
|
||||
bool
|
||||
canConsumeTicket(ReadView const& view, STTx const& tx);
|
||||
|
||||
} // namespace xrpl::proposal
|
||||
@@ -316,20 +316,6 @@ transferRate(ReadView const& view, Asset const& asset);
|
||||
[[nodiscard]] Rate
|
||||
transferRate(ReadView const& view, STAmount const& amount);
|
||||
|
||||
/**
|
||||
* Returns the amount a sender must part with for `amount` to reach the
|
||||
* receiver once the issuer takes the transfer fee. Rounds up.
|
||||
*/
|
||||
[[nodiscard]] STAmount
|
||||
addTransferFee(STAmount const& amount, Rate const& rate);
|
||||
|
||||
/**
|
||||
* Returns the amount a receiver gets when `amount` leaves the sender and the
|
||||
* issuer takes the transfer fee. Rounds down.
|
||||
*/
|
||||
[[nodiscard]] STAmount
|
||||
subtractTransferFee(STAmount const& amount, Rate const& rate);
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
//
|
||||
// Holding operations (Asset-based dispatchers)
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
#pragma once
|
||||
|
||||
#include <xrpl/basics/Number.h>
|
||||
#include <xrpl/beast/utility/Journal.h>
|
||||
#include <xrpl/ledger/ReadView.h>
|
||||
#include <xrpl/protocol/AccountID.h>
|
||||
#include <xrpl/protocol/Asset.h>
|
||||
#include <xrpl/protocol/Protocol.h>
|
||||
#include <xrpl/protocol/Rules.h>
|
||||
#include <xrpl/protocol/STAmount.h>
|
||||
#include <xrpl/protocol/STLedgerEntry.h>
|
||||
#include <xrpl/protocol/TER.h>
|
||||
@@ -14,239 +12,11 @@
|
||||
#include <cstdint>
|
||||
#include <expected>
|
||||
#include <optional>
|
||||
#include <string_view>
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
class STTx;
|
||||
|
||||
/**
|
||||
* @brief Return the Vault's current assets total.
|
||||
*
|
||||
* FixedPrecision Vaults return AssetsAvailable plus AssetsDeployed, with no
|
||||
* rounding to the asset's 16-digit precision: the sum is exact at Number's
|
||||
* active 19-digit mantissa width (the large mantissa range, enabled by
|
||||
* featureSingleAssetVault or featureLendingProtocol), rounds Downward regardless of the
|
||||
* caller's ambient mode beyond that, and is not rounded to match the cached
|
||||
* sfAssetsTotal field. Legacy and CashBasis Vaults return the stored
|
||||
* AssetsTotal.
|
||||
*
|
||||
* @param vault The vault SLE.
|
||||
* @return The current assets total.
|
||||
*/
|
||||
[[nodiscard]] Number
|
||||
getAssetsTotal(SLE::ConstRef vault);
|
||||
|
||||
/**
|
||||
* @brief A change to a FixedPrecision Vault's balances.
|
||||
*/
|
||||
struct VaultBalanceChange
|
||||
{
|
||||
/**
|
||||
* Signed change to AssetsAvailable. Must be the exact amount moved on the
|
||||
* Vault pseudo-account in the same transaction. Absent means no cash
|
||||
* moved; adjustVaultBalances then uses zero of the Vault's own asset.
|
||||
*/
|
||||
std::optional<STAmount> cash = std::nullopt;
|
||||
|
||||
/**
|
||||
* Signed change to AssetsDeployed: open Loan principal, exact.
|
||||
*/
|
||||
Number deployed = 0;
|
||||
|
||||
/**
|
||||
* Signed change to scheduled, unpaid interest (YieldUnrealized), exact.
|
||||
*/
|
||||
Number yield = 0;
|
||||
|
||||
/**
|
||||
* Signed change to impaired principal (LossUnrealized), exact.
|
||||
*/
|
||||
Number loss = 0;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Apply a balance change to a FixedPrecision Vault. The only writer of
|
||||
* AssetsAvailable, AssetsDeployed, AssetsTotal, YieldUnrealized and
|
||||
* LossUnrealized on these Vaults after creation.
|
||||
*
|
||||
* @param vault The vault SLE. Must be FixedPrecision.
|
||||
* @param change The balance change to apply.
|
||||
* @param j Journal for logging the fatal (tefBAD_LEDGER) case.
|
||||
*
|
||||
* @return tesSUCCESS; tecLIMIT_EXCEEDED if LossUnrealized would exceed
|
||||
* AssetsDeployed; tefBAD_LEDGER if AssetsAvailable, AssetsDeployed,
|
||||
* LossUnrealized or YieldUnrealized would become negative.
|
||||
*/
|
||||
[[nodiscard]] TER
|
||||
adjustVaultBalances(SLE::Ref vault, VaultBalanceChange const& change, beast::Journal j);
|
||||
|
||||
/**
|
||||
* @brief Apply a signed cash movement to a Vault of any version. FixedPrecision
|
||||
* Vaults go through adjustVaultBalances; Legacy and CashBasis Vaults add the
|
||||
* amount to both AssetsTotal and AssetsAvailable.
|
||||
*
|
||||
* @param vault The vault SLE.
|
||||
* @param delta Signed change to AssetsAvailable; the exact amount moved on the
|
||||
* Vault pseudo-account.
|
||||
* @param j Journal passed through to adjustVaultBalances.
|
||||
*
|
||||
* @return tesSUCCESS, or the adjustVaultBalances error for FixedPrecision Vaults.
|
||||
*/
|
||||
[[nodiscard]] TER
|
||||
adjustVaultCash(SLE::Ref vault, STAmount const& delta, beast::Journal j);
|
||||
|
||||
/**
|
||||
* Return the Vault's current live exponent.
|
||||
*
|
||||
* Legacy and CashBasis Vaults use the exponent of AssetsTotal. FixedPrecision
|
||||
* Vaults use max(base exponent, exponent of AssetsTotal): the grid is never
|
||||
* finer than -Scale and coarsens once AssetsTotal outgrows 16 digits there.
|
||||
*/
|
||||
[[nodiscard]] int
|
||||
getVaultScale(SLE::ConstRef vault);
|
||||
|
||||
/**
|
||||
* Return the Vault's base exponent.
|
||||
*
|
||||
* Legacy and CashBasis Vaults use their current live exponent. FixedPrecision
|
||||
* Vaults use -Scale, or 0 for integral assets.
|
||||
*/
|
||||
[[nodiscard]] int
|
||||
getVaultBaseScale(SLE::ConstRef vault);
|
||||
|
||||
/**
|
||||
* Round an amount at the Vault's posterior live exponent.
|
||||
*/
|
||||
[[nodiscard]] STAmount
|
||||
roundToPosteriorVaultScale(
|
||||
SLE::ConstRef vault,
|
||||
STAmount const& amount,
|
||||
Number::RoundingMode roundingMode);
|
||||
|
||||
namespace detail {
|
||||
|
||||
/**
|
||||
* Return the Vault's posterior live exponent after applying an unrounded delta.
|
||||
*/
|
||||
[[nodiscard]] int
|
||||
getPosteriorVaultScale(SLE::ConstRef vault, STAmount const& delta);
|
||||
|
||||
/**
|
||||
* Shared grid-floor core of getVaultScale / getPosteriorVaultScale /
|
||||
* getPosteriorBrokerCoverScale: baseScale when reference is zero, otherwise
|
||||
* max(baseScale, scale(reference, asset)), rounded Downward regardless of
|
||||
* the caller's ambient rounding mode.
|
||||
*/
|
||||
[[nodiscard]] int
|
||||
liveScale(Number const& reference, Asset const& asset, int baseScale);
|
||||
|
||||
/**
|
||||
* Shared core of getPosteriorVaultScale and getPosteriorBrokerCoverScale:
|
||||
* the exponent reference + delta would have under version's live-scale rule
|
||||
* (floored at baseScale for FixedPrecision, unbounded for Legacy/CashBasis).
|
||||
*
|
||||
* @param version The Vault's LEVersion.
|
||||
* @param asset The Vault's underlying asset.
|
||||
* @param baseScale The Vault's base exponent; unused outside the
|
||||
* FixedPrecision case.
|
||||
* @param reference The balance the delta is applied to (AssetsAvailable,
|
||||
* AssetsTotal, or a LoanBroker's CoverAvailable).
|
||||
* @param delta The unrounded change to reference.
|
||||
*/
|
||||
[[nodiscard]] int
|
||||
posteriorAssetScale(
|
||||
VaultVersion version,
|
||||
Asset const& asset,
|
||||
int baseScale,
|
||||
Number const& reference,
|
||||
Number const& delta);
|
||||
|
||||
/**
|
||||
* Shared core of creditToPosteriorAvailableScale and
|
||||
* creditToPosteriorBrokerCoverScale: floors reference + raw at the sum's
|
||||
* own live scale (never finer than baseScale) and returns the difference
|
||||
* from reference, so the delta applied is exactly what moves reference onto
|
||||
* the floored sum. See creditToPosteriorAvailableScale's doc for why the
|
||||
* sum, not raw alone, must be floored. raw is the exact amount; it is not
|
||||
* rounded on its own before being added to reference. The sum and its scale
|
||||
* are computed once, in roundingMode, so a Downward credit is always
|
||||
* between zero and raw.
|
||||
*/
|
||||
[[nodiscard]] STAmount
|
||||
creditToPosteriorScale(
|
||||
Asset const& asset,
|
||||
Number const& reference,
|
||||
int baseScale,
|
||||
Number const& raw,
|
||||
Number::RoundingMode roundingMode);
|
||||
|
||||
} // namespace detail
|
||||
|
||||
/**
|
||||
* Round a FixedPrecision cash inflow (LoanPay's credit, LoanManage's
|
||||
* default cover credit) into AssetsAvailable, never AssetsTotal, which is
|
||||
* a derived cache, by flooring the sum rather than the delta:
|
||||
* credit equals floor16(AssetsAvailable + raw) minus AssetsAvailable, never
|
||||
* finer than the Vault's base exponent (-Scale).
|
||||
*
|
||||
* Flooring only the delta at its own posterior grid is not enough: the sum
|
||||
* can still need a 17th digit when AssetsAvailable is off the coarser grid
|
||||
* right after crossing a power of ten. Example at Scale 6: AssetsAvailable
|
||||
* equals 9999999999.999999 (16 digits already) and a credit of 0.000011
|
||||
* floors to 0.00001 on its own (finer) grid, but 9999999999.999999 plus
|
||||
* 0.00001 equals 10000000000.000009, which still needs 17 digits. Flooring
|
||||
* the sum instead, floor16(9999999999.999999 + 0.000011) minus
|
||||
* 9999999999.999999, is exact by construction, since STAmount's own
|
||||
* 16-digit canonical form of the sum is what gets subtracted from.
|
||||
*
|
||||
* raw is taken as an exact Number: rounding it to 16 digits before adding
|
||||
* it to AssetsAvailable could drop a tail that moves the floored sum.
|
||||
*/
|
||||
[[nodiscard]] STAmount
|
||||
creditToPosteriorAvailableScale(
|
||||
SLE::ConstRef vault,
|
||||
Number const& raw,
|
||||
Number::RoundingMode roundingMode);
|
||||
|
||||
/**
|
||||
* Open-zone capacity ceiling: 9 * 10^(15 + baseScale).
|
||||
*
|
||||
* Defined only for FixedPrecision Vaults, where this is 9 * 10^(15 - P).
|
||||
*/
|
||||
[[nodiscard]] Number
|
||||
getVaultOpenLimit(SLE::ConstRef vault);
|
||||
|
||||
/**
|
||||
* Check whether amount is an admissible optional inflow.
|
||||
*
|
||||
* Legacy and CashBasis Vaults always succeed. FixedPrecision Vaults must
|
||||
* remain at their base scale after applying the rounded amount, and the
|
||||
* posterior capacity (AssetsTotal + YieldUnrealized + rounded amount) must
|
||||
* stay within the Open zone.
|
||||
*
|
||||
* The amount is rounded toward zero at the posterior live exponent.
|
||||
*/
|
||||
[[nodiscard]] TER
|
||||
checkOptionalVaultInflow(SLE::ConstRef vault, STAmount const& amount);
|
||||
|
||||
/**
|
||||
* Open-zone capacity after adding roundedAmount: AssetsTotal + YieldUnrealized
|
||||
* + roundedAmount, computed TowardsZero regardless of the caller's ambient
|
||||
* rounding mode. Defined only for FixedPrecision Vaults.
|
||||
*
|
||||
* Shared by checkOptionalVaultInflow and LoanSet's loan-origination capacity
|
||||
* check, which pair this formula with different scale checks (posterior vs.
|
||||
* current), so only the capacity formula itself -- not the whole check -- is
|
||||
* shared here.
|
||||
*
|
||||
* @param vault The vault SLE. Must be FixedPrecision.
|
||||
* @param roundedAmount The amount to add, already rounded to the relevant
|
||||
* posterior or base scale by the caller.
|
||||
*/
|
||||
[[nodiscard]] Number
|
||||
vaultOpenZoneCapacity(SLE::ConstRef vault, Number const& roundedAmount);
|
||||
|
||||
/**
|
||||
* From the perspective of a vault, return the number of shares to give
|
||||
* depositor when they offer a fixed amount of assets. Note, since shares are
|
||||
@@ -276,93 +46,31 @@ assetsToSharesDeposit(SLE::ConstRef vault, SLE::ConstRef issuance, STAmount cons
|
||||
sharesToAssetsDeposit(SLE::ConstRef vault, SLE::ConstRef issuance, STAmount const& shares);
|
||||
|
||||
/**
|
||||
* Adjusts a requested asset change (delta) to match the decimal scale of the
|
||||
* updated total vault assets. This ensures sfAssetsTotal, sfAssetsAvailable,
|
||||
* Adjusts a requested asset change (`delta`) to match the decimal scale of the
|
||||
* updated total vault assets. This ensures `sfAssetsTotal`, `sfAssetsAvailable`,
|
||||
* and the actual asset transfer change by the exact same representable amount.
|
||||
*
|
||||
* Rounding strategy:
|
||||
* - Legacy/CashBasis debits (withdrawals): rounds down the magnitude of delta
|
||||
* on the new scale to prevent paying out more than requested.
|
||||
* - Legacy/CashBasis credits: floors the resulting total asset balance and
|
||||
* returns the difference from the current total.
|
||||
* - FixedPrecision credits only (VaultDeposit): rounds the delta toward zero
|
||||
* at the scale getAssetsTotal (AssetsAvailable + AssetsDeployed, derived,
|
||||
* not the stored AssetsTotal cache) would have after applying delta.
|
||||
* FixedPrecision outflows (VaultWithdraw, VaultClawback) never call this;
|
||||
* they use clampVaultOutflow instead.
|
||||
* - Debits (withdrawals): Rounds down `|delta|` on the new scale to prevent
|
||||
* paying out more than requested.
|
||||
* - Credits (deposits): Floors the resulting total asset balance and returns the
|
||||
* difference from the current total. This prevents crediting the vault with
|
||||
* more assets than the user deposited.
|
||||
*
|
||||
* Key rules:
|
||||
* - The returned magnitude never exceeds the magnitude of delta.
|
||||
* - Returns tecPRECISION_LOSS if the change is smaller than 1 ULP of the target scale
|
||||
* - The returned magnitude never exceeds `|delta|`.
|
||||
* - Returns `tecPRECISION_LOSS` if the change is smaller than 1 ULP of the target scale
|
||||
* (prevents share operations when totals cannot change).
|
||||
* - For integer assets (XRP, MPT), rounding is a no-op.
|
||||
*
|
||||
* @param vault The vault ledger entry.
|
||||
* @param delta The requested signed change to sfAssetsTotal.
|
||||
* @return The rounded, positive magnitude, or tecPRECISION_LOSS if the
|
||||
* @return The rounded, positive magnitude, or `tecPRECISION_LOSS` if the
|
||||
* change is below representable precision.
|
||||
*/
|
||||
[[nodiscard]] std::expected<STAmount, TER>
|
||||
clampToAssetsTotalScale(SLE::ConstRef vault, STAmount const& delta);
|
||||
|
||||
/**
|
||||
* Outflow clamp dispatcher for VaultWithdraw and VaultClawback: rounds a
|
||||
* negative delta to the scale appropriate for the Vault's version -- for
|
||||
* FixedPrecision, at AssetsAvailable's own posterior scale (the balance the
|
||||
* cash actually leaves); for Legacy/CashBasis, clampToAssetsTotalScale --
|
||||
* so both call sites share one dispatch point. Same tecPRECISION_LOSS and
|
||||
* integral-asset and magnitude-never-exceeds-delta rules as
|
||||
* clampToAssetsTotalScale.
|
||||
*
|
||||
* @param vault The vault ledger entry.
|
||||
* @param delta The requested signed outflow (negative).
|
||||
* @return The rounded, positive magnitude, or tecPRECISION_LOSS if the
|
||||
* change is below representable precision.
|
||||
*/
|
||||
[[nodiscard]] std::expected<STAmount, TER>
|
||||
clampVaultOutflow(SLE::ConstRef vault, STAmount const& delta);
|
||||
|
||||
/**
|
||||
* Returns the Vault's base exponent for asset at scale, before a Vault
|
||||
* object exists to read it from: 0 for an integral asset, -scale otherwise.
|
||||
* Used by VaultCreate::preclaim, which only has the proposed Scale field,
|
||||
* not yet a Vault SLE; getVaultBaseScale is the post-creation equivalent.
|
||||
*
|
||||
* @param asset The (prospective) Vault's underlying asset.
|
||||
* @param scale The (prospective) Vault's sfScale value, ignored for
|
||||
* integral assets.
|
||||
*/
|
||||
[[nodiscard]] int
|
||||
vaultBaseScale(Asset const& asset, std::uint8_t scale);
|
||||
|
||||
/**
|
||||
* Returns true iff value is exactly representable both as an STAmount of
|
||||
* asset and on the base grid at baseScale (no precision lost rounding
|
||||
* TowardsZero at baseScale). Returns false, rather than throwing, for a value
|
||||
* out of range for the asset. Shared representability check for
|
||||
* VaultCreate::preclaim, VaultSet::preclaim and LoanBrokerSet::preclaim's
|
||||
* AssetsMaximum / DebtMaximum / cover-rate field checks.
|
||||
*
|
||||
* @param asset The vault's underlying asset.
|
||||
* @param value The value to check.
|
||||
* @param baseScale The vault's base exponent (see vaultBaseScale /
|
||||
* getVaultBaseScale).
|
||||
*/
|
||||
[[nodiscard]] bool
|
||||
isOnVaultBaseGrid(Asset const& asset, Number const& value, int baseScale);
|
||||
|
||||
/**
|
||||
* Checks that a requested AssetsMaximum is exactly representable on the
|
||||
* Vault's base grid, otherwise associateAsset would silently round the cap
|
||||
* the owner asked for. Used by VaultSet::preclaim on an existing Vault.
|
||||
*
|
||||
* @param vault The vault ledger entry.
|
||||
* @param amount The requested AssetsMaximum.
|
||||
* @return tesSUCCESS, or tecPRECISION_LOSS if amount is not representable.
|
||||
*/
|
||||
[[nodiscard]] TER
|
||||
checkAssetsMaximum(SLE::ConstRef vault, Number const& amount);
|
||||
|
||||
/**
|
||||
* Controls whether to truncate shares instead of rounding.
|
||||
*/
|
||||
@@ -390,24 +98,21 @@ enum class WaiveUnrealizedLoss : bool { No = false, Yes = true };
|
||||
assetsTotalForWithdrawal(SLE::ConstRef vault, WaiveUnrealizedLoss waive);
|
||||
|
||||
/**
|
||||
* Reject a cash outflow that is dust (see debitIsNonZeroDust) or whose check
|
||||
* overflows Number, which is easy to hit with a large Scale.
|
||||
* Returns true if debiting `amount` from `total` (the current value of a
|
||||
* vault's sfAssetsTotal or sfAssetsAvailable) would canonicalize to the
|
||||
* same STAmount value. This happens when `amount` is non-zero but too small
|
||||
* to change the stored total at STAmount's precision. Shares would still
|
||||
* move, so the ValidVault invariant would fail after apply; callers use
|
||||
* this to reject the transaction upfront instead.
|
||||
*
|
||||
* @param vault The vault SLE.
|
||||
* @param amount The amount to debit.
|
||||
* @param j Journal for the debug log.
|
||||
* @param name Transaction name used as the log prefix.
|
||||
*
|
||||
* @return tesSUCCESS; tecPRECISION_LOSS if amount is dust; tecPATH_DRY if the
|
||||
* check overflows. Overflow means the transaction cannot apply, but
|
||||
* ledger state is still consistent.
|
||||
* @param asset The vault's underlying asset, used to canonicalize both
|
||||
* sides the same way the ledger will when the field is stored.
|
||||
* @param total The field's current value.
|
||||
* @param amount The amount to debit. Zero always returns false; that case
|
||||
* is rejected separately.
|
||||
*/
|
||||
[[nodiscard]] TER
|
||||
checkDebitNotDust(
|
||||
SLE::ConstRef vault,
|
||||
Number const& amount,
|
||||
beast::Journal j,
|
||||
std::string_view name);
|
||||
[[nodiscard]] bool
|
||||
debitIsNonZeroDust(Asset const& asset, Number const& total, Number const& amount);
|
||||
|
||||
/**
|
||||
* From the perspective of a vault, return the number of shares to demand from
|
||||
@@ -453,7 +158,7 @@ sharesToAssetsWithdraw(
|
||||
WaiveUnrealizedLoss waive = WaiveUnrealizedLoss::No);
|
||||
|
||||
/**
|
||||
* Returns true iff account holds all of the vault's outstanding shares,
|
||||
* Returns true iff `account` holds all of the vault's outstanding shares —
|
||||
* i.e. is the sole remaining shareholder. Returns false if the account
|
||||
* holds no shares or fewer than the total outstanding.
|
||||
*
|
||||
@@ -466,13 +171,11 @@ sharesToAssetsWithdraw(
|
||||
isSoleShareholder(ReadView const& view, AccountID const& account, SLE::ConstRef issuance);
|
||||
|
||||
/**
|
||||
* Resolves a Vault's LEVersion.
|
||||
*
|
||||
* LEVersion is the single point every accounting and rounding helper
|
||||
* should call to decide which protocol a Vault follows. It is written
|
||||
* at VaultCreate and is not updated afterwards, so a Vault created
|
||||
* under an older amendment keeps that behaviour after later amendments
|
||||
* activate. Absent sfLEVersion resolves to VaultVersion::Legacy.
|
||||
* Resolves a Vault's LEVersion, the single point every accounting touch
|
||||
* point should call to determine which recognition model (instant interest
|
||||
* recognition vs. cash-basis) a Vault uses. Vaults created before featureLendingProtocolV1_1
|
||||
* activated never have sfLEVersion set, which resolves here to
|
||||
* VaultVersion::Legacy.
|
||||
*
|
||||
* @param vault The vault SLE.
|
||||
*
|
||||
@@ -482,34 +185,6 @@ isSoleShareholder(ReadView const& view, AccountID const& account, SLE::ConstRef
|
||||
[[nodiscard]] VaultVersion
|
||||
getVaultVersion(SLE::ConstRef vault);
|
||||
|
||||
/**
|
||||
* Decodes an already-extracted sfLEVersion value with the same range check as
|
||||
* getVaultVersion. Usable from contexts, such as an invariant's ledger-entry
|
||||
* snapshot, that keep the field value but not the owning SLE.
|
||||
*
|
||||
* @param leVersion The value of sfLEVersion, or nullopt if absent.
|
||||
*
|
||||
* @return The decoded LEVersion, or VaultVersion::Legacy if absent.
|
||||
*/
|
||||
[[nodiscard]] VaultVersion
|
||||
decodeVaultVersion(std::optional<std::uint8_t> leVersion);
|
||||
|
||||
/**
|
||||
* Resolves which LEVersion a newly-created Vault should get under the
|
||||
* currently active amendments. This is the only place that decides Vault
|
||||
* version policy; VaultCreate calls it instead of checking amendments
|
||||
* directly.
|
||||
*
|
||||
* @param rules The active ledger rules.
|
||||
*
|
||||
* @return VaultVersion::FixedPrecision once featureLendingProtocolV1_1,
|
||||
* featureLendingProtocolV1_2 and fixCleanup3_4_0 are all enabled;
|
||||
* VaultVersion::CashBasis
|
||||
* once featureLendingProtocolV1_1 is enabled; VaultVersion::Legacy otherwise.
|
||||
*/
|
||||
[[nodiscard]] VaultVersion
|
||||
getVaultDefaultVersion(Rules const& rules);
|
||||
|
||||
/**
|
||||
* Resolves the VaultKind of a vault SLE. Returns VaultKind::ClosedEnded when
|
||||
* sfVaultKind is present and equal to that value; anything else (including an
|
||||
|
||||
@@ -6,31 +6,21 @@
|
||||
#include <xrpl/protocol/jss.h>
|
||||
|
||||
#include <cstddef>
|
||||
#include <string_view>
|
||||
#include <type_traits>
|
||||
#include <utility>
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
/**
|
||||
* API version numbers used in later API versions
|
||||
* The `api_version` numbers this server serves.
|
||||
*
|
||||
* Requests with a version number in the range
|
||||
* [apiMinimumSupportedVersion, apiMaximumSupportedVersion]
|
||||
* are supported.
|
||||
* A request naming a version in [kApiMinimumSupportedVersion,
|
||||
* kApiMaximumSupportedVersion] is served. With `[beta_rpc_api]` set to `1` in
|
||||
* the config the range extends to kApiBetaVersion.
|
||||
*
|
||||
* If [beta_rpc_api] is enabled in config, the version numbers
|
||||
* in the range [apiMinimumSupportedVersion, apiBetaVersion]
|
||||
* are supported.
|
||||
*
|
||||
* Network Requests without explicit version numbers use
|
||||
* apiVersionIfUnspecified. apiVersionIfUnspecified is 1,
|
||||
* because all the RPC requests with a version >= 2 must
|
||||
* explicitly specify the version in the requests.
|
||||
* Note that apiVersionIfUnspecified will be lower than
|
||||
* apiMinimumSupportedVersion when we stop supporting API
|
||||
* version 1.
|
||||
*
|
||||
* Command line Requests use apiCommandLineVersion.
|
||||
* A request naming no version is served at kApiVersionIfUnspecified, which is 1
|
||||
* because a request wanting any later version states it.
|
||||
*/
|
||||
|
||||
namespace rpc {
|
||||
@@ -57,6 +47,16 @@ static_assert(kApiMaximumSupportedVersion >= kApiMinimumSupportedVersion);
|
||||
static_assert(kApiBetaVersion >= kApiMaximumSupportedVersion);
|
||||
static_assert(kApiMaximumValidVersion >= kApiMaximumSupportedVersion);
|
||||
|
||||
/**
|
||||
* Values accepted in the `ripplerpc` request field, which selects the shape of
|
||||
* the JSON-RPC reply envelope. Distinct from `kJsonRpcVersion` in JsonRpc.h,
|
||||
* which names the JSON-RPC protocol itself, and from the `api_version`
|
||||
* constants above, which select the content of the response.
|
||||
*/
|
||||
inline constexpr std::string_view kRippleRpcVersion1{"1.0"};
|
||||
inline constexpr std::string_view kRippleRpcVersion2{"2.0"};
|
||||
inline constexpr std::string_view kRippleRpcVersion3{"3.0"};
|
||||
|
||||
inline void
|
||||
setVersion(json::Value& parent, unsigned int apiVersion, bool betaEnabled)
|
||||
{
|
||||
|
||||
@@ -348,19 +348,16 @@ isAuditorMirrorCurrent(SLE const& issuance, SLE const& mptoken);
|
||||
areMirrorsCurrent(SLE const& issuance, SLE const& mptoken);
|
||||
|
||||
/**
|
||||
* @brief Set the holder's issuer mirror epoch, and the issuer mirror key, to match the issuance's
|
||||
* current key.
|
||||
* @brief Set the holder's issuer mirror epoch to match the issuance's current issuer key epoch.
|
||||
*
|
||||
* Call this after writing the issuer mirror ciphertext under the issuance's
|
||||
* currently registered issuer key, so that the mirror reads as current afterwards.
|
||||
* Also records sfIssuerMirrorEncryptionKey as the issuance's current issuer key
|
||||
* (both fields stay absent at epoch 0, where the original issuer key applies).
|
||||
*
|
||||
* @param issuance The MPTokenIssuance ledger object.
|
||||
* @param mptoken The holder's MPToken ledger entry to update.
|
||||
*/
|
||||
void
|
||||
setIssuerMirrorCurrent(SLE const& issuance, SLE& mptoken);
|
||||
setIssuerMirrorEpoch(SLE const& issuance, SLE& mptoken);
|
||||
|
||||
/**
|
||||
* @brief Set the holder's auditor mirror epoch to match the issuance's current auditor key epoch.
|
||||
@@ -375,18 +372,16 @@ void
|
||||
setAuditorMirrorEpoch(SLE const& issuance, SLE& mptoken);
|
||||
|
||||
/**
|
||||
* @brief Set the holder's MPToken mirror epochs, and the issuer mirror key, to match the issuance's
|
||||
* current keys.
|
||||
* @brief Set the holder's MPToken mirror epochs to match the issuance's current key epochs.
|
||||
*
|
||||
* Call this after writing mirror ciphertexts under the issuance's currently
|
||||
* registered keys, so that the mirrors read as current afterwards. Also records
|
||||
* sfIssuerMirrorEncryptionKey via setIssuerMirrorCurrent.
|
||||
* registered keys, so that the mirrors read as current afterwards.
|
||||
*
|
||||
* @param issuance The MPTokenIssuance ledger object.
|
||||
* @param mptoken The holder's MPToken ledger entry to update.
|
||||
*/
|
||||
void
|
||||
setMirrorsCurrent(SLE const& issuance, SLE& mptoken);
|
||||
setMirrorEpochs(SLE const& issuance, SLE& mptoken);
|
||||
|
||||
/**
|
||||
* @brief Verifies revealed amount encryptions for all recipients.
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#include <xrpl/json/json_value.h>
|
||||
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
@@ -144,7 +145,55 @@ enum ErrorCodeI {
|
||||
RpcEntryNotFound = 98,
|
||||
RpcUnexpectedLedgerType = 99,
|
||||
|
||||
RpcLast = RpcUnexpectedLedgerType, // rpcLAST should always equal the last code.
|
||||
// submit + simulate
|
||||
RpcInvalidTransaction = 100,
|
||||
RpcInternalSubmit = 101,
|
||||
RpcInternalJson = 102,
|
||||
RpcInternalSimulate = 103,
|
||||
|
||||
// transaction_entry
|
||||
RpcFieldNotFoundTransaction = 104,
|
||||
RpcNotYetImplemented = 105,
|
||||
RpcTransactionNotFound = 106,
|
||||
|
||||
// transaction_entry
|
||||
RpcMalformedRequest = 107,
|
||||
|
||||
// ledger_accept
|
||||
RpcNotStandAlone = 108,
|
||||
|
||||
// ledger_entry, API version 1 only
|
||||
RpcUnknownOption = 109,
|
||||
|
||||
// ledger_entry field validation, one code per malformed field. The ledger_entry helpers report
|
||||
// invalidParams (31) for all of them, so each token needs a row of its own. Alphabetical by
|
||||
// token here only: the enum is append-only once a code ships.
|
||||
//
|
||||
// `malformedIssue` duplicates `issueMalformed` (93): same message, same status, two tokens,
|
||||
// both on the wire already. Do not add a third spelling.
|
||||
RpcMalformedAccount = 110,
|
||||
RpcMalformedAddress = 111,
|
||||
RpcMalformedAuthorized = 112,
|
||||
RpcMalformedAuthorizedCredentials = 113,
|
||||
RpcMalformedBridgeAccount = 114,
|
||||
RpcMalformedBroker = 115,
|
||||
RpcMalformedCurrency = 116,
|
||||
RpcMalformedDirRoot = 117,
|
||||
RpcMalformedDocumentID = 118,
|
||||
RpcMalformedIssue = 119,
|
||||
RpcMalformedIssuingChainDoor = 120,
|
||||
RpcMalformedLockingChainDoor = 121,
|
||||
RpcMalformedMPTIssuanceID = 122,
|
||||
RpcMalformedMPTokenIssuance = 123,
|
||||
RpcMalformedOwner = 124,
|
||||
RpcMalformedSeq = 125,
|
||||
RpcMalformedSponsee = 126,
|
||||
RpcMalformedSponsor = 127,
|
||||
RpcMalformedXChainOwnedClaimID = 128,
|
||||
RpcMalformedXChainOwnedCreateAccountClaimID = 129,
|
||||
|
||||
// RpcLast should always equal the last code.
|
||||
RpcLast = RpcMalformedXChainOwnedCreateAccountClaimID,
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -180,11 +229,6 @@ struct ErrorInfo
|
||||
{
|
||||
}
|
||||
|
||||
constexpr ErrorInfo(ErrorCodeI code, char const* token, char const* message)
|
||||
: code(code), token(token), message(message), httpStatus(200)
|
||||
{
|
||||
}
|
||||
|
||||
constexpr ErrorInfo(ErrorCodeI code, char const* token, char const* message, int httpStatus)
|
||||
: code(code), token(token), message(message), httpStatus(httpStatus)
|
||||
{
|
||||
@@ -202,6 +246,18 @@ struct ErrorInfo
|
||||
ErrorInfo const&
|
||||
getErrorInfo(ErrorCodeI code);
|
||||
|
||||
/**
|
||||
* Returns the error code that owns @p token.
|
||||
*
|
||||
* A linear scan over views measured at compile time, run once per error reply.
|
||||
* A duplicate token is a build error, so the answer is never ambiguous.
|
||||
*
|
||||
* @param token The error token to resolve.
|
||||
* @return The code the table gives @p token, or RpcUnknown if no row names it.
|
||||
*/
|
||||
ErrorCodeI
|
||||
codeForToken(std::string_view token);
|
||||
|
||||
/**
|
||||
* Add or update the json update to reflect the error code.
|
||||
*/
|
||||
|
||||
@@ -10,25 +10,6 @@ namespace xrpl {
|
||||
// This was the reference fee units used in the old fee calculation.
|
||||
inline constexpr std::uint32_t kFeeUnitsDeprecated = 10;
|
||||
|
||||
// Number of micro-drops in one drop.
|
||||
inline constexpr std::uint32_t microDropsPerDrop{1'000'000};
|
||||
|
||||
/**
|
||||
* Hard protocol bounds on the Feature Extension fee settings. A voted value
|
||||
* can never fall outside these, so `preflight`, which has no view, may bound
|
||||
* against them. Gas and bytecode limits are capped; gas price has a floor
|
||||
* (zero would make WASM execution effectively free).
|
||||
*/
|
||||
inline constexpr std::uint32_t kMaxGasLimit{2'000'000};
|
||||
inline constexpr std::uint32_t kMaxBytecodeSizeLimit{200'000};
|
||||
inline constexpr std::uint32_t kMinGasPrice{1};
|
||||
|
||||
// The following default values of fee settings will seed into FeeSettings
|
||||
// and write to the ledger on featureSmartEscrow activation.
|
||||
inline constexpr std::uint32_t kDefaultGasLimit{1'000'000};
|
||||
inline constexpr std::uint32_t kDefaultBytecodeSizeLimit{100'000};
|
||||
inline constexpr std::uint32_t kDefaultGasPrice{1'000'000};
|
||||
|
||||
/**
|
||||
* Reflects the fee settings for a particular ledger.
|
||||
*
|
||||
@@ -52,47 +33,13 @@ struct Fees
|
||||
*/
|
||||
XRPAmount increment{0};
|
||||
|
||||
/**
|
||||
* @brief Gas limit for Feature Extensions (instructions).
|
||||
*
|
||||
* This and the one below default to the protocol values rather than 0,
|
||||
* because an explicit 0 is the voted kill switch.
|
||||
*/
|
||||
std::uint32_t gasLimit{kDefaultGasLimit};
|
||||
|
||||
/**
|
||||
* @brief Bytecode size limit for Feature Extensions (bytes).
|
||||
*/
|
||||
std::uint32_t bytecodeSizeLimit{kDefaultBytecodeSizeLimit};
|
||||
|
||||
/**
|
||||
* @brief Price of WASM gas (micro-drops).
|
||||
*/
|
||||
std::uint32_t gasPrice{kDefaultGasPrice};
|
||||
|
||||
explicit Fees() = default;
|
||||
Fees(Fees const&) = default;
|
||||
Fees&
|
||||
operator=(Fees const&) = default;
|
||||
|
||||
Fees(XRPAmount base, XRPAmount reserve, XRPAmount increment)
|
||||
: base{base}, reserve{reserve}, increment{increment}
|
||||
{
|
||||
}
|
||||
|
||||
Fees(
|
||||
XRPAmount base,
|
||||
XRPAmount reserve,
|
||||
XRPAmount increment,
|
||||
std::uint32_t gasLimit,
|
||||
std::uint32_t bytecodeSizeLimit,
|
||||
std::uint32_t gasPrice)
|
||||
: base{base}
|
||||
, reserve{reserve}
|
||||
, increment{increment}
|
||||
, gasLimit{gasLimit}
|
||||
, bytecodeSizeLimit{bytecodeSizeLimit}
|
||||
, gasPrice{gasPrice}
|
||||
: base(base), reserve(reserve), increment(increment)
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
#pragma once
|
||||
|
||||
#include <xrpl/json/json_forwards.h>
|
||||
|
||||
#include <string_view>
|
||||
|
||||
namespace xrpl::rpc {
|
||||
|
||||
/**
|
||||
* Constants of the JSON-RPC 2.0 protocol itself.
|
||||
*
|
||||
* Kept apart from the `api_version` and `ripplerpc` constants in ApiVersion.h,
|
||||
* which go when support for API versions 1 and 2 goes.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Value of the `jsonrpc` member of a request and of its reply, fixed at "2.0"
|
||||
* by the JSON-RPC specification.
|
||||
*/
|
||||
inline constexpr std::string_view kJsonRpcVersion{"2.0"};
|
||||
|
||||
/**
|
||||
* Codes for the `code` member of a JSON-RPC error object.
|
||||
*
|
||||
* The specification reserves -32768 to -32000 for the protocol and leaves
|
||||
* -32000 to -32099 of it to the implementation.
|
||||
*
|
||||
* kJsonRpcServerError is the code for an error an XRPL handler reports.
|
||||
*
|
||||
* The codes from kJsonRpcServerOverloaded on lie outside the
|
||||
* implementation-defined sub-range, which the specification does not allow.
|
||||
* They are the codes every shipped version reports, so moving one breaks the
|
||||
* clients matching on it.
|
||||
*/
|
||||
inline constexpr json::Int kJsonRpcServerError{-32000};
|
||||
inline constexpr json::Int kJsonRpcInvalidRequest{-32600};
|
||||
inline constexpr json::Int kJsonRpcMethodNotFound{-32601};
|
||||
inline constexpr json::Int kJsonRpcInvalidParams{-32602};
|
||||
inline constexpr json::Int kJsonRpcServerOverloaded{-32604};
|
||||
inline constexpr json::Int kJsonRpcForbidden{-32605};
|
||||
inline constexpr json::Int kJsonRpcWrongVersion{-32606};
|
||||
|
||||
} // namespace xrpl::rpc
|
||||
@@ -311,46 +311,24 @@ constexpr std::uint8_t kVaultStrategyFirstComeFirstServe = 1;
|
||||
* Default IOU scale factor for a Vault
|
||||
*/
|
||||
constexpr std::uint8_t kVaultDefaultIouScale = 6;
|
||||
|
||||
/**
|
||||
* Maximum Scale for a Vault created before featureLendingProtocolV1_2.
|
||||
* Chosen so 1 IOU can always convert to shares:
|
||||
* 10^19 > maxMPTokenAmount (2^64-1) > 10^18.
|
||||
* Maximum scale factor for a Vault. The number is chosen to ensure that
|
||||
* 1 IOU can be always converted to shares.
|
||||
* 10^19 > maxMPTokenAmount (2^64-1) > 10^18
|
||||
*/
|
||||
constexpr std::uint8_t kLegacyVaultMaximumIouScale = 18;
|
||||
constexpr std::uint8_t kVaultMaximumIouScale = 18;
|
||||
|
||||
/**
|
||||
* Maximum Scale for a Vault created under featureLendingProtocolV1_2.
|
||||
*/
|
||||
constexpr std::uint8_t kFixedPrecisionVaultMaximumIouScale = 10;
|
||||
|
||||
/**
|
||||
* Vault ledger-entry schema versions, persisted as sfLEVersion.
|
||||
*
|
||||
* LEVersion records which protocol a Vault was created under so later
|
||||
* amendments can change the rules for new Vaults without rewriting
|
||||
* existing ones. VaultCreate writes it from the then-active lending
|
||||
* amendments; later transactions do not update it. A Vault created
|
||||
* under an older amendment keeps that amendment's behaviour for its
|
||||
* lifetime, even after a newer lending amendment activates.
|
||||
*
|
||||
* Absent sfLEVersion is implicit Legacy (version 0): instant interest
|
||||
* recognition and a dynamic AssetsTotal scale. CashBasis (V1.1) uses
|
||||
* cash-basis recognition on the same dynamic scale. FixedPrecision
|
||||
* (V1.2) keeps cash-basis recognition and adds the lifetime base grid.
|
||||
* Vault ledger-entry schema versions. Assigned to newly created
|
||||
* Vaults once featureLendingProtocolV1_1 is enabled. Vaults created before
|
||||
* activation are left without LEVersion (implicit legacy version 0,
|
||||
* instant interest recognition).
|
||||
*/
|
||||
enum class VaultVersion : uint8_t {
|
||||
Legacy = 0,
|
||||
CashBasis,
|
||||
FixedPrecision,
|
||||
};
|
||||
|
||||
// Code compares VaultVersion values with < and >= (e.g. "CashBasis or later"),
|
||||
// so each later version must stay numerically larger than the one before.
|
||||
static_assert(
|
||||
VaultVersion::Legacy < VaultVersion::CashBasis &&
|
||||
VaultVersion::CashBasis < VaultVersion::FixedPrecision);
|
||||
|
||||
/**
|
||||
* Vault kind. Distinguishes closed-ended vaults from the default open-ended
|
||||
* kind. Persisted as sfVaultKind (UINT8); absent means OpenEnded.
|
||||
|
||||
@@ -696,16 +696,6 @@ divRound(STAmount const& v1, STAmount const& v2, Asset const& asset, bool roundU
|
||||
STAmount
|
||||
divRoundStrict(STAmount const& v1, STAmount const& v2, Asset const& asset, bool roundUp);
|
||||
|
||||
/**
|
||||
* Returns amt * num / den, rounded in the requested direction.
|
||||
*
|
||||
* Dispatches to the XRPAmount, IOUAmount or MPTAmount overload. XRP and MPT
|
||||
* results are exact; an IOU result is rounded to the 16-digit mantissa in the
|
||||
* requested direction. Only Upward and Downward are supported.
|
||||
*/
|
||||
[[nodiscard]] STAmount
|
||||
mulRatio(STAmount const& amt, std::uint32_t num, std::uint32_t den, Number::RoundingMode rounding);
|
||||
|
||||
// Someone is offering X for Y, what is the rate?
|
||||
// Rate: smaller is better, the taker wants the most out: in/out
|
||||
// VFALCO TODO Return a Quality object
|
||||
@@ -776,21 +766,6 @@ roundToAsset(
|
||||
return roundToScale(ret, scale);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether an arbitrary precision Number can be converted to an STAmount
|
||||
* of a given Asset without exceeding the asset's range.
|
||||
*
|
||||
* Use this before `roundToAsset` or `STAmount{asset, value}` on unchecked
|
||||
* input, since those throw for out-of-range values, e.g. XRP above
|
||||
* `STAmount::kMaxNativeN`.
|
||||
*
|
||||
* @param asset The relevant asset
|
||||
* @param value The value to be checked
|
||||
* @return true if the value is in range for the asset; false otherwise.
|
||||
*/
|
||||
[[nodiscard]] bool
|
||||
isRepresentable(Asset const& asset, Number const& value);
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
inline bool
|
||||
|
||||
@@ -231,8 +231,6 @@ enum TERcodes : TERUnderlyingType {
|
||||
terNO_DELEGATE_PERMISSION, // Delegate does not have permission
|
||||
terLOCKED, // MPT is locked
|
||||
terNO_PERMISSION, // No permission but retry
|
||||
terTICKET_RESERVED, // Ticket is reserved and cannot be consumed
|
||||
// until the reservation is released
|
||||
};
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
#pragma once
|
||||
|
||||
#include <xrpl/basics/MathUtilities.h>
|
||||
#include <xrpl/basics/Number.h>
|
||||
#include <xrpl/basics/contract.h>
|
||||
#include <xrpl/beast/utility/Zero.h>
|
||||
@@ -70,13 +69,7 @@ public:
|
||||
constexpr XRPAmount
|
||||
operator*(value_type const& rhs) const
|
||||
{
|
||||
if (auto const result = checkedMul(drops_, rhs))
|
||||
return XRPAmount{*result};
|
||||
// Not reached in constant evaluation: an overflowing constant
|
||||
// expression fails to compile instead.
|
||||
XRPAmount product{*this};
|
||||
product *= rhs;
|
||||
return product;
|
||||
return XRPAmount{drops_ * rhs};
|
||||
}
|
||||
|
||||
friend constexpr XRPAmount
|
||||
@@ -89,23 +82,37 @@ public:
|
||||
XRPAmount&
|
||||
operator+=(XRPAmount const& other)
|
||||
{
|
||||
return *this += other.drops();
|
||||
drops_ += other.drops();
|
||||
return *this;
|
||||
}
|
||||
|
||||
XRPAmount&
|
||||
operator-=(XRPAmount const& other)
|
||||
{
|
||||
return *this -= other.drops();
|
||||
drops_ -= other.drops();
|
||||
return *this;
|
||||
}
|
||||
|
||||
XRPAmount&
|
||||
operator+=(value_type const& rhs);
|
||||
operator+=(value_type const& rhs)
|
||||
{
|
||||
drops_ += rhs;
|
||||
return *this;
|
||||
}
|
||||
|
||||
XRPAmount&
|
||||
operator-=(value_type const& rhs);
|
||||
operator-=(value_type const& rhs)
|
||||
{
|
||||
drops_ -= rhs;
|
||||
return *this;
|
||||
}
|
||||
|
||||
XRPAmount&
|
||||
operator*=(value_type const& rhs);
|
||||
operator*=(value_type const& rhs)
|
||||
{
|
||||
drops_ *= rhs;
|
||||
return *this;
|
||||
}
|
||||
|
||||
XRPAmount
|
||||
operator-() const
|
||||
|
||||
@@ -17,7 +17,6 @@
|
||||
|
||||
XRPL_FEATURE(Cosign, Supported::No, VoteBehavior::DefaultNo)
|
||||
XRPL_FEATURE(SmartEscrow, Supported::No, VoteBehavior::DefaultNo)
|
||||
// Requires LendingProtocolV1_1. New vaults take FixedPrecision plus cash-basis.
|
||||
XRPL_FEATURE(LendingProtocolV1_2, Supported::No, VoteBehavior::DefaultNo)
|
||||
XRPL_FIX (Cleanup3_5_0, Supported::Yes, VoteBehavior::DefaultNo)
|
||||
XRPL_FEATURE(ConfidentialMPTKeyRotation, Supported::No, VoteBehavior::DefaultNo)
|
||||
@@ -61,6 +60,7 @@ XRPL_FIX (InnerObjTemplate2, Supported::Yes, VoteBehavior::DefaultNo
|
||||
XRPL_FIX (EnforceNFTokenTrustline, Supported::Yes, VoteBehavior::DefaultNo)
|
||||
XRPL_FIX (ReducedOffersV2, Supported::Yes, VoteBehavior::DefaultNo)
|
||||
XRPL_FEATURE(NFTokenMintOffer, Supported::Yes, VoteBehavior::DefaultNo)
|
||||
XRPL_FIX (AMMv1_1, Supported::Yes, VoteBehavior::DefaultNo)
|
||||
XRPL_FIX (PreviousTxnID, Supported::Yes, VoteBehavior::DefaultNo)
|
||||
XRPL_FIX (XChainRewardRounding, Supported::Yes, VoteBehavior::DefaultNo)
|
||||
XRPL_FIX (EmptyDID, Supported::Yes, VoteBehavior::DefaultNo)
|
||||
@@ -106,7 +106,6 @@ XRPL_RETIRE_FIX(1623)
|
||||
XRPL_RETIRE_FIX(1781)
|
||||
XRPL_RETIRE_FIX(AmendmentMajorityCalc)
|
||||
XRPL_RETIRE_FIX(AMMOverflowOffer)
|
||||
XRPL_RETIRE_FIX(AMMv1_1)
|
||||
XRPL_RETIRE_FIX(CheckThreading)
|
||||
XRPL_RETIRE_FIX(DisallowIncomingV1)
|
||||
XRPL_RETIRE_FIX(InnerObjTemplate)
|
||||
|
||||
@@ -417,7 +417,6 @@ LEDGER_ENTRY(ltMPTOKEN_ISSUANCE, 0x007e, MPTokenIssuance, mpt_issuance, ({
|
||||
{sfAuditorEncryptionKey, SoeOptional},
|
||||
{sfIssuerKeyEpoch, SoeOptional},
|
||||
{sfAuditorKeyEpoch, SoeOptional},
|
||||
{sfInitialIssuerEncryptionKey, SoeOptional},
|
||||
{sfConfidentialOutstandingAmount, SoeDefault},
|
||||
}))
|
||||
|
||||
@@ -439,7 +438,6 @@ LEDGER_ENTRY(ltMPTOKEN, 0x007f, MPToken, mptoken, ({
|
||||
{sfAuditorEncryptedBalance, SoeOptional},
|
||||
{sfIssuerKeyMirrorEpoch, SoeOptional},
|
||||
{sfAuditorKeyMirrorEpoch, SoeOptional},
|
||||
{sfIssuerMirrorEncryptionKey, SoeOptional},
|
||||
{sfHolderEncryptionKey, SoeOptional},
|
||||
{sfRecoveryKey, SoeOptional},
|
||||
}))
|
||||
@@ -516,8 +514,6 @@ LEDGER_ENTRY(ltVAULT, 0x0084, Vault, vault, ({
|
||||
{sfAssetsAvailable, SoeDefault},
|
||||
{sfAssetsMaximum, SoeDefault},
|
||||
{sfLossUnrealized, SoeDefault},
|
||||
{sfYieldUnrealized, SoeDefault},
|
||||
{sfAssetsDeployed, SoeDefault},
|
||||
{sfShareMPTID, SoeRequired},
|
||||
{sfWithdrawalPolicy, SoeRequired},
|
||||
{sfScale, SoeDefault},
|
||||
|
||||
@@ -240,8 +240,6 @@ TYPED_SFIELD(sfPrincipalRequested, NUMBER, 14)
|
||||
TYPED_SFIELD(sfTotalValueOutstanding, NUMBER, 15, SField::kSmdNeedsAsset | SField::kSmdDefault)
|
||||
TYPED_SFIELD(sfPeriodicPayment, NUMBER, 16)
|
||||
TYPED_SFIELD(sfManagementFeeOutstanding, NUMBER, 17, SField::kSmdNeedsAsset | SField::kSmdDefault)
|
||||
TYPED_SFIELD(sfYieldUnrealized, NUMBER, 18, SField::kSmdNeedsAsset | SField::kSmdDefault)
|
||||
TYPED_SFIELD(sfAssetsDeployed, NUMBER, 19, SField::kSmdNeedsAsset | SField::kSmdDefault)
|
||||
|
||||
// 32-bit signed (common)
|
||||
TYPED_SFIELD(sfLoanScale, INT32, 1)
|
||||
@@ -333,8 +331,6 @@ TYPED_SFIELD(sfAmountCommitment, VL, 45)
|
||||
TYPED_SFIELD(sfBalanceCommitment, VL, 46)
|
||||
TYPED_SFIELD(sfBytecode, VL, 47)
|
||||
TYPED_SFIELD(sfRecoveryKey, VL, 48)
|
||||
TYPED_SFIELD(sfInitialIssuerEncryptionKey, VL, 49)
|
||||
TYPED_SFIELD(sfIssuerMirrorEncryptionKey, VL, 50)
|
||||
|
||||
// account (common)
|
||||
TYPED_SFIELD(sfAccount, ACCOUNT, 1)
|
||||
|
||||
@@ -1175,20 +1175,6 @@ TRANSACTION(ttTRANSACTION_PROPOSAL_CREATE, 95, TransactionProposalCreate,
|
||||
{sfExpiration, SoeRequired},
|
||||
}))
|
||||
|
||||
// ttTRANSACTION_PROPOSAL_SIGN arrives in a later PR and takes the next unassigned type.
|
||||
|
||||
/** This transaction deletes a TransactionProposal, releasing its owner's
|
||||
reserve. While the proposal is live only its Owner or its target account
|
||||
may cancel; once it is terminal anyone may (On-Chain Cosigner spec §7). */
|
||||
#if TRANSACTION_INCLUDE
|
||||
# include <xrpl/tx/transactors/proposal/TransactionProposalCancel.h>
|
||||
#endif
|
||||
TRANSACTION(ttTRANSACTION_PROPOSAL_CANCEL, 94, TransactionProposalCancel,
|
||||
({.amendment = featureCosign}),
|
||||
({
|
||||
{sfProposalID, SoeRequired},
|
||||
}))
|
||||
|
||||
/** This system-generated transaction type is used to update the status of the various amendments.
|
||||
|
||||
For details, see: https://xrpl.org/amendments.html
|
||||
|
||||
@@ -245,16 +245,13 @@ JSS(ephemeral_key); // out: ValidatorInfo
|
||||
JSS(error); // out: error
|
||||
JSS(errored); //
|
||||
JSS(error_code); // out: error
|
||||
JSS(error_exception); // out: Submit
|
||||
JSS(error_exception); // out: Submit, Simulate
|
||||
JSS(error_message); // out: error
|
||||
JSS(expand); // in: handler/Ledger
|
||||
JSS(expected_date); // out: any (warnings)
|
||||
JSS(expected_date_UTC); // out: any (warnings)
|
||||
JSS(expected_ledger_size); // out: TxQ
|
||||
JSS(expiration); // out: AccountOffers, AccountChannels, ValidatorList, amm_info
|
||||
JSS(gas_limit); // out: NetworkOPs
|
||||
JSS(bytecode_size_limit); // out: NetworkOPs
|
||||
JSS(gas_price); // out: NetworkOPs
|
||||
JSS(fail_hard); // in: Sign, Submit
|
||||
JSS(failed); // out: InboundLedger
|
||||
JSS(feature); // in: Feature
|
||||
@@ -562,7 +559,7 @@ JSS(signing_key); // out: NetworkOPs
|
||||
JSS(signing_keys); // out: ValidatorList
|
||||
JSS(signing_time); // out: NetworkOPs
|
||||
JSS(signer_lists); // in/out: AccountInfo
|
||||
JSS(size); // out: get_aggregate_price
|
||||
JSS(size); // out: get_aggregate_price, ServerHandler
|
||||
JSS(snapshot); // in: Subscribe
|
||||
JSS(source_account); // in: PathRequest, RipplePathFind
|
||||
JSS(source_amount); // in: PathRequest, RipplePathFind
|
||||
|
||||
@@ -316,30 +316,6 @@ public:
|
||||
return this->sle_->isFieldPresent(sfAuditorKeyMirrorEpoch);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get sfIssuerMirrorEncryptionKey (SoeOptional)
|
||||
* @return The field value, or std::nullopt if not present.
|
||||
*/
|
||||
[[nodiscard]]
|
||||
protocol_autogen::Optional<SF_VL::type::value_type>
|
||||
getIssuerMirrorEncryptionKey() const
|
||||
{
|
||||
if (hasIssuerMirrorEncryptionKey())
|
||||
return this->sle_->at(sfIssuerMirrorEncryptionKey);
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Check if sfIssuerMirrorEncryptionKey is present.
|
||||
* @return True if the field is present, false otherwise.
|
||||
*/
|
||||
[[nodiscard]]
|
||||
bool
|
||||
hasIssuerMirrorEncryptionKey() const
|
||||
{
|
||||
return this->sle_->isFieldPresent(sfIssuerMirrorEncryptionKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get sfHolderEncryptionKey (SoeOptional)
|
||||
* @return The field value, or std::nullopt if not present.
|
||||
@@ -589,17 +565,6 @@ public:
|
||||
return *this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Set sfIssuerMirrorEncryptionKey (SoeOptional)
|
||||
* @return Reference to this builder for method chaining.
|
||||
*/
|
||||
MPTokenBuilder&
|
||||
setIssuerMirrorEncryptionKey(std::decay_t<typename SF_VL::type::value_type> const& value)
|
||||
{
|
||||
object_[sfIssuerMirrorEncryptionKey] = value;
|
||||
return *this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Set sfHolderEncryptionKey (SoeOptional)
|
||||
* @return Reference to this builder for method chaining.
|
||||
|
||||
@@ -399,30 +399,6 @@ public:
|
||||
return this->sle_->isFieldPresent(sfAuditorKeyEpoch);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get sfInitialIssuerEncryptionKey (SoeOptional)
|
||||
* @return The field value, or std::nullopt if not present.
|
||||
*/
|
||||
[[nodiscard]]
|
||||
protocol_autogen::Optional<SF_VL::type::value_type>
|
||||
getInitialIssuerEncryptionKey() const
|
||||
{
|
||||
if (hasInitialIssuerEncryptionKey())
|
||||
return this->sle_->at(sfInitialIssuerEncryptionKey);
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Check if sfInitialIssuerEncryptionKey is present.
|
||||
* @return True if the field is present, false otherwise.
|
||||
*/
|
||||
[[nodiscard]]
|
||||
bool
|
||||
hasInitialIssuerEncryptionKey() const
|
||||
{
|
||||
return this->sle_->isFieldPresent(sfInitialIssuerEncryptionKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get sfConfidentialOutstandingAmount (SoeDefault)
|
||||
* @return The field value, or std::nullopt if not present.
|
||||
@@ -694,17 +670,6 @@ public:
|
||||
return *this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Set sfInitialIssuerEncryptionKey (SoeOptional)
|
||||
* @return Reference to this builder for method chaining.
|
||||
*/
|
||||
MPTokenIssuanceBuilder&
|
||||
setInitialIssuerEncryptionKey(std::decay_t<typename SF_VL::type::value_type> const& value)
|
||||
{
|
||||
object_[sfInitialIssuerEncryptionKey] = value;
|
||||
return *this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Set sfConfidentialOutstandingAmount (SoeDefault)
|
||||
* @return Reference to this builder for method chaining.
|
||||
|
||||
@@ -242,54 +242,6 @@ public:
|
||||
return this->sle_->isFieldPresent(sfLossUnrealized);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get sfYieldUnrealized (SoeDefault)
|
||||
* @return The field value, or std::nullopt if not present.
|
||||
*/
|
||||
[[nodiscard]]
|
||||
protocol_autogen::Optional<SF_NUMBER::type::value_type>
|
||||
getYieldUnrealized() const
|
||||
{
|
||||
if (hasYieldUnrealized())
|
||||
return this->sle_->at(sfYieldUnrealized);
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Check if sfYieldUnrealized is present.
|
||||
* @return True if the field is present, false otherwise.
|
||||
*/
|
||||
[[nodiscard]]
|
||||
bool
|
||||
hasYieldUnrealized() const
|
||||
{
|
||||
return this->sle_->isFieldPresent(sfYieldUnrealized);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get sfAssetsDeployed (SoeDefault)
|
||||
* @return The field value, or std::nullopt if not present.
|
||||
*/
|
||||
[[nodiscard]]
|
||||
protocol_autogen::Optional<SF_NUMBER::type::value_type>
|
||||
getAssetsDeployed() const
|
||||
{
|
||||
if (hasAssetsDeployed())
|
||||
return this->sle_->at(sfAssetsDeployed);
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Check if sfAssetsDeployed is present.
|
||||
* @return True if the field is present, false otherwise.
|
||||
*/
|
||||
[[nodiscard]]
|
||||
bool
|
||||
hasAssetsDeployed() const
|
||||
{
|
||||
return this->sle_->isFieldPresent(sfAssetsDeployed);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get sfShareMPTID (SoeRequired)
|
||||
* @return The field value.
|
||||
@@ -619,28 +571,6 @@ public:
|
||||
return *this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Set sfYieldUnrealized (SoeDefault)
|
||||
* @return Reference to this builder for method chaining.
|
||||
*/
|
||||
VaultBuilder&
|
||||
setYieldUnrealized(std::decay_t<typename SF_NUMBER::type::value_type> const& value)
|
||||
{
|
||||
object_[sfYieldUnrealized] = value;
|
||||
return *this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Set sfAssetsDeployed (SoeDefault)
|
||||
* @return Reference to this builder for method chaining.
|
||||
*/
|
||||
VaultBuilder&
|
||||
setAssetsDeployed(std::decay_t<typename SF_NUMBER::type::value_type> const& value)
|
||||
{
|
||||
object_[sfAssetsDeployed] = value;
|
||||
return *this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Set sfShareMPTID (SoeRequired)
|
||||
* @return Reference to this builder for method chaining.
|
||||
|
||||
@@ -1,131 +0,0 @@
|
||||
// This file is auto-generated. Do not edit.
|
||||
#pragma once
|
||||
|
||||
#include <xrpl/protocol/STTx.h>
|
||||
#include <xrpl/protocol/STParsedJSON.h>
|
||||
#include <xrpl/protocol/jss.h>
|
||||
#include <xrpl/protocol_autogen/TransactionBase.h>
|
||||
#include <xrpl/protocol_autogen/TransactionBuilderBase.h>
|
||||
#include <xrpl/json/json_value.h>
|
||||
|
||||
#include <stdexcept>
|
||||
#include <optional>
|
||||
|
||||
namespace xrpl::transactions {
|
||||
|
||||
class TransactionProposalCancelBuilder;
|
||||
|
||||
/**
|
||||
* @brief Transaction: TransactionProposalCancel
|
||||
*
|
||||
* Type: ttTRANSACTION_PROPOSAL_CANCEL (94)
|
||||
* Delegable: Delegation::NotDelegable
|
||||
* Amendment: featureCosign
|
||||
* Privileges: Privilege::NoPriv
|
||||
*
|
||||
* Immutable wrapper around STTx providing type-safe field access.
|
||||
* Use TransactionProposalCancelBuilder to construct new transactions.
|
||||
*/
|
||||
class TransactionProposalCancel : public TransactionBase
|
||||
{
|
||||
public:
|
||||
static constexpr xrpl::TxType txType = ttTRANSACTION_PROPOSAL_CANCEL;
|
||||
|
||||
/**
|
||||
* @brief Construct a TransactionProposalCancel transaction wrapper from an existing STTx object.
|
||||
* @throws std::runtime_error if the transaction type doesn't match.
|
||||
*/
|
||||
explicit TransactionProposalCancel(std::shared_ptr<STTx const> tx)
|
||||
: TransactionBase(std::move(tx))
|
||||
{
|
||||
// Verify transaction type
|
||||
if (tx_->getTxnType() != txType)
|
||||
{
|
||||
throw std::runtime_error("Invalid transaction type for TransactionProposalCancel");
|
||||
}
|
||||
}
|
||||
|
||||
// Transaction-specific field getters
|
||||
|
||||
/**
|
||||
* @brief Get sfProposalID (SoeRequired)
|
||||
* @return The field value.
|
||||
*/
|
||||
[[nodiscard]]
|
||||
SF_UINT256::type::value_type
|
||||
getProposalID() const
|
||||
{
|
||||
return this->tx_->at(sfProposalID);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Builder for TransactionProposalCancel transactions.
|
||||
*
|
||||
* Provides a fluent interface for constructing transactions with method chaining.
|
||||
* Uses STObject internally for flexible transaction construction.
|
||||
* Inherits common field setters from TransactionBuilderBase.
|
||||
*/
|
||||
class TransactionProposalCancelBuilder : public TransactionBuilderBase<TransactionProposalCancelBuilder>
|
||||
{
|
||||
public:
|
||||
/**
|
||||
* @brief Construct a new TransactionProposalCancelBuilder with required fields.
|
||||
* @param account The account initiating the transaction.
|
||||
* @param proposalID The sfProposalID field value.
|
||||
* @param sequence Optional sequence number for the transaction.
|
||||
* @param fee Optional fee for the transaction.
|
||||
*/
|
||||
TransactionProposalCancelBuilder(SF_ACCOUNT::type::value_type account,
|
||||
std::decay_t<typename SF_UINT256::type::value_type> const& proposalID, std::optional<SF_UINT32::type::value_type> sequence = std::nullopt,
|
||||
std::optional<SF_AMOUNT::type::value_type> fee = std::nullopt
|
||||
)
|
||||
: TransactionBuilderBase<TransactionProposalCancelBuilder>(ttTRANSACTION_PROPOSAL_CANCEL, account, sequence, fee)
|
||||
{
|
||||
setProposalID(proposalID);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Construct a TransactionProposalCancelBuilder from an existing STTx object.
|
||||
* @param tx The existing transaction to copy from.
|
||||
* @throws std::runtime_error if the transaction type doesn't match.
|
||||
*/
|
||||
TransactionProposalCancelBuilder(std::shared_ptr<STTx const> tx)
|
||||
{
|
||||
if (tx->getTxnType() != ttTRANSACTION_PROPOSAL_CANCEL)
|
||||
{
|
||||
throw std::runtime_error("Invalid transaction type for TransactionProposalCancelBuilder");
|
||||
}
|
||||
object_ = *tx;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Transaction-specific field setters
|
||||
*/
|
||||
|
||||
/**
|
||||
* @brief Set sfProposalID (SoeRequired)
|
||||
* @return Reference to this builder for method chaining.
|
||||
*/
|
||||
TransactionProposalCancelBuilder&
|
||||
setProposalID(std::decay_t<typename SF_UINT256::type::value_type> const& value)
|
||||
{
|
||||
object_[sfProposalID] = value;
|
||||
return *this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Build and return the TransactionProposalCancel wrapper.
|
||||
* @param publicKey The public key for signing.
|
||||
* @param secretKey The secret key for signing.
|
||||
* @return The constructed transaction wrapper.
|
||||
*/
|
||||
TransactionProposalCancel
|
||||
build(PublicKey const& publicKey, SecretKey const& secretKey)
|
||||
{
|
||||
sign(publicKey, secretKey);
|
||||
return TransactionProposalCancel{std::make_shared<STTx>(std::move(object_))};
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace xrpl::transactions
|
||||
@@ -7,6 +7,20 @@
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
/**
|
||||
* Writes an HTTP reply carrying @p strMsg with status @p nStatus to @p output,
|
||||
* and logs the status at trace. The body is not logged here: it may carry a
|
||||
* credential this library cannot mask, so the caller logs it masked.
|
||||
*
|
||||
* A 401 with an empty body is answered with the fixed authentication page.
|
||||
* The status line carries the phrase Beast's registry gives @p nStatus, except
|
||||
* for 401 and 503, which carry a phrase of this server's own.
|
||||
*
|
||||
* @param nStatus The HTTP status code.
|
||||
* @param strMsg The body.
|
||||
* @param output Where the reply bytes are written.
|
||||
* @param j The journal the status is logged to.
|
||||
*/
|
||||
void
|
||||
httpReply(int nStatus, std::string const& strMsg, json::Output const&, beast::Journal j);
|
||||
|
||||
|
||||
+35
-263
@@ -2,7 +2,6 @@
|
||||
|
||||
#include <xrpl/basics/Blob.h>
|
||||
#include <xrpl/basics/IntrusivePointer.h>
|
||||
#include <xrpl/basics/Log.h>
|
||||
#include <xrpl/basics/SHAMapHash.h>
|
||||
#include <xrpl/basics/base_uint.h>
|
||||
#include <xrpl/beast/utility/Journal.h>
|
||||
@@ -18,7 +17,6 @@
|
||||
#include <xrpl/shamap/SHAMapNodeID.h>
|
||||
#include <xrpl/shamap/SHAMapTreeNode.h>
|
||||
|
||||
#include <atomic>
|
||||
#include <condition_variable>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
@@ -42,7 +40,7 @@ class SHAMapSyncFilter;
|
||||
/**
|
||||
* Describes the current state of a given SHAMap
|
||||
*/
|
||||
enum class SHAMapState : std::uint8_t {
|
||||
enum class SHAMapState {
|
||||
/**
|
||||
* The map is in flux and objects can be added and removed.
|
||||
*
|
||||
@@ -122,42 +120,16 @@ private:
|
||||
*/
|
||||
std::uint32_t cowid_ = 1;
|
||||
|
||||
// ledgerSeq_, state_ and full_ are touched on the nodestore fetch path and on a
|
||||
// getMissingNodes() walk, neither of which may block, so pin them lock-free.
|
||||
static_assert(std::atomic<std::uint32_t>::is_always_lock_free);
|
||||
static_assert(std::atomic<SHAMapState>::is_always_lock_free);
|
||||
static_assert(std::atomic<bool>::is_always_lock_free);
|
||||
|
||||
/**
|
||||
* The sequence of the ledger that this map references, if any.
|
||||
*
|
||||
* Written when a map's ledger sequence is established (Ledger::setFull(),
|
||||
* InboundLedger) while a nodestore reader thread reads it. Relaxed either
|
||||
* way: it serves as a lookup hint for a nodestore keyed by hash.
|
||||
*/
|
||||
std::atomic<std::uint32_t> ledgerSeq_ = 0;
|
||||
std::uint32_t ledgerSeq_ = 0;
|
||||
|
||||
SHAMapTreeNodePtr root_;
|
||||
|
||||
/**
|
||||
* The map's state.
|
||||
*
|
||||
* A getMissingNodes() walk writes it, through setInvalid() and
|
||||
* clearSynching(), while whatever drives the acquisition reads it.
|
||||
* The walk runs with the acquisition's lock released, so this is atomic
|
||||
* rather than guarded.
|
||||
*/
|
||||
mutable std::atomic<SHAMapState> state_;
|
||||
mutable SHAMapState state_;
|
||||
SHAMapType const type_;
|
||||
bool backed_ = true; // Map is backed by the database
|
||||
|
||||
/**
|
||||
* Map is believed complete in database.
|
||||
*
|
||||
* Atomic because finishFetch() clears it on any nodestore reader thread,
|
||||
* several of which can run at once.
|
||||
*/
|
||||
mutable std::atomic<bool> full_ = false;
|
||||
bool backed_ = true; // Map is backed by the database
|
||||
mutable bool full_ = false; // Map is believed complete in database
|
||||
|
||||
public:
|
||||
/**
|
||||
@@ -180,14 +152,7 @@ public:
|
||||
SHAMap&
|
||||
operator=(SHAMap const&) = delete;
|
||||
|
||||
/**
|
||||
* Take a snapshot of the given map.
|
||||
*
|
||||
* @param other The map to snapshot. An Invalid source yields an Invalid
|
||||
* snapshot.
|
||||
* @param isMutable Whether the snapshot may be modified. Ignored when other
|
||||
* is Invalid.
|
||||
*/
|
||||
// Take a snapshot of the given map:
|
||||
SHAMap(SHAMap const& other, bool isMutable);
|
||||
|
||||
// build new map
|
||||
@@ -225,33 +190,17 @@ public:
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Return a new map that is a snapshot of this one.
|
||||
*
|
||||
* Handles copy on write for mutable snapshots. An invalid map yields an
|
||||
* invalid snapshot.
|
||||
*
|
||||
* @param isMutable Whether the snapshot may be modified.
|
||||
* @return The snapshot.
|
||||
*/
|
||||
// Returns a new map that's a snapshot of this one.
|
||||
// Handles copy on write for mutable snapshots.
|
||||
std::shared_ptr<SHAMap>
|
||||
snapShot(bool isMutable) const;
|
||||
|
||||
/**
|
||||
* Mark this map as "should be full": the local server wants every node
|
||||
* it references in durable storage, so a nodestore miss is a gap to report.
|
||||
*
|
||||
* Publishes the ledger sequence before the flag. The sequence store is
|
||||
* relaxed and the flag store is release, and finishFetch() reads the
|
||||
* sequence only after acquiring the flag, so the one miss that withdraws
|
||||
* the flag always reports the sequence stored here. Keeping both stores in
|
||||
* one place is what makes that order impossible to get wrong.
|
||||
*
|
||||
* @param ledgerSeq Sequence of the ledger this map belongs to, which the
|
||||
* gap report names.
|
||||
*/
|
||||
/* Mark this SHAMap as "should be full", indicating
|
||||
that the local server wants all the corresponding nodes
|
||||
in durable storage.
|
||||
*/
|
||||
void
|
||||
setFull(std::uint32_t ledgerSeq);
|
||||
setFull();
|
||||
|
||||
void
|
||||
setLedgerSeq(std::uint32_t lseq);
|
||||
@@ -312,17 +261,12 @@ public:
|
||||
lowerBound(UInt256 const& id) const;
|
||||
|
||||
/**
|
||||
* Visit every node in this SHAMap.
|
||||
* Visit every node in this SHAMap
|
||||
*
|
||||
* The walk ends at the first node it cannot read, so a caller that needs a
|
||||
* complete walk to be correct must check the return value.
|
||||
*
|
||||
* @param function Called with every node visited. Returning false from it
|
||||
* ends the walk, which on its own keeps the result true.
|
||||
* @return True when the walk read every node it reached. False when a node
|
||||
* could not be read.
|
||||
* @param function called with every node visited.
|
||||
* If function returns false, visitNodes exits.
|
||||
*/
|
||||
bool
|
||||
void
|
||||
visitNodes(std::function<bool(SHAMapTreeNode&)> const& function) const;
|
||||
|
||||
/**
|
||||
@@ -336,12 +280,11 @@ public:
|
||||
visitDifferences(SHAMap const* have, std::function<bool(SHAMapTreeNode const&)> const&) const;
|
||||
|
||||
/**
|
||||
* Visit every leaf node in this SHAMap.
|
||||
* Visit every leaf node in this SHAMap
|
||||
*
|
||||
* @param function Called with every non-inner node visited.
|
||||
* @return What visitNodes reports about the walk.
|
||||
* @param function called with every non inner node visited.
|
||||
*/
|
||||
bool
|
||||
void
|
||||
visitLeaves(std::function<void(boost::intrusive_ptr<SHAMapItem const> const&)> const&) const;
|
||||
|
||||
// comparison/sync functions
|
||||
@@ -353,13 +296,9 @@ public:
|
||||
* concurrency, to discover nodes referenced in the
|
||||
* SHAMap but not available locally.
|
||||
*
|
||||
* Only a leaf may occupy a position at or beyond kLeafDepth. A map that
|
||||
* breaks that is marked Invalid and the traversal is abandoned, so callers
|
||||
* ask isValid() to tell an empty result from a satisfied map.
|
||||
*
|
||||
* @param maxNodes The maximum number of found nodes to return
|
||||
* @param filter The filter to use when retrieving nodes
|
||||
* @return The nodes known to be missing, or empty if the map is Invalid
|
||||
* @param return The nodes known to be missing
|
||||
*/
|
||||
std::vector<std::pair<SHAMapNodeID, UInt256>>
|
||||
getMissingNodes(int maxNodes, SHAMapSyncFilter const* filter);
|
||||
@@ -402,9 +341,6 @@ public:
|
||||
* This function is used when receiving the root node of a SHAMap from a peer during ledger
|
||||
* synchronization. The node must already have been deserialized.
|
||||
*
|
||||
* A root offered under a hash the map does not hold names another tree and
|
||||
* is reported as invalid data. A root matching that hash is a duplicate.
|
||||
*
|
||||
* @param hash The expected hash of the root node.
|
||||
* @param rootNode A deserialized root node to add.
|
||||
* @param filter Optional sync filter to track received nodes.
|
||||
@@ -424,10 +360,6 @@ public:
|
||||
* is inserted at the position specified by nodeID. The node must already have been
|
||||
* deserialized.
|
||||
*
|
||||
* A node that no valid tree can hold makes the map Invalid, which is
|
||||
* terminal: the root hash committed to an impossible shape, so no peer
|
||||
* can satisfy it.
|
||||
*
|
||||
* @param nodeID The position in the tree where this node belongs.
|
||||
* @param treeNode A deserialized tree node to add.
|
||||
* @param filter Optional sync filter to track received nodes.
|
||||
@@ -443,57 +375,16 @@ public:
|
||||
SHAMapTreeNodePtr treeNode,
|
||||
SHAMapSyncFilter const* filter);
|
||||
|
||||
/**
|
||||
* Mark this map as immutable, so it can no longer be modified.
|
||||
*
|
||||
* @return false if the map is Invalid and was left unchanged, true
|
||||
* otherwise.
|
||||
*/
|
||||
[[nodiscard]] bool
|
||||
// status functions
|
||||
void
|
||||
setImmutable();
|
||||
|
||||
/**
|
||||
* @return Whether the map is being synced against a hash it was given, so
|
||||
* its hash is fixed while nodes may still be added.
|
||||
*/
|
||||
[[nodiscard]] bool
|
||||
bool
|
||||
isSynching() const;
|
||||
|
||||
/**
|
||||
* @return Whether the map is settled, with its hash and nodes fixed.
|
||||
*/
|
||||
[[nodiscard]] bool
|
||||
isImmutable() const;
|
||||
|
||||
/**
|
||||
* Mark this map as syncing, fixing its hash while still allowing missing
|
||||
* nodes to be added.
|
||||
*
|
||||
* The map must be freshly constructed. The body asserts that the map is not
|
||||
* Invalid, so a caller that ignores the precondition stops a build with
|
||||
* assertions enabled.
|
||||
*/
|
||||
void
|
||||
setSynching();
|
||||
|
||||
/**
|
||||
* Mark this map as no longer syncing, so it can be modified again.
|
||||
*
|
||||
* Moves only a Synching map. An Immutable map stays settled, a Modifying
|
||||
* map has nothing to clear, and Invalid is terminal.
|
||||
*/
|
||||
void
|
||||
clearSynching();
|
||||
|
||||
/**
|
||||
* Whether the map can still be the map it claims to be.
|
||||
*
|
||||
* A map that is merely missing nodes is valid, and stays valid until
|
||||
* something proves the tree it is syncing against cannot exist.
|
||||
*
|
||||
* @return Whether the map has not been proven impossible.
|
||||
*/
|
||||
[[nodiscard]] bool
|
||||
bool
|
||||
isValid() const;
|
||||
|
||||
// caution: otherMap must be accessed only by this function
|
||||
@@ -633,51 +524,6 @@ private:
|
||||
using DeltaRef =
|
||||
std::pair<boost::intrusive_ptr<SHAMapItem const>, boost::intrusive_ptr<SHAMapItem const>>;
|
||||
|
||||
/**
|
||||
* The sequence of the ledger this map references, read atomically.
|
||||
*
|
||||
* @return The sequence, or zero if the map references no ledger.
|
||||
*/
|
||||
[[nodiscard]] std::uint32_t
|
||||
ledgerSeq() const;
|
||||
|
||||
/**
|
||||
* The current state, read atomically.
|
||||
*
|
||||
* Orders state_ alone. The tree's nodes carry no ordering guarantees.
|
||||
*
|
||||
* @return The state as of the call, which a concurrent walk may already
|
||||
* have moved past.
|
||||
*/
|
||||
[[nodiscard]] SHAMapState
|
||||
state() const;
|
||||
|
||||
/**
|
||||
* Record that the map is provably not the one it claims to be.
|
||||
*
|
||||
* Private because only the map itself can prove that, from a node that
|
||||
* contradicts the hashes it is syncing against. Cannot fail, since
|
||||
* Invalid outranks every other state; see trySetState().
|
||||
*/
|
||||
void
|
||||
setInvalid();
|
||||
|
||||
/**
|
||||
* Move the map to a new state, atomically.
|
||||
*
|
||||
* With clearSynching(), the only writer of state_ past construction, so
|
||||
* the order between the states lives in one place: Invalid outranks all
|
||||
* of them and is always stored, while every other transition is refused
|
||||
* once the map is Invalid, which is what makes that verdict terminal.
|
||||
* clearSynching() is narrower and moves only Synching.
|
||||
*
|
||||
* @param desired The state to move to.
|
||||
* @return false if the map is Invalid and the requested state is not,
|
||||
* leaving it unchanged; true otherwise.
|
||||
*/
|
||||
bool
|
||||
trySetState(SHAMapState desired);
|
||||
|
||||
// tree node cache operations
|
||||
SHAMapTreeNodePtr
|
||||
cacheLookup(SHAMapHash const& hash) const;
|
||||
@@ -893,120 +739,46 @@ private:
|
||||
};
|
||||
|
||||
inline void
|
||||
SHAMap::setFull(std::uint32_t ledgerSeq)
|
||||
SHAMap::setFull()
|
||||
{
|
||||
ledgerSeq_.store(ledgerSeq, std::memory_order_relaxed);
|
||||
full_.store(true, std::memory_order_release);
|
||||
full_ = true;
|
||||
}
|
||||
|
||||
inline void
|
||||
SHAMap::setLedgerSeq(std::uint32_t lseq)
|
||||
{
|
||||
ledgerSeq_.store(lseq, std::memory_order_relaxed);
|
||||
ledgerSeq_ = lseq;
|
||||
}
|
||||
|
||||
inline std::uint32_t
|
||||
SHAMap::ledgerSeq() const
|
||||
{
|
||||
return ledgerSeq_.load(std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
inline SHAMapState
|
||||
SHAMap::state() const
|
||||
{
|
||||
return state_.load(std::memory_order_acquire);
|
||||
}
|
||||
|
||||
inline bool
|
||||
SHAMap::trySetState(SHAMapState desired)
|
||||
{
|
||||
// Invalid is stored outright: a walk reaching that verdict has to win against a thread
|
||||
// settling the map.
|
||||
if (desired == SHAMapState::Invalid)
|
||||
{
|
||||
state_.store(SHAMapState::Invalid, std::memory_order_release);
|
||||
return true;
|
||||
}
|
||||
|
||||
// A failed exchange both reports the state and refreshes expected, so no load is needed
|
||||
// ahead of the loop.
|
||||
auto expected = SHAMapState::Modifying;
|
||||
while (expected != SHAMapState::Invalid)
|
||||
{
|
||||
if (state_.compare_exchange_weak(
|
||||
expected, desired, std::memory_order_acq_rel, std::memory_order_acquire))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
inline bool
|
||||
inline void
|
||||
SHAMap::setImmutable()
|
||||
{
|
||||
SOMETIMES(!isValid(), "xrpl::SHAMap::setImmutable : map is invalid");
|
||||
return trySetState(SHAMapState::Immutable);
|
||||
XRPL_ASSERT(state_ != SHAMapState::Invalid, "xrpl::SHAMap::setImmutable : state is valid");
|
||||
state_ = SHAMapState::Immutable;
|
||||
}
|
||||
|
||||
inline bool
|
||||
SHAMap::isSynching() const
|
||||
{
|
||||
return state() == SHAMapState::Synching;
|
||||
}
|
||||
|
||||
inline bool
|
||||
SHAMap::isImmutable() const
|
||||
{
|
||||
return state() == SHAMapState::Immutable;
|
||||
return state_ == SHAMapState::Synching;
|
||||
}
|
||||
|
||||
inline void
|
||||
SHAMap::setSynching()
|
||||
{
|
||||
// Guarded, so this is not a way out of Invalid.
|
||||
if (!trySetState(SHAMapState::Synching))
|
||||
{
|
||||
// Only ever called on a freshly constructed map.
|
||||
// LCOV_EXCL_START
|
||||
UNREACHABLE("xrpl::SHAMap::setSynching : map is invalid");
|
||||
// LCOV_EXCL_STOP
|
||||
}
|
||||
state_ = SHAMapState::Synching;
|
||||
}
|
||||
|
||||
inline void
|
||||
SHAMap::clearSynching()
|
||||
{
|
||||
// Only Synching moves. A walk can end after the ledger settled, and then the map stays
|
||||
// Immutable; a Modifying map has nothing to clear; an invalid map stays invalid. Only that
|
||||
// last refusal is logged, since a concurrent walk is what produces it.
|
||||
auto expected = SHAMapState::Synching;
|
||||
if (state_.compare_exchange_strong(
|
||||
expected, SHAMapState::Modifying, std::memory_order_acq_rel, std::memory_order_acquire))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
bool const invalid = expected == SHAMapState::Invalid;
|
||||
SOMETIMES(invalid, "xrpl::SHAMap::clearSynching : map is invalid");
|
||||
if (invalid)
|
||||
{
|
||||
JLOG(journal_.warn()) << "Refused to clear synching on an invalid map, root hash "
|
||||
<< root_->getHash();
|
||||
}
|
||||
state_ = SHAMapState::Modifying;
|
||||
}
|
||||
|
||||
inline bool
|
||||
SHAMap::isValid() const
|
||||
{
|
||||
return state() != SHAMapState::Invalid;
|
||||
}
|
||||
|
||||
inline void
|
||||
SHAMap::setInvalid()
|
||||
{
|
||||
// Through trySetState() like every other transition, so state_ has one writer funnel.
|
||||
trySetState(SHAMapState::Invalid);
|
||||
return state_ != SHAMapState::Invalid;
|
||||
}
|
||||
|
||||
inline void
|
||||
|
||||
@@ -31,19 +31,7 @@ private:
|
||||
*/
|
||||
TaggedPointer hashesAndChildren_;
|
||||
|
||||
// Pin that wrapping fullBelowGen_ in an atomic keeps the packed layout, and that the load
|
||||
// isFullBelow() does once per node of every walk stays lock-free.
|
||||
static_assert(std::atomic<std::uint32_t>::is_always_lock_free);
|
||||
static_assert(sizeof(std::atomic<std::uint32_t>) == sizeof(std::uint32_t));
|
||||
static_assert(alignof(std::atomic<std::uint32_t>) == alignof(std::uint32_t));
|
||||
|
||||
/**
|
||||
* Written from more than one thread, since canonicalization shares a node
|
||||
* between maps and a walk can run with the acquisition lock released (see
|
||||
* SHAMap::state_). Relaxed both ways, since a generation is only compared
|
||||
* for equality and the children it vouches for are published through lock_.
|
||||
*/
|
||||
std::atomic<std::uint32_t> fullBelowGen_ = 0;
|
||||
std::uint32_t fullBelowGen_ = 0;
|
||||
std::uint16_t isBranch_ = 0;
|
||||
|
||||
/**
|
||||
@@ -216,13 +204,13 @@ SHAMapInnerNode::getBranchCount() const
|
||||
inline bool
|
||||
SHAMapInnerNode::isFullBelow(std::uint32_t generation) const
|
||||
{
|
||||
return fullBelowGen_.load(std::memory_order_relaxed) == generation;
|
||||
return fullBelowGen_ == generation;
|
||||
}
|
||||
|
||||
inline void
|
||||
SHAMapInnerNode::setFullBelowGen(std::uint32_t gen)
|
||||
{
|
||||
fullBelowGen_.store(gen, std::memory_order_relaxed);
|
||||
fullBelowGen_ = gen;
|
||||
}
|
||||
|
||||
} // namespace xrpl
|
||||
@@ -375,22 +375,6 @@ public:
|
||||
/////////////////////////////////////////////////////
|
||||
|
||||
// Interface used by AccountDelete
|
||||
/**
|
||||
* This method removes one Ticket ledger-object from the ledger. AccountDelete uses it to
|
||||
* clean up Ticket objects en masse.
|
||||
*
|
||||
* This method reduces the TicketCount and the OwnerCount of the Owner AccountRoot object. If
|
||||
* a TransactionProposal reserved this Ticket, that proposal is deleted as well: once the
|
||||
* Ticket is gone, its proposed transaction can never be applied (tefNO_TICKET).
|
||||
*
|
||||
* @param view the apply view for making changes.
|
||||
* @param account the account which owns the Ticket object.
|
||||
* @param ticketIndex ticketIndex is used to construct the ledger-entry ID of Ticket object.
|
||||
* @param j the journal reference used to log messages.
|
||||
*
|
||||
* @return the final TER after all invariant checks. `tefBAD_LEDGER` is returned if Ticket,
|
||||
* Owner AccountRoot is not found in the apply-view ledger.
|
||||
*/
|
||||
static TER
|
||||
ticketDelete(
|
||||
ApplyView& view,
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include <xrpl/basics/base_uint.h>
|
||||
#include <xrpl/beast/utility/Journal.h>
|
||||
#include <xrpl/ledger/ReadView.h>
|
||||
#include <xrpl/protocol/AccountID.h>
|
||||
#include <xrpl/protocol/STLedgerEntry.h>
|
||||
#include <xrpl/protocol/STTx.h>
|
||||
#include <xrpl/protocol/TER.h>
|
||||
@@ -23,7 +21,6 @@
|
||||
#include <boost/multiprecision/cpp_int.hpp>
|
||||
|
||||
#include <cstdint>
|
||||
#include <map>
|
||||
#include <set>
|
||||
#include <string>
|
||||
#include <tuple>
|
||||
@@ -366,27 +363,11 @@ public:
|
||||
* unique to pseudo-accounts. Check that all pseudo-accounts are following the
|
||||
* rules, and that only pseudo-accounts look like pseudo-accounts.
|
||||
*
|
||||
* Post-fixCleanup3_5_0, a pseudo-account also owns only entries that belong
|
||||
* to its own AMM, Vault or LoanBroker:
|
||||
* - AMM: its AMM entry, its holdings of the two pool assets, and the trust
|
||||
* lines of its LP token,
|
||||
* - Vault: its share MPTokenIssuance, its holding of the vault asset, and the
|
||||
* LoanBrokers operating on this vault,
|
||||
* - LoanBroker: its Loans and its holding of the vault asset (the cover).
|
||||
*
|
||||
* A pseudo-account cannot sign transactions, so it could never accept or
|
||||
* remove any other object linked into its owner directory. Such an object
|
||||
* would pin the directory and block deletion of the owning object.
|
||||
*/
|
||||
class ValidPseudoAccounts
|
||||
{
|
||||
std::vector<std::string> errors_;
|
||||
|
||||
// Entries newly linked into owner directories, keyed by the directory
|
||||
// owner. Whether an owner is a pseudo-account is decided in finalize,
|
||||
// where the view is available.
|
||||
std::map<AccountID, std::vector<uint256>> ownerDirAdditions_;
|
||||
|
||||
public:
|
||||
void
|
||||
visitEntry(bool, SLE::ConstRef, SLE::ConstRef);
|
||||
|
||||
@@ -8,7 +8,6 @@
|
||||
#include <xrpl/protocol/AccountID.h>
|
||||
#include <xrpl/protocol/Asset.h>
|
||||
#include <xrpl/protocol/MPTIssue.h>
|
||||
#include <xrpl/protocol/Protocol.h>
|
||||
#include <xrpl/protocol/Rules.h>
|
||||
#include <xrpl/protocol/STLedgerEntry.h>
|
||||
#include <xrpl/protocol/STTx.h>
|
||||
@@ -30,7 +29,7 @@ namespace xrpl {
|
||||
* - vault must have MPTokenIssuance for shares
|
||||
* - vault without shares outstanding must have no shares
|
||||
* - loss unrealized does not exceed the difference between assets total and
|
||||
* assets available (Legacy/CashBasis), or AssetsDeployed (FixedPrecision)
|
||||
* assets available
|
||||
* - assets available do not exceed assets total
|
||||
* - vault deposit increases assets and share issuance, and adds to:
|
||||
* total assets, assets available, shares outstanding
|
||||
@@ -47,14 +46,6 @@ namespace xrpl {
|
||||
* - vault withdrawal may not succeed when the vault phase is Investment
|
||||
* - closed-ended loan origination (ttLOAN_SET) may only succeed when the
|
||||
* vault phase is Investment
|
||||
* - FixedPrecision only: AssetsDeployed is non-negative and exactly
|
||||
* representable at the asset's precision; AssetsDeployed changes only for
|
||||
* ttLOAN_SET, ttLOAN_PAY and ttLOAN_MANAGE; AssetsDeployed is zero right
|
||||
* after ttVAULT_CREATE and whenever shares outstanding return to zero; the
|
||||
* stored AssetsTotal cache equals AssetsAvailable + AssetsDeployed, rounded
|
||||
* Downward to the asset's precision
|
||||
* - FixedPrecision only: YieldUnrealized is non-negative; YieldUnrealized
|
||||
* changes only for ttLOAN_SET, ttLOAN_PAY and ttLOAN_MANAGE
|
||||
*
|
||||
* Immutability of VaultKind, SubscriptionDate and RedemptionDate is enforced
|
||||
* by NoModifiedUnmodifiableFields (see InvariantCheck.cpp). From
|
||||
@@ -77,9 +68,6 @@ class ValidVault
|
||||
Number assetsAvailable = 0;
|
||||
Number assetsMaximum = 0;
|
||||
Number lossUnrealized = 0;
|
||||
Number assetsDeployed = 0;
|
||||
Number yieldUnrealized = 0;
|
||||
VaultVersion version = VaultVersion::Legacy;
|
||||
std::optional<std::uint8_t> vaultKind;
|
||||
std::optional<std::uint32_t> subscriptionDate;
|
||||
std::optional<std::uint32_t> redemptionDate;
|
||||
@@ -229,39 +217,6 @@ private:
|
||||
[[nodiscard]] bool
|
||||
finalizeLoanSet(ReadView const& view, beast::Journal const& j) const;
|
||||
|
||||
/**
|
||||
* @brief Check that a vault's AssetsTotal and AssetsAvailable deltas add
|
||||
* up against the real transfer, for deposit/withdraw/clawback.
|
||||
*
|
||||
* Shared body of the ttVAULT_DEPOSIT, ttVAULT_WITHDRAW and
|
||||
* ttVAULT_CLAWBACK "assets outstanding"/"assets available" add-up checks,
|
||||
* which differ only in the rounded/exact deltas plugged in and the verb
|
||||
* used in the log message.
|
||||
*
|
||||
* @param afterVault Snapshot of the vault after the transaction.
|
||||
* @param beforeVault Snapshot of the vault before the transaction.
|
||||
* @param vaultAsset The vault's underlying asset.
|
||||
* @param fix340Enabled Whether fixCleanup3_4_0 is enabled.
|
||||
* @param minScale The posterior scale computed by computeVaultMinScale.
|
||||
* @param roundedVaultDelta The real transfer's delta, rounded to minScale.
|
||||
* @param exactVaultDelta The real transfer's exact (unrounded) delta.
|
||||
* @param verb The transaction name used in the log message ("deposit",
|
||||
* "withdrawal" or "clawback").
|
||||
* @param j Journal for logging.
|
||||
* @return false if either add-up check fails.
|
||||
*/
|
||||
[[nodiscard]] static bool
|
||||
checkTotalsAddUp(
|
||||
Vault const& afterVault,
|
||||
Vault const& beforeVault,
|
||||
Asset const& vaultAsset,
|
||||
bool fix340Enabled,
|
||||
std::int32_t minScale,
|
||||
Number const& roundedVaultDelta,
|
||||
Number const& exactVaultDelta,
|
||||
char const* verb,
|
||||
beast::Journal const& j);
|
||||
|
||||
public:
|
||||
// Compute the coarsest scale required to represent all numbers
|
||||
[[nodiscard]] static std::int32_t
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
#include <xrpl/ledger/ApplyView.h>
|
||||
#include <xrpl/ledger/ReadView.h>
|
||||
#include <xrpl/protocol/Asset.h>
|
||||
#include <xrpl/protocol/STAmount.h>
|
||||
#include <xrpl/protocol/STTx.h>
|
||||
#include <xrpl/protocol/TER.h>
|
||||
#include <xrpl/protocol/XRPAmount.h>
|
||||
@@ -46,29 +45,9 @@ public:
|
||||
SLE::Ref loanSle,
|
||||
SLE::Ref brokerSle,
|
||||
SLE::Ref vaultSle,
|
||||
Asset const& vaultAsset,
|
||||
beast::Journal j);
|
||||
|
||||
/**
|
||||
* @brief Compute the First-Loss Capital cover amount for a defaulted Loan on a FixedPrecision
|
||||
* Vault, capped at the LoanBroker's CoverAvailable and floored so that AssetsAvailable + cover
|
||||
* is exactly on its posterior grid.
|
||||
*
|
||||
* The LoanBroker absorbs the rounding. When CoverAvailable - cover needs finer digits than the
|
||||
* broker's posterior grid, it rounds to nearest, as the broker's trust line does, so
|
||||
* CoverAvailable can move by up to half a posterior ulp more or less than the cover. When the
|
||||
* two grids differ, no cover is exact on both sides; the Vault side is kept exact.
|
||||
*
|
||||
* The one exception: when AssetsAvailable has digits finer than the 16-digit cover can carry,
|
||||
* no cover cancels them, and the balance writer rounds AssetsAvailable + cover to nearest,
|
||||
* by at most half a posterior ulp. VaultDeposit and LoanPay credits have the same limit.
|
||||
* @param loanSle The Loan being defaulted.
|
||||
* @param brokerSle The Loan's LoanBroker.
|
||||
* @param vaultSle The LoanBroker's Vault.
|
||||
* @return The cover amount, in the Vault's Asset.
|
||||
*/
|
||||
static STAmount
|
||||
calculateDefaultCover(SLE::Ref loanSle, SLE::Ref brokerSle, SLE::Ref vaultSle);
|
||||
|
||||
/**
|
||||
* Helper function that might be needed by other transactors
|
||||
*/
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <xrpl/beast/utility/Journal.h>
|
||||
#include <xrpl/ledger/ReadView.h>
|
||||
#include <xrpl/protocol/STLedgerEntry.h>
|
||||
#include <xrpl/protocol/STTx.h>
|
||||
#include <xrpl/protocol/TER.h>
|
||||
#include <xrpl/protocol/XRPAmount.h>
|
||||
#include <xrpl/tx/ApplyContext.h>
|
||||
#include <xrpl/tx/Transactor.h>
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
class TransactionProposalCancel : public Transactor
|
||||
{
|
||||
public:
|
||||
static constexpr auto kConsequencesFactory = ConsequencesFactoryType::Normal;
|
||||
|
||||
explicit TransactionProposalCancel(ApplyContext& ctx) : Transactor(ctx)
|
||||
{
|
||||
}
|
||||
|
||||
static NotTEC
|
||||
preflight(PreflightContext const& ctx);
|
||||
|
||||
static TER
|
||||
preclaim(PreclaimContext const& ctx);
|
||||
|
||||
TER
|
||||
doApply() override;
|
||||
|
||||
void
|
||||
visitInvariantEntry(bool isDelete, SLE::ConstRef before, SLE::ConstRef after) override;
|
||||
|
||||
[[nodiscard]] bool
|
||||
finalizeInvariants(
|
||||
STTx const& tx,
|
||||
TER result,
|
||||
XRPAmount fee,
|
||||
ReadView const& view,
|
||||
beast::Journal const& j) override;
|
||||
};
|
||||
|
||||
} // namespace xrpl
|
||||
@@ -52,13 +52,6 @@ private:
|
||||
TER
|
||||
applyFee();
|
||||
|
||||
/**
|
||||
* Write the default Feature Extension fee settings into FeeSettings on
|
||||
* `featureSmartEscrow` activation.
|
||||
*/
|
||||
void
|
||||
initializeVMFees();
|
||||
|
||||
TER
|
||||
applyUNLModify();
|
||||
};
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
#include <cstring>
|
||||
#include <limits>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <utility>
|
||||
|
||||
namespace json {
|
||||
@@ -241,10 +242,14 @@ Value::Value(xrpl::Number const& value) : type_(ValueType::String), allocated_(t
|
||||
value_.stringVal = valueAllocator()->duplicateStringValue(tmp.c_str(), tmp.length());
|
||||
}
|
||||
|
||||
Value::Value(std::string const& value) : type_(ValueType::String), allocated_(true)
|
||||
Value::Value(std::string const& value) : Value(std::string_view{value})
|
||||
{
|
||||
value_.stringVal =
|
||||
valueAllocator()->duplicateStringValue(value.c_str(), (unsigned int)value.length());
|
||||
}
|
||||
|
||||
Value::Value(std::string_view value) : type_(ValueType::String), allocated_(true)
|
||||
{
|
||||
value_.stringVal = valueAllocator()->duplicateStringValue(
|
||||
value.data(), static_cast<unsigned int>(value.length()));
|
||||
}
|
||||
|
||||
Value::Value(StaticString const& value) : type_(ValueType::String)
|
||||
|
||||
@@ -200,23 +200,11 @@ Ledger::Ledger(
|
||||
sle->at(sfReserveIncrement) = *f;
|
||||
sle->at(sfReferenceFeeUnits) = kFeeUnitsDeprecated;
|
||||
}
|
||||
if (std::ranges::find(amendments, featureSmartEscrow) != amendments.end())
|
||||
{
|
||||
sle->at(sfGasLimit) = fees.gasLimit;
|
||||
sle->at(sfBytecodeSizeLimit) = fees.bytecodeSizeLimit;
|
||||
sle->at(sfGasPrice) = fees.gasPrice;
|
||||
}
|
||||
rawInsert(sle);
|
||||
}
|
||||
|
||||
stateMap_.flushDirty(NodeObjectType::AccountNode);
|
||||
// Built locally. See Ledger::setImmutable().
|
||||
if (!setImmutable())
|
||||
{
|
||||
// LCOV_EXCL_START
|
||||
logicError("Ledger::Ledger(CreateGenesisT, ...): genesis ledger map is invalid");
|
||||
// LCOV_EXCL_STOP
|
||||
}
|
||||
setImmutable();
|
||||
}
|
||||
|
||||
Ledger::Ledger(
|
||||
@@ -227,7 +215,7 @@ Ledger::Ledger(
|
||||
Fees const& fees,
|
||||
Family& family,
|
||||
beast::Journal j)
|
||||
: immutable_(false)
|
||||
: immutable_(true)
|
||||
, txMap_(SHAMapType::TRANSACTION, info.txHash, family)
|
||||
, stateMap_(SHAMapType::STATE, info.accountHash, family)
|
||||
, fees_(fees)
|
||||
@@ -250,20 +238,8 @@ Ledger::Ledger(
|
||||
JLOG(j.warn()) << "Don't have state data root for ledger" << header_.seq;
|
||||
}
|
||||
|
||||
// Loaded locally. See Ledger::setImmutable().
|
||||
if (setMapsImmutable())
|
||||
{
|
||||
immutable_ = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
// LCOV_EXCL_START
|
||||
JLOG(j.error()) << "Invalid map for ledger " << header_.seq;
|
||||
UNREACHABLE("xrpl::Ledger::Ledger(LedgerHeader const&, ...) : map is invalid");
|
||||
// Treat it as a damaged ledger: the code below recomputes the hash and re-acquires.
|
||||
loaded = false;
|
||||
// LCOV_EXCL_STOP
|
||||
}
|
||||
txMap_.setImmutable();
|
||||
stateMap_.setImmutable();
|
||||
|
||||
if (!setup())
|
||||
loaded = false;
|
||||
@@ -334,50 +310,27 @@ Ledger::Ledger(
|
||||
setup();
|
||||
}
|
||||
|
||||
bool
|
||||
void
|
||||
Ledger::setImmutable(bool rehash)
|
||||
{
|
||||
// A map found structurally invalid during sync must never be made immutable: isValid() tests
|
||||
// only for Invalid, and an immutable ledger is treated as persistable. Asked before anything is
|
||||
// written, so a refusal leaves the header exactly as it was rather than half relabeled.
|
||||
if (!mapsValid())
|
||||
return false;
|
||||
|
||||
// Read here but written to the header below, once the maps are settled: getHash() can unshare a
|
||||
// dirty tree, so it has to run while the map is still mutable, while a write to the header must
|
||||
// wait until both maps have made it. Skipped once the ledger is immutable, since its maps can
|
||||
// no longer change.
|
||||
bool const deriveMapHashes = !immutable_ && rehash;
|
||||
UInt256 const txHash = deriveMapHashes ? txMap_.getHash().asUInt256() : UInt256{};
|
||||
UInt256 const accountHash = deriveMapHashes ? stateMap_.getHash().asUInt256() : UInt256{};
|
||||
|
||||
// Both were valid at the check above, but a concurrent walk can invalidate one in between (see
|
||||
// SHAMap::state_), so the result is checked. setInvalid() outranks Immutable and can land
|
||||
// after both maps have been settled, so this narrows the window it guards.
|
||||
bool const bothImmutable = setMapsImmutable();
|
||||
SOMETIMES(!bothImmutable, "xrpl::Ledger::setImmutable : map invalidated while going immutable");
|
||||
if (!bothImmutable)
|
||||
return false;
|
||||
|
||||
// Written only now, so losing the race above leaves the header describing what the ledger was
|
||||
// built from rather than a map that has since been abandoned. Forced rather than conditional,
|
||||
// since this is the only place the hash transitions to valid.
|
||||
if (deriveMapHashes)
|
||||
// Force update, since this is the only
|
||||
// place the hash transitions to valid
|
||||
if (!immutable_ && rehash)
|
||||
{
|
||||
header_.txHash = txHash;
|
||||
header_.accountHash = accountHash;
|
||||
header_.txHash = txMap_.getHash().asUInt256();
|
||||
header_.accountHash = stateMap_.getHash().asUInt256();
|
||||
}
|
||||
|
||||
if (rehash)
|
||||
header_.hash = calculateLedgerHash(header_);
|
||||
|
||||
// Set last, so isImmutable() reports only a ledger whose maps are both immutable.
|
||||
immutable_ = true;
|
||||
txMap_.setImmutable();
|
||||
stateMap_.setImmutable();
|
||||
setup();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool
|
||||
void
|
||||
Ledger::setAccepted(
|
||||
NetClock::time_point closeTime,
|
||||
NetClock::duration closeResolution,
|
||||
@@ -389,17 +342,7 @@ Ledger::setAccepted(
|
||||
header_.closeTime = closeTime;
|
||||
header_.closeTimeResolution = closeResolution;
|
||||
header_.closeFlags = correctCloseTime ? 0 : kSLcfNoConsensusTime;
|
||||
// Built locally. See Ledger::setImmutable().
|
||||
if (!setImmutable())
|
||||
{
|
||||
// LCOV_EXCL_START
|
||||
JLOG(j_.error()) << "Invalid map for accepted ledger " << header_.seq;
|
||||
UNREACHABLE("xrpl::Ledger::setAccepted : map is invalid");
|
||||
return false;
|
||||
// LCOV_EXCL_STOP
|
||||
}
|
||||
|
||||
return true;
|
||||
setImmutable();
|
||||
}
|
||||
|
||||
bool
|
||||
@@ -623,7 +566,6 @@ Ledger::setup()
|
||||
{
|
||||
bool oldFees = false;
|
||||
bool newFees = false;
|
||||
bool extensionFees = false;
|
||||
{
|
||||
auto const baseFee = sle->at(~sfBaseFee);
|
||||
auto const reserveBase = sle->at(~sfReserveBase);
|
||||
@@ -658,20 +600,6 @@ Ledger::setup()
|
||||
assign(fees_.increment, reserveIncrementXRP);
|
||||
newFees = baseFeeXRP || reserveBaseXRP || reserveIncrementXRP;
|
||||
}
|
||||
{
|
||||
auto const gasLimit = sle->at(~sfGasLimit);
|
||||
auto const bytecodeSizeLimit = sle->at(~sfBytecodeSizeLimit);
|
||||
auto const gasPrice = sle->at(~sfGasPrice);
|
||||
|
||||
// An absent field resolves to the protocol constant.
|
||||
if (rules_.enabled(featureSmartEscrow))
|
||||
{
|
||||
fees_.gasLimit = gasLimit.value_or(kDefaultGasLimit);
|
||||
fees_.bytecodeSizeLimit = bytecodeSizeLimit.value_or(kDefaultBytecodeSizeLimit);
|
||||
fees_.gasPrice = gasPrice.value_or(kDefaultGasPrice);
|
||||
}
|
||||
extensionFees = gasLimit || bytecodeSizeLimit || gasPrice;
|
||||
}
|
||||
if (oldFees && newFees)
|
||||
{
|
||||
// Should be all of one or the other, but not both
|
||||
@@ -682,12 +610,6 @@ Ledger::setup()
|
||||
// Can't populate the new fees before the amendment is enabled
|
||||
ret = false;
|
||||
}
|
||||
if (!rules_.enabled(featureSmartEscrow) && extensionFees)
|
||||
{
|
||||
// Can't populate the extension fees before the amendment is
|
||||
// enabled
|
||||
ret = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (SHAMapMissingNode const&)
|
||||
|
||||
@@ -435,6 +435,8 @@ PaymentSandbox::issuerSelfDebitHookMPT(
|
||||
std::uint64_t amount,
|
||||
std::int64_t origBalance)
|
||||
{
|
||||
XRPL_ASSERT(amount > 0, "PaymentSandbox::issuerSelfDebitHookMPT: amount must be > 0");
|
||||
|
||||
tab_.issuerSelfDebitMPT(issue, amount, origBalance);
|
||||
}
|
||||
|
||||
|
||||
@@ -511,14 +511,9 @@ doWithdraw(
|
||||
AccountID const& dstAcct,
|
||||
AccountID const& sourceAcct,
|
||||
XRPAmount priorBalance,
|
||||
STAmount const& destinationAmount,
|
||||
STAmount const& sourceAmount,
|
||||
STAmount const& amount,
|
||||
beast::Journal j)
|
||||
{
|
||||
XRPL_ASSERT(
|
||||
destinationAmount.asset() == sourceAmount.asset(),
|
||||
"xrpl::doWithdraw : delivered and source amounts use the same asset");
|
||||
|
||||
auto const dstSle = ctx.view.read(keylet::account(dstAcct));
|
||||
|
||||
// Create a trust line or MPToken for a self-destination only when there
|
||||
@@ -528,10 +523,9 @@ doWithdraw(
|
||||
// create+delete MPTokens in the same transaction.
|
||||
if (dstAcct == senderAcct)
|
||||
{
|
||||
if (destinationAmount > beast::kZero || !ctx.view.rules().enabled(fixCleanup3_4_0))
|
||||
if (amount > beast::kZero || !ctx.view.rules().enabled(fixCleanup3_4_0))
|
||||
{
|
||||
if (auto const ter =
|
||||
addEmptyHolding(ctx, senderAcct, priorBalance, destinationAmount.asset(), j);
|
||||
if (auto const ter = addEmptyHolding(ctx, senderAcct, priorBalance, amount.asset(), j);
|
||||
!isTesSuccess(ter) && ter != tecDUPLICATE)
|
||||
return ter;
|
||||
}
|
||||
@@ -546,33 +540,17 @@ doWithdraw(
|
||||
if (accountHolds(
|
||||
ctx.view,
|
||||
sourceAcct,
|
||||
destinationAmount.asset(),
|
||||
amount.asset(),
|
||||
FreezeHandling::IgnoreFreeze,
|
||||
AuthHandling::IgnoreAuth,
|
||||
j) < sourceAmount)
|
||||
j) < amount)
|
||||
{
|
||||
// LCOV_EXCL_START
|
||||
JLOG(j.error()) << "doWithdraw: source account holds fewer assets than the withdrawal.";
|
||||
JLOG(j.error()) << "doWithdraw: negative balance of broker cover assets.";
|
||||
return tefINTERNAL;
|
||||
// LCOV_EXCL_STOP
|
||||
}
|
||||
|
||||
// A transfer fee applies. Redeem the gross amount with the issuer, then
|
||||
// issue the net amount to the destination.
|
||||
if (sourceAmount != destinationAmount)
|
||||
{
|
||||
auto const issuer = destinationAmount.getIssuer();
|
||||
XRPL_ASSERT(
|
||||
!destinationAmount.native() && sourceAcct != issuer && dstAcct != issuer,
|
||||
"xrpl::doWithdraw : transfer fee applies between token holders");
|
||||
|
||||
if (auto const ter = directSendNoFee(ctx.view, sourceAcct, issuer, sourceAmount, false, j);
|
||||
!isTesSuccess(ter))
|
||||
return ter; // LCOV_EXCL_LINE
|
||||
|
||||
return directSendNoFee(ctx.view, issuer, dstAcct, destinationAmount, false, j);
|
||||
}
|
||||
|
||||
// A reserve sponsor only covers tx.Account's own objects, so resolve the
|
||||
// sponsor against the destination. accountSend can auto-create a holding
|
||||
// for dstAcct; keying on the destination ensures a third-party destination's
|
||||
@@ -584,7 +562,7 @@ doWithdraw(
|
||||
// Move the funds directly from the broker's pseudo-account to the
|
||||
// dstAcct
|
||||
return accountSend(
|
||||
ctx.view, sourceAcct, dstAcct, destinationAmount, j, *sponsorSle, WaiveTransferFee::Yes);
|
||||
ctx.view, sourceAcct, dstAcct, amount, j, *sponsorSle, WaiveTransferFee::Yes);
|
||||
}
|
||||
|
||||
TER
|
||||
|
||||
@@ -213,19 +213,49 @@ adjustAmountsByLPTokens(
|
||||
|
||||
if (lpTokensActual < lpTokens)
|
||||
{
|
||||
bool const ammRoundingEnabled = [&] {
|
||||
if (auto const& rules = getCurrentTransactionRules();
|
||||
rules && rules->enabled(fixAMMv1_1))
|
||||
return true;
|
||||
return false;
|
||||
}();
|
||||
|
||||
// Equal trade
|
||||
if (amount2)
|
||||
{
|
||||
Number const fr = lpTokensActual / lpTokens;
|
||||
auto const amountActual = toSTAmount(amount.asset(), fr * amount);
|
||||
auto const amount2Actual = toSTAmount(amount2->asset(), fr * *amount2);
|
||||
if (!ammRoundingEnabled)
|
||||
{
|
||||
return std::make_tuple(
|
||||
amountActual < amount ? amountActual : amount,
|
||||
amount2Actual < amount2 ? amount2Actual : amount2,
|
||||
lpTokensActual);
|
||||
}
|
||||
|
||||
return std::make_tuple(amountActual, amount2Actual, lpTokensActual);
|
||||
}
|
||||
|
||||
// Single trade
|
||||
auto const amountActual = isDeposit == IsDeposit::Yes
|
||||
? ammAssetIn(amountBalance, lptAMMBalance, lpTokensActual, tfee)
|
||||
: ammAssetOut(amountBalance, lptAMMBalance, lpTokensActual, tfee);
|
||||
auto const amountActual = [&] {
|
||||
if (isDeposit == IsDeposit::Yes)
|
||||
{
|
||||
return ammAssetIn(amountBalance, lptAMMBalance, lpTokensActual, tfee);
|
||||
}
|
||||
if (!ammRoundingEnabled)
|
||||
{
|
||||
return ammAssetOut(amountBalance, lptAMMBalance, lpTokens, tfee);
|
||||
}
|
||||
|
||||
return ammAssetOut(amountBalance, lptAMMBalance, lpTokensActual, tfee);
|
||||
}();
|
||||
if (!ammRoundingEnabled)
|
||||
{
|
||||
return amountActual < amount
|
||||
? std::make_tuple(amountActual, std::nullopt, lpTokensActual)
|
||||
: std::make_tuple(amount, std::nullopt, lpTokensActual);
|
||||
}
|
||||
|
||||
return std::make_tuple(amountActual, std::nullopt, lpTokensActual);
|
||||
}
|
||||
@@ -950,7 +980,7 @@ verifyAndAdjustLPTokenBalance(
|
||||
}
|
||||
else
|
||||
{
|
||||
return std::unexpected<TER>(tecAMM_INVALID_TOKENS); // LCOV_EXCL_LINE
|
||||
return std::unexpected<TER>(tecAMM_INVALID_TOKENS);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
|
||||
@@ -66,161 +66,6 @@ canApplyToBrokerCover(
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
namespace detail {
|
||||
|
||||
[[nodiscard]] int
|
||||
getPosteriorBrokerCoverScale(SLE::ConstRef vault, SLE::ConstRef broker, Number const& delta)
|
||||
{
|
||||
XRPL_ASSERT(
|
||||
vault && vault->getType() == ltVAULT,
|
||||
"xrpl::detail::getPosteriorBrokerCoverScale : valid Vault sle");
|
||||
XRPL_ASSERT(
|
||||
broker && broker->getType() == ltLOAN_BROKER,
|
||||
"xrpl::detail::getPosteriorBrokerCoverScale : valid LoanBroker sle");
|
||||
|
||||
return posteriorAssetScale(
|
||||
getVaultVersion(vault),
|
||||
vault->at(sfAsset),
|
||||
getVaultBaseScale(vault),
|
||||
broker->at(sfCoverAvailable),
|
||||
delta);
|
||||
}
|
||||
|
||||
[[nodiscard]] STAmount
|
||||
roundToPosteriorBrokerCoverScale(
|
||||
SLE::ConstRef vault,
|
||||
SLE::ConstRef broker,
|
||||
STAmount const& delta,
|
||||
Number::RoundingMode roundingMode)
|
||||
{
|
||||
XRPL_ASSERT(
|
||||
vault && vault->getType() == ltVAULT,
|
||||
"xrpl::detail::roundToPosteriorBrokerCoverScale : valid Vault sle");
|
||||
XRPL_ASSERT(
|
||||
broker && broker->getType() == ltLOAN_BROKER,
|
||||
"xrpl::detail::roundToPosteriorBrokerCoverScale : valid LoanBroker sle");
|
||||
XRPL_ASSERT(
|
||||
delta.asset() == vault->at(sfAsset),
|
||||
"xrpl::detail::roundToPosteriorBrokerCoverScale : delta and Vault asset match");
|
||||
if (delta.integral())
|
||||
return delta;
|
||||
return roundToScale(delta, getPosteriorBrokerCoverScale(vault, broker, delta), roundingMode);
|
||||
}
|
||||
|
||||
} // namespace detail
|
||||
|
||||
[[nodiscard]] STAmount
|
||||
creditToPosteriorBrokerCoverScale(
|
||||
SLE::ConstRef vault,
|
||||
SLE::ConstRef broker,
|
||||
Number const& raw,
|
||||
Number::RoundingMode roundingMode)
|
||||
{
|
||||
XRPL_ASSERT(
|
||||
vault && vault->getType() == ltVAULT,
|
||||
"xrpl::creditToPosteriorBrokerCoverScale : valid Vault sle");
|
||||
XRPL_ASSERT(
|
||||
broker && broker->getType() == ltLOAN_BROKER,
|
||||
"xrpl::creditToPosteriorBrokerCoverScale : valid LoanBroker sle");
|
||||
Asset const asset = vault->at(sfAsset);
|
||||
if (asset.integral())
|
||||
{
|
||||
NumberRoundModeGuard const rg(roundingMode);
|
||||
return STAmount{asset, raw};
|
||||
}
|
||||
|
||||
Number const reference = broker->at(sfCoverAvailable);
|
||||
return detail::creditToPosteriorScale(
|
||||
asset, reference, getVaultBaseScale(vault), raw, roundingMode);
|
||||
}
|
||||
|
||||
[[nodiscard]] STAmount
|
||||
debitToPosteriorBrokerCoverScale(
|
||||
SLE::ConstRef vault,
|
||||
SLE::ConstRef broker,
|
||||
STAmount const& amount,
|
||||
Number::RoundingMode roundingMode)
|
||||
{
|
||||
XRPL_ASSERT(
|
||||
vault && vault->getType() == ltVAULT,
|
||||
"xrpl::debitToPosteriorBrokerCoverScale : valid Vault sle");
|
||||
XRPL_ASSERT(
|
||||
broker && broker->getType() == ltLOAN_BROKER,
|
||||
"xrpl::debitToPosteriorBrokerCoverScale : valid LoanBroker sle");
|
||||
XRPL_ASSERT(
|
||||
amount.asset() == vault->at(sfAsset),
|
||||
"xrpl::debitToPosteriorBrokerCoverScale : amount and Vault asset match");
|
||||
XRPL_ASSERT(!amount.negative(), "xrpl::debitToPosteriorBrokerCoverScale : non-negative amount");
|
||||
return -detail::roundToPosteriorBrokerCoverScale(vault, broker, -amount, roundingMode);
|
||||
}
|
||||
|
||||
[[nodiscard]] TER
|
||||
checkOptionalBrokerCoverInflow(SLE::ConstRef vault, SLE::ConstRef broker, STAmount const& amount)
|
||||
{
|
||||
XRPL_ASSERT(
|
||||
vault && vault->getType() == ltVAULT,
|
||||
"xrpl::checkOptionalBrokerCoverInflow : valid Vault sle");
|
||||
XRPL_ASSERT(
|
||||
broker && broker->getType() == ltLOAN_BROKER,
|
||||
"xrpl::checkOptionalBrokerCoverInflow : valid LoanBroker sle");
|
||||
XRPL_ASSERT(
|
||||
amount.asset() == vault->at(sfAsset),
|
||||
"xrpl::checkOptionalBrokerCoverInflow : amount and Vault asset match");
|
||||
XRPL_ASSERT(!amount.negative(), "xrpl::checkOptionalBrokerCoverInflow : non-negative amount");
|
||||
if (getVaultVersion(vault) != VaultVersion::FixedPrecision)
|
||||
return tesSUCCESS;
|
||||
|
||||
// amount is the effective credit (already floored on the posterior
|
||||
// CoverAvailable grid by creditToPosteriorBrokerCoverScale); rounding it
|
||||
// again as a standalone delta could hide a crossing.
|
||||
STAmount const& rounded = amount;
|
||||
int const baseScale = getVaultBaseScale(vault);
|
||||
// Keep this explicit even though the Open-limit capacity check below rejects
|
||||
// every coarsening transition too. The protocol defines both conditions
|
||||
// independently.
|
||||
if (detail::getPosteriorBrokerCoverScale(vault, broker, rounded) != baseScale)
|
||||
return tecLIMIT_EXCEEDED;
|
||||
|
||||
Number const posterior = [&] {
|
||||
NumberRoundModeGuard const rg(Number::RoundingMode::TowardsZero);
|
||||
return broker->at(sfCoverAvailable) + rounded;
|
||||
}();
|
||||
if (posterior > getVaultOpenLimit(vault))
|
||||
return tecLIMIT_EXCEEDED;
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
void
|
||||
adjustBrokerDebtTotal(
|
||||
SLE::Ref brokerSle,
|
||||
SLE::ConstRef vaultSle,
|
||||
Number const& delta,
|
||||
int vaultScale)
|
||||
{
|
||||
// On FixedPrecision Vaults, LoanSet adds principal to DebtTotal exactly, LoanPay and default
|
||||
// subtract exact amounts. Thus DebtTotal must not be rounded coarser than the base scale.
|
||||
if (getVaultVersion(vaultSle) == VaultVersion::FixedPrecision)
|
||||
{
|
||||
brokerSle->at(sfDebtTotal) += delta;
|
||||
}
|
||||
else
|
||||
{
|
||||
adjustImpreciseNumber(brokerSle->at(sfDebtTotal), delta, vaultSle->at(sfAsset), vaultScale);
|
||||
}
|
||||
}
|
||||
|
||||
Number
|
||||
minimumBrokerCover(Number const& debtTotal, TenthBips32 coverRateMinimum, SLE::ConstRef vaultSle)
|
||||
{
|
||||
XRPL_ASSERT(
|
||||
vaultSle && vaultSle->getType() == ltVAULT, "xrpl::minimumBrokerCover : valid Vault sle");
|
||||
NumberRoundModeGuard const mg(Number::RoundingMode::Upward);
|
||||
return roundToAsset(
|
||||
vaultSle->at(sfAsset),
|
||||
tenthBipsOfValue(debtTotal, coverRateMinimum),
|
||||
getVaultBaseScale(vaultSle));
|
||||
}
|
||||
|
||||
bool
|
||||
checkLendingProtocolDependencies(Rules const& rules, STTx const& tx)
|
||||
{
|
||||
@@ -405,13 +250,14 @@ loanPaymentDeltas(LoanPaymentParts const& parts)
|
||||
|
||||
namespace {
|
||||
|
||||
// Cash-basis accounting applies to Vaults created under
|
||||
// featureLendingProtocolV1_1 or a later version. Vaults created before
|
||||
// activation keep instant interest recognition forever.
|
||||
// Cash-basis accounting applies only when featureLendingProtocolV1_1 is
|
||||
// enabled AND the specific Vault was created under it (LEVersion ==
|
||||
// VaultVersion::CashBasis). Vaults created before activation keep instant
|
||||
// interest recognition forever, even after the amendment later turns on.
|
||||
bool
|
||||
cashBasisEnabled(SLE::ConstRef vaultSle)
|
||||
{
|
||||
return getVaultVersion(vaultSle) >= VaultVersion::CashBasis;
|
||||
return getVaultVersion(vaultSle) == VaultVersion::CashBasis;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
@@ -1047,14 +1047,6 @@ maxMPTAmount(SLE const& sleIssuance)
|
||||
return sleIssuance[~sfMaximumAmount].value_or(kMaxMpTokenAmount);
|
||||
}
|
||||
|
||||
std::optional<std::int64_t>
|
||||
maxMPTAmount(ReadView const& view, MPTID const& mptID)
|
||||
{
|
||||
if (auto const sle = view.read(keylet::mptokenIssuance(mptID)))
|
||||
return maxMPTAmount(*sle);
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
std::int64_t
|
||||
availableMPTAmount(SLE const& sleIssuance)
|
||||
{
|
||||
|
||||
@@ -1,51 +1,18 @@
|
||||
#include <xrpl/ledger/helpers/ProposalHelpers.h>
|
||||
|
||||
#include <xrpl/basics/Log.h>
|
||||
#include <xrpl/basics/Slice.h>
|
||||
#include <xrpl/beast/utility/Journal.h>
|
||||
#include <xrpl/beast/utility/instrumentation.h>
|
||||
#include <xrpl/ledger/ApplyView.h>
|
||||
#include <xrpl/ledger/ReadView.h>
|
||||
#include <xrpl/ledger/View.h>
|
||||
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
|
||||
#include <xrpl/protocol/AccountID.h>
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
#include <xrpl/protocol/Keylet.h>
|
||||
#include <xrpl/protocol/LedgerFormats.h>
|
||||
#include <xrpl/protocol/SField.h>
|
||||
#include <xrpl/protocol/STArray.h>
|
||||
#include <xrpl/protocol/STLedgerEntry.h>
|
||||
#include <xrpl/protocol/STObject.h>
|
||||
#include <xrpl/protocol/STTx.h>
|
||||
#include <xrpl/protocol/Serializer.h>
|
||||
#include <xrpl/protocol/TER.h>
|
||||
#include <xrpl/protocol/TxFlags.h>
|
||||
#include <xrpl/protocol/TxFormats.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <optional>
|
||||
|
||||
namespace xrpl::proposal {
|
||||
|
||||
namespace {
|
||||
|
||||
// Proposals do not nest, so a proposed transaction may carry none of these
|
||||
// types, neither as its own payload nor as a Batch inner transaction.
|
||||
constexpr auto kProposalTxTypes = std::to_array<TxType>({
|
||||
ttTRANSACTION_PROPOSAL_CREATE,
|
||||
ttTRANSACTION_PROPOSAL_CANCEL,
|
||||
});
|
||||
|
||||
} // namespace
|
||||
|
||||
bool
|
||||
isValidProposalTxnType(STObject const& proposedTx)
|
||||
isValidProposal(STObject const& proposedTx)
|
||||
{
|
||||
auto const type = proposedTx.getFieldU16(sfTransactionType);
|
||||
|
||||
if (std::ranges::find(kProposalTxTypes, type) != kProposalTxTypes.end())
|
||||
if (isProposalTx(proposedTx))
|
||||
return false;
|
||||
|
||||
if (isPseudoTx(proposedTx))
|
||||
@@ -55,24 +22,13 @@ isValidProposalTxnType(STObject const& proposedTx)
|
||||
(proposedTx.getFieldU32(sfFlags) & tfInnerBatchTxn) != 0u)
|
||||
return false;
|
||||
|
||||
if (type == ttBATCH && proposedTx.isFieldPresent(sfRawTransactions))
|
||||
if (proposedTx.getFieldU16(sfTransactionType) == ttBATCH &&
|
||||
proposedTx.isFieldPresent(sfRawTransactions))
|
||||
{
|
||||
STArray const& innerTxns = proposedTx.getFieldArray(sfRawTransactions);
|
||||
for (STObject const& inner : innerTxns)
|
||||
{
|
||||
// The only production caller reaches this walk through STTx
|
||||
// construction, which rejects a typeless inner first, so this is
|
||||
// re-checked rather than relied upon.
|
||||
if (!inner.isFieldPresent(sfTransactionType))
|
||||
return false;
|
||||
|
||||
auto const innerType = inner.getFieldU16(sfTransactionType);
|
||||
|
||||
// A nested Batch is rejected alongside nested proposals and
|
||||
// pseudo-transactions: STTx construction forbids inner Batches, so
|
||||
// a proposal must not be able to store one either.
|
||||
if (std::ranges::find(kProposalTxTypes, innerType) != kProposalTxTypes.end() ||
|
||||
innerType == ttBATCH || isPseudoTx(inner))
|
||||
if (isProposalTx(inner) || isPseudoTx(inner))
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -80,112 +36,4 @@ isValidProposalTxnType(STObject const& proposedTx)
|
||||
return true;
|
||||
}
|
||||
|
||||
bool
|
||||
isTerminal(
|
||||
ReadView const& view,
|
||||
std::optional<std::uint32_t> expiration,
|
||||
STObject const& proposedTx)
|
||||
{
|
||||
if (hasExpired(view, expiration))
|
||||
return true;
|
||||
|
||||
// Mirror the ordinary transactor's tefMAX_LEDGER rule: view.seq() > bound.
|
||||
// At view.seq() == bound the payload can still be submitted, so the
|
||||
// proposal is still live (On-Chain Cosigner spec §4.5).
|
||||
return proposedTx.isFieldPresent(sfLastLedgerSequence) &&
|
||||
view.seq() > proposedTx.getFieldU32(sfLastLedgerSequence);
|
||||
}
|
||||
|
||||
TER
|
||||
deleteProposal(ApplyView& view, SLE::pointer const& sleProposal, beast::Journal j)
|
||||
{
|
||||
if (!sleProposal || sleProposal->getType() != ltTRANSACTION_PROPOSAL ||
|
||||
!view.exists(Keylet{ltTRANSACTION_PROPOSAL, sleProposal->key()}))
|
||||
{
|
||||
// LCOV_EXCL_START
|
||||
JLOG(j.fatal()) << "Invalid TransactionProposal deletion.";
|
||||
return tecINTERNAL;
|
||||
// LCOV_EXCL_STOP
|
||||
}
|
||||
|
||||
AccountID const owner = sleProposal->getAccountID(sfOwner);
|
||||
|
||||
std::uint64_t const page{(*sleProposal)[sfOwnerNode]};
|
||||
if (!view.dirRemove(keylet::ownerDir(owner), page, sleProposal->key(), true))
|
||||
{
|
||||
// LCOV_EXCL_START
|
||||
JLOG(j.fatal()) << "Unable to delete TransactionProposal from owner.";
|
||||
return tefBAD_LEDGER;
|
||||
// LCOV_EXCL_STOP
|
||||
}
|
||||
|
||||
auto const sleOwner = view.peek(keylet::account(owner));
|
||||
if (!sleOwner)
|
||||
{
|
||||
// LCOV_EXCL_START
|
||||
JLOG(j.fatal()) << "Could not find TransactionProposal owner account root.";
|
||||
return tefBAD_LEDGER;
|
||||
// LCOV_EXCL_STOP
|
||||
}
|
||||
|
||||
// Release the reserve against the Owner or, if the entry carries a
|
||||
// reserve sponsor, against that sponsor.
|
||||
decreaseOwnerCountForObject(
|
||||
view,
|
||||
sleOwner,
|
||||
sleProposal,
|
||||
proposalOwnerCount(sleProposal->getFieldObject(sfProposedTransaction)),
|
||||
j);
|
||||
|
||||
view.erase(sleProposal);
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
bool
|
||||
payloadMatches(STObject const& proposedTx, STObject const& tx)
|
||||
{
|
||||
XRPL_ASSERT(
|
||||
proposedTx.isFieldPresent(sfTransactionType) && tx.isFieldPresent(sfTransactionType),
|
||||
"xrpl::proposal::payloadMatches : transaction-shaped inputs");
|
||||
|
||||
// The signing-payload serialization omits every kNotSigning field: the
|
||||
// signature containers a proposal lets evolve (TxnSignature, Signers,
|
||||
// BatchSigners, CounterpartySignature, SponsorSignature) plus Signature
|
||||
// and MasterSignature, which the account's own signature does not cover
|
||||
// either (see the header). What remains is the field set fixed at
|
||||
// creation — except SigningPubKey, which signing payloads include but
|
||||
// which is also mutable here (stored empty, filled if the target signs
|
||||
// with its own key). Neutralize it on both sides before comparing.
|
||||
auto const payloadBytes = [](STObject const& obj) {
|
||||
STObject copy{obj};
|
||||
copy.setFieldVL(sfSigningPubKey, Slice{});
|
||||
Serializer s;
|
||||
copy.addWithoutSigningFields(s);
|
||||
return s.getData();
|
||||
};
|
||||
|
||||
return payloadBytes(proposedTx) == payloadBytes(tx);
|
||||
}
|
||||
|
||||
bool
|
||||
canConsumeTicket(ReadView const& view, STTx const& tx)
|
||||
{
|
||||
auto const seqProx = tx.getSeqProxy();
|
||||
if (!seqProx.isTicket())
|
||||
{
|
||||
// The caller only reaches here for a Ticket-based transaction.
|
||||
// LCOV_EXCL_START
|
||||
UNREACHABLE("xrpl::proposal::canConsumeTicket : tx spends a Sequence");
|
||||
return false;
|
||||
// LCOV_EXCL_STOP
|
||||
}
|
||||
|
||||
auto const sleProposal =
|
||||
view.read(keylet::txProposal(tx.getAccountID(sfAccount), seqProx.value()));
|
||||
if (!sleProposal)
|
||||
return true;
|
||||
|
||||
return payloadMatches(sleProposal->getFieldObject(sfProposedTransaction), tx);
|
||||
}
|
||||
|
||||
} // namespace xrpl::proposal
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
#include <xrpl/basics/Log.h>
|
||||
#include <xrpl/basics/MathUtilities.h>
|
||||
#include <xrpl/basics/Number.h>
|
||||
#include <xrpl/beast/utility/Journal.h>
|
||||
#include <xrpl/beast/utility/Zero.h>
|
||||
#include <xrpl/beast/utility/instrumentation.h>
|
||||
@@ -552,22 +551,6 @@ transferRate(ReadView const& view, STAmount const& amount)
|
||||
return transferRate(view, amount.asset());
|
||||
}
|
||||
|
||||
STAmount
|
||||
addTransferFee(STAmount const& amount, Rate const& rate)
|
||||
{
|
||||
if (rate == kParityRate)
|
||||
return amount;
|
||||
return mulRatio(amount, rate.value, QUALITY_ONE, Number::RoundingMode::Upward);
|
||||
}
|
||||
|
||||
STAmount
|
||||
subtractTransferFee(STAmount const& amount, Rate const& rate)
|
||||
{
|
||||
if (rate == kParityRate)
|
||||
return amount;
|
||||
return mulRatio(amount, QUALITY_ONE, rate.value, Number::RoundingMode::Downward);
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
//
|
||||
// Holding operations
|
||||
@@ -990,11 +973,19 @@ accountSendIOU(
|
||||
SLE::Ref sponsorSle,
|
||||
WaiveTransferFee waiveFee)
|
||||
{
|
||||
if (saAmount < beast::kZero || saAmount.holds<MPTIssue>())
|
||||
if (view.rules().enabled(fixAMMv1_1))
|
||||
{
|
||||
if (saAmount < beast::kZero || saAmount.holds<MPTIssue>())
|
||||
{
|
||||
return tecINTERNAL; // LCOV_EXCL_LINE
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// LCOV_EXCL_START
|
||||
JLOG(j.fatal()) << "accountSendIOU: negative amount or invalid asset";
|
||||
return tecINTERNAL;
|
||||
XRPL_ASSERT(
|
||||
saAmount >= beast::kZero && !saAmount.holds<MPTIssue>(),
|
||||
"xrpl::accountSendIOU : minimum amount and not MPT");
|
||||
// LCOV_EXCL_STOP
|
||||
}
|
||||
|
||||
@@ -1364,8 +1355,9 @@ directSendNoLimitMPT(
|
||||
{
|
||||
// Number math loses precision on large MPT amounts, which can
|
||||
// overcharge the sender. MPTs are integral, so compute the cost
|
||||
// exactly, matching the payment engine.
|
||||
saActual = addTransferFee(saAmount, rate);
|
||||
// exactly and round it up, matching the payment engine.
|
||||
auto const cost = mulRatio(saAmount.mpt(), rate.value, QUALITY_ONE, true);
|
||||
saActual = STAmount(saAmount.asset(), cost.value());
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -1490,8 +1482,9 @@ directSendNoLimitMultiMPT(
|
||||
{
|
||||
// Number math loses precision on large MPT amounts, which can
|
||||
// overcharge the sender. MPTs are integral, so compute the
|
||||
// cost exactly, matching the payment engine.
|
||||
actualSend = addTransferFee(amount, rate);
|
||||
// cost exactly and round it up, matching the payment engine.
|
||||
auto const cost = mulRatio(amount.mpt(), rate.value, QUALITY_ONE, true);
|
||||
actualSend = STAmount(amount.asset(), cost.value());
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
@@ -1,16 +1,12 @@
|
||||
#include <xrpl/ledger/helpers/VaultHelpers.h>
|
||||
|
||||
#include <xrpl/basics/Log.h>
|
||||
#include <xrpl/basics/Number.h>
|
||||
#include <xrpl/beast/utility/Journal.h>
|
||||
#include <xrpl/beast/utility/Zero.h>
|
||||
#include <xrpl/beast/utility/instrumentation.h>
|
||||
#include <xrpl/ledger/ReadView.h>
|
||||
#include <xrpl/ledger/View.h>
|
||||
#include <xrpl/ledger/helpers/CredentialHelpers.h>
|
||||
#include <xrpl/protocol/AccountID.h>
|
||||
#include <xrpl/protocol/Asset.h>
|
||||
#include <xrpl/protocol/Feature.h>
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
#include <xrpl/protocol/LedgerFormats.h> // IWYU pragma: keep
|
||||
#include <xrpl/protocol/Protocol.h>
|
||||
@@ -21,480 +17,13 @@
|
||||
#include <xrpl/protocol/STTx.h>
|
||||
#include <xrpl/protocol/TER.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdint>
|
||||
#include <expected>
|
||||
#include <optional>
|
||||
#include <stdexcept>
|
||||
#include <string_view>
|
||||
#include <utility>
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
namespace {
|
||||
|
||||
[[nodiscard]] int
|
||||
fixedBaseScale(SLE::ConstRef vault)
|
||||
{
|
||||
XRPL_ASSERT(vault && vault->getType() == ltVAULT, "xrpl::fixedBaseScale : valid Vault sle");
|
||||
Asset const asset = vault->at(sfAsset);
|
||||
// sfScale is never written for an integral asset (VaultCreate only sets
|
||||
// it when scale != 0, and integral assets always get scale 0), so it
|
||||
// must not be read here; vaultBaseScale ignores the scale argument for
|
||||
// an integral asset anyway.
|
||||
if (asset.integral())
|
||||
return 0;
|
||||
return vaultBaseScale(asset, vault->at(sfScale));
|
||||
}
|
||||
|
||||
// Thin, vault-specific wrapper over posteriorAssetScale: used by
|
||||
// getPosteriorVaultScale and the cash outflow rounding below.
|
||||
[[nodiscard]] int
|
||||
posteriorScale(SLE::ConstRef vault, Number const& reference, Number const& delta)
|
||||
{
|
||||
return detail::posteriorAssetScale(
|
||||
getVaultVersion(vault), vault->at(sfAsset), fixedBaseScale(vault), reference, delta);
|
||||
}
|
||||
|
||||
// FixedPrecision-only counterpart to clampToAssetsTotalScale, for cash
|
||||
// outflows (VaultWithdraw, VaultClawback): rounds the magnitude of delta
|
||||
// toward zero at AssetsAvailable's own posterior scale, the balance the cash
|
||||
// actually leaves, instead of the derived AssetsTotal cache. Same
|
||||
// tecPRECISION_LOSS and integral-asset and magnitude-never-exceeds-delta
|
||||
// rules as clampToAssetsTotalScale. Not part of the public interface; reached
|
||||
// only through clampVaultOutflow.
|
||||
//
|
||||
// For an outflow it is safe to round the delta directly, unlike the credit
|
||||
// case: amount on the posterior grid of AssetsAvailable always leaves
|
||||
// AssetsAvailable minus amount representable, since the posterior grid is
|
||||
// exactly the grid AssetsAvailable itself will canonicalize to after the
|
||||
// subtraction.
|
||||
[[nodiscard]] std::expected<STAmount, TER>
|
||||
clampToAvailableScale(SLE::ConstRef vault, STAmount const& delta)
|
||||
{
|
||||
XRPL_ASSERT(
|
||||
delta.asset() == vault->at(sfAsset),
|
||||
"xrpl::clampToAvailableScale : delta and vault asset match");
|
||||
XRPL_ASSERT(
|
||||
getVaultVersion(vault) == VaultVersion::FixedPrecision,
|
||||
"xrpl::clampToAvailableScale : FixedPrecision Vault");
|
||||
XRPL_ASSERT(delta.negative(), "xrpl::clampToAvailableScale : outflow delta is negative");
|
||||
|
||||
Asset const asset = vault->at(sfAsset);
|
||||
STAmount magnitude = delta.negative() ? -delta : delta;
|
||||
if (asset.integral())
|
||||
return magnitude;
|
||||
|
||||
STAmount const rounded = roundToScale(
|
||||
delta,
|
||||
posteriorScale(vault, vault->at(sfAssetsAvailable), delta),
|
||||
Number::RoundingMode::TowardsZero);
|
||||
STAmount actualDelta = rounded.negative() ? -rounded : rounded;
|
||||
|
||||
XRPL_ASSERT(
|
||||
abs(actualDelta) <= abs(delta),
|
||||
"xrpl::clampToAvailableScale : actual delta smaller or equal to calculated delta");
|
||||
|
||||
// Reject changes below scale precision (1 ULP) to prevent share balance changes
|
||||
// without corresponding asset movements.
|
||||
if (actualDelta <= beast::kZero)
|
||||
return std::unexpected(tecPRECISION_LOSS);
|
||||
|
||||
return actualDelta;
|
||||
}
|
||||
|
||||
[[nodiscard]] VaultKind
|
||||
decodeVaultKind(std::optional<std::uint8_t> vaultKind)
|
||||
{
|
||||
if (vaultKind && *vaultKind == std::to_underlying(VaultKind::ClosedEnded))
|
||||
return VaultKind::ClosedEnded;
|
||||
return VaultKind::OpenEnded;
|
||||
}
|
||||
|
||||
// Write cached AssetsTotal from getAssetsTotal, as the exact total rounded
|
||||
// Downward to the Vault asset's 16-digit STAmount representation. Below
|
||||
// coarsening this equals the exact total; above it, the cache is a
|
||||
// deliberate floor of the exact value. Internal to adjustVaultBalances,
|
||||
// the only writer of AssetsAvailable, AssetsDeployed, AssetsTotal,
|
||||
// YieldUnrealized and LossUnrealized on a FixedPrecision Vault after
|
||||
// creation; not part of the public interface.
|
||||
void
|
||||
syncAssetsTotal(SLE::Ref vault)
|
||||
{
|
||||
XRPL_ASSERT(vault && vault->getType() == ltVAULT, "xrpl::syncAssetsTotal : valid Vault sle");
|
||||
Asset const asset = vault->at(sfAsset);
|
||||
|
||||
// adjustVaultBalances writes AssetsAvailable as an already-canonical
|
||||
// STAmount before calling this, so the associateAsset pass a transactor
|
||||
// runs afterwards is always a no-op. If this fires, AssetsAvailable was
|
||||
// written some other way.
|
||||
XRPL_ASSERT(
|
||||
(STAmount{asset, vault->at(sfAssetsAvailable)} == vault->at(sfAssetsAvailable)),
|
||||
"xrpl::syncAssetsTotal : AssetsAvailable is a 16-digit STAmount value");
|
||||
|
||||
NumberRoundModeGuard const rg(Number::RoundingMode::Downward);
|
||||
vault->at(sfAssetsTotal) = STAmount{asset, getAssetsTotal(vault)};
|
||||
}
|
||||
|
||||
[[nodiscard]] bool
|
||||
debitIsNonZeroDust(SLE::ConstRef vault, Number const& amount)
|
||||
{
|
||||
if (amount == 0)
|
||||
return false;
|
||||
|
||||
auto const total = getVaultVersion(vault) == VaultVersion::FixedPrecision
|
||||
? vault->at(sfAssetsAvailable)
|
||||
: getAssetsTotal(vault);
|
||||
auto const asset = vault->at(sfAsset);
|
||||
return STAmount{asset, total - amount} == STAmount{asset, total};
|
||||
}
|
||||
} // namespace
|
||||
|
||||
namespace detail {
|
||||
|
||||
[[nodiscard]] int
|
||||
liveScale(Number const& reference, Asset const& asset, int baseScale)
|
||||
{
|
||||
if (reference == beast::kZero)
|
||||
return baseScale;
|
||||
// Round Downward, not the ambient mode: the live exponent must match the
|
||||
// exponent syncAssetsTotal actually stores (also Downward), regardless of
|
||||
// what rounding mode the caller's own arithmetic is using.
|
||||
NumberRoundModeGuard const rg(Number::RoundingMode::Downward);
|
||||
return std::max(baseScale, scale(reference, asset));
|
||||
}
|
||||
|
||||
[[nodiscard]] int
|
||||
posteriorAssetScale(
|
||||
VaultVersion version,
|
||||
Asset const& asset,
|
||||
int baseScale,
|
||||
Number const& reference,
|
||||
Number const& delta)
|
||||
{
|
||||
// ToNearest for the sum and for scale()'s canonicalization of it, as the
|
||||
// Legacy/CashBasis clamp always did; liveScale sets its own mode.
|
||||
NumberRoundModeGuard const rg(Number::RoundingMode::ToNearest);
|
||||
Number const posterior = reference + delta;
|
||||
|
||||
switch (version)
|
||||
{
|
||||
case VaultVersion::Legacy:
|
||||
case VaultVersion::CashBasis:
|
||||
return scale(posterior, asset);
|
||||
case VaultVersion::FixedPrecision:
|
||||
return liveScale(posterior, asset, baseScale);
|
||||
}
|
||||
// LCOV_EXCL_START
|
||||
UNREACHABLE("xrpl::detail::posteriorAssetScale : valid VaultVersion");
|
||||
return Number::kMinExponent - 1;
|
||||
// LCOV_EXCL_STOP
|
||||
}
|
||||
|
||||
[[nodiscard]] STAmount
|
||||
creditToPosteriorScale(
|
||||
Asset const& asset,
|
||||
Number const& reference,
|
||||
int baseScale,
|
||||
Number const& raw,
|
||||
Number::RoundingMode roundingMode)
|
||||
{
|
||||
// Floor the SUM (reference + raw), not just raw. See
|
||||
// creditToPosteriorAvailableScale's doc: a delta floored on its own grid
|
||||
// can still leave a 17-digit sum once the reference has crossed a power
|
||||
// of ten.
|
||||
//
|
||||
// Both reference + raw and flooredSum - reference can carry more digits
|
||||
// than Number holds (the sum is on the posterior grid, the reference on
|
||||
// the finer one). Compute both under roundingMode, not the caller's
|
||||
// ambient mode, so the credit this returns never exceeds raw.
|
||||
//
|
||||
// Take the grid from this same sum. A sum just under a power of ten that
|
||||
// rounds up to it in another mode would pick the coarser grid, and
|
||||
// flooring the unrounded sum on it would land below reference.
|
||||
NumberRoundModeGuard const rg(roundingMode);
|
||||
Number const sum = reference + raw;
|
||||
int const atScale = liveScale(sum, asset, baseScale);
|
||||
Number const flooredSum = roundToAsset(asset, sum, atScale, roundingMode);
|
||||
STAmount const credit{asset, flooredSum - reference};
|
||||
XRPL_ASSERT_PARTS(
|
||||
roundingMode != Number::RoundingMode::Downward ||
|
||||
(credit >= beast::kZero && Number(credit) <= raw),
|
||||
"xrpl::detail::creditToPosteriorScale",
|
||||
"credit is between zero and raw");
|
||||
return credit;
|
||||
}
|
||||
|
||||
[[nodiscard]] int
|
||||
getPosteriorVaultScale(SLE::ConstRef vault, STAmount const& delta)
|
||||
{
|
||||
XRPL_ASSERT(
|
||||
vault && vault->getType() == ltVAULT,
|
||||
"xrpl::detail::getPosteriorVaultScale : valid Vault sle");
|
||||
XRPL_ASSERT(
|
||||
delta.asset() == vault->at(sfAsset),
|
||||
"xrpl::detail::getPosteriorVaultScale : delta and Vault asset match");
|
||||
return posteriorScale(vault, getAssetsTotal(vault), delta);
|
||||
}
|
||||
|
||||
} // namespace detail
|
||||
|
||||
[[nodiscard]] Number
|
||||
getAssetsTotal(SLE::ConstRef vault)
|
||||
{
|
||||
XRPL_ASSERT(vault && vault->getType() == ltVAULT, "xrpl::getAssetsTotal : valid Vault sle");
|
||||
|
||||
if (getVaultVersion(vault) != VaultVersion::FixedPrecision)
|
||||
return vault->at(sfAssetsTotal);
|
||||
|
||||
// AssetsAvailable is at -Scale (or coarser) and AssetsDeployed is always at
|
||||
// -Scale, so the sum is exact while it fits 19 digits. Fix the mode so a
|
||||
// sum that does not fit rounds the same way for every caller.
|
||||
NumberRoundModeGuard const rg(Number::RoundingMode::Downward);
|
||||
return vault->at(sfAssetsAvailable) + vault->at(sfAssetsDeployed);
|
||||
}
|
||||
|
||||
[[nodiscard]] TER
|
||||
adjustVaultBalances(SLE::Ref vault, VaultBalanceChange const& change, beast::Journal j)
|
||||
{
|
||||
XRPL_ASSERT(
|
||||
vault && vault->getType() == ltVAULT, "xrpl::adjustVaultBalances : valid Vault sle");
|
||||
XRPL_ASSERT(
|
||||
getVaultVersion(vault) == VaultVersion::FixedPrecision,
|
||||
"xrpl::adjustVaultBalances : FixedPrecision Vault");
|
||||
Asset const asset = vault->at(sfAsset);
|
||||
STAmount const cash = change.cash.value_or(STAmount{asset, 0});
|
||||
XRPL_ASSERT(cash.asset() == asset, "xrpl::adjustVaultBalances : cash and Vault asset match");
|
||||
// This is the mode the ledger's own transfers (trust line / MPT balance
|
||||
// updates) normalize in; computing AA' with any other ambient mode would
|
||||
// not reproduce what the real transfer just did to the backing balance.
|
||||
XRPL_ASSERT(
|
||||
Number::getround() == Number::RoundingMode::ToNearest,
|
||||
"xrpl::adjustVaultBalances : ambient rounding mode is ToNearest");
|
||||
|
||||
// AA' is the exact trust-line/MPT arithmetic the ledger uses for the
|
||||
// transfer itself: a Number sum re-canonicalized to the asset's 16-digit
|
||||
// STAmount precision. MPT and XRP sums are already integral, so this is
|
||||
// a no-op rounding for them.
|
||||
//
|
||||
// AssetsDeployed, LossUnrealized and YieldUnrealized move no cash, so they stay
|
||||
// exact Number sums; the asserts below check each fits 16 digits, rather
|
||||
// than letting an STAmount conversion round it silently.
|
||||
STAmount const availableAfter{asset, Number(vault->at(sfAssetsAvailable)) + Number(cash)};
|
||||
Number const assetsDeployedAfter = Number(vault->at(sfAssetsDeployed)) + change.deployed;
|
||||
Number const lossUnrealizedAfter = Number(vault->at(sfLossUnrealized)) + change.loss;
|
||||
// LoanSet, LoanPay and default each move YieldUnrealized by the change in
|
||||
// the Loan's own TotalValueOutstanding - PrincipalOutstanding -
|
||||
// ManagementFeeOutstanding, so it always equals that sum over the
|
||||
// Vault's Loans and cannot go negative.
|
||||
Number const yieldUnrealizedAfter = Number(vault->at(sfYieldUnrealized)) + change.yield;
|
||||
|
||||
if (availableAfter < beast::kZero || assetsDeployedAfter < beast::kZero ||
|
||||
lossUnrealizedAfter < beast::kZero || yieldUnrealizedAfter < beast::kZero)
|
||||
{
|
||||
// LCOV_EXCL_START
|
||||
JLOG(j.fatal()) << "adjustVaultBalances: a balance would become negative."
|
||||
<< " AssetsAvailable: " << Number(availableAfter)
|
||||
<< ", AssetsDeployed: " << assetsDeployedAfter
|
||||
<< ", LossUnrealized: " << lossUnrealizedAfter
|
||||
<< ", YieldUnrealized: " << yieldUnrealizedAfter;
|
||||
return tefBAD_LEDGER;
|
||||
// LCOV_EXCL_STOP
|
||||
}
|
||||
|
||||
// The impairment rule: a Loan can only be impaired for as much as it
|
||||
// still owes. Reachable only through LoanManage's impair.
|
||||
if (lossUnrealizedAfter > assetsDeployedAfter)
|
||||
return tecLIMIT_EXCEEDED;
|
||||
|
||||
// Every caller derives principal from a Loan's own PrincipalOutstanding so the new total must
|
||||
// stay representable at the asset's own 16-digit precision too.
|
||||
XRPL_ASSERT(
|
||||
(STAmount{asset, assetsDeployedAfter} == assetsDeployedAfter),
|
||||
"xrpl::adjustVaultBalances : AssetsDeployed is a 16-digit STAmount value");
|
||||
XRPL_ASSERT(
|
||||
(STAmount{asset, lossUnrealizedAfter} == lossUnrealizedAfter),
|
||||
"xrpl::adjustVaultBalances : LossUnrealized is a 16-digit STAmount value");
|
||||
XRPL_ASSERT(
|
||||
(STAmount{asset, yieldUnrealizedAfter} == yieldUnrealizedAfter),
|
||||
"xrpl::adjustVaultBalances : YieldUnrealized is a 16-digit STAmount value");
|
||||
|
||||
vault->at(sfAssetsAvailable) = availableAfter;
|
||||
vault->at(sfAssetsDeployed) = assetsDeployedAfter;
|
||||
vault->at(sfLossUnrealized) = lossUnrealizedAfter;
|
||||
vault->at(sfYieldUnrealized) = yieldUnrealizedAfter;
|
||||
syncAssetsTotal(vault);
|
||||
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
[[nodiscard]] TER
|
||||
adjustVaultCash(SLE::Ref vault, STAmount const& delta, beast::Journal j)
|
||||
{
|
||||
if (getVaultVersion(vault) == VaultVersion::FixedPrecision)
|
||||
return adjustVaultBalances(vault, {.cash = delta}, j);
|
||||
|
||||
vault->at(sfAssetsTotal) += delta;
|
||||
vault->at(sfAssetsAvailable) += delta;
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
[[nodiscard]] int
|
||||
getVaultScale(SLE::ConstRef vault)
|
||||
{
|
||||
XRPL_ASSERT(vault && vault->getType() == ltVAULT, "xrpl::getVaultScale : valid Vault sle");
|
||||
|
||||
Number const assetsTotal = getAssetsTotal(vault);
|
||||
switch (getVaultVersion(vault))
|
||||
{
|
||||
case VaultVersion::Legacy:
|
||||
case VaultVersion::CashBasis:
|
||||
return scale(assetsTotal, vault->at(sfAsset));
|
||||
case VaultVersion::FixedPrecision:
|
||||
return detail::liveScale(assetsTotal, vault->at(sfAsset), fixedBaseScale(vault));
|
||||
}
|
||||
// LCOV_EXCL_START
|
||||
UNREACHABLE("xrpl::getVaultScale : valid VaultVersion");
|
||||
return Number::kMinExponent - 1;
|
||||
// LCOV_EXCL_STOP
|
||||
}
|
||||
|
||||
[[nodiscard]] int
|
||||
getVaultBaseScale(SLE::ConstRef vault)
|
||||
{
|
||||
XRPL_ASSERT(vault && vault->getType() == ltVAULT, "xrpl::getVaultBaseScale : valid Vault sle");
|
||||
|
||||
switch (getVaultVersion(vault))
|
||||
{
|
||||
case VaultVersion::Legacy:
|
||||
case VaultVersion::CashBasis:
|
||||
return getVaultScale(vault);
|
||||
case VaultVersion::FixedPrecision:
|
||||
return fixedBaseScale(vault);
|
||||
}
|
||||
// LCOV_EXCL_START
|
||||
UNREACHABLE("xrpl::getVaultBaseScale : valid VaultVersion");
|
||||
return Number::kMinExponent - 1;
|
||||
// LCOV_EXCL_STOP
|
||||
}
|
||||
|
||||
[[nodiscard]] STAmount
|
||||
roundToPosteriorVaultScale(
|
||||
SLE::ConstRef vault,
|
||||
STAmount const& amount,
|
||||
Number::RoundingMode roundingMode)
|
||||
{
|
||||
XRPL_ASSERT(
|
||||
vault && vault->getType() == ltVAULT, "xrpl::roundToPosteriorVaultScale : valid Vault sle");
|
||||
XRPL_ASSERT(
|
||||
amount.asset() == vault->at(sfAsset),
|
||||
"xrpl::roundToPosteriorVaultScale : amount and Vault asset match");
|
||||
if (amount.integral())
|
||||
return amount;
|
||||
return roundToScale(amount, detail::getPosteriorVaultScale(vault, amount), roundingMode);
|
||||
}
|
||||
|
||||
[[nodiscard]] STAmount
|
||||
creditToPosteriorAvailableScale(
|
||||
SLE::ConstRef vault,
|
||||
Number const& raw,
|
||||
Number::RoundingMode roundingMode)
|
||||
{
|
||||
XRPL_ASSERT(
|
||||
vault && vault->getType() == ltVAULT,
|
||||
"xrpl::creditToPosteriorAvailableScale : valid Vault sle");
|
||||
Asset const asset = vault->at(sfAsset);
|
||||
if (asset.integral())
|
||||
{
|
||||
NumberRoundModeGuard const rg(roundingMode);
|
||||
return STAmount{asset, raw};
|
||||
}
|
||||
|
||||
Number const reference = vault->at(sfAssetsAvailable);
|
||||
return detail::creditToPosteriorScale(
|
||||
asset, reference, fixedBaseScale(vault), raw, roundingMode);
|
||||
}
|
||||
|
||||
[[nodiscard]] Number
|
||||
getVaultOpenLimit(SLE::ConstRef vault)
|
||||
{
|
||||
XRPL_ASSERT(vault && vault->getType() == ltVAULT, "xrpl::getVaultOpenLimit : valid Vault sle");
|
||||
XRPL_ASSERT(
|
||||
getVaultVersion(vault) == VaultVersion::FixedPrecision,
|
||||
"xrpl::getVaultOpenLimit : FixedPrecision Vault");
|
||||
return Number{9, 15 + getVaultBaseScale(vault)};
|
||||
}
|
||||
|
||||
[[nodiscard]] TER
|
||||
checkOptionalVaultInflow(SLE::ConstRef vault, STAmount const& amount)
|
||||
{
|
||||
XRPL_ASSERT(
|
||||
vault && vault->getType() == ltVAULT, "xrpl::checkOptionalVaultInflow : valid Vault sle");
|
||||
XRPL_ASSERT(
|
||||
amount.asset() == vault->at(sfAsset),
|
||||
"xrpl::checkOptionalVaultInflow : amount and Vault asset match");
|
||||
XRPL_ASSERT(!amount.negative(), "xrpl::checkOptionalVaultInflow : non-negative amount");
|
||||
if (amount.negative())
|
||||
return tefINTERNAL; // LCOV_EXCL_LINE
|
||||
if (getVaultVersion(vault) != VaultVersion::FixedPrecision)
|
||||
return tesSUCCESS;
|
||||
|
||||
STAmount const rounded =
|
||||
roundToPosteriorVaultScale(vault, amount, Number::RoundingMode::TowardsZero);
|
||||
int const baseScale = getVaultBaseScale(vault);
|
||||
// Keep this explicit even though a non-negative YieldUnrealized makes the
|
||||
// Open-zone capacity ceiling reject every coarsening transition too. The
|
||||
// protocol defines both conditions independently.
|
||||
if (detail::getPosteriorVaultScale(vault, rounded) != baseScale)
|
||||
return tecLIMIT_EXCEEDED;
|
||||
|
||||
if (vaultOpenZoneCapacity(vault, rounded) > getVaultOpenLimit(vault))
|
||||
return tecLIMIT_EXCEEDED;
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
[[nodiscard]] Number
|
||||
vaultOpenZoneCapacity(SLE::ConstRef vault, Number const& roundedAmount)
|
||||
{
|
||||
XRPL_ASSERT(
|
||||
vault && vault->getType() == ltVAULT, "xrpl::vaultOpenZoneCapacity : valid Vault sle");
|
||||
XRPL_ASSERT(
|
||||
getVaultVersion(vault) == VaultVersion::FixedPrecision,
|
||||
"xrpl::vaultOpenZoneCapacity : FixedPrecision Vault");
|
||||
NumberRoundModeGuard const rg(Number::RoundingMode::TowardsZero);
|
||||
return getAssetsTotal(vault) + vault->at(sfYieldUnrealized) + roundedAmount;
|
||||
}
|
||||
|
||||
[[nodiscard]] TER
|
||||
checkDebitNotDust(
|
||||
SLE::ConstRef vault,
|
||||
Number const& amount,
|
||||
beast::Journal j,
|
||||
std::string_view name)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (debitIsNonZeroDust(vault, amount))
|
||||
{
|
||||
JLOG(j.debug()) << name << ": amount too small to change stored vault balance";
|
||||
return tecPRECISION_LOSS;
|
||||
}
|
||||
}
|
||||
// LCOV_EXCL_START
|
||||
catch (std::overflow_error const&)
|
||||
{
|
||||
// It's easy to hit this exception from Number with large enough Scale
|
||||
// so we avoid spamming the log and only use debug here.
|
||||
JLOG(j.debug()) //
|
||||
<< name << ": overflow error with"
|
||||
<< " scale=" << (int)vault->at(sfScale) //
|
||||
<< ", assetsTotal=" << vault->at(sfAssetsTotal) << ", amount=" << amount;
|
||||
return tecPATH_DRY;
|
||||
}
|
||||
// LCOV_EXCL_STOP
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
[[nodiscard]] std::optional<STAmount>
|
||||
assetsToSharesDeposit(SLE::ConstRef vault, SLE::ConstRef issuance, STAmount const& assets)
|
||||
{
|
||||
@@ -505,7 +34,7 @@ assetsToSharesDeposit(SLE::ConstRef vault, SLE::ConstRef issuance, STAmount cons
|
||||
if (assets.negative() || assets.asset() != vault->at(sfAsset))
|
||||
return std::nullopt; // LCOV_EXCL_LINE
|
||||
|
||||
Number const assetTotal = getAssetsTotal(vault);
|
||||
Number const assetTotal = vault->at(sfAssetsTotal);
|
||||
STAmount shares{vault->at(sfShareMPTID)};
|
||||
if (assetTotal == 0)
|
||||
{
|
||||
@@ -529,7 +58,7 @@ sharesToAssetsDeposit(SLE::ConstRef vault, SLE::ConstRef issuance, STAmount cons
|
||||
if (shares.negative() || shares.asset() != vault->at(sfShareMPTID))
|
||||
return std::nullopt; // LCOV_EXCL_LINE
|
||||
|
||||
Number const assetTotal = getAssetsTotal(vault);
|
||||
Number const assetTotal = vault->at(sfAssetsTotal);
|
||||
STAmount assets{vault->at(sfAsset)};
|
||||
if (assetTotal == 0)
|
||||
{
|
||||
@@ -556,52 +85,37 @@ clampToAssetsTotalScale(SLE::ConstRef vault, STAmount const& delta)
|
||||
{
|
||||
return magnitude;
|
||||
}
|
||||
Number const assetsTotal = vault->at(sfAssetsTotal);
|
||||
|
||||
// Calculate the scale after applying the delta using ToNearest rounding.
|
||||
// This aligns the delta with scale checks used by vault invariants.
|
||||
int const postScale = [&] {
|
||||
NumberRoundModeGuard const rg(Number::RoundingMode::ToNearest);
|
||||
return scale(assetsTotal + delta, asset);
|
||||
}();
|
||||
|
||||
STAmount actualDelta;
|
||||
if (getVaultVersion(vault) == VaultVersion::FixedPrecision)
|
||||
if (delta.negative())
|
||||
{
|
||||
// FixedPrecision only reaches this branch for credits (VaultDeposit);
|
||||
// outflows go through clampVaultOutflow -> clampToAvailableScale,
|
||||
// which rounds at AssetsAvailable's own posterior scale instead.
|
||||
XRPL_ASSERT(
|
||||
!delta.negative(), "xrpl::clampToAssetsTotalScale : FixedPrecision credit only");
|
||||
STAmount const rounded =
|
||||
roundToPosteriorVaultScale(vault, delta, Number::RoundingMode::TowardsZero);
|
||||
actualDelta = rounded.negative() ? -rounded : rounded;
|
||||
// For withdrawals (debits), floor the magnitude to the target scale
|
||||
// to ensure exact grid alignment without paying out extra assets.
|
||||
actualDelta = roundToScale(magnitude, postScale, Number::RoundingMode::Downward);
|
||||
}
|
||||
else
|
||||
{
|
||||
Number const assetsTotal = getAssetsTotal(vault);
|
||||
|
||||
// Calculate the scale after applying the delta using ToNearest rounding.
|
||||
// This aligns the delta with scale checks used by vault invariants.
|
||||
int const postScale = [&] {
|
||||
NumberRoundModeGuard const rg(Number::RoundingMode::ToNearest);
|
||||
return scale(assetsTotal + delta, asset);
|
||||
// For deposits (credits), derive actualDelta from the floored posterior total.
|
||||
// This prevents grid alignment issues from crediting the vault more than deposited.
|
||||
//
|
||||
// Sum using Downward rounding so intermediate precision doesn't round up
|
||||
// and exceed the original requested amount.
|
||||
Number const posterior = [&] {
|
||||
NumberRoundModeGuard const rg(Number::RoundingMode::Downward);
|
||||
return assetsTotal + magnitude;
|
||||
}();
|
||||
|
||||
if (delta.negative())
|
||||
{
|
||||
// For withdrawals (debits), floor the magnitude to the target scale
|
||||
// to ensure exact grid alignment without paying out extra assets.
|
||||
actualDelta = roundToScale(magnitude, postScale, Number::RoundingMode::Downward);
|
||||
}
|
||||
else
|
||||
{
|
||||
// For deposits (credits), derive actualDelta from the floored posterior total.
|
||||
// This prevents grid alignment issues from crediting the vault more than deposited.
|
||||
//
|
||||
// Sum using Downward rounding so intermediate precision doesn't round up
|
||||
// and exceed the original requested amount.
|
||||
Number const posterior = [&] {
|
||||
NumberRoundModeGuard const rg(Number::RoundingMode::Downward);
|
||||
return assetsTotal + magnitude;
|
||||
}();
|
||||
|
||||
Number const roundedPosterior =
|
||||
roundToAsset(asset, posterior, postScale, Number::RoundingMode::Downward);
|
||||
actualDelta = STAmount{asset, roundedPosterior - assetsTotal};
|
||||
}
|
||||
Number const roundedPosterior =
|
||||
roundToAsset(asset, posterior, postScale, Number::RoundingMode::Downward);
|
||||
actualDelta = STAmount{asset, roundedPosterior - assetsTotal};
|
||||
}
|
||||
|
||||
XRPL_ASSERT(
|
||||
@@ -616,49 +130,23 @@ clampToAssetsTotalScale(SLE::ConstRef vault, STAmount const& delta)
|
||||
return actualDelta;
|
||||
}
|
||||
|
||||
[[nodiscard]] std::expected<STAmount, TER>
|
||||
clampVaultOutflow(SLE::ConstRef vault, STAmount const& delta)
|
||||
{
|
||||
XRPL_ASSERT(delta.negative(), "xrpl::clampVaultOutflow : outflow delta is negative");
|
||||
return getVaultVersion(vault) == VaultVersion::FixedPrecision
|
||||
? clampToAvailableScale(vault, delta)
|
||||
: clampToAssetsTotalScale(vault, delta);
|
||||
}
|
||||
|
||||
[[nodiscard]] int
|
||||
vaultBaseScale(Asset const& asset, std::uint8_t scale)
|
||||
{
|
||||
if (asset.integral())
|
||||
return 0;
|
||||
return -static_cast<int>(scale);
|
||||
}
|
||||
|
||||
[[nodiscard]] bool
|
||||
isOnVaultBaseGrid(Asset const& asset, Number const& value, int baseScale)
|
||||
{
|
||||
// isRepresentable first: building an STAmount from an out-of-range value
|
||||
// throws, and this checks user-supplied values in preclaim.
|
||||
return isRepresentable(asset, value) && STAmount{asset, value} == value &&
|
||||
roundToAsset(asset, value, baseScale, Number::RoundingMode::TowardsZero) == value;
|
||||
}
|
||||
|
||||
[[nodiscard]] TER
|
||||
checkAssetsMaximum(SLE::ConstRef vault, Number const& amount)
|
||||
{
|
||||
return isOnVaultBaseGrid(vault->at(sfAsset), amount, getVaultBaseScale(vault))
|
||||
? TER{tesSUCCESS}
|
||||
: TER{tecPRECISION_LOSS};
|
||||
}
|
||||
|
||||
[[nodiscard]] Number
|
||||
assetsTotalForWithdrawal(SLE::ConstRef vault, WaiveUnrealizedLoss waive)
|
||||
{
|
||||
Number assetTotal = getAssetsTotal(vault);
|
||||
Number assetTotal = vault->at(sfAssetsTotal);
|
||||
if (waive == WaiveUnrealizedLoss::No)
|
||||
assetTotal -= vault->at(sfLossUnrealized);
|
||||
return assetTotal;
|
||||
}
|
||||
|
||||
[[nodiscard]] bool
|
||||
debitIsNonZeroDust(Asset const& asset, Number const& total, Number const& amount)
|
||||
{
|
||||
if (amount == 0)
|
||||
return false;
|
||||
return STAmount{asset, total - amount} == STAmount{asset, total};
|
||||
}
|
||||
|
||||
[[nodiscard]] std::optional<STAmount>
|
||||
assetsToSharesWithdraw(
|
||||
SLE::ConstRef vault,
|
||||
@@ -729,42 +217,36 @@ isSoleShareholder(ReadView const& view, AccountID const& account, SLE::ConstRef
|
||||
return sleToken->getFieldU64(sfMPTAmount) == outstanding;
|
||||
}
|
||||
|
||||
[[nodiscard]] VaultVersion
|
||||
decodeVaultVersion(std::optional<std::uint8_t> leVersion)
|
||||
{
|
||||
if (!leVersion)
|
||||
return VaultVersion::Legacy;
|
||||
if (*leVersion > std::to_underlying(VaultVersion::FixedPrecision))
|
||||
{
|
||||
// LCOV_EXCL_START
|
||||
UNREACHABLE("xrpl::decodeVaultVersion : invalid vault version");
|
||||
return VaultVersion::Legacy;
|
||||
// LCOV_EXCL_STOP
|
||||
}
|
||||
return static_cast<VaultVersion>(*leVersion);
|
||||
}
|
||||
|
||||
[[nodiscard]] VaultVersion
|
||||
getVaultVersion(SLE::ConstRef vault)
|
||||
{
|
||||
XRPL_ASSERT(vault && vault->getType() == ltVAULT, "xrpl::getVaultVersion : valid Vault sle");
|
||||
return decodeVaultVersion(vault->at(~sfLEVersion));
|
||||
if (!vault->isFieldPresent(sfLEVersion))
|
||||
return VaultVersion::Legacy;
|
||||
|
||||
auto const version = vault->at(sfLEVersion);
|
||||
if (version > std::to_underlying(VaultVersion::CashBasis))
|
||||
{
|
||||
// LCOV_EXCL_START
|
||||
UNREACHABLE("xrpl::getVaultVersion : invalid vault version");
|
||||
return VaultVersion::Legacy;
|
||||
// LCOV_EXCL_STOP
|
||||
}
|
||||
return static_cast<VaultVersion>(version);
|
||||
}
|
||||
|
||||
[[nodiscard]] VaultVersion
|
||||
getVaultDefaultVersion(Rules const& rules)
|
||||
namespace {
|
||||
|
||||
[[nodiscard]] VaultKind
|
||||
decodeVaultKind(std::optional<std::uint8_t> vaultKind)
|
||||
{
|
||||
// FixedPrecision requires featureLendingProtocolV1_1 (closed-ended Vaults and
|
||||
// cash-basis accounting) and fixCleanup3_4_0, so a FixedPrecision Vault always
|
||||
// sees both enabled. fixCleanup3_2_0 is already enabled on the network.
|
||||
if (rules.enabled(featureLendingProtocolV1_1) && rules.enabled(featureLendingProtocolV1_2) &&
|
||||
rules.enabled(fixCleanup3_4_0))
|
||||
return VaultVersion::FixedPrecision;
|
||||
if (rules.enabled(featureLendingProtocolV1_1))
|
||||
return VaultVersion::CashBasis;
|
||||
return VaultVersion::Legacy;
|
||||
if (vaultKind && *vaultKind == std::to_underlying(VaultKind::ClosedEnded))
|
||||
return VaultKind::ClosedEnded;
|
||||
return VaultKind::OpenEnded;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
[[nodiscard]] VaultKind
|
||||
getVaultKind(SLE::ConstRef vault)
|
||||
{
|
||||
|
||||
@@ -435,25 +435,18 @@ areMirrorsCurrent(SLE const& issuance, SLE const& mptoken)
|
||||
}
|
||||
|
||||
void
|
||||
setIssuerMirrorCurrent(SLE const& issuance, SLE& mptoken)
|
||||
setIssuerMirrorEpoch(SLE const& issuance, SLE& mptoken)
|
||||
{
|
||||
XRPL_ASSERT(
|
||||
issuance.getType() == ltMPTOKEN_ISSUANCE,
|
||||
"xrpl::setIssuerMirrorCurrent : issuance MPTokenIssuance object");
|
||||
"xrpl::setIssuerMirrorEpoch : issuance MPTokenIssuance object");
|
||||
XRPL_ASSERT(
|
||||
mptoken.getType() == ltMPTOKEN, "xrpl::setIssuerMirrorCurrent : mptoken MPToken object");
|
||||
mptoken.getType() == ltMPTOKEN, "xrpl::setIssuerMirrorEpoch : mptoken MPToken object");
|
||||
|
||||
// Unlike the auditor mirror, the issuer mirror is not optional: every
|
||||
// confidential MPToken carries one, so there is no existence check here.
|
||||
//
|
||||
// Record the key and epoch under which the mirror is now encrypted. Both fields remain absent
|
||||
// at epoch 0: such mirrors use the original issuer key. The original key is stored in
|
||||
// sfInitialIssuerEncryptionKey after the first rotation.
|
||||
if (auto const epoch = issuance[~sfIssuerKeyEpoch].value_or(0); epoch != 0)
|
||||
{
|
||||
mptoken[sfIssuerKeyMirrorEpoch] = epoch;
|
||||
mptoken[sfIssuerMirrorEncryptionKey] = issuance[sfIssuerEncryptionKey];
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
@@ -473,9 +466,9 @@ setAuditorMirrorEpoch(SLE const& issuance, SLE& mptoken)
|
||||
}
|
||||
|
||||
void
|
||||
setMirrorsCurrent(SLE const& issuance, SLE& mptoken)
|
||||
setMirrorEpochs(SLE const& issuance, SLE& mptoken)
|
||||
{
|
||||
setIssuerMirrorCurrent(issuance, mptoken);
|
||||
setIssuerMirrorEpoch(issuance, mptoken);
|
||||
setAuditorMirrorEpoch(issuance, mptoken);
|
||||
}
|
||||
|
||||
|
||||
@@ -5,8 +5,10 @@
|
||||
#include <xrpl/protocol/jss.h>
|
||||
|
||||
#include <array>
|
||||
#include <cstddef>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
namespace xrpl {
|
||||
namespace rpc {
|
||||
@@ -20,88 +22,122 @@ namespace detail {
|
||||
// will remain in the object file. But the string literals will remain.
|
||||
//
|
||||
// There's a certain amount of tension in determining the correct HTTP
|
||||
// status to associate with a given RPC error. Initially all RPC errors
|
||||
// returned 200 (OK). And that's the default behavior if no HTTP status code
|
||||
// is specified below.
|
||||
// status to associate with a given RPC error. Every row below names its
|
||||
// HTTP status, and a row that omits it does not compile, since ErrorInfo has
|
||||
// no constructor that leaves the status out.
|
||||
//
|
||||
// The codes currently selected target the load balancer fail-over use case.
|
||||
// If a query fails on one node but is likely to have a positive outcome
|
||||
// on a different node, then the failure should return a 4xx/5xx range
|
||||
// status code.
|
||||
|
||||
// The rows below are aligned by hand into four columns, so that a wrong status or a mistyped token
|
||||
// is visible by scanning one column. That cannot be had inside the 100-column limit once the
|
||||
// longest enumerator and the longest message sit in one row. So this region is exempt from the
|
||||
// limit by design rather than by oversight.
|
||||
// clang-format off
|
||||
static constexpr ErrorInfo kUnorderedErrorInfos[]{
|
||||
{RpcActMalformed, "actMalformed", "Account malformed."},
|
||||
{RpcActNotFound, "actNotFound", "Account not found."},
|
||||
{RpcAlreadyMultisig, "alreadyMultisig", "Already multisigned."},
|
||||
{RpcAlreadySingleSig, "alreadySingleSig", "Already single-signed."},
|
||||
{RpcAmendmentBlocked, "amendmentBlocked", "Amendment blocked, need upgrade.", 503},
|
||||
{RpcExpiredValidatorList, "unlBlocked", "Validator list expired.", 503},
|
||||
{RpcAtxDeprecated, "deprecated", "Use the new API or specify a ledger range.", 400},
|
||||
{RpcBadKeyType, "badKeyType", "Bad key type.", 400},
|
||||
{RpcBadFeature, "badFeature", "Feature unknown or invalid.", 500},
|
||||
{RpcBadIssuer, "badIssuer", "Issuer account malformed.", 400},
|
||||
{RpcBadMarket, "badMarket", "No such market.", 404},
|
||||
{RpcBadSecret, "badSecret", "Secret does not match account.", 403},
|
||||
{RpcBadSeed, "badSeed", "Disallowed seed.", 403},
|
||||
{RpcBadSyntax, "badSyntax", "Syntax error.", 400},
|
||||
{RpcChannelMalformed, "channelMalformed", "Payment channel is malformed.", 400},
|
||||
{RpcChannelAmtMalformed, "channelAmtMalformed", "Payment channel amount is malformed.", 400},
|
||||
{RpcCommandMissing, "commandMissing", "Missing command entry.", 400},
|
||||
{RpcDbDeserialization, "dbDeserialization", "Database deserialization error.", 502},
|
||||
{RpcDstActMalformed, "dstActMalformed", "Destination account is malformed.", 400},
|
||||
{RpcDstActMissing, "dstActMissing", "Destination account not provided.", 400},
|
||||
{RpcDstActNotFound, "dstActNotFound", "Destination account not found.", 404},
|
||||
{RpcDstAmtMalformed, "dstAmtMalformed", "Destination amount/currency/issuer is malformed.", 400},
|
||||
{RpcDstAmtMissing, "dstAmtMissing", "Destination amount/currency/issuer is missing.", 400},
|
||||
{RpcDstIsrMalformed, "dstIsrMalformed", "Destination issuer is malformed.", 400},
|
||||
{RpcExcessiveLgrRange, "excessiveLgrRange", "Ledger range exceeds 1000.", 400},
|
||||
{RpcForbidden, "forbidden", "Bad credentials.", 403},
|
||||
{RpcHighFee, "highFee", "Current transaction fee exceeds your limit.", 402},
|
||||
{RpcInternal, "internal", "Internal error.", 500},
|
||||
{RpcInvalidLgrRange, "invalidLgrRange", "Ledger range is invalid.", 400},
|
||||
{RpcInvalidParams, "invalidParams", "Invalid parameters.", 400},
|
||||
{RpcInvalidHotwallet, "invalidHotWallet", "Invalid hotwallet.", 400},
|
||||
{RpcIssueMalformed, "issueMalformed", "Issue is malformed.", 400},
|
||||
{RpcJsonRpc, "json_rpc", "JSON-RPC transport error.", 500},
|
||||
{RpcLgrIdxsInvalid, "lgrIdxsInvalid", "Ledger indexes invalid.", 400},
|
||||
{RpcLgrIdxMalformed, "lgrIdxMalformed", "Ledger index malformed.", 400},
|
||||
{RpcLgrNotFound, "lgrNotFound", "Ledger not found.", 404},
|
||||
{RpcLgrNotValidated, "lgrNotValidated", "Ledger not validated.", 202},
|
||||
{RpcMasterDisabled, "masterDisabled", "Master key is disabled.", 403},
|
||||
{RpcNotEnabled, "notEnabled", "Not enabled in configuration.", 501},
|
||||
{RpcNotImpl, "notImpl", "Not implemented.", 501},
|
||||
{RpcNotReady, "notReady", "Not ready to handle this request.", 503},
|
||||
{RpcNotSupported, "notSupported", "Operation not supported.", 501},
|
||||
{RpcNoClosed, "noClosed", "Closed ledger is unavailable.", 503},
|
||||
{RpcNoCurrent, "noCurrent", "Current ledger is unavailable.", 503},
|
||||
{RpcNotSynced, "notSynced", "Not synced to the network.", 503},
|
||||
{RpcNoEvents, "noEvents", "Current transport does not support events.", 405},
|
||||
{RpcNoNetwork, "noNetwork", "Not synced to the network.", 503},
|
||||
{RpcWrongNetwork, "wrongNetwork", "Wrong network.", 503},
|
||||
{RpcNoPermission, "noPermission", "You don't have permission for this command.", 401},
|
||||
{RpcNoPfRequest, "noPathRequest", "No pathfinding request in progress.", 404},
|
||||
{RpcObjectNotFound, "objectNotFound", "The requested object was not found.", 404},
|
||||
{RpcPublicMalformed, "publicMalformed", "Public key is malformed.", 400},
|
||||
{RpcSendmaxMalformed, "sendMaxMalformed", "SendMax amount malformed.", 400},
|
||||
{RpcSigningMalformed, "signingMalformed", "Signing of transaction is malformed.", 400},
|
||||
{RpcSlowDown, "slowDown", "You are placing too much load on the server.", 429},
|
||||
{RpcSrcActMalformed, "srcActMalformed", "Source account is malformed.", 400},
|
||||
{RpcSrcActMissing, "srcActMissing", "Source account not provided.", 400},
|
||||
{RpcSrcActNotFound, "srcActNotFound", "Source account not found.", 404},
|
||||
{RpcDelegateActNotFound, "delegateActNotFound", "Delegate account not found.", 404},
|
||||
{RpcSrcCurMalformed, "srcCurMalformed", "Source currency is malformed.", 400},
|
||||
{RpcSrcIsrMalformed, "srcIsrMalformed", "Source issuer is malformed.", 400},
|
||||
{RpcStreamMalformed, "malformedStream", "Stream malformed.", 400},
|
||||
{RpcTooBusy, "tooBusy", "The server is too busy to help you now.", 503},
|
||||
{RpcTxnNotFound, "txnNotFound", "Transaction not found.", 404},
|
||||
{RpcUnknownCommand, "unknownCmd", "Unknown method.", 405},
|
||||
{RpcOracleMalformed, "oracleMalformed", "Oracle request is malformed.", 400},
|
||||
{RpcBadCredentials, "badCredentials", "Credentials do not exist, are not accepted, or have expired.", 400},
|
||||
{RpcTxSigned, "transactionSigned", "Transaction should not be signed.", 400},
|
||||
{RpcDomainMalformed, "domainMalformed", "Domain is malformed.", 400},
|
||||
{RpcEntryNotFound, "entryNotFound", "Entry not found.", 400},
|
||||
{RpcUnexpectedLedgerType, "unexpectedLedgerType", "Unexpected ledger type.", 400},
|
||||
{RpcActMalformed, "actMalformed", "Account malformed.", 400},
|
||||
{RpcActNotFound, "actNotFound", "Account not found.", 404},
|
||||
{RpcAlreadyMultisig, "alreadyMultisig", "Already multisigned.", 400},
|
||||
{RpcAlreadySingleSig, "alreadySingleSig", "Already single-signed.", 400},
|
||||
{RpcAmendmentBlocked, "amendmentBlocked", "Amendment blocked, need upgrade.", 503},
|
||||
{RpcExpiredValidatorList, "unlBlocked", "Validator list expired.", 503},
|
||||
{RpcAtxDeprecated, "deprecated", "Use the new API or specify a ledger range.", 400},
|
||||
{RpcBadKeyType, "badKeyType", "Bad key type.", 400},
|
||||
{RpcBadFeature, "badFeature", "Feature unknown or invalid.", 500},
|
||||
{RpcBadIssuer, "badIssuer", "Issuer account malformed.", 400},
|
||||
{RpcBadMarket, "badMarket", "No such market.", 404},
|
||||
{RpcBadSecret, "badSecret", "Secret does not match account.", 403},
|
||||
{RpcBadSeed, "badSeed", "Disallowed seed.", 403},
|
||||
{RpcBadSyntax, "badSyntax", "Syntax error.", 400},
|
||||
{RpcChannelMalformed, "channelMalformed", "Payment channel is malformed.", 400},
|
||||
{RpcChannelAmtMalformed, "channelAmtMalformed", "Payment channel amount is malformed.", 400},
|
||||
{RpcCommandMissing, "commandMissing", "Missing command entry.", 400},
|
||||
{RpcDbDeserialization, "dbDeserialization", "Database deserialization error.", 502},
|
||||
{RpcDstActMalformed, "dstActMalformed", "Destination account is malformed.", 400},
|
||||
{RpcDstActMissing, "dstActMissing", "Destination account not provided.", 400},
|
||||
{RpcDstActNotFound, "dstActNotFound", "Destination account not found.", 404},
|
||||
{RpcDstAmtMalformed, "dstAmtMalformed", "Destination amount/currency/issuer is malformed.", 400},
|
||||
{RpcDstAmtMissing, "dstAmtMissing", "Destination amount/currency/issuer is missing.", 400},
|
||||
{RpcDstIsrMalformed, "dstIsrMalformed", "Destination issuer is malformed.", 400},
|
||||
{RpcExcessiveLgrRange, "excessiveLgrRange", "Ledger range exceeds 1000.", 400},
|
||||
{RpcForbidden, "forbidden", "Bad credentials.", 403},
|
||||
{RpcHighFee, "highFee", "Current transaction fee exceeds your limit.", 402},
|
||||
{RpcInternal, "internal", "Internal error.", 500},
|
||||
{RpcInvalidLgrRange, "invalidLgrRange", "Ledger range is invalid.", 400},
|
||||
{RpcInvalidParams, "invalidParams", "Invalid parameters.", 400},
|
||||
{RpcInvalidHotwallet, "invalidHotWallet", "Invalid hotwallet.", 400},
|
||||
{RpcIssueMalformed, "issueMalformed", "Issue is malformed.", 400},
|
||||
{RpcJsonRpc, "json_rpc", "JSON-RPC transport error.", 500},
|
||||
{RpcLgrIdxsInvalid, "lgrIdxsInvalid", "Ledger indexes invalid.", 400},
|
||||
{RpcLgrIdxMalformed, "lgrIdxMalformed", "Ledger index malformed.", 400},
|
||||
{RpcLgrNotFound, "lgrNotFound", "Ledger not found.", 404},
|
||||
{RpcLgrNotValidated, "lgrNotValidated", "Ledger not validated.", 202},
|
||||
{RpcMasterDisabled, "masterDisabled", "Master key is disabled.", 403},
|
||||
{RpcNotEnabled, "notEnabled", "Not enabled in configuration.", 501},
|
||||
{RpcNotImpl, "notImpl", "Not implemented.", 501},
|
||||
{RpcNotReady, "notReady", "Not ready to handle this request.", 503},
|
||||
{RpcNotSupported, "notSupported", "Operation not supported.", 501},
|
||||
{RpcNoClosed, "noClosed", "Closed ledger is unavailable.", 503},
|
||||
{RpcNoCurrent, "noCurrent", "Current ledger is unavailable.", 503},
|
||||
{RpcNotSynced, "notSynced", "Not synced to the network.", 503},
|
||||
{RpcNoEvents, "noEvents", "Current transport does not support events.", 405},
|
||||
{RpcNoNetwork, "noNetwork", "Not synced to the network.", 503},
|
||||
{RpcWrongNetwork, "wrongNetwork", "Wrong network.", 503},
|
||||
{RpcNoPermission, "noPermission", "You don't have permission for this command.", 401},
|
||||
{RpcNoPfRequest, "noPathRequest", "No pathfinding request in progress.", 404},
|
||||
{RpcObjectNotFound, "objectNotFound", "The requested object was not found.", 404},
|
||||
{RpcPublicMalformed, "publicMalformed", "Public key is malformed.", 400},
|
||||
{RpcSendmaxMalformed, "sendMaxMalformed", "SendMax amount malformed.", 400},
|
||||
{RpcSigningMalformed, "signingMalformed", "Signing of transaction is malformed.", 400},
|
||||
{RpcSlowDown, "slowDown", "You are placing too much load on the server.", 429},
|
||||
{RpcSrcActMalformed, "srcActMalformed", "Source account is malformed.", 400},
|
||||
{RpcSrcActMissing, "srcActMissing", "Source account not provided.", 400},
|
||||
{RpcSrcActNotFound, "srcActNotFound", "Source account not found.", 404},
|
||||
{RpcDelegateActNotFound, "delegateActNotFound", "Delegate account not found.", 404},
|
||||
{RpcSrcCurMalformed, "srcCurMalformed", "Source currency is malformed.", 400},
|
||||
{RpcSrcIsrMalformed, "srcIsrMalformed", "Source issuer is malformed.", 400},
|
||||
{RpcStreamMalformed, "malformedStream", "Stream malformed.", 400},
|
||||
{RpcTooBusy, "tooBusy", "The server is too busy to help you now.", 503},
|
||||
{RpcTxnNotFound, "txnNotFound", "Transaction not found.", 404},
|
||||
{RpcUnknownCommand, "unknownCmd", "Unknown method.", 405},
|
||||
{RpcOracleMalformed, "oracleMalformed", "Oracle request is malformed.", 400},
|
||||
{RpcBadCredentials, "badCredentials", "Credentials do not exist, are not accepted, or have expired.", 400},
|
||||
{RpcTxSigned, "transactionSigned", "Transaction should not be signed.", 400},
|
||||
{RpcDomainMalformed, "domainMalformed", "Domain is malformed.", 400},
|
||||
{RpcEntryNotFound, "entryNotFound", "Entry not found.", 400},
|
||||
{RpcUnexpectedLedgerType, "unexpectedLedgerType", "Unexpected ledger type.", 400},
|
||||
{RpcInvalidTransaction, "invalidTransaction", "Transaction is invalid.", 400},
|
||||
{RpcInternalSubmit, "internalSubmit", "Internal error during submit.", 500},
|
||||
{RpcInternalJson, "internalJson", "Internal error during JSON handling.", 500},
|
||||
{RpcInternalSimulate, "internalSimulate", "Internal error during simulate.", 500},
|
||||
{RpcFieldNotFoundTransaction, "fieldNotFoundTransaction", "Missing required field.", 400},
|
||||
{RpcNotYetImplemented, "notYetImplemented", "Not yet implemented.", 501},
|
||||
{RpcTransactionNotFound, "transactionNotFound", "Transaction not found.", 404},
|
||||
{RpcMalformedRequest, "malformedRequest", "Request is malformed.", 400},
|
||||
{RpcNotStandAlone, "notStandAlone", "Server is not running stand-alone.", 501},
|
||||
{RpcUnknownOption, "unknownOption", "Unknown option.", 400},
|
||||
{RpcMalformedAccount, "malformedAccount", "Account is malformed.", 400},
|
||||
{RpcMalformedAddress, "malformedAddress", "Address is malformed.", 400},
|
||||
{RpcMalformedAuthorized, "malformedAuthorized", "Authorized is malformed.", 400},
|
||||
{RpcMalformedAuthorizedCredentials, "malformedAuthorizedCredentials", "AuthorizedCredentials is malformed.", 400},
|
||||
{RpcMalformedBridgeAccount, "malformedBridgeAccount", "BridgeAccount is malformed.", 400},
|
||||
{RpcMalformedBroker, "malformedBroker", "Broker is malformed.", 400},
|
||||
{RpcMalformedCurrency, "malformedCurrency", "Currency is malformed.", 400},
|
||||
{RpcMalformedDirRoot, "malformedDirRoot", "DirRoot is malformed.", 400},
|
||||
{RpcMalformedDocumentID, "malformedDocumentID", "DocumentID is malformed.", 400},
|
||||
{RpcMalformedIssue, "malformedIssue", "Issue is malformed.", 400},
|
||||
{RpcMalformedIssuingChainDoor, "malformedIssuingChainDoor", "IssuingChainDoor is malformed.", 400},
|
||||
{RpcMalformedLockingChainDoor, "malformedLockingChainDoor", "LockingChainDoor is malformed.", 400},
|
||||
{RpcMalformedMPTIssuanceID, "malformedMPTIssuanceID", "MPTIssuanceID is malformed.", 400},
|
||||
{RpcMalformedMPTokenIssuance, "malformedMPTokenIssuance", "MPTokenIssuance is malformed.", 400},
|
||||
{RpcMalformedOwner, "malformedOwner", "Owner is malformed.", 400},
|
||||
{RpcMalformedSeq, "malformedSeq", "Seq is malformed.", 400},
|
||||
{RpcMalformedSponsee, "malformedSponsee", "Sponsee is malformed.", 400},
|
||||
{RpcMalformedSponsor, "malformedSponsor", "Sponsor is malformed.", 400},
|
||||
{RpcMalformedXChainOwnedClaimID, "malformedXChainOwnedClaimID", "XChainOwnedClaimID is malformed.", 400},
|
||||
{RpcMalformedXChainOwnedCreateAccountClaimID, "malformedXChainOwnedCreateAccountClaimID", "XChainOwnedCreateAccountClaimID is malformed.", 400},
|
||||
};
|
||||
// clang-format on
|
||||
|
||||
@@ -117,7 +153,7 @@ sortErrorInfos(ErrorInfo const (&unordered)[N]) -> std::array<ErrorInfo, M>
|
||||
if (info.code <= RpcSuccess || info.code > RpcLast)
|
||||
throw(std::out_of_range("Invalid error_code_i"));
|
||||
|
||||
// The first valid code follows rpcSUCCESS immediately.
|
||||
// The first valid code follows RpcSuccess immediately.
|
||||
static_assert(RpcSuccess == 0, "Unexpected error_code_i layout.");
|
||||
int const index{info.code - 1};
|
||||
|
||||
@@ -128,10 +164,10 @@ sortErrorInfos(ErrorInfo const (&unordered)[N]) -> std::array<ErrorInfo, M>
|
||||
}
|
||||
|
||||
// Verify that all entries are filled in starting with 1 and proceeding
|
||||
// to rpcLAST.
|
||||
// to RpcLast.
|
||||
//
|
||||
// It's okay for there to be missing entries; they will contain the code
|
||||
// rpcUNKNOWN. But other than that all entries should match their index.
|
||||
// RpcUnknown. But other than that all entries should match their index.
|
||||
int codeCount{0};
|
||||
int expect{RpcBadSyntax - 1};
|
||||
for (ErrorInfo const& info : ret)
|
||||
@@ -156,6 +192,68 @@ constexpr auto kSortedErrorInfos{sortErrorInfos<RpcLast>(kUnorderedErrorInfos)};
|
||||
|
||||
constexpr ErrorInfo kUnknownError;
|
||||
|
||||
/**
|
||||
* Asserts that no two rows of @p sorted name the same token.
|
||||
*
|
||||
* A duplicate is then a build error rather than a reply reporting the wrong
|
||||
* `error_code`. `consteval` admits no run-time call, so the two `throw`
|
||||
* statements are how a constant evaluation fails and are otherwise unreachable.
|
||||
*
|
||||
* @param sorted The table, ordered by code, with a gap for every unused code.
|
||||
* @return True. It reports failure by failing to be a constant expression.
|
||||
*/
|
||||
template <std::size_t N>
|
||||
consteval bool
|
||||
tokensAreUnique(std::array<ErrorInfo, N> const& sorted)
|
||||
{
|
||||
for (std::size_t i{0}; i < N; ++i)
|
||||
{
|
||||
// Gaps in the table name no error, so they name no token either.
|
||||
if (sorted[i].code == RpcUnknown)
|
||||
continue;
|
||||
|
||||
std::string_view const token{sorted[i].token.cStr()};
|
||||
if (token.empty())
|
||||
throw(std::invalid_argument("Empty token in list"));
|
||||
|
||||
for (std::size_t j{i + 1}; j < N; ++j)
|
||||
{
|
||||
if (sorted[j].code != RpcUnknown && std::string_view{sorted[j].token.cStr()} == token)
|
||||
throw(std::invalid_argument("Duplicate token in list"));
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static_assert(tokensAreUnique(kSortedErrorInfos), "Duplicate token in list");
|
||||
|
||||
/**
|
||||
* The tokens of @p sorted, measured at compile time.
|
||||
*
|
||||
* A lookup compares a caller's token against these rather than against the
|
||||
* table's own `char const*`, which it would have to measure first: comparing a
|
||||
* view with a pointer calls strlen on every row the scan passes. Built from the
|
||||
* same table, so the two cannot name different tokens.
|
||||
*
|
||||
* @param sorted The table, ordered by code, with a gap for every unused code.
|
||||
* @return One view per row, the empty view for a gap. No token a caller can
|
||||
* pass is equal to the empty view.
|
||||
*/
|
||||
template <std::size_t N>
|
||||
consteval std::array<std::string_view, N>
|
||||
tokenViews(std::array<ErrorInfo, N> const& sorted)
|
||||
{
|
||||
std::array<std::string_view, N> ret{};
|
||||
for (std::size_t i{0}; i < N; ++i)
|
||||
{
|
||||
if (sorted[i].code != RpcUnknown)
|
||||
ret[i] = std::string_view{sorted[i].token.cStr()};
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
constexpr auto kSortedTokens{tokenViews(kSortedErrorInfos)};
|
||||
|
||||
} // namespace detail
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
@@ -186,6 +284,21 @@ getErrorInfo(ErrorCodeI code)
|
||||
return detail::kSortedErrorInfos[code - 1];
|
||||
}
|
||||
|
||||
ErrorCodeI
|
||||
codeForToken(std::string_view token)
|
||||
{
|
||||
if (token.empty())
|
||||
return RpcUnknown;
|
||||
|
||||
for (std::size_t i{0}; i < detail::kSortedTokens.size(); ++i)
|
||||
{
|
||||
// A gap holds the empty view, which the token is not, so a gap names no code here either.
|
||||
if (detail::kSortedTokens[i] == token)
|
||||
return detail::kSortedErrorInfos[i].code;
|
||||
}
|
||||
return RpcUnknown;
|
||||
}
|
||||
|
||||
json::Value
|
||||
makeError(ErrorCodeI code)
|
||||
{
|
||||
|
||||
@@ -1,24 +1,10 @@
|
||||
#include <xrpl/protocol/MPTAmount.h>
|
||||
|
||||
#include <xrpl/basics/MathUtilities.h>
|
||||
#include <xrpl/basics/contract.h>
|
||||
#include <xrpl/protocol/Feature.h>
|
||||
#include <xrpl/protocol/Rules.h>
|
||||
|
||||
#include <stdexcept>
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
MPTAmount&
|
||||
MPTAmount::operator+=(MPTAmount const& other)
|
||||
{
|
||||
if (auto const result = checkedAdd(value_, other.value()))
|
||||
{
|
||||
value_ = *result;
|
||||
return *this;
|
||||
}
|
||||
if (isFeatureEnabled(fixCleanup3_5_0, /*resultIfNoRules*/ true))
|
||||
Throw<std::overflow_error>("MPTAmount::operator+= overflow");
|
||||
value_ += other.value();
|
||||
return *this;
|
||||
}
|
||||
@@ -26,13 +12,6 @@ MPTAmount::operator+=(MPTAmount const& other)
|
||||
MPTAmount&
|
||||
MPTAmount::operator-=(MPTAmount const& other)
|
||||
{
|
||||
if (auto const result = checkedSub(value_, other.value()))
|
||||
{
|
||||
value_ = *result;
|
||||
return *this;
|
||||
}
|
||||
if (isFeatureEnabled(fixCleanup3_5_0, /*resultIfNoRules*/ true))
|
||||
Throw<std::overflow_error>("MPTAmount::operator-= overflow");
|
||||
value_ -= other.value();
|
||||
return *this;
|
||||
}
|
||||
|
||||
@@ -3,14 +3,10 @@
|
||||
#include <xrpl/beast/utility/Zero.h> // IWYU pragma: keep
|
||||
#include <xrpl/beast/utility/instrumentation.h>
|
||||
#include <xrpl/protocol/Asset.h>
|
||||
#include <xrpl/protocol/Feature.h>
|
||||
#include <xrpl/protocol/Rules.h>
|
||||
#include <xrpl/protocol/STAmount.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <limits> // IWYU pragma: keep
|
||||
#include <optional>
|
||||
#include <stdexcept>
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
@@ -85,46 +81,13 @@ Quality::ceilInStrict(Amounts const& amount, STAmount const& limit, bool roundUp
|
||||
return ceilInImpl<divRoundStrict>(amount, limit, roundUp, *this);
|
||||
}
|
||||
|
||||
// Returns std::nullopt if a native result can't be represented, instead of
|
||||
// throwing. Pathfinding runs without transaction
|
||||
// rules, so default to the fixed behavior there to match transactions once the
|
||||
// amendment is enabled.
|
||||
template <STAmount (*MulRoundFunc)(STAmount const&, STAmount const&, Asset const&, bool)>
|
||||
static std::optional<STAmount>
|
||||
tryMulRound(STAmount const& v1, STAmount const& v2, Asset const& asset, bool roundUp)
|
||||
{
|
||||
try
|
||||
{
|
||||
return MulRoundFunc(v1, v2, asset, roundUp);
|
||||
}
|
||||
catch (std::overflow_error const&)
|
||||
{
|
||||
// std::overflow_error is a std::runtime_error, so it must be caught
|
||||
// first to keep the handler below from swallowing it. It means the
|
||||
// computation itself overflowed, not that the result is out of range.
|
||||
// Callers handle it, e.g. BookStep removes the offer.
|
||||
throw;
|
||||
}
|
||||
catch (std::runtime_error const&)
|
||||
{
|
||||
if (!asset.native() || !isFeatureEnabled(fixCleanup3_5_0, /*resultIfNoRules*/ true))
|
||||
throw;
|
||||
return std::nullopt;
|
||||
}
|
||||
}
|
||||
|
||||
template <STAmount (*MulRoundFunc)(STAmount const&, STAmount const&, Asset const&, bool)>
|
||||
static Amounts
|
||||
ceilOutImpl(Amounts const& amount, STAmount const& limit, bool roundUp, Quality const& quality)
|
||||
{
|
||||
if (amount.out > limit)
|
||||
{
|
||||
// An unrepresentable XRP product is larger than amount.in, so the
|
||||
// clamp below would return amount.in anyway.
|
||||
Amounts result(
|
||||
tryMulRound<MulRoundFunc>(limit, quality.rate(), amount.in.asset(), roundUp)
|
||||
.value_or(amount.in),
|
||||
limit);
|
||||
Amounts result(MulRoundFunc(limit, quality.rate(), amount.in.asset(), roundUp), limit);
|
||||
// Clamp in
|
||||
if (result.in > amount.in)
|
||||
result.in = amount.in;
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
#include <xrpl/protocol/STAmount.h>
|
||||
|
||||
#include <xrpl/basics/Log.h>
|
||||
#include <xrpl/basics/MathUtilities.h>
|
||||
#include <xrpl/basics/Number.h>
|
||||
#include <xrpl/basics/contract.h>
|
||||
#include <xrpl/basics/safe_cast.h>
|
||||
@@ -385,11 +384,10 @@ operator+(STAmount const& v1, STAmount const& v2)
|
||||
return {v1.getFName(), v1.asset(), v2.mantissa(), v2.exponent(), v2.negative()};
|
||||
}
|
||||
|
||||
// XRPAmount and MPTAmount check for int64 overflow under fixCleanup3_5_0.
|
||||
if (v1.native())
|
||||
return {v1.getFName(), (v1.xrp() + v2.xrp()).drops()};
|
||||
return {v1.getFName(), getSNValue(v1) + getSNValue(v2)};
|
||||
if (v1.holds<MPTIssue>())
|
||||
return {v1.asset_, (v1.mpt() + v2.mpt()).value()};
|
||||
return {v1.asset_, v1.mpt().value() + v2.mpt().value()};
|
||||
|
||||
auto x = v1;
|
||||
x = v1.iou() + v2.iou();
|
||||
@@ -445,22 +443,6 @@ getRate(STAmount const& offerOut, STAmount const& offerIn)
|
||||
}
|
||||
}
|
||||
|
||||
bool
|
||||
isRepresentable(Asset const& asset, Number const& value)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Out-of-range values throw std::overflow_error from Number or
|
||||
// IOUAmount, or std::runtime_error from STAmount::canonicalize.
|
||||
[[maybe_unused]] STAmount const amount{asset, value};
|
||||
return true;
|
||||
}
|
||||
catch (std::runtime_error const&)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Safely checks if two STAmount values can be added without overflow,
|
||||
* underflow, or precision loss.
|
||||
@@ -492,7 +474,16 @@ canAdd(STAmount const& a, STAmount const& b)
|
||||
|
||||
// XRP case (overflow & underflow check)
|
||||
if (isXRP(a) && isXRP(b))
|
||||
return checkedAdd(a.xrp().drops(), b.xrp().drops()).has_value();
|
||||
{
|
||||
XRPAmount const aVal = a.xrp();
|
||||
XRPAmount const bVal = b.xrp();
|
||||
|
||||
return !(
|
||||
(bVal > XRPAmount{0} &&
|
||||
aVal > XRPAmount{std::numeric_limits<XRPAmount::value_type>::max()} - bVal) ||
|
||||
(bVal < XRPAmount{0} &&
|
||||
aVal < XRPAmount{std::numeric_limits<XRPAmount::value_type>::min()} - bVal));
|
||||
}
|
||||
|
||||
// IOU case (precision check)
|
||||
auto const ret = std::visit(
|
||||
@@ -509,7 +500,15 @@ canAdd(STAmount const& a, STAmount const& b)
|
||||
|
||||
// MPT (overflow & underflow check)
|
||||
if constexpr (kIsMptissueV<TIss1> && kIsMptissueV<TIss2>)
|
||||
return checkedAdd(a.mpt().value(), b.mpt().value()).has_value();
|
||||
{
|
||||
MPTAmount const aVal = a.mpt();
|
||||
MPTAmount const bVal = b.mpt();
|
||||
return !(
|
||||
(bVal > MPTAmount{0} &&
|
||||
aVal > MPTAmount{std::numeric_limits<MPTAmount::value_type>::max()} - bVal) ||
|
||||
(bVal < MPTAmount{0} &&
|
||||
aVal < MPTAmount{std::numeric_limits<MPTAmount::value_type>::min()} - bVal));
|
||||
}
|
||||
return std::nullopt;
|
||||
},
|
||||
a.asset().value(),
|
||||
@@ -560,7 +559,11 @@ canSubtract(STAmount const& a, STAmount const& b)
|
||||
return false;
|
||||
|
||||
// Check for overflow
|
||||
return checkedSub(aVal.drops(), bVal.drops()).has_value();
|
||||
if (bVal < XRPAmount{0} &&
|
||||
aVal > XRPAmount{std::numeric_limits<XRPAmount::value_type>::max()} + bVal)
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// IOU case (no underflow)
|
||||
@@ -583,7 +586,10 @@ canSubtract(STAmount const& a, STAmount const& b)
|
||||
return false;
|
||||
|
||||
// Overflow check
|
||||
return checkedSub(aVal.value(), bVal.value()).has_value();
|
||||
if (bVal < MPTAmount{0} &&
|
||||
aVal > MPTAmount{std::numeric_limits<MPTAmount::value_type>::max()} + bVal)
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
return std::nullopt;
|
||||
},
|
||||
@@ -1225,19 +1231,6 @@ divide(STAmount const& num, STAmount const& den, Asset const& asset)
|
||||
if (num == beast::kZero)
|
||||
return {asset};
|
||||
|
||||
// The legacy path below assumes 16-digit mantissas. An MPT mantissa is
|
||||
// the raw 63-bit value and is never scaled down, so an MPT numerator can
|
||||
// overflow the quotient and an MPT denominator shortens it, moving the +5
|
||||
// nudge into a significant digit. Use Number arithmetic under
|
||||
// MPTokensV2 whenever an MPT is involved, as mulRound/divRound do. Pin
|
||||
// the rounding mode so the result does not depend on the caller's.
|
||||
if (isFeatureEnabled(featureMPTokensV2, /*resultIfNoRules*/ true) &&
|
||||
(asset.holds<MPTIssue>() || num.holds<MPTIssue>() || den.holds<MPTIssue>()))
|
||||
{
|
||||
NumberRoundModeGuard const rg(Number::RoundingMode::ToNearest);
|
||||
return STAmount{asset, Number{num} / Number{den}};
|
||||
}
|
||||
|
||||
std::uint64_t numVal = num.mantissa();
|
||||
std::uint64_t denVal = den.mantissa();
|
||||
int numOffset = num.exponent();
|
||||
@@ -1267,21 +1260,11 @@ divide(STAmount const& num, STAmount const& den, Asset const& asset)
|
||||
// numerator by 10^17 (the product is in the range of
|
||||
// 10^32 to 10^33) followed by a division, so the result
|
||||
// is in the range of 10^16 to 10^15.
|
||||
std::uint64_t quotient = muldiv(numVal, kTenTO17, denVal);
|
||||
int offset = numOffset - denOffset - 17;
|
||||
|
||||
// An integral (XRP) numerator is not scaled down to 16 digits, so the
|
||||
// quotient can land in [2^63, 2^64). The STAmount constructor reads
|
||||
// that as a negative IOU mantissa, and the +5 can wrap. Drop a digit
|
||||
// first; the result is canonicalized to 16 digits anyway.
|
||||
if (quotient > static_cast<std::uint64_t>(std::numeric_limits<std::int64_t>::max()) - 5 &&
|
||||
isFeatureEnabled(featureMPTokensV2, /*resultIfNoRules*/ true))
|
||||
{
|
||||
quotient /= 10;
|
||||
++offset;
|
||||
}
|
||||
|
||||
return STAmount(asset, quotient + 5, offset, num.negative() != den.negative());
|
||||
return STAmount(
|
||||
asset,
|
||||
muldiv(numVal, kTenTO17, denVal) + 5,
|
||||
numOffset - denOffset - 17,
|
||||
num.negative() != den.negative());
|
||||
}
|
||||
|
||||
STAmount
|
||||
@@ -1487,28 +1470,28 @@ roundNumberResult(
|
||||
// and for materializing the final integral amount.
|
||||
NumberRoundModeGuard const finalRound(roundMode(resultNegative, roundUp));
|
||||
auto result = STAmount{asset, number};
|
||||
// An integral result can't underflow: when roundUp is set, roundMode()
|
||||
// above selects Upward, and materializing a Number into an integral
|
||||
// STAmount honors that mode (Number::operator rep()), so any positive
|
||||
// value rounds up to at least 1.
|
||||
[[maybe_unused]] bool const nonzeroPositiveRoundUp =
|
||||
roundUp && !resultNegative && number != beast::kZero;
|
||||
ALWAYS(
|
||||
!nonzeroPositiveRoundUp || !asset.integral() || result != beast::kZero,
|
||||
"xrpl::roundNumberResult : positive rounded-up integral result is representable");
|
||||
!nonzeroPositiveRoundUp || result != beast::kZero,
|
||||
"xrpl::roundNumberResult : positive rounded-up MPT result is representable");
|
||||
|
||||
if (roundUp && !resultNegative && !result)
|
||||
{
|
||||
// Preserve existing mulRound/divRound behavior for a positive result
|
||||
// too small to represent in the target asset. An IOU result with an
|
||||
// MPT operand (e.g. 1 MPT / 1e95) below the smallest IOU is flushed to
|
||||
// zero by IOUAmount normalization regardless of the rounding mode, so
|
||||
// snap it up to the smallest positive IOU. The only zero case for an
|
||||
// integral result is a zero operand, which the mulRound/divRound
|
||||
// callers handle before reaching this function.
|
||||
// Intended to preserve existing mulRound/divRound behavior for a
|
||||
// positive result too small to represent in the target asset.
|
||||
//
|
||||
// Unreachable in practice: when roundUp is set, roundMode() above
|
||||
// selects Upward, and materializing a Number into an STAmount honors
|
||||
// that mode (Number::operator rep()), so any positive value rounds up
|
||||
// to at least the smallest representable unit. Hence, a positive result
|
||||
// is never !result here; the only zero case is a zero operand, which
|
||||
// the mulRound/divRound callers handle before reaching this function.
|
||||
// LCOV_EXCL_START
|
||||
if (asset.integral())
|
||||
return STAmount{asset, 1}; // LCOV_EXCL_LINE
|
||||
return STAmount{asset, 1};
|
||||
return STAmount{asset, STAmount::kMinValue, STAmount::kMinOffset, false};
|
||||
// LCOV_EXCL_STOP
|
||||
}
|
||||
|
||||
return result;
|
||||
@@ -1557,19 +1540,11 @@ mulRoundImpl(STAmount const& v1, STAmount const& v2, Asset const& asset, bool ro
|
||||
|
||||
bool const resultNegative = v1.negative() != v2.negative();
|
||||
|
||||
// The legacy path below assumes 16-digit mantissas. An MPT mantissa is the
|
||||
// raw 63-bit balance, so any MPT operand (not just an MPT result) can
|
||||
// overflow it: a large MPT amount times an IOU-shaped rate into an XRP or
|
||||
// IOU result is the offer-crossing remainder and partial-fill case. Use
|
||||
// Number arithmetic under MPTokensV2 whenever an MPT is involved.
|
||||
//
|
||||
// Outside a transaction (no current rules, e.g. pathfinding) default to
|
||||
// the Number path: an MPT operand can only reach here through MPT
|
||||
// offers or AMMs, which require MPTokensV2, so this keeps RPC quotes on
|
||||
// the same arithmetic as the transaction they describe.
|
||||
if (isFeatureEnabled(featureMPTokensV2, /*resultIfNoRules*/ true) &&
|
||||
(asset.holds<MPTIssue>() || v1.holds<MPTIssue>() || v2.holds<MPTIssue>()))
|
||||
if (asset.holds<MPTIssue>() && isFeatureEnabled(featureMPTokensV2, false))
|
||||
{
|
||||
// MPT DEX can combine 63-bit MPT amounts with IOU-shaped transfer
|
||||
// rates. Use Number arithmetic under MPTokensV2 so the rounded
|
||||
// operation is not limited by the legacy uint64_t scaled mantissa.
|
||||
Number result;
|
||||
{
|
||||
NumberRoundModeGuard const operationRound(roundMode(resultNegative, roundUp));
|
||||
@@ -1667,12 +1642,9 @@ divRoundImpl(STAmount const& num, STAmount const& den, Asset const& asset, bool
|
||||
|
||||
bool const resultNegative = (num.negative() != den.negative());
|
||||
|
||||
// See mulRoundImpl: any MPT operand, not just an MPT result, and the
|
||||
// Number path when there are no current rules.
|
||||
// fixCleanup3_5_0: the legacy path below overflows on large MPT amounts.
|
||||
auto const enabled = isFeatureEnabled(featureMPTokensV2, /*resultIfNoRules*/ true) ||
|
||||
isFeatureEnabled(fixCleanup3_5_0, /*resultIfNoRules*/ true);
|
||||
if ((asset.holds<MPTIssue>() || num.holds<MPTIssue>() || den.holds<MPTIssue>()) && enabled)
|
||||
if (asset.holds<MPTIssue>() &&
|
||||
(isFeatureEnabled(featureMPTokensV2, true) || isFeatureEnabled(fixCleanup3_5_0, true)))
|
||||
{
|
||||
// Match the multiply path above: Number performs the rounded
|
||||
// operation, then STAmount materializes the final MPT amount using the
|
||||
@@ -1763,24 +1735,6 @@ divRoundStrict(STAmount const& num, STAmount const& den, Asset const& asset, boo
|
||||
return divRoundImpl<NumberRoundModeGuard>(num, den, asset, roundUp);
|
||||
}
|
||||
|
||||
STAmount
|
||||
mulRatio(STAmount const& amt, std::uint32_t num, std::uint32_t den, Number::RoundingMode rounding)
|
||||
{
|
||||
XRPL_ASSERT(
|
||||
rounding == Number::RoundingMode::Upward || rounding == Number::RoundingMode::Downward,
|
||||
"xrpl::mulRatio(STAmount) : rounding is Upward or Downward");
|
||||
bool const roundUp = rounding == Number::RoundingMode::Upward;
|
||||
return amt.asset().visit(
|
||||
[&](Issue const& issue) -> STAmount {
|
||||
if (amt.native())
|
||||
return mulRatio(amt.xrp(), num, den, roundUp);
|
||||
return {mulRatio(amt.iou(), num, den, roundUp), issue};
|
||||
},
|
||||
[&](MPTIssue const& issue) -> STAmount {
|
||||
return {mulRatio(amt.mpt(), num, den, roundUp), issue};
|
||||
});
|
||||
}
|
||||
|
||||
[[nodiscard]] bool
|
||||
STAmount::isZeroAtScale(int scale) const
|
||||
{
|
||||
|
||||
@@ -228,7 +228,6 @@ transResults()
|
||||
MAKE_ERROR(terNO_DELEGATE_PERMISSION, "Delegated account lacks permission to perform this transaction."),
|
||||
MAKE_ERROR(terLOCKED, "Fund is locked."),
|
||||
MAKE_ERROR(terNO_PERMISSION, "No permission to perform requested operation."),
|
||||
MAKE_ERROR(terTICKET_RESERVED, "Ticket is reserved."),
|
||||
|
||||
MAKE_ERROR(tesSUCCESS, "The transaction was applied. Only final in a validated ledger."),
|
||||
};
|
||||
|
||||
@@ -1,54 +0,0 @@
|
||||
#include <xrpl/protocol/XRPAmount.h>
|
||||
|
||||
#include <xrpl/basics/MathUtilities.h>
|
||||
#include <xrpl/basics/contract.h>
|
||||
#include <xrpl/protocol/Feature.h>
|
||||
#include <xrpl/protocol/Rules.h>
|
||||
|
||||
#include <stdexcept>
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
XRPAmount&
|
||||
XRPAmount::operator+=(value_type const& rhs)
|
||||
{
|
||||
if (auto const result = checkedAdd(drops_, rhs))
|
||||
{
|
||||
drops_ = *result;
|
||||
return *this;
|
||||
}
|
||||
if (isFeatureEnabled(fixCleanup3_5_0, /*resultIfNoRules*/ true))
|
||||
Throw<std::overflow_error>("XRPAmount::operator+= overflow");
|
||||
drops_ += rhs;
|
||||
return *this;
|
||||
}
|
||||
|
||||
XRPAmount&
|
||||
XRPAmount::operator-=(value_type const& rhs)
|
||||
{
|
||||
if (auto const result = checkedSub(drops_, rhs))
|
||||
{
|
||||
drops_ = *result;
|
||||
return *this;
|
||||
}
|
||||
if (isFeatureEnabled(fixCleanup3_5_0, /*resultIfNoRules*/ true))
|
||||
Throw<std::overflow_error>("XRPAmount::operator-= overflow");
|
||||
drops_ -= rhs;
|
||||
return *this;
|
||||
}
|
||||
|
||||
XRPAmount&
|
||||
XRPAmount::operator*=(value_type const& rhs)
|
||||
{
|
||||
if (auto const result = checkedMul(drops_, rhs))
|
||||
{
|
||||
drops_ = *result;
|
||||
return *this;
|
||||
}
|
||||
if (isFeatureEnabled(fixCleanup3_5_0, /*resultIfNoRules*/ true))
|
||||
Throw<std::overflow_error>("XRPAmount::operator*= overflow");
|
||||
drops_ *= rhs;
|
||||
return *this;
|
||||
}
|
||||
|
||||
} // namespace xrpl
|
||||
@@ -6,7 +6,10 @@
|
||||
#include <xrpl/protocol/BuildInfo.h>
|
||||
#include <xrpl/protocol/SystemParameters.h>
|
||||
|
||||
#include <boost/beast/http/status.hpp>
|
||||
|
||||
#include <ctime>
|
||||
#include <format>
|
||||
#include <string>
|
||||
|
||||
namespace xrpl {
|
||||
@@ -33,7 +36,10 @@ getHTTPHeaderTimestamp()
|
||||
void
|
||||
httpReply(int nStatus, std::string const& content, json::Output const& output, beast::Journal j)
|
||||
{
|
||||
JLOG(j.trace()) << "HTTP Reply " << nStatus << " " << content;
|
||||
// The status only. The body is a reply, which carries a credential when the
|
||||
// command is a keygen, and this library cannot mask one: the caller logs the
|
||||
// masked body at debug.
|
||||
JLOG(j.trace()) << "HTTP Reply " << nStatus;
|
||||
|
||||
if (content.empty() && nStatus == 401)
|
||||
{
|
||||
@@ -69,42 +75,29 @@ httpReply(int nStatus, std::string const& content, json::Output const& output, b
|
||||
return;
|
||||
}
|
||||
|
||||
// NOLINTNEXTLINE(bugprone-switch-missing-default-case)
|
||||
switch (nStatus)
|
||||
{
|
||||
// The status every successful reply carries, so a literal rather than a format call.
|
||||
case 200:
|
||||
output("HTTP/1.1 200 OK\r\n");
|
||||
break;
|
||||
case 202:
|
||||
output("HTTP/1.1 202 Accepted\r\n");
|
||||
break;
|
||||
case 400:
|
||||
output("HTTP/1.1 400 Bad Request\r\n");
|
||||
break;
|
||||
// Two statuses this server phrases itself rather than taking from the registry.
|
||||
case 401:
|
||||
output("HTTP/1.1 401 Authorization Required\r\n");
|
||||
break;
|
||||
case 403:
|
||||
output("HTTP/1.1 403 Forbidden\r\n");
|
||||
break;
|
||||
case 404:
|
||||
output("HTTP/1.1 404 Not Found\r\n");
|
||||
break;
|
||||
case 405:
|
||||
output("HTTP/1.1 405 Method Not Allowed\r\n");
|
||||
break;
|
||||
case 429:
|
||||
output("HTTP/1.1 429 Too Many Requests\r\n");
|
||||
break;
|
||||
case 500:
|
||||
output("HTTP/1.1 500 Internal Server Error\r\n");
|
||||
break;
|
||||
case 501:
|
||||
output("HTTP/1.1 501 Not Implemented\r\n");
|
||||
break;
|
||||
case 503:
|
||||
output("HTTP/1.1 503 Server is overloaded\r\n");
|
||||
break;
|
||||
default:
|
||||
// A reply whose first line is a header is not an HTTP response. Beast knows the whole
|
||||
// registry, so a status the error table gains needs no case here.
|
||||
output(
|
||||
std::format(
|
||||
"HTTP/1.1 {} {}\r\n",
|
||||
nStatus,
|
||||
boost::beast::http::obsolete_reason(
|
||||
static_cast<boost::beast::http::status>(nStatus))));
|
||||
break;
|
||||
}
|
||||
|
||||
output(getHTTPHeaderTimestamp());
|
||||
|
||||
@@ -26,7 +26,6 @@
|
||||
|
||||
#include <boost/smart_ptr/intrusive_ptr.hpp>
|
||||
|
||||
#include <atomic>
|
||||
#include <cstdint>
|
||||
#include <exception>
|
||||
#include <functional>
|
||||
@@ -78,23 +77,14 @@ SHAMap::SHAMap(SHAMap const& other, bool isMutable)
|
||||
: f_(other.f_)
|
||||
, journal_(other.f_.journal())
|
||||
, cowid_(other.cowid_ + 1)
|
||||
, ledgerSeq_(other.ledgerSeq())
|
||||
, ledgerSeq_(other.ledgerSeq_)
|
||||
, root_(other.root_)
|
||||
, state_(isMutable ? SHAMapState::Modifying : SHAMapState::Immutable)
|
||||
, type_(other.type_)
|
||||
, backed_(other.backed_)
|
||||
{
|
||||
// A snapshot shares the source's root, so Invalid carries over. Read once into a local, since
|
||||
// the source's state can change while this runs.
|
||||
auto const otherState = other.state();
|
||||
auto const ownState = [&] {
|
||||
if (otherState == SHAMapState::Invalid)
|
||||
return SHAMapState::Invalid;
|
||||
return isMutable ? SHAMapState::Modifying : SHAMapState::Immutable;
|
||||
}();
|
||||
state_.store(ownState, std::memory_order_release);
|
||||
|
||||
// If either map may change, they cannot share nodes
|
||||
if ((ownState != SHAMapState::Immutable) || (otherState != SHAMapState::Immutable))
|
||||
if ((state_ != SHAMapState::Immutable) || (other.state_ != SHAMapState::Immutable))
|
||||
{
|
||||
unshare();
|
||||
}
|
||||
@@ -115,7 +105,7 @@ SHAMap::dirtyUp(NodePathStack& stack, UInt256 const& target, SHAMapTreeNodePtr c
|
||||
// child can be an inner node or a leaf
|
||||
|
||||
XRPL_ASSERT(
|
||||
(state() != SHAMapState::Synching) && (state() != SHAMapState::Immutable),
|
||||
(state_ != SHAMapState::Synching) && (state_ != SHAMapState::Immutable),
|
||||
"xrpl::SHAMap::dirtyUp : valid state");
|
||||
XRPL_ASSERT(child && (child->cowid() == cowid_), "xrpl::SHAMap::dirtyUp : valid child input");
|
||||
|
||||
@@ -180,7 +170,7 @@ SHAMapTreeNodePtr
|
||||
SHAMap::fetchNodeFromDB(SHAMapHash const& hash) const
|
||||
{
|
||||
XRPL_ASSERT(backed_, "xrpl::SHAMap::fetchNodeFromDB : is backed");
|
||||
auto const obj = f_.db().fetchNodeObject(hash.asUInt256(), ledgerSeq());
|
||||
auto const obj = f_.db().fetchNodeObject(hash.asUInt256(), ledgerSeq_);
|
||||
return finishFetch(hash, obj);
|
||||
}
|
||||
|
||||
@@ -193,17 +183,10 @@ SHAMap::finishFetch(SHAMapHash const& hash, std::shared_ptr<NodeObject> const& o
|
||||
{
|
||||
if (!object)
|
||||
{
|
||||
// A missing node disproves full_, so withdraw it and report the gap. The exchange
|
||||
// makes exactly one reader report it, and its acquire pairs with the release store in
|
||||
// setFull(). It needs no release: the only other reader of full_ is this same path,
|
||||
// and a reader that loses the exchange does nothing with what the winner did. The load
|
||||
// ahead of it is relaxed, since all it decides is whether to try the exchange: a stale
|
||||
// true costs one exchange that loses, and a stale false means another reader won, or
|
||||
// setFull() is not visible yet and the gap goes unreported.
|
||||
if (full_.load(std::memory_order_relaxed) &&
|
||||
full_.exchange(false, std::memory_order_acquire))
|
||||
if (full_)
|
||||
{
|
||||
f_.missingNodeAcquireBySeq(ledgerSeq(), hash.asUInt256());
|
||||
full_ = false;
|
||||
f_.missingNodeAcquireBySeq(ledgerSeq_, hash.asUInt256());
|
||||
}
|
||||
return {};
|
||||
}
|
||||
@@ -236,7 +219,7 @@ SHAMap::checkFilter(SHAMapHash const& hash, SHAMapSyncFilter const* filter) cons
|
||||
auto node = SHAMapTreeNode::makeFromPrefix(makeSlice(*nodeData), hash);
|
||||
if (node)
|
||||
{
|
||||
filter->gotNode(true, hash, ledgerSeq(), std::move(*nodeData), node->getType());
|
||||
filter->gotNode(true, hash, ledgerSeq_, std::move(*nodeData), node->getType());
|
||||
if (backed_)
|
||||
canonicalize(hash, node);
|
||||
}
|
||||
@@ -343,6 +326,9 @@ SHAMap::descend(SHAMapInnerNode& parent, unsigned int branch) const
|
||||
return node;
|
||||
|
||||
node = fetchNode(parent.getChildHash(branch));
|
||||
if (!node)
|
||||
return {};
|
||||
|
||||
node = parent.canonicalizeChild(branch, std::move(node));
|
||||
return node;
|
||||
}
|
||||
@@ -354,7 +340,7 @@ SHAMap::descendNoStore(SHAMapInnerNode& parent, unsigned int branch) const
|
||||
{
|
||||
SHAMapTreeNodePtr ret = parent.getChild(branch);
|
||||
if (!ret && backed_)
|
||||
ret = fetchNodeNT(parent.getChildHash(branch));
|
||||
ret = fetchNode(parent.getChildHash(branch));
|
||||
return ret;
|
||||
}
|
||||
|
||||
@@ -413,7 +399,7 @@ SHAMap::descendAsync(
|
||||
{
|
||||
f_.db().asyncFetch(
|
||||
hash.asUInt256(),
|
||||
ledgerSeq(),
|
||||
ledgerSeq_,
|
||||
[this, hash, cb{std::move(callback)}](std::shared_ptr<NodeObject> const& object) {
|
||||
auto const node = finishFetch(hash, object);
|
||||
cb(node, hash);
|
||||
@@ -438,7 +424,7 @@ SHAMap::unshareNode(intr_ptr::SharedPtr<Node> node, SHAMapNodeID const& nodeID)
|
||||
if (node->cowid() != cowid_)
|
||||
{
|
||||
// have a CoW
|
||||
XRPL_ASSERT(state() != SHAMapState::Immutable, "xrpl::SHAMap::unshareNode : not immutable");
|
||||
XRPL_ASSERT(state_ != SHAMapState::Immutable, "xrpl::SHAMap::unshareNode : not immutable");
|
||||
node = intr_ptr::staticPointerCast<Node>(node->clone(cowid_));
|
||||
if (nodeID.isRoot())
|
||||
root_ = node;
|
||||
@@ -651,7 +637,7 @@ bool
|
||||
SHAMap::delItem(UInt256 const& id)
|
||||
{
|
||||
// delete the item with this ID
|
||||
XRPL_ASSERT(state() != SHAMapState::Immutable, "xrpl::SHAMap::delItem : not immutable");
|
||||
XRPL_ASSERT(state_ != SHAMapState::Immutable, "xrpl::SHAMap::delItem : not immutable");
|
||||
|
||||
NodePathStack stack;
|
||||
walkTowardsKey(id, &stack);
|
||||
@@ -733,7 +719,7 @@ SHAMap::delItem(UInt256 const& id)
|
||||
bool
|
||||
SHAMap::addGiveItem(SHAMapNodeType type, boost::intrusive_ptr<SHAMapItem const> item)
|
||||
{
|
||||
XRPL_ASSERT(state() != SHAMapState::Immutable, "xrpl::SHAMap::addGiveItem : not immutable");
|
||||
XRPL_ASSERT(state_ != SHAMapState::Immutable, "xrpl::SHAMap::addGiveItem : not immutable");
|
||||
XRPL_ASSERT(type != SHAMapNodeType::TnInner, "xrpl::SHAMap::addGiveItem : valid type input");
|
||||
|
||||
// add the specified item, does not update
|
||||
@@ -824,7 +810,7 @@ SHAMap::updateGiveItem(SHAMapNodeType type, boost::intrusive_ptr<SHAMapItem cons
|
||||
// can't change the tag but can change the hash
|
||||
UInt256 const tag = item->key();
|
||||
|
||||
XRPL_ASSERT(state() != SHAMapState::Immutable, "xrpl::SHAMap::updateGiveItem : not immutable");
|
||||
XRPL_ASSERT(state_ != SHAMapState::Immutable, "xrpl::SHAMap::updateGiveItem : not immutable");
|
||||
|
||||
NodePathStack stack;
|
||||
walkTowardsKey(tag, &stack);
|
||||
@@ -915,7 +901,7 @@ SHAMap::writeNode(NodeObjectType t, SHAMapTreeNodePtr node) const
|
||||
|
||||
Serializer s;
|
||||
node->serializeWithPrefix(s);
|
||||
f_.db().store(t, std::move(s.modData()), node->getHash().asUInt256(), ledgerSeq());
|
||||
f_.db().store(t, std::move(s.modData()), node->getHash().asUInt256(), ledgerSeq_);
|
||||
return node;
|
||||
}
|
||||
|
||||
|
||||
@@ -13,11 +13,9 @@
|
||||
#include <boost/smart_ptr/intrusive_ptr.hpp>
|
||||
|
||||
#include <array>
|
||||
#include <exception>
|
||||
#include <mutex>
|
||||
#include <sstream>
|
||||
#include <stack>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
@@ -285,36 +283,21 @@ bool
|
||||
SHAMap::walkMapParallel(std::vector<SHAMapMissingNode>& missingNodes, int maxMissing) const
|
||||
{
|
||||
if (!root_->isInner()) // root_ is only node, and we have it
|
||||
return true;
|
||||
|
||||
// Only the nodes this call records count towards the result, so remember what the
|
||||
// caller already had.
|
||||
auto const initialMissing = missingNodes.size();
|
||||
return false;
|
||||
|
||||
using StackEntry = intr_ptr::SharedPtr<SHAMapInnerNode>;
|
||||
std::array<SHAMapTreeNodePtr, SHAMapInnerNode::kBranchFactor> topChildren;
|
||||
{
|
||||
// This loop runs before the workers start, so it needs no lock.
|
||||
auto const& innerRoot = intr_ptr::staticPointerCast<SHAMapInnerNode>(root_);
|
||||
for (auto i = 0u; i < SHAMapInnerNode::kBranchFactor; ++i)
|
||||
{
|
||||
if (innerRoot->isEmptyBranch(i))
|
||||
continue;
|
||||
|
||||
topChildren[i] = descendNoStore(*innerRoot, i);
|
||||
if (!topChildren[i])
|
||||
{
|
||||
// A root child that cannot be read hides its whole subtree. Record it here,
|
||||
// because the loop below skips a null child without visiting it.
|
||||
missingNodes.emplace_back(type_, innerRoot->getChildHash(i));
|
||||
if (--maxMissing <= 0)
|
||||
return false;
|
||||
}
|
||||
if (!innerRoot->isEmptyBranch(i))
|
||||
topChildren[i] = descendNoStore(*innerRoot, i);
|
||||
}
|
||||
}
|
||||
std::vector<std::thread> workers;
|
||||
workers.reserve(SHAMapInnerNode::kBranchFactor);
|
||||
std::vector<std::string> exceptions;
|
||||
std::vector<SHAMapMissingNode> exceptions;
|
||||
exceptions.reserve(SHAMapInnerNode::kBranchFactor);
|
||||
|
||||
std::array<std::stack<StackEntry, std::vector<StackEntry>>, SHAMapInnerNode::kBranchFactor>
|
||||
@@ -363,14 +346,6 @@ SHAMap::walkMapParallel(std::vector<SHAMapMissingNode>& missingNodes, int maxMis
|
||||
else
|
||||
{
|
||||
std::scoped_lock const l{m};
|
||||
|
||||
// Another worker may have spent the budget while this one
|
||||
// was walking, so test it before adding to the list and not
|
||||
// only after. Testing only after lets every worker still
|
||||
// running add one more node than the caller asked for.
|
||||
if (maxMissing <= 0)
|
||||
return;
|
||||
|
||||
missingNodes.emplace_back(type_, node->getChildHash(i));
|
||||
if (--maxMissing <= 0)
|
||||
return;
|
||||
@@ -378,14 +353,10 @@ SHAMap::walkMapParallel(std::vector<SHAMapMissingNode>& missingNodes, int maxMis
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (std::exception const& e)
|
||||
catch (SHAMapMissingNode const& e)
|
||||
{
|
||||
// LCOV_EXCL_START
|
||||
// A worker must not let an exception leave its thread, so record it
|
||||
// and let the join below report it.
|
||||
std::scoped_lock const l(m);
|
||||
exceptions.emplace_back(e.what());
|
||||
// LCOV_EXCL_STOP
|
||||
exceptions.push_back(e);
|
||||
}
|
||||
},
|
||||
std::move(nodeStacks[rootChildIndex]));
|
||||
@@ -395,29 +366,14 @@ SHAMap::walkMapParallel(std::vector<SHAMapMissingNode>& missingNodes, int maxMis
|
||||
worker.join();
|
||||
|
||||
std::scoped_lock const l(m);
|
||||
if (!exceptions.empty())
|
||||
{
|
||||
// LCOV_EXCL_START
|
||||
std::stringstream ss;
|
||||
ss << "Exception(s) in ledger load: ";
|
||||
for (auto const& e : exceptions)
|
||||
ss << e << ", ";
|
||||
JLOG(journal_.error()) << ss.str();
|
||||
return false;
|
||||
// LCOV_EXCL_STOP
|
||||
}
|
||||
|
||||
// A node the workers could not read is recorded in `missingNodes` rather than thrown,
|
||||
// so the result has to consult it too.
|
||||
auto const found = missingNodes.size() - initialMissing;
|
||||
if (found != 0)
|
||||
{
|
||||
JLOG(journal_.error()) << "Missing node(s) in ledger load: " << found
|
||||
<< ", first: " << missingNodes[initialMissing].what();
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
if (exceptions.empty())
|
||||
return true;
|
||||
std::stringstream ss;
|
||||
ss << "Exception(s) in ledger load: ";
|
||||
for (auto const& e : exceptions)
|
||||
ss << e.what() << ", ";
|
||||
JLOG(journal_.error()) << ss.str();
|
||||
return false;
|
||||
}
|
||||
|
||||
} // namespace xrpl
|
||||
@@ -16,7 +16,6 @@
|
||||
#include <xrpl/shamap/detail/TaggedPointer.h>
|
||||
#include <xrpl/shamap/detail/TaggedPointer.ipp>
|
||||
|
||||
#include <atomic>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <mutex>
|
||||
@@ -78,9 +77,7 @@ SHAMapInnerNode::clone(std::uint32_t cowid) const
|
||||
auto p = intr_ptr::makeShared<SHAMapInnerNode>(cowid, branchCount);
|
||||
p->hash_ = hash_;
|
||||
p->isBranch_ = isBranch_;
|
||||
// Relaxed, as everywhere. p is not reachable by another thread until this returns.
|
||||
p->fullBelowGen_.store(
|
||||
fullBelowGen_.load(std::memory_order_relaxed), std::memory_order_relaxed);
|
||||
p->fullBelowGen_ = fullBelowGen_;
|
||||
SHAMapHash* cloneHashes = nullptr;
|
||||
SHAMapHash* thisHashes = nullptr;
|
||||
SHAMapTreeNodePtr* cloneChildren = nullptr;
|
||||
|
||||
@@ -31,55 +31,28 @@
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
namespace {
|
||||
|
||||
/**
|
||||
* Whether a depth is at or past the deepest an inner node may occupy.
|
||||
*
|
||||
* Nibbles run out at SHAMap::kLeafDepth, so only a leaf may sit there. True for
|
||||
* every deeper position too, which lies past the end of a key.
|
||||
*
|
||||
* @param depth The depth to judge.
|
||||
* @return Whether an inner node at that depth makes the map impossible.
|
||||
*/
|
||||
[[nodiscard]] bool
|
||||
isLeafDepth(unsigned int depth)
|
||||
{
|
||||
return depth >= SHAMap::kLeafDepth;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
bool
|
||||
void
|
||||
SHAMap::visitLeaves(
|
||||
std::function<void(boost::intrusive_ptr<SHAMapItem const> const& item)> const& leafFunction)
|
||||
const
|
||||
{
|
||||
return visitNodes([&leafFunction](SHAMapTreeNode& node) {
|
||||
visitNodes([&leafFunction](SHAMapTreeNode& node) {
|
||||
if (!node.isInner())
|
||||
leafFunction(safeDowncast<SHAMapLeafNode&>(node).peekItem());
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
bool
|
||||
void
|
||||
SHAMap::visitNodes(std::function<bool(SHAMapTreeNode&)> const& function) const
|
||||
{
|
||||
if (!root_)
|
||||
{
|
||||
// LCOV_EXCL_START
|
||||
UNREACHABLE("xrpl::SHAMap::visitNodes : non-null root");
|
||||
return true;
|
||||
// LCOV_EXCL_STOP
|
||||
}
|
||||
return;
|
||||
|
||||
// The root is readable by definition, so a stop here still reports a complete walk.
|
||||
if (!function(*root_))
|
||||
return true;
|
||||
function(*root_);
|
||||
|
||||
// A map whose root is a leaf holds one node, and this walk just visited it.
|
||||
if (!root_->isInner())
|
||||
return true;
|
||||
return;
|
||||
|
||||
using StackEntry = std::pair<unsigned int, intr_ptr::SharedPtr<SHAMapInnerNode>>;
|
||||
std::stack<StackEntry, std::vector<StackEntry>> stack;
|
||||
@@ -94,17 +67,8 @@ SHAMap::visitNodes(std::function<bool(SHAMapTreeNode&)> const& function) const
|
||||
if (!node->isEmptyBranch(pos))
|
||||
{
|
||||
SHAMapTreeNodePtr const child = descendNoStore(*node, pos);
|
||||
if (!child)
|
||||
{
|
||||
// No caller can use a partial walk, so end it here rather than pay
|
||||
// for the rest of the map. The callers that act on the result log
|
||||
// it at a level an operator reads, so keep this one at debug.
|
||||
JLOG(journal_.debug())
|
||||
<< "visitNodes: unreadable child node " << node->getChildHash(pos);
|
||||
return false;
|
||||
}
|
||||
if (!function(*child))
|
||||
return true;
|
||||
return;
|
||||
|
||||
if (child->isLeaf())
|
||||
{
|
||||
@@ -139,8 +103,6 @@ SHAMap::visitNodes(std::function<bool(SHAMapTreeNode&)> const& function) const
|
||||
std::tie(pos, node) = stack.top();
|
||||
stack.pop();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void
|
||||
@@ -181,10 +143,13 @@ SHAMap::visitDifferences(
|
||||
if (!function(*node))
|
||||
return;
|
||||
|
||||
// Only a leaf belongs at kLeafDepth. The node is still reported, since the wire form
|
||||
// carries no depth and the recipient hooks blobs by hash. Its children are skipped, since
|
||||
// getChildNodeID has no answer past kLeafDepth.
|
||||
if (isLeafDepth(nodeID.getDepth()))
|
||||
// Nibbles run out at kLeafDepth, so only a leaf belongs there. A well-formed map never
|
||||
// holds an inner node at that depth: addKnownNode marks the map invalid rather than hooking
|
||||
// one in, and fetch-pack data is hash-verified against a validated root, so reaching this
|
||||
// means a defect or a corrupt store, not something a peer can provoke. Report the node
|
||||
// anyway - the wire form carries no depth, and the recipient hooks blobs in by hash - but
|
||||
// skip the children rather than letting getChildNodeID throw on them.
|
||||
if (nodeID.getDepth() >= kLeafDepth)
|
||||
{
|
||||
// LCOV_EXCL_START
|
||||
UNREACHABLE("xrpl::SHAMap::visitDifferences : inner node at leaf depth");
|
||||
@@ -242,12 +207,7 @@ SHAMap::gmnProcessNodes(MissingNodes& mn, MissingNodes::StackEntry& se)
|
||||
// we already know this child node is missing
|
||||
fullBelow = false;
|
||||
}
|
||||
// The depth test precedes the cache lookup for the same reason it does in addKnownNode():
|
||||
// the cache is keyed by node hash and shared across maps, and a hash covers a node's
|
||||
// children but not its depth. Skipping the shortcut forgoes an optimization only.
|
||||
else if (
|
||||
!backed_ || isLeafDepth(nodeID.getDepth() + 1) ||
|
||||
!f_.getFullBelowCache()->touchIfExists(childHash.asUInt256()))
|
||||
else if (!backed_ || !f_.getFullBelowCache()->touchIfExists(childHash.asUInt256()))
|
||||
{
|
||||
bool pending = false;
|
||||
auto d = descendAsync(
|
||||
@@ -278,17 +238,6 @@ SHAMap::gmnProcessNodes(MissingNodes& mn, MissingNodes::StackEntry& se)
|
||||
if (--mn.max <= 0)
|
||||
return;
|
||||
}
|
||||
else if (d->isInner() && isLeafDepth(nodeID.getDepth() + 1))
|
||||
{
|
||||
// Only a leaf belongs that deep (see isLeafDepth and SHAMap::addKnownNode). A node
|
||||
// resolved locally reaches the walk without passing through addKnownNode(), so the
|
||||
// walk reaches this verdict itself. Ordered ahead of the full-below test below,
|
||||
// which canonicalization shares across maps.
|
||||
JLOG(journal_.warn()) << "Inner node at branch " << branch << " below " << nodeID
|
||||
<< " makes the map invalid";
|
||||
setInvalid();
|
||||
return;
|
||||
}
|
||||
else if (d->isInner() && !safeDowncast<SHAMapInnerNode*>(d)->isFullBelow(mn.generation))
|
||||
{
|
||||
mn.stack.push(se);
|
||||
@@ -352,8 +301,6 @@ SHAMap::gmnProcessDeferredReads(MissingNodes& mn)
|
||||
}
|
||||
else if ((mn.max > 0) && mn.missingHashes.insert(nodeHash).second)
|
||||
{
|
||||
// getChildNodeID() is safe here: gmnProcessNodes refuses to descend into an inner node
|
||||
// at kLeafDepth, so a deferred parent sits at most one level above it.
|
||||
mn.missingNodes.emplace_back(parentID.getChildNodeID(branch), nodeHash.asUInt256());
|
||||
--mn.max;
|
||||
}
|
||||
@@ -364,20 +311,17 @@ SHAMap::gmnProcessDeferredReads(MissingNodes& mn)
|
||||
mn.deferred = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a list of node IDs and hashes for nodes that are part of this SHAMap
|
||||
* but not available locally. The filter can hold alternate sources of
|
||||
* nodes that are not permanently stored locally
|
||||
*/
|
||||
std::vector<std::pair<SHAMapNodeID, UInt256>>
|
||||
SHAMap::getMissingNodes(int max, SHAMapSyncFilter const* filter)
|
||||
{
|
||||
XRPL_ASSERT(root_->getHash().isNonZero(), "xrpl::SHAMap::getMissingNodes : nonzero root hash");
|
||||
XRPL_ASSERT(max > 0, "xrpl::SHAMap::getMissingNodes : valid max input");
|
||||
|
||||
if (!isValid())
|
||||
{
|
||||
// The root node's own hash, since getHash() unshares the tree on a zero hash.
|
||||
JLOG(journal_.warn()) << "getMissingNodes called on an invalid map, root hash "
|
||||
<< root_->getHash() << " seq " << ledgerSeq();
|
||||
return {};
|
||||
}
|
||||
|
||||
MissingNodes mn(
|
||||
max,
|
||||
filter,
|
||||
@@ -410,11 +354,6 @@ SHAMap::getMissingNodes(int max, SHAMapSyncFilter const* filter)
|
||||
{
|
||||
gmnProcessNodes(mn, pos);
|
||||
|
||||
// The walk just invalidated the map. The loop stops descending here but falls through
|
||||
// to the drain below, since every posted read must be drained while `mn` is alive.
|
||||
if (!isValid())
|
||||
break;
|
||||
|
||||
if (mn.max <= 0)
|
||||
break;
|
||||
|
||||
@@ -444,11 +383,6 @@ SHAMap::getMissingNodes(int max, SHAMapSyncFilter const* filter)
|
||||
if (mn.deferred != 0)
|
||||
gmnProcessDeferredReads(mn);
|
||||
|
||||
// Reads are drained, so the map can be abandoned. What was collected belongs to a tree
|
||||
// that cannot exist.
|
||||
if (!isValid())
|
||||
return {};
|
||||
|
||||
if (mn.max <= 0)
|
||||
return std::move(mn.missingNodes);
|
||||
|
||||
@@ -482,11 +416,6 @@ SHAMap::getMissingNodes(int max, SHAMapSyncFilter const* filter)
|
||||
|
||||
} while (node != nullptr);
|
||||
|
||||
// addKnownNode() on another thread can write the verdict after the loop's own test, so the map
|
||||
// is judged once more before the result is returned.
|
||||
if (!isValid())
|
||||
return {}; // LCOV_EXCL_LINE: only that other thread reaches this, so no test does
|
||||
|
||||
if (mn.missingNodes.empty())
|
||||
clearSynching();
|
||||
|
||||
@@ -598,19 +527,11 @@ SHAMap::addRootNode(
|
||||
XRPL_ASSERT(cowid_ >= 1, "xrpl::SHAMap::addRootNode : valid cowid");
|
||||
XRPL_ASSERT(rootNode, "xrpl::SHAMap::addRootNode : non-null root node");
|
||||
|
||||
// A map syncs against one hash and installs a root once, so a root already held is a duplicate
|
||||
// only once it hashes to the hash asked for.
|
||||
// we already have a root_ node
|
||||
if (root_->getHash().isNonZero())
|
||||
{
|
||||
JLOG(journal_.trace()) << "Got root node, already have one";
|
||||
|
||||
if (root_->getHash() != hash)
|
||||
{
|
||||
JLOG(journal_.warn()) << "Root node offered under hash " << hash
|
||||
<< ", but the map holds " << root_->getHash();
|
||||
return SHAMapAddNode::invalid();
|
||||
}
|
||||
|
||||
XRPL_ASSERT(root_->getHash() == hash, "xrpl::SHAMap::addRootNode : valid hash");
|
||||
return SHAMapAddNode::duplicate();
|
||||
}
|
||||
|
||||
@@ -634,7 +555,7 @@ SHAMap::addRootNode(
|
||||
Serializer s;
|
||||
root_->serializeWithPrefix(s);
|
||||
filter->gotNode(
|
||||
false, root_->getHash(), ledgerSeq(), std::move(s.modData()), root_->getType());
|
||||
false, root_->getHash(), ledgerSeq_, std::move(s.modData()), root_->getType());
|
||||
}
|
||||
|
||||
return SHAMapAddNode::useful();
|
||||
@@ -677,13 +598,7 @@ SHAMap::addKnownNode(
|
||||
}
|
||||
|
||||
auto childHash = inner->getChildHash(branch);
|
||||
|
||||
// Depth before the cache: the cache is keyed by node hash and shared across the family, and
|
||||
// a hash covers a node's children but not its depth, so the same subtree can be cached as
|
||||
// complete at one depth and reached at another. Skipping the shortcut only forgoes an
|
||||
// optimization.
|
||||
if (!isLeafDepth(currNodeID.getDepth() + 1) &&
|
||||
f_.getFullBelowCache()->touchIfExists(childHash.asUInt256()))
|
||||
if (f_.getFullBelowCache()->touchIfExists(childHash.asUInt256()))
|
||||
{
|
||||
return SHAMapAddNode::duplicate();
|
||||
}
|
||||
@@ -701,32 +616,25 @@ SHAMap::addKnownNode(
|
||||
return SHAMapAddNode::invalid();
|
||||
}
|
||||
|
||||
// Only leaves may sit at kLeafDepth (see isLeafDepth), so an inner node there makes the map
|
||||
// impossible. The node is reported as bad data.
|
||||
//
|
||||
// Every node from the root down hash-verified to get here, so it is the requested root hash
|
||||
// itself that commits to a shape no valid tree can have. The verdict belongs to that hash
|
||||
// rather than to our copy of the tree.
|
||||
bool const badDepth = treeNode->isInner() && isLeafDepth(currNodeID.getDepth());
|
||||
SOMETIMES(badDepth, "xrpl::SHAMap::addKnownNode : map is invalid");
|
||||
if (badDepth)
|
||||
// Inner nodes must be at a level strictly less than 64
|
||||
// but leaf nodes (while notionally at level 64) can be
|
||||
// at any depth up to and including 64:
|
||||
if ((currNodeID.getDepth() > kLeafDepth) ||
|
||||
(treeNode->isInner() && currNodeID.getDepth() == kLeafDepth))
|
||||
{
|
||||
JLOG(journal_.warn()) << "Node " << nodeID << " makes the map invalid at "
|
||||
<< currNodeID;
|
||||
setInvalid();
|
||||
return SHAMapAddNode::invalid();
|
||||
// Map is provably invalid
|
||||
state_ = SHAMapState::Invalid;
|
||||
return SHAMapAddNode::useful();
|
||||
}
|
||||
|
||||
// The data hashes to the child at currNodeID but claims to belong at nodeID, so it is not
|
||||
// the node that was asked for. Only the label is wrong, so the map stays sound and the node
|
||||
// is still obtainable from another sender.
|
||||
bool const badPosition = (currNodeID != nodeID);
|
||||
SOMETIMES(badPosition, "xrpl::SHAMap::addKnownNode : node ID does not match its position");
|
||||
if (badPosition)
|
||||
if (currNodeID != nodeID)
|
||||
{
|
||||
JLOG(journal_.warn()) << "Unable to hook node " << nodeID << ", stuck at "
|
||||
<< currNodeID;
|
||||
return SHAMapAddNode::invalid();
|
||||
// Either this node is broken or we didn't request it (yet)
|
||||
JLOG(journal_.warn()) << "unable to hook node " << nodeID;
|
||||
JLOG(journal_.info()) << " stuck at " << currNodeID;
|
||||
JLOG(journal_.info()) << "got depth=" << nodeID.getDepth()
|
||||
<< ", walked to= " << currNodeID.getDepth();
|
||||
return SHAMapAddNode::useful();
|
||||
}
|
||||
|
||||
if (backed_)
|
||||
@@ -739,7 +647,7 @@ SHAMap::addKnownNode(
|
||||
Serializer s;
|
||||
treeNode->serializeWithPrefix(s);
|
||||
filter->gotNode(
|
||||
false, childHash, ledgerSeq(), std::move(s.modData()), treeNode->getType());
|
||||
false, childHash, ledgerSeq_, std::move(s.modData()), treeNode->getType());
|
||||
}
|
||||
|
||||
return SHAMapAddNode::useful();
|
||||
@@ -858,7 +766,7 @@ SHAMap::hasLeafNode(UInt256 const& tag, SHAMapHash const& targetNodeHash) const
|
||||
// Same kLeafDepth hazard as in visitDifferences above. That guard bounds the caller's own
|
||||
// traversal, not the map queried here, and the loop below descends from this map's root
|
||||
// independently, so this check is what keeps a malformed map from reaching getChildNodeID.
|
||||
if (isLeafDepth(nodeID.getDepth()))
|
||||
if (nodeID.getDepth() >= kLeafDepth)
|
||||
{
|
||||
// LCOV_EXCL_START
|
||||
UNREACHABLE("xrpl::SHAMap::hasLeafNode : inner node at leaf depth");
|
||||
@@ -941,8 +849,8 @@ SHAMap::verifyProofPath(UInt256 const& rootHash, UInt256 const& key, std::vector
|
||||
// there. These nodes come off the wire, so a peer can still claim an inner one;
|
||||
// reject it rather than passing this depth to selectBranch.
|
||||
SOMETIMES(
|
||||
isLeafDepth(depth), "xrpl::SHAMap::verifyProofPath : inner at leaf depth");
|
||||
if (isLeafDepth(depth))
|
||||
depth >= kLeafDepth, "xrpl::SHAMap::verifyProofPath : inner at leaf depth");
|
||||
if (depth >= kLeafDepth)
|
||||
return false;
|
||||
|
||||
auto const nodeId = SHAMapNodeID::createID(depth, key);
|
||||
@@ -951,9 +859,10 @@ SHAMap::verifyProofPath(UInt256 const& rootHash, UInt256 const& key, std::vector
|
||||
}
|
||||
else
|
||||
{
|
||||
// The hash chain up to rootHash proves this leaf sits where the path claims. Any
|
||||
// leaf whose subtree hashes the same at every level above satisfies that chain, so
|
||||
// the terminal leaf's own key is what ties the proof to `key`.
|
||||
// The hash chain up to rootHash only proves this leaf sits where the path claims,
|
||||
// not that it is the leaf for `key`: a peer could substitute any other leaf whose
|
||||
// subtree hashes to the same value at every level above it. Checking the terminal
|
||||
// leaf's own key is what ties the proof to `key` specifically.
|
||||
if (leafKey(*node) != key)
|
||||
return false;
|
||||
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
#include <xrpl/ledger/helpers/DelegateHelpers.h>
|
||||
#include <xrpl/ledger/helpers/NFTokenHelpers.h>
|
||||
#include <xrpl/ledger/helpers/OfferHelpers.h>
|
||||
#include <xrpl/ledger/helpers/ProposalHelpers.h>
|
||||
#include <xrpl/ledger/helpers/RippleStateHelpers.h>
|
||||
#include <xrpl/ledger/helpers/SponsorHelpers.h>
|
||||
#include <xrpl/protocol/AccountID.h>
|
||||
@@ -328,9 +327,10 @@ Transactor::preflight2(PreflightContext const& ctx)
|
||||
|
||||
auto const sigValid = checkValidity(ctx.registry.get().getHashRouter(), ctx.tx, ctx.rules);
|
||||
if (sigValid.first == Validity::SigBad)
|
||||
{
|
||||
{ // LCOV_EXCL_START
|
||||
JLOG(ctx.j.debug()) << "preflight2: bad signature. " << sigValid.second;
|
||||
return temINVALID;
|
||||
// LCOV_EXCL_STOP
|
||||
}
|
||||
|
||||
// Do not add any checks after this point that are relevant for
|
||||
@@ -755,19 +755,6 @@ Transactor::checkSeqProxy(ReadView const& view, STTx const& tx, beast::Journal j
|
||||
<< "a_seq=" << aSeq << " t_seq=" << tSeqProx;
|
||||
return tefNO_TICKET;
|
||||
}
|
||||
|
||||
// While a TransactionProposal keyed to this Ticket exists, the
|
||||
// Ticket is reserved: only the proposal's own proposed transaction
|
||||
// may consume it, so unrelated activity cannot invalidate the
|
||||
// proposal while signatures are being collected (On-Chain Cosigner spec §4.2.1).
|
||||
// Cancelling the proposal frees the Ticket, so this is a retry, not
|
||||
// a final failure. No proposal can exist before the Cosign amendment,
|
||||
// so the gate only makes that dependency explicit.
|
||||
if (view.rules().enabled(featureCosign) && !proposal::canConsumeTicket(view, tx))
|
||||
{
|
||||
JLOG(j.trace()) << "applyTransaction: ticket " << tSeqProx << " is reserved";
|
||||
return terTICKET_RESERVED;
|
||||
}
|
||||
}
|
||||
|
||||
return tesSUCCESS;
|
||||
@@ -879,28 +866,8 @@ Transactor::ticketDelete(
|
||||
// Update the Ticket owner's reserve.
|
||||
decreaseOwnerCountForObject(view, sleAccount, sleTicket, 1, j);
|
||||
|
||||
std::uint32_t const ticketSeq{(*sleTicket)[sfTicketSequence]};
|
||||
|
||||
// Remove Ticket from ledger.
|
||||
view.erase(sleTicket);
|
||||
|
||||
// Once the Ticket is gone, a TransactionProposal keyed to it can never
|
||||
// execute: its proposed transaction would fail with tefNO_TICKET. Clean
|
||||
// up the stale proposal and release its Owner's reserve (On-Chain Cosigner spec §4.5).
|
||||
// The reservation check in checkSeqProxy means this is reached only by
|
||||
// the proposal's own proposed transaction executing (or failing with a
|
||||
// claimed-fee tec), or by AccountDelete sweeping the target account's
|
||||
// Tickets. No proposal can exist before the Cosign amendment, so the gate
|
||||
// only makes that dependency explicit.
|
||||
if (view.rules().enabled(featureCosign))
|
||||
{
|
||||
if (auto const sleProposal = view.peek(keylet::txProposal(account, ticketSeq)))
|
||||
{
|
||||
if (TER const ter = proposal::deleteProposal(view, sleProposal, j); !isTesSuccess(ter))
|
||||
return ter; // LCOV_EXCL_LINE
|
||||
}
|
||||
}
|
||||
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
|
||||
@@ -9,7 +9,6 @@
|
||||
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
|
||||
#include <xrpl/ledger/helpers/TokenHelpers.h>
|
||||
#include <xrpl/protocol/AccountID.h>
|
||||
#include <xrpl/protocol/Asset.h>
|
||||
#include <xrpl/protocol/Feature.h>
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
#include <xrpl/protocol/Issue.h>
|
||||
@@ -23,7 +22,6 @@
|
||||
#include <xrpl/protocol/STLedgerEntry.h>
|
||||
#include <xrpl/protocol/STNumber.h> // IWYU pragma: keep
|
||||
#include <xrpl/protocol/STTx.h>
|
||||
#include <xrpl/protocol/STVector256.h>
|
||||
#include <xrpl/protocol/SystemParameters.h>
|
||||
#include <xrpl/protocol/TER.h>
|
||||
#include <xrpl/protocol/TxFormats.h>
|
||||
@@ -1012,78 +1010,6 @@ ValidClawback::finalize(
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
// Whether `entry` is the trust line or MPToken through which `account` holds
|
||||
// `asset`. XRP has no such entry.
|
||||
static bool
|
||||
isHolding(SLE const& entry, Asset const& asset, AccountID const& account)
|
||||
{
|
||||
return asset.visit(
|
||||
[&](Issue const& issue) {
|
||||
return !isXRP(issue) && keylet::trustLine(account, issue).check(entry);
|
||||
},
|
||||
[&](MPTIssue const& issue) {
|
||||
return keylet::mptoken(issue.getMptID(), account).check(entry);
|
||||
});
|
||||
}
|
||||
|
||||
// Whether `entry`, linked into the owner directory of `pseudo`, belongs to the
|
||||
// AMM, Vault or LoanBroker that the pseudo-account was created for.
|
||||
static bool
|
||||
isOwnedByPseudoAccount(ReadView const& view, SLE const& pseudo, SLE const& entry)
|
||||
{
|
||||
auto const pseudoAcctID = pseudo[sfAccount];
|
||||
|
||||
if (pseudo.isFieldPresent(sfAMMID))
|
||||
{
|
||||
if (entry.getType() == ltAMM)
|
||||
return entry.key() == pseudo[sfAMMID];
|
||||
|
||||
auto const amm = view.read(keylet::amm(pseudo[sfAMMID]));
|
||||
if (!amm)
|
||||
return false; // LCOV_EXCL_LINE
|
||||
if (isHolding(entry, (*amm)[sfAsset], pseudoAcctID) ||
|
||||
isHolding(entry, (*amm)[sfAsset2], pseudoAcctID))
|
||||
return true;
|
||||
|
||||
// The AMM issues LP tokens, so each liquidity provider's trust line
|
||||
// links into its directory.
|
||||
if (entry.getType() != ltRIPPLE_STATE)
|
||||
return false;
|
||||
auto const low = entry[sfLowLimit].getIssuer();
|
||||
auto const high = entry[sfHighLimit].getIssuer();
|
||||
auto const lpCurrency = (*amm)[sfLPTokenBalance].get<Issue>().currency;
|
||||
return (low == pseudoAcctID || high == pseudoAcctID) &&
|
||||
entry.key() == keylet::trustLine(low, high, lpCurrency).key;
|
||||
}
|
||||
|
||||
if (pseudo.isFieldPresent(sfVaultID))
|
||||
{
|
||||
if (entry.getType() == ltLOAN_BROKER)
|
||||
return entry[sfVaultID] == pseudo[sfVaultID];
|
||||
|
||||
auto const vault = view.read(keylet::vault(pseudo[sfVaultID]));
|
||||
if (!vault)
|
||||
return false; // LCOV_EXCL_LINE
|
||||
return keylet::mptokenIssuance((*vault)[sfShareMPTID]).check(entry) ||
|
||||
isHolding(entry, (*vault)[sfAsset], pseudoAcctID);
|
||||
}
|
||||
|
||||
if (pseudo.isFieldPresent(sfLoanBrokerID))
|
||||
{
|
||||
if (entry.getType() == ltLOAN)
|
||||
return entry[sfLoanBrokerID] == pseudo[sfLoanBrokerID];
|
||||
|
||||
auto const broker = view.read(keylet::loanBroker(pseudo[sfLoanBrokerID]));
|
||||
if (!broker)
|
||||
return false; // LCOV_EXCL_LINE
|
||||
auto const vault = view.read(keylet::vault((*broker)[sfVaultID]));
|
||||
return vault && isHolding(entry, (*vault)[sfAsset], pseudoAcctID);
|
||||
}
|
||||
|
||||
// A new pseudo-account kind must say what it owns.
|
||||
return false; // LCOV_EXCL_LINE
|
||||
}
|
||||
|
||||
void
|
||||
ValidPseudoAccounts::visitEntry(bool isDelete, SLE::ConstRef before, SLE::ConstRef after)
|
||||
{
|
||||
@@ -1093,33 +1019,6 @@ ValidPseudoAccounts::visitEntry(bool isDelete, SLE::ConstRef before, SLE::ConstR
|
||||
return;
|
||||
}
|
||||
|
||||
// Only owner directory pages carry sfOwner; book directories don't.
|
||||
if (after && after->getType() == ltDIR_NODE && after->isFieldPresent(sfOwner))
|
||||
{
|
||||
auto const& afterIndexes = after->getFieldV256(sfIndexes);
|
||||
std::vector<uint256> added;
|
||||
if (!before)
|
||||
{
|
||||
added.assign(afterIndexes.begin(), afterIndexes.end());
|
||||
}
|
||||
else
|
||||
{
|
||||
auto const& beforeIndexes = before->getFieldV256(sfIndexes);
|
||||
for (auto const& index : afterIndexes)
|
||||
{
|
||||
if (!std::ranges::contains(beforeIndexes, index))
|
||||
added.push_back(index);
|
||||
}
|
||||
}
|
||||
|
||||
if (!added.empty())
|
||||
{
|
||||
auto& owned = ownerDirAdditions_[after->getAccountID(sfOwner)];
|
||||
owned.insert(owned.end(), added.begin(), added.end());
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (after && after->getType() == ltACCOUNT_ROOT)
|
||||
{
|
||||
bool const isPseudo = [&] {
|
||||
@@ -1201,39 +1100,6 @@ ValidPseudoAccounts::finalize(
|
||||
if (enforce)
|
||||
return false;
|
||||
}
|
||||
|
||||
// Pre-fixCleanup3_5_0: what a pseudo-account owns is not checked.
|
||||
// Post-fixCleanup3_5_0: it may own only what belongs to its AMM, Vault or
|
||||
// LoanBroker.
|
||||
if (!view.rules().enabled(fixCleanup3_5_0))
|
||||
return true;
|
||||
|
||||
for (auto const& [owner, indexes] : ownerDirAdditions_)
|
||||
{
|
||||
auto const root = view.read(keylet::account(owner));
|
||||
if (!root || !isPseudoAccount(root))
|
||||
continue;
|
||||
|
||||
for (auto const& index : indexes)
|
||||
{
|
||||
auto const sle = view.read(keylet::unchecked(index));
|
||||
if (!sle)
|
||||
{
|
||||
JLOG(j.fatal()) << "Invariant failed: pseudo-account " << toBase58(owner)
|
||||
<< " owner directory links a missing object " << to_string(index);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!isOwnedByPseudoAccount(view, *root, *sle))
|
||||
{
|
||||
JLOG(j.fatal()) << "Invariant failed: pseudo-account " << toBase58(owner)
|
||||
<< " may not own " << ledgerEntryTypeName(*sle) << " "
|
||||
<< to_string(index);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
#include <xrpl/beast/utility/Journal.h>
|
||||
#include <xrpl/beast/utility/Zero.h>
|
||||
#include <xrpl/ledger/ReadView.h>
|
||||
#include <xrpl/ledger/helpers/LendingHelpers.h>
|
||||
#include <xrpl/ledger/helpers/TokenHelpers.h>
|
||||
#include <xrpl/ledger/helpers/VaultHelpers.h>
|
||||
#include <xrpl/protocol/Asset.h>
|
||||
#include <xrpl/protocol/Feature.h>
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
@@ -160,7 +160,7 @@ ValidLoanBroker::finalize(
|
||||
roundToAsset(
|
||||
Asset{vault->at(sfAsset)},
|
||||
debtTotal,
|
||||
getVaultScale(vault),
|
||||
getAssetsTotalScale(vault),
|
||||
Number::RoundingMode::TowardsZero) != beast::kZero)
|
||||
{
|
||||
JLOG(j.fatal())
|
||||
|
||||
@@ -339,7 +339,6 @@ ValidMPTIssuance::finalize(
|
||||
// Cap on MPToken creates and deletes while featureLendingProtocol is enabled.
|
||||
// - LoanSet: at most two creates and no deletes.
|
||||
// - VaultWithdraw: at most one create and one delete.
|
||||
// - LoanBrokerDelete (post-fixCleanup3_5_0): at most one create and one delete.
|
||||
// - Other MayAuthorizeMpt types: created + deleted <= 1.
|
||||
// - MustAuthorizeMpt still requires exactly one create or delete below.
|
||||
auto const mptokensExceedAuthorizeCap = [&] {
|
||||
@@ -352,10 +351,6 @@ ValidMPTIssuance::finalize(
|
||||
if (txnType == ttVAULT_WITHDRAW)
|
||||
return mptokensCreated_ > 1 || mptokensDeleted_ > 1;
|
||||
}
|
||||
// The cover payout may create the owner's MPToken while the
|
||||
// pseudo-account's MPToken is deleted.
|
||||
if (rules.enabled(fixCleanup3_5_0) && txnType == ttLOAN_BROKER_DELETE)
|
||||
return mptokensCreated_ > 1 || mptokensDeleted_ > 1;
|
||||
return (mptokensCreated_ + mptokensDeleted_) > 1;
|
||||
};
|
||||
if (mptokensExceedAuthorizeCap())
|
||||
@@ -1023,25 +1018,24 @@ ValidMPTTransfer::finalize(
|
||||
continue;
|
||||
}
|
||||
|
||||
// This issuance is the LoanManage default's own vault asset, so the
|
||||
// broker/vault freeze and CanTransfer exemptions apply to it -- an
|
||||
// unrelated MPT the same accounts happen to hold is still caught.
|
||||
bool const isLoanDefaultAsset = loanDefaultAccounts &&
|
||||
loanDefaultAccounts->asset.holds<MPTIssue>() &&
|
||||
loanDefaultAccounts->asset.get<MPTIssue>().getMptID() == mptID;
|
||||
|
||||
// These transactions settle an existing MPT position out of a protocol
|
||||
// account. They do not require CanTransfer.
|
||||
// These transactions are recovery/settlement paths. They may move an
|
||||
// existing MPT position even after the issuer clears CanTransfer, so
|
||||
// holders are not trapped in AMM, vault, or loan protocol accounts.
|
||||
auto const waivesCanTransfer = txnType == ttAMM_WITHDRAW ||
|
||||
(view.rules().enabled(fixCleanup3_2_0) &&
|
||||
(txnType == ttVAULT_WITHDRAW || txnType == ttLOAN_BROKER_COVER_WITHDRAW ||
|
||||
txnType == ttLOAN_PAY)) ||
|
||||
(view.rules().enabled(fixCleanup3_5_0) &&
|
||||
(isLoanDefaultAsset || txnType == ttLOAN_BROKER_DELETE));
|
||||
txnType == ttLOAN_PAY));
|
||||
auto const canTransfer = sleIssuance->isFlag(lsfMPTCanTransfer) || waivesCanTransfer;
|
||||
auto const canTrade = sleIssuance->isFlag(lsfMPTCanTrade);
|
||||
auto const reqAuth = sleIssuance->isFlag(lsfMPTRequireAuth);
|
||||
|
||||
// This issuance is the LoanManage default's own vault asset, so the
|
||||
// broker/vault freeze exemption applies to it -- an unrelated MPT
|
||||
// issuance the same accounts happen to hold is still caught.
|
||||
bool const isLoanDefaultAsset = loanDefaultAccounts &&
|
||||
loanDefaultAccounts->asset.holds<MPTIssue>() &&
|
||||
loanDefaultAccounts->asset.get<MPTIssue>().getMptID() == mptID;
|
||||
|
||||
for (auto const& [account, value] : values)
|
||||
{
|
||||
// Classify each account as a sender or receiver based on whether their MPTAmount
|
||||
|
||||
@@ -7,14 +7,12 @@
|
||||
#include <xrpl/beast/utility/instrumentation.h>
|
||||
#include <xrpl/ledger/ReadView.h>
|
||||
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
|
||||
#include <xrpl/ledger/helpers/TokenHelpers.h>
|
||||
#include <xrpl/ledger/helpers/VaultHelpers.h>
|
||||
#include <xrpl/protocol/Feature.h>
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
#include <xrpl/protocol/Issue.h>
|
||||
#include <xrpl/protocol/LedgerFormats.h>
|
||||
#include <xrpl/protocol/Protocol.h>
|
||||
#include <xrpl/protocol/Rate.h>
|
||||
#include <xrpl/protocol/SField.h>
|
||||
#include <xrpl/protocol/STAmount.h>
|
||||
#include <xrpl/protocol/STLedgerEntry.h>
|
||||
@@ -65,9 +63,6 @@ ValidVault::Vault::make(SLE const& from)
|
||||
self.assetsAvailable = from.at(sfAssetsAvailable);
|
||||
self.assetsMaximum = from.at(sfAssetsMaximum);
|
||||
self.lossUnrealized = from.at(sfLossUnrealized);
|
||||
self.assetsDeployed = from[~sfAssetsDeployed].value_or(Number{0});
|
||||
self.yieldUnrealized = from[~sfYieldUnrealized].value_or(Number{0});
|
||||
self.version = decodeVaultVersion(from[~sfLEVersion]);
|
||||
self.vaultKind = from[~sfVaultKind];
|
||||
self.subscriptionDate = from[~sfSubscriptionDate];
|
||||
self.redemptionDate = from[~sfRedemptionDate];
|
||||
@@ -413,70 +408,6 @@ ValidVault::computeVaultMinScale(DeltaInfo const& vaultDelta, Rules const& rules
|
||||
return computeCoarsestScale({vaultDelta, totalDelta, availableDelta});
|
||||
}
|
||||
|
||||
bool
|
||||
ValidVault::checkTotalsAddUp(
|
||||
Vault const& afterVault,
|
||||
Vault const& beforeVault,
|
||||
Asset const& vaultAsset,
|
||||
bool fix340Enabled,
|
||||
std::int32_t minScale,
|
||||
Number const& roundedVaultDelta,
|
||||
Number const& exactVaultDelta,
|
||||
char const* verb,
|
||||
beast::Journal const& j)
|
||||
{
|
||||
bool result = true;
|
||||
|
||||
// FixedPrecision: the stored AssetsTotal is a Downward-floored cache
|
||||
// (AA + AD), not an exact running total, so comparing its delta against
|
||||
// the real transfer can be off by more than one unit purely from
|
||||
// independent floor-rounding of the two snapshots -- not an accounting
|
||||
// bug. The stored-cache-equals-derived-total check elsewhere in finalize,
|
||||
// plus the AssetsDeployed-only-changed-by-lending-transactors check,
|
||||
// already cover AssetsTotal; only compare it here on Legacy/CashBasis,
|
||||
// where AssetsTotal is written exactly alongside AA.
|
||||
if (afterVault.version != VaultVersion::FixedPrecision)
|
||||
{
|
||||
auto const assetTotalDelta =
|
||||
roundToAsset(vaultAsset, afterVault.assetsTotal - beforeVault.assetsTotal, minScale);
|
||||
bool const totalAddsUp = fix340Enabled
|
||||
? agreesWithinOneUnit(assetTotalDelta, roundedVaultDelta, vaultAsset, minScale)
|
||||
: assetTotalDelta == roundedVaultDelta;
|
||||
if (!totalAddsUp)
|
||||
{
|
||||
JLOG(j.fatal()) << "Invariant failed: " << verb
|
||||
<< " and assets outstanding must add up";
|
||||
result = false;
|
||||
}
|
||||
}
|
||||
|
||||
auto const assetAvailableDelta = roundToAsset(
|
||||
vaultAsset, afterVault.assetsAvailable - beforeVault.assetsAvailable, minScale);
|
||||
// FixedPrecision: AssetsAvailable is the exact, single writer
|
||||
// (adjustVaultBalances)-managed balance, so its delta must match the real
|
||||
// transfer exactly, with no rounding tolerance -- compare the exact
|
||||
// deltas, not the values rounded to the posterior AssetsTotal scale.
|
||||
bool const availableAddsUp = [&] {
|
||||
if (afterVault.version == VaultVersion::FixedPrecision)
|
||||
{
|
||||
return (afterVault.assetsAvailable - beforeVault.assetsAvailable) == exactVaultDelta;
|
||||
}
|
||||
if (fix340Enabled)
|
||||
{
|
||||
return agreesWithinOneUnit(
|
||||
assetAvailableDelta, roundedVaultDelta, vaultAsset, minScale);
|
||||
}
|
||||
return assetAvailableDelta == roundedVaultDelta;
|
||||
}();
|
||||
if (!availableAddsUp)
|
||||
{
|
||||
JLOG(j.fatal()) << "Invariant failed: " << verb << " and assets available must add up";
|
||||
result = false;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
bool
|
||||
ValidVault::finalize(
|
||||
STTx const& tx,
|
||||
@@ -487,7 +418,6 @@ ValidVault::finalize(
|
||||
{
|
||||
bool const enforce = view.rules().enabled(featureSingleAssetVault);
|
||||
bool const fix340Enabled = view.rules().enabled(fixCleanup3_4_0);
|
||||
bool const fix350Enabled = view.rules().enabled(fixCleanup3_5_0);
|
||||
|
||||
if (!isTesSuccess(ret))
|
||||
return true; // Do not perform checks
|
||||
@@ -655,13 +585,6 @@ ValidVault::finalize(
|
||||
"vault must have no assets available";
|
||||
result = false;
|
||||
}
|
||||
if (afterVault.version == VaultVersion::FixedPrecision &&
|
||||
afterVault.assetsDeployed != kZero)
|
||||
{
|
||||
JLOG(j.fatal()) << "Invariant failed: updated zero sized "
|
||||
"vault must have no AssetsDeployed";
|
||||
result = false;
|
||||
}
|
||||
}
|
||||
else if (updatedShares->sharesTotal > updatedShares->sharesMaximum)
|
||||
{
|
||||
@@ -686,14 +609,6 @@ ValidVault::finalize(
|
||||
else
|
||||
{
|
||||
bool const gapExceeded = [&] {
|
||||
if (afterVault.version == VaultVersion::FixedPrecision)
|
||||
{
|
||||
// FixedPrecision: both LossUnrealized and AssetsDeployed are exact
|
||||
// (never rounded), so compare them directly. AssetsTotal is
|
||||
// just a rounded cache of AssetsAvailable + AssetsDeployed and
|
||||
// is not the reference this check needs.
|
||||
return afterVault.lossUnrealized > afterVault.assetsDeployed;
|
||||
}
|
||||
if (!fix340Enabled)
|
||||
{
|
||||
return afterVault.lossUnrealized >
|
||||
@@ -716,43 +631,6 @@ ValidVault::finalize(
|
||||
}
|
||||
}
|
||||
|
||||
if (afterVault.version == VaultVersion::FixedPrecision)
|
||||
{
|
||||
if (afterVault.assetsDeployed < kZero)
|
||||
{
|
||||
JLOG(j.fatal()) << "Invariant failed: AssetsDeployed must not be negative";
|
||||
result = false;
|
||||
}
|
||||
else if (
|
||||
Number(STAmount{afterVault.asset, afterVault.assetsDeployed}) !=
|
||||
afterVault.assetsDeployed)
|
||||
{
|
||||
JLOG(j.fatal()) //
|
||||
<< "Invariant failed: AssetsDeployed must be exactly representable "
|
||||
"at the vault asset's precision";
|
||||
result = false;
|
||||
}
|
||||
|
||||
Number const expectedAssetsTotal = [&] {
|
||||
NumberRoundModeGuard const rg(Number::RoundingMode::Downward);
|
||||
return Number(
|
||||
STAmount{afterVault.asset, afterVault.assetsAvailable + afterVault.assetsDeployed});
|
||||
}();
|
||||
if (afterVault.assetsTotal != expectedAssetsTotal)
|
||||
{
|
||||
JLOG(j.fatal()) //
|
||||
<< "Invariant failed: stored AssetsTotal must equal AssetsAvailable "
|
||||
"plus AssetsDeployed, rounded Downward";
|
||||
result = false;
|
||||
}
|
||||
|
||||
if (afterVault.yieldUnrealized < kZero)
|
||||
{
|
||||
JLOG(j.fatal()) << "Invariant failed: YieldUnrealized must not be negative";
|
||||
result = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (fix340Enabled && afterVault.lossUnrealized < kZero)
|
||||
{
|
||||
JLOG(j.fatal()) << "Invariant failed: loss unrealized must not be negative";
|
||||
@@ -781,18 +659,6 @@ ValidVault::finalize(
|
||||
return !enforce; // That's all we can do here
|
||||
}
|
||||
|
||||
// ttLOAN_SET, ttLOAN_PAY and ttLOAN_MANAGE are the only transactors allowed to
|
||||
// write AssetsDeployed and YieldUnrealized; any new transactor that writes
|
||||
// either field must be added to both allow-lists below.
|
||||
if (afterVault.version == VaultVersion::FixedPrecision && !beforeVault_.empty() &&
|
||||
afterVault.assetsDeployed != beforeVault_[0].assetsDeployed && txnType != ttLOAN_SET &&
|
||||
txnType != ttLOAN_PAY && txnType != ttLOAN_MANAGE)
|
||||
{
|
||||
JLOG(j.fatal()) << //
|
||||
"Invariant failed: vault transaction must not change AssetsDeployed";
|
||||
result = false;
|
||||
}
|
||||
|
||||
if (!beforeVault_.empty() && afterVault.lossUnrealized != beforeVault_[0].lossUnrealized &&
|
||||
txnType != ttLOAN_MANAGE && txnType != ttLOAN_PAY)
|
||||
{
|
||||
@@ -802,16 +668,6 @@ ValidVault::finalize(
|
||||
result = false;
|
||||
}
|
||||
|
||||
if (afterVault.version == VaultVersion::FixedPrecision && !beforeVault_.empty() &&
|
||||
afterVault.yieldUnrealized != beforeVault_[0].yieldUnrealized && txnType != ttLOAN_SET &&
|
||||
txnType != ttLOAN_PAY && txnType != ttLOAN_MANAGE)
|
||||
{
|
||||
JLOG(j.fatal()) << //
|
||||
"Invariant failed: vault transaction must not change yield "
|
||||
"unrealized";
|
||||
result = false;
|
||||
}
|
||||
|
||||
// Immutability of VaultKind, SubscriptionDate and RedemptionDate is enforced by
|
||||
// NoModifiedUnmodifiableFields in InvariantCheck.cpp.
|
||||
|
||||
@@ -860,9 +716,7 @@ ValidVault::finalize(
|
||||
}
|
||||
|
||||
if (afterVault.assetsAvailable != kZero || afterVault.assetsTotal != kZero ||
|
||||
afterVault.lossUnrealized != kZero || updatedShares->sharesTotal != 0 ||
|
||||
(afterVault.version == VaultVersion::FixedPrecision &&
|
||||
(afterVault.assetsDeployed != kZero || afterVault.yieldUnrealized != kZero)))
|
||||
afterVault.lossUnrealized != kZero || updatedShares->sharesTotal != 0)
|
||||
{
|
||||
JLOG(j.fatal()) //
|
||||
<< "Invariant failed: created vault must be empty";
|
||||
@@ -1026,17 +880,7 @@ ValidVault::finalize(
|
||||
result = false;
|
||||
}
|
||||
|
||||
// FixedPrecision: AssetsAvailable is exact and the pseudo-account
|
||||
// delta is exact; rounding either one down to the posterior
|
||||
// AssetsTotal scale before the sign check can turn a legitimate
|
||||
// sub-unit deposit into a false zero. Compare the exact deltas
|
||||
// instead; Legacy/CashBasis keep the rounded comparison.
|
||||
bool const isFixedPrecision = afterVault.version == VaultVersion::FixedPrecision;
|
||||
Number const exactVaultDeltaAssets = maybeVaultDeltaAssets->delta;
|
||||
Number const signCheckDeltaAssets =
|
||||
isFixedPrecision ? exactVaultDeltaAssets : vaultDeltaAssets;
|
||||
|
||||
if (signCheckDeltaAssets <= kZero)
|
||||
if (vaultDeltaAssets <= kZero)
|
||||
{
|
||||
JLOG(j.fatal()) << //
|
||||
"Invariant failed: deposit must increase vault balance";
|
||||
@@ -1132,17 +976,29 @@ ValidVault::finalize(
|
||||
result = false;
|
||||
}
|
||||
|
||||
if (!checkTotalsAddUp(
|
||||
afterVault,
|
||||
beforeVault,
|
||||
vaultAsset,
|
||||
fix340Enabled,
|
||||
minScale,
|
||||
vaultDeltaAssets,
|
||||
exactVaultDeltaAssets,
|
||||
"deposit",
|
||||
j))
|
||||
auto const assetTotalDelta = roundToAsset(
|
||||
vaultAsset, afterVault.assetsTotal - beforeVault.assetsTotal, minScale);
|
||||
bool const totalAddsUp = fix340Enabled
|
||||
? agreesWithinOneUnit(assetTotalDelta, vaultDeltaAssets, vaultAsset, minScale)
|
||||
: assetTotalDelta == vaultDeltaAssets;
|
||||
if (!totalAddsUp)
|
||||
{
|
||||
JLOG(j.fatal())
|
||||
<< "Invariant failed: deposit and assets outstanding must add up";
|
||||
result = false;
|
||||
}
|
||||
|
||||
auto const assetAvailableDelta = roundToAsset(
|
||||
vaultAsset, afterVault.assetsAvailable - beforeVault.assetsAvailable, minScale);
|
||||
bool const availableAddsUp = fix340Enabled
|
||||
? agreesWithinOneUnit(
|
||||
assetAvailableDelta, vaultDeltaAssets, vaultAsset, minScale)
|
||||
: assetAvailableDelta == vaultDeltaAssets;
|
||||
if (!availableAddsUp)
|
||||
{
|
||||
JLOG(j.fatal()) << "Invariant failed: deposit and assets available must add up";
|
||||
result = false;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
@@ -1198,17 +1054,7 @@ ValidVault::finalize(
|
||||
auto const vaultPseudoDeltaAssets =
|
||||
roundToAsset(vaultAsset, vaultDeltaAssets.delta, minScale);
|
||||
|
||||
// FixedPrecision: AssetsAvailable is exact and the pseudo-account
|
||||
// delta is exact; rounding either one down to the posterior
|
||||
// AssetsTotal scale before the sign check can turn a legitimate
|
||||
// sub-unit withdrawal into a false zero and reject it. Compare
|
||||
// the exact deltas instead; Legacy/CashBasis keep the rounded
|
||||
// comparison.
|
||||
bool const isFixedPrecision = afterVault.version == VaultVersion::FixedPrecision;
|
||||
Number const signCheckDeltaAssets =
|
||||
isFixedPrecision ? vaultDeltaAssets.delta : vaultPseudoDeltaAssets;
|
||||
|
||||
if (!zeroDeltaIsLegitimate && signCheckDeltaAssets >= kZero)
|
||||
if (!zeroDeltaIsLegitimate && vaultPseudoDeltaAssets >= kZero)
|
||||
{
|
||||
JLOG(j.fatal()) << "Invariant failed: withdrawal must decrease vault balance";
|
||||
result = false;
|
||||
@@ -1298,18 +1144,6 @@ ValidVault::finalize(
|
||||
|
||||
auto const localPseudoDeltaAssets =
|
||||
roundToAsset(vaultAsset, vaultPseudoDeltaAssets, localMinScale);
|
||||
auto const rate = transferRate(view, vaultAsset);
|
||||
bool const feeAdjustedWithdrawal = fix350Enabled && distinctDestination &&
|
||||
!vaultAsset.native() && tx[sfAccount] != vaultAsset.getIssuer() &&
|
||||
rate != kParityRate;
|
||||
auto expectedDestinationDelta = localPseudoDeltaAssets * -1;
|
||||
if (feeAdjustedWithdrawal)
|
||||
{
|
||||
expectedDestinationDelta =
|
||||
subtractTransferFee(
|
||||
STAmount{vaultAsset, expectedDestinationDelta}, rate)
|
||||
.value();
|
||||
}
|
||||
// For IOU assets near a precision boundary the destination's STAmount
|
||||
// exponent can shift, making part of the sent value unrepresentable at
|
||||
// the receiver's new scale — that portion is irreversibly absorbed by the
|
||||
@@ -1318,8 +1152,7 @@ ValidVault::finalize(
|
||||
// the destination's scale. Floor rounding is used so that values exactly
|
||||
// at the step boundary are not mistakenly dismissed. Any representable
|
||||
// discrepancy indicates a real accounting bug and must be caught.
|
||||
auto const destroyedIsSubUlp = !feeAdjustedWithdrawal &&
|
||||
tolerateZeroDelta &&
|
||||
auto const destroyedIsSubUlp = tolerateZeroDelta &&
|
||||
roundToAsset(
|
||||
vaultAsset,
|
||||
vaultDeltaAssets.delta * -1 - destinationDelta.delta,
|
||||
@@ -1327,11 +1160,11 @@ ValidVault::finalize(
|
||||
Number::RoundingMode::Downward) == kZero;
|
||||
bool const withdrawAddsUp = fix340Enabled
|
||||
? agreesWithinOneUnit(
|
||||
expectedDestinationDelta,
|
||||
localPseudoDeltaAssets * -1,
|
||||
roundedDestinationDelta,
|
||||
vaultAsset,
|
||||
localMinScale)
|
||||
: expectedDestinationDelta == roundedDestinationDelta;
|
||||
: localPseudoDeltaAssets * -1 == roundedDestinationDelta;
|
||||
if (!destroyedIsSubUlp && !withdrawAddsUp)
|
||||
{
|
||||
JLOG(j.fatal()) << "Invariant failed: " << //
|
||||
@@ -1372,17 +1205,33 @@ ValidVault::finalize(
|
||||
result = false;
|
||||
}
|
||||
|
||||
if (!checkTotalsAddUp(
|
||||
afterVault,
|
||||
beforeVault,
|
||||
vaultAsset,
|
||||
fix340Enabled,
|
||||
minScale,
|
||||
vaultPseudoDeltaAssets,
|
||||
vaultDeltaAssets.delta,
|
||||
"withdrawal",
|
||||
j))
|
||||
auto const assetTotalDelta = roundToAsset(
|
||||
vaultAsset, afterVault.assetsTotal - beforeVault.assetsTotal, minScale);
|
||||
// Note, vaultBalance is negative (see check above)
|
||||
bool const totalAddsUp = fix340Enabled
|
||||
? agreesWithinOneUnit(
|
||||
assetTotalDelta, vaultPseudoDeltaAssets, vaultAsset, minScale)
|
||||
: assetTotalDelta == vaultPseudoDeltaAssets;
|
||||
if (!totalAddsUp)
|
||||
{
|
||||
JLOG(j.fatal())
|
||||
<< "Invariant failed: withdrawal and assets outstanding must add up";
|
||||
result = false;
|
||||
}
|
||||
|
||||
auto const assetAvailableDelta = roundToAsset(
|
||||
vaultAsset, afterVault.assetsAvailable - beforeVault.assetsAvailable, minScale);
|
||||
|
||||
bool const availableAddsUp = fix340Enabled
|
||||
? agreesWithinOneUnit(
|
||||
assetAvailableDelta, vaultPseudoDeltaAssets, vaultAsset, minScale)
|
||||
: assetAvailableDelta == vaultPseudoDeltaAssets;
|
||||
if (!availableAddsUp)
|
||||
{
|
||||
JLOG(j.fatal())
|
||||
<< "Invariant failed: withdrawal and assets available must add up";
|
||||
result = false;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
@@ -1414,33 +1263,39 @@ ValidVault::finalize(
|
||||
computeVaultMinScale(*maybeVaultDeltaAssets, view.rules());
|
||||
auto const vaultDeltaAssets =
|
||||
roundToAsset(vaultAsset, maybeVaultDeltaAssets->delta, minScale);
|
||||
// FixedPrecision: AssetsAvailable is exact and the
|
||||
// pseudo-account delta is exact; rounding either one down
|
||||
// to the posterior AssetsTotal scale before the sign
|
||||
// check can turn a legitimate sub-unit clawback into a
|
||||
// false zero and reject it. Compare the exact deltas
|
||||
// instead; Legacy/CashBasis keep the rounded comparison.
|
||||
bool const isFixedPrecision =
|
||||
afterVault.version == VaultVersion::FixedPrecision;
|
||||
Number const signCheckDeltaAssets =
|
||||
isFixedPrecision ? maybeVaultDeltaAssets->delta : vaultDeltaAssets;
|
||||
if (signCheckDeltaAssets >= kZero)
|
||||
if (vaultDeltaAssets >= kZero)
|
||||
{
|
||||
JLOG(j.fatal()) << "Invariant failed: clawback must decrease vault balance";
|
||||
result = false;
|
||||
}
|
||||
|
||||
if (!checkTotalsAddUp(
|
||||
afterVault,
|
||||
beforeVault,
|
||||
vaultAsset,
|
||||
fix340Enabled,
|
||||
minScale,
|
||||
vaultDeltaAssets,
|
||||
maybeVaultDeltaAssets->delta,
|
||||
"clawback",
|
||||
j))
|
||||
auto const assetsTotalDelta = roundToAsset(
|
||||
vaultAsset, afterVault.assetsTotal - beforeVault.assetsTotal, minScale);
|
||||
bool const totalAddsUp = fix340Enabled
|
||||
? agreesWithinOneUnit(
|
||||
assetsTotalDelta, vaultDeltaAssets, vaultAsset, minScale)
|
||||
: assetsTotalDelta == vaultDeltaAssets;
|
||||
if (!totalAddsUp)
|
||||
{
|
||||
JLOG(j.fatal()) << //
|
||||
"Invariant failed: clawback and assets outstanding must add up";
|
||||
result = false;
|
||||
}
|
||||
|
||||
auto const assetAvailableDelta = roundToAsset(
|
||||
vaultAsset,
|
||||
afterVault.assetsAvailable - beforeVault.assetsAvailable,
|
||||
minScale);
|
||||
bool const availableAddsUp = fix340Enabled
|
||||
? agreesWithinOneUnit(
|
||||
assetAvailableDelta, vaultDeltaAssets, vaultAsset, minScale)
|
||||
: assetAvailableDelta == vaultDeltaAssets;
|
||||
if (!availableAddsUp)
|
||||
{
|
||||
JLOG(j.fatal()) << //
|
||||
"Invariant failed: clawback and assets available must add up";
|
||||
result = false;
|
||||
}
|
||||
}
|
||||
else if (!isVaultEmpty(beforeVault))
|
||||
{
|
||||
|
||||
@@ -177,7 +177,7 @@ AMMLiquidity<TIn, TOut>::getOffer(ReadView const& view, std::optional<Quality> c
|
||||
return maxOffer(balances);
|
||||
}
|
||||
if (auto const amounts =
|
||||
changeSpotPriceQuality(balances, *clobQuality, tradingFee_, j_))
|
||||
changeSpotPriceQuality(balances, *clobQuality, tradingFee_, view.rules(), j_))
|
||||
{
|
||||
return AMMOffer<TIn, TOut>(*this, *amounts, balances, Quality{*amounts});
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
#include <xrpl/protocol/MPTIssue.h>
|
||||
#include <xrpl/protocol/Quality.h>
|
||||
#include <xrpl/protocol/Rate.h>
|
||||
#include <xrpl/protocol/Rules.h>
|
||||
#include <xrpl/protocol/SField.h>
|
||||
#include <xrpl/protocol/STAmount.h>
|
||||
#include <xrpl/protocol/TER.h>
|
||||
@@ -82,11 +83,6 @@ protected:
|
||||
std::optional<AMMLiquidity<TIn, TOut>> ammLiquidity_;
|
||||
beast::Journal const j_;
|
||||
Asset const strandDeliver_;
|
||||
// MaximumAmount of book_.in if it is an MPT. Taking an MPT offer has the
|
||||
// issuer send TakerPays to the owner, and a send above the cap is rejected
|
||||
// (isMPTOverflow). forEachOffer limits the offer's input to the cap so it
|
||||
// fills partially instead of failing the strand.
|
||||
std::optional<TIn> maxIn_;
|
||||
|
||||
struct Cache
|
||||
{
|
||||
@@ -110,12 +106,6 @@ private:
|
||||
, j_(ctx.j)
|
||||
, strandDeliver_(ctx.strandDeliver)
|
||||
{
|
||||
if constexpr (std::is_same_v<TIn, MPTAmount>)
|
||||
{
|
||||
if (auto const max = maxMPTAmount(ctx.view, in.get<MPTIssue>().getMptID()))
|
||||
maxIn_.emplace(*max);
|
||||
}
|
||||
|
||||
if (auto const ammSle = ctx.view.read(keylet::amm(in, out));
|
||||
ammSle && ammSle->getFieldAmount(sfLPTokenBalance) != beast::kZero)
|
||||
{
|
||||
@@ -354,7 +344,8 @@ public:
|
||||
Quality const& ofrQ,
|
||||
DebtDirection prevStepDir,
|
||||
WaiveTransferFee waiveFee,
|
||||
OfferType) const
|
||||
OfferType,
|
||||
Rules const&) const
|
||||
{
|
||||
// Charge the offer owner, not the sender
|
||||
// Charge a fee even if the owner is the same as the issuer
|
||||
@@ -525,7 +516,8 @@ public:
|
||||
Quality const& ofrQ,
|
||||
DebtDirection prevStepDir,
|
||||
WaiveTransferFee waiveFee,
|
||||
OfferType offerType) const
|
||||
OfferType offerType,
|
||||
Rules const& rules) const
|
||||
{
|
||||
// Offer x-ing does not charge a transfer fee when the offer's owner
|
||||
// is the same as the strand dst. It is important that
|
||||
@@ -537,6 +529,10 @@ public:
|
||||
// Single path AMM offer has to factor in the transfer in rate
|
||||
// when calculating the upper bound quality and the quality function
|
||||
// because single path AMM's offer quality is not constant.
|
||||
if (!rules.enabled(fixAMMv1_1))
|
||||
{
|
||||
return ofrQ;
|
||||
}
|
||||
if (offerType == OfferType::Clob ||
|
||||
(this->ammLiquidity_ && this->ammLiquidity_->multiPath()))
|
||||
{
|
||||
@@ -588,7 +584,7 @@ BookStep<TIn, TOut, TDerived>::qualityUpperBound(ReadView const& v, DebtDirectio
|
||||
: WaiveTransferFee::No;
|
||||
|
||||
Quality const q = static_cast<TDerived const*>(this)->adjustQualityWithFees(
|
||||
v, std::get<Quality>(*res), prevStepDir, waiveFee, std::get<OfferType>(*res));
|
||||
v, std::get<Quality>(*res), prevStepDir, waiveFee, std::get<OfferType>(*res), v.rules());
|
||||
return {q, dir};
|
||||
}
|
||||
|
||||
@@ -607,7 +603,7 @@ BookStep<TIn, TOut, TDerived>::getQualityFunc(ReadView const& v, DebtDirection p
|
||||
{
|
||||
auto static const kQOne = Quality{STAmount::kURateOne};
|
||||
auto const q = static_cast<TDerived const*>(this)->adjustQualityWithFees(
|
||||
v, kQOne, prevStepDir, WaiveTransferFee::Yes, OfferType::Amm);
|
||||
v, kQOne, prevStepDir, WaiveTransferFee::Yes, OfferType::Amm, v.rules());
|
||||
if (q == kQOne)
|
||||
return {res, dir};
|
||||
QualityFunction qf{q, QualityFunction::CLOBLikeTag{}};
|
||||
@@ -622,7 +618,8 @@ BookStep<TIn, TOut, TDerived>::getQualityFunc(ReadView const& v, DebtDirection p
|
||||
// always has quality set
|
||||
prevStepDir,
|
||||
WaiveTransferFee::No,
|
||||
OfferType::Clob);
|
||||
OfferType::Clob,
|
||||
v.rules());
|
||||
return {QualityFunction{q, QualityFunction::CLOBLikeTag{}}, dir};
|
||||
}
|
||||
|
||||
@@ -633,38 +630,6 @@ BookStep<TIn, TOut, TDerived>::offersUsed() const
|
||||
return offersUsed_;
|
||||
}
|
||||
|
||||
// Limit the offer to the given input (net of the taker's transfer fee), and
|
||||
// the step output and owner's charge with it. The caller sets stpAmt.in.
|
||||
template <class TIn, class TOut, class Offer>
|
||||
static void
|
||||
limitOfferIn(
|
||||
Offer const& offer,
|
||||
TAmounts<TIn, TOut>& ofrAmt,
|
||||
TAmounts<TIn, TOut>& stpAmt,
|
||||
TOut& ownerGives,
|
||||
std::uint32_t transferRateOut,
|
||||
TIn const& inLmt)
|
||||
{
|
||||
// It turns out we can prevent order book blocking by (strictly)
|
||||
// rounding down the ceil_in() result. By rounding down we guarantee
|
||||
// that the quality of an offer left in the ledger is as good or
|
||||
// better than the quality of the containing order book page.
|
||||
//
|
||||
// This adjustment changes transaction outcomes, so it must be made
|
||||
// under an amendment.
|
||||
ofrAmt = offer.limitIn(ofrAmt, inLmt, /* roundUp */ false);
|
||||
stpAmt.out = ofrAmt.out;
|
||||
// Round up for MPT output so the offer owner pays the full
|
||||
// ceil(amount × rate) fee, matching direct Payment semantics. IOU uses
|
||||
// floating-point arithmetic so the floor/ceil distinction is sub-epsilon
|
||||
// there; preserve the historical false to avoid changing IOU behavior.
|
||||
ownerGives = mulRatio(
|
||||
ofrAmt.out,
|
||||
transferRateOut,
|
||||
QUALITY_ONE,
|
||||
/*roundUp*/ std::is_same_v<TOut, MPTAmount>);
|
||||
}
|
||||
|
||||
// Adjust the offer amount and step amount subject to the given input limit
|
||||
template <class TIn, class TOut, class Offer>
|
||||
static void
|
||||
@@ -681,7 +646,24 @@ limitStepIn(
|
||||
{
|
||||
stpAmt.in = limit;
|
||||
auto const inLmt = mulRatio(stpAmt.in, QUALITY_ONE, transferRateIn, /*roundUp*/ false);
|
||||
limitOfferIn(offer, ofrAmt, stpAmt, ownerGives, transferRateOut, inLmt);
|
||||
// It turns out we can prevent order book blocking by (strictly)
|
||||
// rounding down the ceil_in() result. By rounding down we guarantee
|
||||
// that the quality of an offer left in the ledger is as good or
|
||||
// better than the quality of the containing order book page.
|
||||
//
|
||||
// This adjustment changes transaction outcomes, so it must be made
|
||||
// under an amendment.
|
||||
ofrAmt = offer.limitIn(ofrAmt, inLmt, /* roundUp */ false);
|
||||
stpAmt.out = ofrAmt.out;
|
||||
// Round up for MPT output so the offer owner pays the full
|
||||
// ceil(amount × rate) fee, matching direct Payment semantics. IOU uses
|
||||
// floating-point arithmetic so the floor/ceil distinction is sub-epsilon
|
||||
// there; preserve the historical false to avoid changing IOU behavior.
|
||||
ownerGives = mulRatio(
|
||||
ofrAmt.out,
|
||||
transferRateOut,
|
||||
QUALITY_ONE,
|
||||
/*roundUp*/ std::is_same_v<TOut, MPTAmount>);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -803,45 +785,29 @@ BookStep<TIn, TOut, TDerived>::forEachOffer(
|
||||
auto ofrAmt = offer.amount();
|
||||
TAmounts stpAmt{ofrAmt.in, ofrAmt.out};
|
||||
auto ownerGives = ofrAmt.out;
|
||||
// A bid above MaximumAmount is limited to it below (see maxIn_). Gross
|
||||
// up no more than the cap: the whole bid may not fit once grossed up
|
||||
// even though a fill at the cap does.
|
||||
bool const capInMPT = prevStep_ && maxIn_ && offer.owner() != assetIn.getIssuer();
|
||||
auto const amtIn = [&](TIn const& in) { return capInMPT && *maxIn_ < in ? *maxIn_ : in; };
|
||||
try
|
||||
{
|
||||
// All arithmetic in this block runs before the offer is consumed.
|
||||
// A crafted MPTokensV2 offer can overflow while transfer rates or
|
||||
// crossing limits are applied; remove that unusable offer instead
|
||||
// of letting it persist as a tecINTERNAL source.
|
||||
stpAmt.in = mulRatio(amtIn(ofrAmt.in), ofrInRate, QUALITY_ONE, /*roundUp*/ true);
|
||||
stpAmt.in = mulRatio(ofrAmt.in, ofrInRate, QUALITY_ONE, /*roundUp*/ true);
|
||||
|
||||
// owner pays the transfer fee. An MPT gross-up that doesn't fit in
|
||||
// an MPT amount exceeds any owner's funds (an MPT issuer's funds
|
||||
// are what it can still issue), so a large, partially funded offer
|
||||
// is limited to its funds below instead of removed as an overflow.
|
||||
std::optional<TOut> grossOut;
|
||||
if constexpr (std::is_same_v<TOut, MPTAmount>)
|
||||
{
|
||||
grossOut = tryMulRatio(ofrAmt.out, ofrOutRate, QUALITY_ONE, /*roundUp*/ true);
|
||||
}
|
||||
else
|
||||
{
|
||||
grossOut = mulRatio(ofrAmt.out, ofrOutRate, QUALITY_ONE, /*roundUp*/ false);
|
||||
}
|
||||
if (grossOut)
|
||||
ownerGives = *grossOut;
|
||||
// owner pays the transfer fee.
|
||||
ownerGives = mulRatio(
|
||||
ofrAmt.out,
|
||||
ofrOutRate,
|
||||
QUALITY_ONE,
|
||||
/*roundUp*/ std::is_same_v<TOut, MPTAmount>);
|
||||
|
||||
auto const funds = offer.isFunded()
|
||||
? ownerGives // Offer owner is IOU issuer; they have unlimited funds
|
||||
? ownerGives // Offer owner is issuer; they have unlimited funds
|
||||
: offers.ownerFunds();
|
||||
|
||||
// Only a CLOB offer: an AMM offer is funded and its output rate is
|
||||
// parity, so its gross-up always fits.
|
||||
if (!grossOut || funds < ownerGives)
|
||||
// Only if CLOB offer
|
||||
if (funds < ownerGives)
|
||||
{
|
||||
// The owner isn't an IOU issuer (those are funded). An MPT
|
||||
// issuer can be here: its funds are what it can still issue.
|
||||
// We already know offer.owner()!=offer.issueOut().account
|
||||
ownerGives = funds;
|
||||
stpAmt.out = mulRatio(ownerGives, QUALITY_ONE, ofrOutRate, /*roundUp*/ false);
|
||||
|
||||
@@ -850,85 +816,32 @@ BookStep<TIn, TOut, TDerived>::forEachOffer(
|
||||
// transaction outcomes, so it must be made under an amendment.
|
||||
ofrAmt = offer.limitOut(ofrAmt, stpAmt.out, /*roundUp*/ false);
|
||||
|
||||
stpAmt.in = mulRatio(amtIn(ofrAmt.in), ofrInRate, QUALITY_ONE, /*roundUp*/ true);
|
||||
|
||||
// Fee clip can floor in or out to 0. OfferStream is
|
||||
// fee-blind, so it misses both: out==0 blocks the book;
|
||||
// in==0 / out>0 consumes ownerGives for free. MPT at
|
||||
// ordinary sizes; IOU only at the smallest out — V2
|
||||
// because that IOU outcome changes.
|
||||
if (sb.rules().enabled(featureMPTokensV2) &&
|
||||
(stpAmt.out <= beast::kZero || ofrAmt.in <= beast::kZero))
|
||||
{
|
||||
removeOffer(
|
||||
"Removing offer whose fee-clipped amount rounds to "
|
||||
"zero");
|
||||
return true;
|
||||
}
|
||||
|
||||
// out = floor(funds / rate), so out × rate <= funds and
|
||||
// ceil(out × rate) <= funds since funds is an integer. Charge
|
||||
// that instead of funds; charging all of funds would burn the
|
||||
// fraction that can't be delivered. IOU keeps funds; the
|
||||
// difference is sub-epsilon there.
|
||||
if constexpr (std::is_same_v<TOut, MPTAmount>)
|
||||
{
|
||||
ownerGives = mulRatio(stpAmt.out, ofrOutRate, QUALITY_ONE, /*roundUp*/ true);
|
||||
}
|
||||
stpAmt.in = mulRatio(ofrAmt.in, ofrInRate, QUALITY_ONE, /*roundUp*/ true);
|
||||
}
|
||||
|
||||
// Limit offer's input if MPT and the offer is not owned by the
|
||||
// issuer (an issuer's send to itself is a no-op).
|
||||
bool inLimited = false;
|
||||
std::optional<TIn> limitIn;
|
||||
// Limit offer's input if MPT, BookStep is the first step (an issuer
|
||||
// is making a cross-currency payment), and this offer is not owned
|
||||
// by the issuer. Otherwise, OutstandingAmount may overflow.
|
||||
auto const& issuer = assetIn.getIssuer();
|
||||
if (isAssetInMPT && offer.owner() != issuer)
|
||||
if (isAssetInMPT && !prevStep_ && offer.owner() != issuer)
|
||||
{
|
||||
if (!prevStep_)
|
||||
// Funds available to issue
|
||||
auto const available = toAmount<TIn>(accountFunds(
|
||||
sb,
|
||||
issuer,
|
||||
assetIn, // STAmount{0}, but the default is not used
|
||||
FreezeHandling::IgnoreFreeze,
|
||||
AuthHandling::IgnoreAuth,
|
||||
j_));
|
||||
if (stpAmt.in > available)
|
||||
{
|
||||
// BookStep is the first step (an issuer is making a
|
||||
// cross-currency payment). Limit to the funds available to
|
||||
// issue, otherwise OutstandingAmount may overflow. This
|
||||
// limits the taker, not the offer: its remainder is still
|
||||
// funded by the owner, so the callback keeps it. With
|
||||
// nothing left to issue there is nothing to consume, so
|
||||
// stop before touching the offer.
|
||||
auto const available = toAmount<TIn>(accountFunds(
|
||||
sb,
|
||||
issuer,
|
||||
assetIn, // STAmount{0}, but the default is not used
|
||||
FreezeHandling::IgnoreFreeze,
|
||||
AuthHandling::IgnoreAuth,
|
||||
j_));
|
||||
if (available <= beast::kZero)
|
||||
return false;
|
||||
if (stpAmt.in > available)
|
||||
{
|
||||
inLimited = true;
|
||||
limitIn = available;
|
||||
}
|
||||
}
|
||||
else if (maxIn_ && ofrAmt.in > *maxIn_)
|
||||
{
|
||||
// Limit to MaximumAmount (see maxIn_). The remainder is
|
||||
// still funded, so the callback keeps the offer instead of
|
||||
// consuming it. stpAmt.in is already the grossed-up cap.
|
||||
inLimited = true;
|
||||
limitOfferIn(offer, ofrAmt, stpAmt, ownerGives, ofrOutRate, *maxIn_);
|
||||
limitStepIn(
|
||||
offer, ofrAmt, stpAmt, ownerGives, ofrInRate, ofrOutRate, available);
|
||||
}
|
||||
}
|
||||
if (limitIn)
|
||||
limitStepIn(offer, ofrAmt, stpAmt, ownerGives, ofrInRate, ofrOutRate, *limitIn);
|
||||
|
||||
// amtIn() grossed up no more than the cap, so the offer must have
|
||||
// been limited to it above or the taker pays for less than is
|
||||
// consumed.
|
||||
XRPL_ASSERT(
|
||||
!capInMPT || ofrAmt.in <= *maxIn_,
|
||||
"xrpl::BookStep::forEachOffer : capped offer input is limited");
|
||||
|
||||
offerAttempted = true;
|
||||
return callback(offer, ofrAmt, stpAmt, ownerGives, ofrInRate, ofrOutRate, inLimited);
|
||||
return callback(offer, ofrAmt, stpAmt, ownerGives, ofrInRate, ofrOutRate);
|
||||
}
|
||||
catch (std::overflow_error const&)
|
||||
{
|
||||
@@ -963,7 +876,7 @@ BookStep<TIn, TOut, TDerived>::forEachOffer(
|
||||
// If offer crossing then use either LOB quality or nullopt
|
||||
// to prevent AMM being blocked by a lower quality LOB.
|
||||
auto const qualityThreshold = [&] -> std::optional<Quality> {
|
||||
if (lobQuality)
|
||||
if (sb.rules().enabled(fixAMMv1_1) && lobQuality)
|
||||
return static_cast<TDerived const*>(this)->qualityThreshold(*lobQuality);
|
||||
return lobQuality;
|
||||
}();
|
||||
@@ -1093,7 +1006,7 @@ BookStep<TIn, TOut, TDerived>::tip(ReadView const& view) const
|
||||
// as the result a LOB offer is partially crossed, and it might take a few
|
||||
// iterations to fully cross the offer.
|
||||
auto const qualityThreshold = [&] -> std::optional<Quality> {
|
||||
if (lobQuality)
|
||||
if (view.rules().enabled(fixAMMv1_1) && lobQuality)
|
||||
return static_cast<TDerived const*>(this)->qualityThreshold(*lobQuality);
|
||||
return std::nullopt;
|
||||
}();
|
||||
@@ -1181,8 +1094,7 @@ BookStep<TIn, TOut, TDerived>::revImp(
|
||||
TAmounts<TIn, TOut> const& stpAmt,
|
||||
TOut const& ownerGives,
|
||||
std::uint32_t transferRateIn,
|
||||
std::uint32_t transferRateOut,
|
||||
bool inLimited) mutable -> bool {
|
||||
std::uint32_t transferRateOut) mutable -> bool {
|
||||
if (remainingOut <= beast::kZero)
|
||||
return false;
|
||||
|
||||
@@ -1194,11 +1106,8 @@ BookStep<TIn, TOut, TDerived>::revImp(
|
||||
remainingOut = out - result.out;
|
||||
this->consumeOffer(sb, offer, ofrAmt, stpAmt, ownerGives);
|
||||
// return true b/c even if the payment is satisfied,
|
||||
// we need to consume the offer. An offer limited by what the
|
||||
// issuer can issue is not consumed, though: its remainder is still
|
||||
// funded, and no more of that MPT can flow through this step
|
||||
// anyway.
|
||||
return !inLimited;
|
||||
// we need to consume the offer
|
||||
return true;
|
||||
}
|
||||
|
||||
auto ofrAdjAmt = ofrAmt;
|
||||
@@ -1301,8 +1210,7 @@ BookStep<TIn, TOut, TDerived>::fwdImp(
|
||||
TAmounts<TIn, TOut> const& stpAmt,
|
||||
TOut const& ownerGives,
|
||||
std::uint32_t transferRateIn,
|
||||
std::uint32_t transferRateOut,
|
||||
bool inLimited) mutable -> bool {
|
||||
std::uint32_t transferRateOut) mutable -> bool {
|
||||
XRPL_ASSERT(cache_, "xrpl::BookStep::fwdImp::eachOffer : cache is set");
|
||||
|
||||
if (remainingIn <= beast::kZero)
|
||||
@@ -1332,10 +1240,8 @@ BookStep<TIn, TOut, TDerived>::fwdImp(
|
||||
savedInsAdj.insert(stpAmt.in);
|
||||
lastOut = savedOutsAdj.insert(stpAmt.out);
|
||||
resultAdj = TAmounts<TIn, TOut>(sum(savedInsAdj), sum(savedOutsAdj));
|
||||
// consume the offer even if stepAmt.in == remainingIn, unless it
|
||||
// was limited by what the issuer can issue: its remainder is
|
||||
// still funded and must stay on the book.
|
||||
processMore = !inLimited;
|
||||
// consume the offer even if stepAmt.in == remainingIn
|
||||
processMore = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
@@ -356,10 +356,10 @@ TOfferStreamBase<TIn, TOut>::step()
|
||||
offer_ = TOffer<TIn, TOut>{};
|
||||
continue;
|
||||
}
|
||||
// The strict reduction only throws std::overflow_error for a
|
||||
// crafted MPT offer, and MPT offers require featureMPTokensV2
|
||||
// (enforced at OfferCreate preflight). So the amendment is always
|
||||
// enabled here and this legacy re-throw is unreachable in practice.
|
||||
// The strict reduction only overflows for a crafted MPT offer, and
|
||||
// MPT offers require featureMPTokensV2 (enforced at OfferCreate
|
||||
// preflight). So the amendment is always enabled here and this
|
||||
// legacy re-throw is unreachable in practice.
|
||||
// LCOV_EXCL_START
|
||||
XRPL_ASSERT(
|
||||
view_.rules().enabled(featureMPTokensV2),
|
||||
|
||||
@@ -354,8 +354,11 @@ AMMWithdraw::applyGuts(Sandbox& sb)
|
||||
|
||||
// Due to rounding, the LPTokenBalance of the last LP
|
||||
// might not match the LP's trustline balance
|
||||
if (auto const res = verifyAndAdjustLPTokenBalance(sb, lpTokens, ammSle, accountID_); !res)
|
||||
return {res.error(), false}; // LCOV_EXCL_LINE
|
||||
if (sb.rules().enabled(fixAMMv1_1))
|
||||
{
|
||||
if (auto const res = verifyAndAdjustLPTokenBalance(sb, lpTokens, ammSle, accountID_); !res)
|
||||
return {res.error(), false};
|
||||
}
|
||||
|
||||
auto const tfee = getTradingFee(ctx_.view(), *ammSle, accountID_);
|
||||
|
||||
@@ -588,7 +591,7 @@ AMMWithdraw::withdraw(
|
||||
|
||||
// Should not happen since the only LP on last withdraw
|
||||
// has the balance set to the lp token trustline balance.
|
||||
if (lpTokensWithdrawActual > lpTokensAMMBalance)
|
||||
if (view.rules().enabled(fixAMMv1_1) && lpTokensWithdrawActual > lpTokensAMMBalance)
|
||||
{
|
||||
// LCOV_EXCL_START
|
||||
JLOG(journal.debug()) << "AMM Withdraw: failed to withdraw, unexpected LP tokens: "
|
||||
|
||||
@@ -24,7 +24,6 @@
|
||||
#include <xrpl/protocol/Issue.h>
|
||||
#include <xrpl/protocol/Keylet.h>
|
||||
#include <xrpl/protocol/LedgerFormats.h>
|
||||
#include <xrpl/protocol/MPTAmount.h>
|
||||
#include <xrpl/protocol/MPTIssue.h>
|
||||
#include <xrpl/protocol/Protocol.h>
|
||||
#include <xrpl/protocol/Quality.h>
|
||||
@@ -51,7 +50,6 @@
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <stdexcept>
|
||||
#include <tuple>
|
||||
#include <utility>
|
||||
|
||||
@@ -415,47 +413,19 @@ OfferCreate::flowCross(
|
||||
// offer taker. Set sendMax to allow for the gateway's cut.
|
||||
Rate gatewayXferRate{QUALITY_ONE};
|
||||
STAmount sendMax = takerAmount.in;
|
||||
// Payment flow code compares quality after the transfer rate is
|
||||
// included, so the threshold is computed from the grossed-up input.
|
||||
STAmount thresholdIn = sendMax;
|
||||
if (!sendMax.native() && (accountID_ != sendMax.getIssuer()))
|
||||
{
|
||||
gatewayXferRate = transferRate(psb, sendMax);
|
||||
if (gatewayXferRate.value != QUALITY_ONE)
|
||||
{
|
||||
try
|
||||
{
|
||||
sendMax = multiplyRound(
|
||||
takerAmount.in, gatewayXferRate, takerAmount.in.asset(), true);
|
||||
thresholdIn = sendMax;
|
||||
}
|
||||
catch (std::overflow_error const&)
|
||||
{
|
||||
// Only an MPT amount can overflow here (IOU rescales and
|
||||
// XRP has no transfer rate), and MPT offers require
|
||||
// featureMPTokensV2 at preflight, so this legacy re-throw
|
||||
// is unreachable in practice.
|
||||
// LCOV_EXCL_START
|
||||
if (!takerAmount.in.holds<MPTIssue>() ||
|
||||
!psb.rules().enabled(featureMPTokensV2))
|
||||
throw;
|
||||
// LCOV_EXCL_STOP
|
||||
|
||||
// The grossed-up MPT amount exceeds 2^63-1. The taker can't
|
||||
// send more than its balance anyway. The threshold only
|
||||
// needs the ratio, so compute it as an IOU, which can't
|
||||
// overflow.
|
||||
sendMax = inStartBalance;
|
||||
thresholdIn = multiplyRound(
|
||||
STAmount{noIssue(), takerAmount.in.mantissa()},
|
||||
gatewayXferRate,
|
||||
noIssue(),
|
||||
true);
|
||||
}
|
||||
sendMax =
|
||||
multiplyRound(takerAmount.in, gatewayXferRate, takerAmount.in.asset(), true);
|
||||
}
|
||||
}
|
||||
|
||||
Quality threshold{takerAmount.out, thresholdIn};
|
||||
// Payment flow code compares quality after the transfer rate is
|
||||
// included. Since transfer rate is incorporated compute threshold.
|
||||
Quality threshold{takerAmount.out, sendMax};
|
||||
|
||||
// If we're creating a passive offer adjust the threshold so we only
|
||||
// cross offers that have a better quality than this one.
|
||||
@@ -900,32 +870,15 @@ OfferCreate::applyGuts(Sandbox& sb, Sandbox& sbCancel)
|
||||
return {tesSUCCESS, true};
|
||||
}
|
||||
|
||||
// A taker redeeming an MPT into this offer pays the transfer fee, and
|
||||
// BookStep grosses up TakerPays by the rate before anything else. If that
|
||||
// doesn't fit in an MPT amount, every fee-paying taker overflows and
|
||||
// BookStep removes the offer on the first attempt (the issuer pays no fee,
|
||||
// but its send is bounded by what it can still issue). Conservative for a
|
||||
// bid above MaximumAmount, which BookStep grosses up only to the cap.
|
||||
// Checked on the remainder: it may fit even if the original amount didn't.
|
||||
bool cannotRest = false;
|
||||
if (mptV2 && saTakerPays.holds<MPTIssue>())
|
||||
{
|
||||
auto const rate = transferRate(sb, saTakerPays);
|
||||
cannotRest = rate != kParityRate &&
|
||||
!tryMulRatio(saTakerPays.mpt(), rate.value, QUALITY_ONE, /*roundUp*/ true);
|
||||
}
|
||||
|
||||
// The remainder rests at uRate, the original pre-crossing rate. A zero
|
||||
// rate (quality not representable) puts it in the directory whose index
|
||||
// equals getBookBase(book), and BookTip scans keys strictly greater, so it
|
||||
// could never be crossed while holding the owner's reserve. Likewise, a
|
||||
// bid whose fee-grossed TakerPays doesn't fit would be removed by BookStep
|
||||
// on the first take. Don't place it; anything that crossed is kept, and a
|
||||
// fully crossed offer has already returned above. Gated to preserve
|
||||
// pre-amendment behavior.
|
||||
if ((mptV2 && uRate == 0) || cannotRest)
|
||||
// could never be crossed while holding the owner's reserve. Don't place
|
||||
// it; anything that crossed is kept, and a fully crossed offer has already
|
||||
// returned above. Gated to preserve pre-amendment behavior.
|
||||
if (mptV2 && uRate == 0)
|
||||
{
|
||||
JLOG(j_.debug()) << "Unrepresentable quality or fee: remainder not placed";
|
||||
JLOG(j_.debug()) << "Unrepresentable quality: remainder not placed";
|
||||
if (!crossed)
|
||||
return {tecKILLED, false};
|
||||
return {tesSUCCESS, true};
|
||||
|
||||
@@ -8,7 +8,6 @@
|
||||
#include <xrpl/ledger/ReadView.h>
|
||||
#include <xrpl/ledger/helpers/LendingHelpers.h>
|
||||
#include <xrpl/ledger/helpers/TokenHelpers.h>
|
||||
#include <xrpl/ledger/helpers/VaultHelpers.h>
|
||||
#include <xrpl/protocol/AccountID.h>
|
||||
#include <xrpl/protocol/Asset.h>
|
||||
#include <xrpl/protocol/Concepts.h>
|
||||
@@ -159,7 +158,7 @@ determineAsset(
|
||||
|
||||
std::expected<STAmount, TER>
|
||||
determineClawAmount(
|
||||
SLE::ConstRef sleBroker,
|
||||
SLE const& sleBroker,
|
||||
Asset const& vaultAsset,
|
||||
std::optional<STAmount> const& amount,
|
||||
SLE::ConstRef vaultSle,
|
||||
@@ -170,19 +169,17 @@ determineClawAmount(
|
||||
if (rules.enabled(fixCleanup3_2_0))
|
||||
{
|
||||
return minimumBrokerCover(
|
||||
sleBroker->at(sfDebtTotal),
|
||||
TenthBips32(sleBroker->at(sfCoverRateMinimum)),
|
||||
vaultSle);
|
||||
sleBroker[sfDebtTotal], TenthBips32(sleBroker[sfCoverRateMinimum]), vaultSle);
|
||||
}
|
||||
|
||||
// Always round the minimum required up
|
||||
NumberRoundModeGuard const mg(Number::RoundingMode::Upward);
|
||||
return tenthBipsOfValue(
|
||||
sleBroker->at(sfDebtTotal), TenthBips32(sleBroker->at(sfCoverRateMinimum)));
|
||||
sleBroker[sfDebtTotal], TenthBips32(sleBroker[sfCoverRateMinimum]));
|
||||
}();
|
||||
// The subtraction probably won't round, but round down if it does.
|
||||
NumberRoundModeGuard const mg(Number::RoundingMode::Downward);
|
||||
return sleBroker->at(sfCoverAvailable) - minRequiredCover;
|
||||
return sleBroker[sfCoverAvailable] - minRequiredCover;
|
||||
}();
|
||||
if (maxClawAmount <= beast::kZero)
|
||||
return std::unexpected(tecINSUFFICIENT_FUNDS);
|
||||
@@ -190,23 +187,12 @@ determineClawAmount(
|
||||
// Use the vaultAsset here, because it will be the right type in all
|
||||
// circumstances. The amount may be an IOU indicating the pseudo-account's
|
||||
// asset, which is correct, but not what is needed here.
|
||||
STAmount requested = [&] {
|
||||
if (!amount || *amount == beast::kZero)
|
||||
return STAmount{vaultAsset, maxClawAmount};
|
||||
Number const magnitude{*amount};
|
||||
if (magnitude > maxClawAmount)
|
||||
return STAmount{vaultAsset, maxClawAmount};
|
||||
return STAmount{vaultAsset, magnitude};
|
||||
}();
|
||||
|
||||
if (getVaultVersion(vaultSle) != VaultVersion::FixedPrecision)
|
||||
return requested;
|
||||
|
||||
STAmount rounded = debitToPosteriorBrokerCoverScale(
|
||||
vaultSle, sleBroker, requested, Number::RoundingMode::TowardsZero);
|
||||
if (rounded == beast::kZero)
|
||||
return std::unexpected(tecPRECISION_LOSS);
|
||||
return rounded;
|
||||
if (!amount || *amount == beast::kZero)
|
||||
return STAmount{vaultAsset, maxClawAmount};
|
||||
Number const magnitude{*amount};
|
||||
if (magnitude > maxClawAmount)
|
||||
return STAmount{vaultAsset, maxClawAmount};
|
||||
return STAmount{vaultAsset, magnitude};
|
||||
}
|
||||
|
||||
template <ValidIssueType T>
|
||||
@@ -308,25 +294,17 @@ LoanBrokerCoverClawback::preclaim(PreclaimContext const& ctx)
|
||||
}
|
||||
|
||||
auto const findClawAmount =
|
||||
determineClawAmount(sleBroker, vaultAsset, amount, vault, ctx.view.rules());
|
||||
determineClawAmount(*sleBroker, vaultAsset, amount, vault, ctx.view.rules());
|
||||
if (!findClawAmount)
|
||||
{
|
||||
JLOG(ctx.j.warn()) << "LoanBroker cover is already at minimum, or the claw amount "
|
||||
"rounds to zero: "
|
||||
<< transToken(findClawAmount.error());
|
||||
JLOG(ctx.j.warn()) << "LoanBroker cover is already at minimum.";
|
||||
return findClawAmount.error();
|
||||
}
|
||||
STAmount const& clawAmount = *findClawAmount;
|
||||
|
||||
// FixedPrecision outflows already rounded at the posterior exponent; the
|
||||
// live CoverAvailable scale used by canApplyToBrokerCover would reject a
|
||||
// re-fining clawback as sub-ULP.
|
||||
if (getVaultVersion(vault) != VaultVersion::FixedPrecision)
|
||||
{
|
||||
if (auto const ret = canApplyToBrokerCover(
|
||||
ctx.view, sleBroker, vaultAsset, clawAmount, ctx.j, "LoanBrokerCoverClawback"))
|
||||
return ret;
|
||||
}
|
||||
if (auto const ret = canApplyToBrokerCover(
|
||||
ctx.view, sleBroker, vaultAsset, clawAmount, ctx.j, "LoanBrokerCoverClawback"))
|
||||
return ret;
|
||||
|
||||
// Explicitly check the balance of the trust line / MPT to make sure the
|
||||
// balance is actually there. It should always match `sfCoverAvailable`, so
|
||||
@@ -379,7 +357,7 @@ LoanBrokerCoverClawback::doApply()
|
||||
auto const vaultAsset = vault->at(sfAsset);
|
||||
|
||||
auto const findClawAmount =
|
||||
determineClawAmount(sleBroker, vaultAsset, amount, vault, view().rules());
|
||||
determineClawAmount(*sleBroker, vaultAsset, amount, vault, view().rules());
|
||||
if (!findClawAmount)
|
||||
return tecINTERNAL; // LCOV_EXCL_LINE
|
||||
STAmount const& clawAmount = *findClawAmount;
|
||||
|
||||
@@ -5,10 +5,8 @@
|
||||
#include <xrpl/beast/utility/Zero.h>
|
||||
#include <xrpl/ledger/helpers/LendingHelpers.h>
|
||||
#include <xrpl/ledger/helpers/TokenHelpers.h>
|
||||
#include <xrpl/ledger/helpers/VaultHelpers.h>
|
||||
#include <xrpl/protocol/Feature.h>
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
#include <xrpl/protocol/Protocol.h>
|
||||
#include <xrpl/protocol/SField.h>
|
||||
#include <xrpl/protocol/STAmount.h>
|
||||
#include <xrpl/protocol/STLedgerEntry.h>
|
||||
@@ -106,35 +104,22 @@ LoanBrokerCoverDeposit::preclaim(PreclaimContext const& ctx)
|
||||
// here in preclaim lets us reject sub-cover-scale dust early with tecPRECISION_LOSS instead of
|
||||
// failing only in doApply.
|
||||
auto const roundedAmount = [&] -> STAmount {
|
||||
if (getVaultVersion(vault) == VaultVersion::FixedPrecision)
|
||||
{
|
||||
// Cover deposit is an inflow to CoverAvailable, so it must floor
|
||||
// the sum, not just the delta.
|
||||
return creditToPosteriorBrokerCoverScale(
|
||||
vault, sleBroker, amount, Number::RoundingMode::Downward);
|
||||
}
|
||||
if (!fix320Enabled)
|
||||
return STAmount{amount};
|
||||
return tx[sfAmount];
|
||||
|
||||
return roundToScale(
|
||||
amount,
|
||||
tx[sfAmount],
|
||||
scale(sleBroker->at(sfCoverAvailable), vaultAsset),
|
||||
Number::RoundingMode::Downward);
|
||||
}();
|
||||
|
||||
// fixCleanup3_2_0 is enabled on the network, so this also covers the
|
||||
// FixedPrecision zero-credit case.
|
||||
if (fix320Enabled && roundedAmount <= beast::kZero)
|
||||
if (fix320Enabled && roundedAmount == beast::kZero)
|
||||
{
|
||||
JLOG(ctx.j.warn()) << "LoanBrokerCoverDeposit: deposit amount: " << amount
|
||||
<< " is zero or negative at loan broker scale";
|
||||
JLOG(ctx.j.warn()) << "LoanBrokerCoverDeposit: deposit amount: " << tx[sfAmount]
|
||||
<< " is zero at loan broker scale";
|
||||
return tecPRECISION_LOSS;
|
||||
}
|
||||
|
||||
if (auto const ter = checkOptionalBrokerCoverInflow(vault, sleBroker, roundedAmount);
|
||||
!isTesSuccess(ter))
|
||||
return ter;
|
||||
|
||||
if (accountHolds(
|
||||
ctx.view,
|
||||
account,
|
||||
@@ -170,13 +155,6 @@ LoanBrokerCoverDeposit::doApply()
|
||||
// see the rationale comment in preclaim.
|
||||
bool const fix320Enabled = view().rules().enabled(fixCleanup3_2_0);
|
||||
auto const amount = [&] -> STAmount {
|
||||
if (getVaultVersion(vault) == VaultVersion::FixedPrecision)
|
||||
{
|
||||
// Cover deposit is an inflow to CoverAvailable, so it must floor
|
||||
// the sum, not just the delta.
|
||||
return creditToPosteriorBrokerCoverScale(
|
||||
vault, broker, tx[sfAmount], Number::RoundingMode::Downward);
|
||||
}
|
||||
if (!fix320Enabled)
|
||||
return tx[sfAmount];
|
||||
|
||||
@@ -186,12 +164,12 @@ LoanBrokerCoverDeposit::doApply()
|
||||
Number::RoundingMode::Downward);
|
||||
}();
|
||||
|
||||
// We validated zero-or-negative amount in preclaim, if we ended up with one now, fail hard.
|
||||
if (amount <= beast::kZero)
|
||||
// We validated zero-amount in preclaim, if we ended up with zero now, fail hard.
|
||||
if (amount == beast::kZero)
|
||||
{
|
||||
// LCOV_EXCL_START
|
||||
JLOG(j_.error()) << "LoanBrokerCoverDeposit: deposit amount: " << tx[sfAmount]
|
||||
<< " is zero or negative";
|
||||
<< " is zero";
|
||||
return tecINTERNAL;
|
||||
// LCOV_EXCL_STOP
|
||||
}
|
||||
|
||||
@@ -8,7 +8,6 @@
|
||||
#include <xrpl/ledger/helpers/CredentialHelpers.h>
|
||||
#include <xrpl/ledger/helpers/LendingHelpers.h>
|
||||
#include <xrpl/ledger/helpers/TokenHelpers.h>
|
||||
#include <xrpl/ledger/helpers/VaultHelpers.h>
|
||||
#include <xrpl/protocol/Feature.h>
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
#include <xrpl/protocol/Protocol.h>
|
||||
@@ -104,28 +103,10 @@ LoanBrokerCoverWithdraw::preclaim(PreclaimContext const& ctx)
|
||||
if (amount.asset() != vaultAsset)
|
||||
return tecWRONG_ASSET;
|
||||
|
||||
STAmount roundedAmount{amount};
|
||||
if (getVaultVersion(vault) == VaultVersion::FixedPrecision)
|
||||
{
|
||||
// FixedPrecision outflows are rounded at the posterior exponent here
|
||||
// instead of going through canApplyToBrokerCover: the live
|
||||
// CoverAvailable scale it uses would reject a re-fining withdrawal as
|
||||
// sub-ULP.
|
||||
roundedAmount = debitToPosteriorBrokerCoverScale(
|
||||
vault, sleBroker, amount, Number::RoundingMode::TowardsZero);
|
||||
if (roundedAmount == beast::kZero)
|
||||
{
|
||||
JLOG(ctx.j.warn()) << "LoanBrokerCoverWithdraw: withdraw amount: " << amount
|
||||
<< " is zero at loan broker scale";
|
||||
return tecPRECISION_LOSS;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (auto const ret = canApplyToBrokerCover(
|
||||
ctx.view, sleBroker, vaultAsset, roundedAmount, ctx.j, "LoanBrokerCoverWithdraw"))
|
||||
return ret;
|
||||
}
|
||||
// Helper handles both IOU and MPT correctly without explicit branching.
|
||||
if (auto const ret = canApplyToBrokerCover(
|
||||
ctx.view, sleBroker, vaultAsset, amount, ctx.j, "LoanBrokerCoverWithdraw"))
|
||||
return ret;
|
||||
|
||||
// The broker's pseudo-account is the source of funds.
|
||||
auto const pseudoAccountID = sleBroker->at(sfAccount);
|
||||
@@ -148,15 +129,7 @@ LoanBrokerCoverWithdraw::preclaim(PreclaimContext const& ctx)
|
||||
AuthType authType = AuthType::WeakAuth;
|
||||
if (account != dstAcct)
|
||||
{
|
||||
// Check the amount actually sent: on FixedPrecision it is the rounded
|
||||
// amount, not sfAmount.
|
||||
if (auto const ret = canWithdraw(
|
||||
ctx.view,
|
||||
account,
|
||||
dstAcct,
|
||||
roundedAmount,
|
||||
tx.isFieldPresent(sfDestinationTag),
|
||||
tx[~sfCredentialIDs]))
|
||||
if (auto const ret = canWithdraw(ctx.view, tx))
|
||||
return ret;
|
||||
|
||||
// The destination account must have consented to receive the asset by
|
||||
@@ -211,9 +184,9 @@ LoanBrokerCoverWithdraw::preclaim(PreclaimContext const& ctx)
|
||||
tenthBipsOfValue(currentDebtTotal, TenthBips32(sleBroker->at(sfCoverRateMinimum))),
|
||||
scale(currentDebtTotal, vaultAsset));
|
||||
}();
|
||||
if (coverAvail < roundedAmount)
|
||||
if (coverAvail < amount)
|
||||
return tecINSUFFICIENT_FUNDS;
|
||||
if ((coverAvail - roundedAmount) < minimumCover)
|
||||
if ((coverAvail - amount) < minimumCover)
|
||||
return tecINSUFFICIENT_FUNDS;
|
||||
|
||||
auto const freezeHandling = fix330Enabled && dstAcct == vaultAsset.getIssuer()
|
||||
@@ -226,7 +199,7 @@ LoanBrokerCoverWithdraw::preclaim(PreclaimContext const& ctx)
|
||||
vaultAsset,
|
||||
freezeHandling,
|
||||
AuthHandling::ZeroIfUnauthorized,
|
||||
ctx.j) < roundedAmount)
|
||||
ctx.j) < amount)
|
||||
return tecINSUFFICIENT_FUNDS;
|
||||
|
||||
return tesSUCCESS;
|
||||
@@ -238,7 +211,7 @@ LoanBrokerCoverWithdraw::doApply()
|
||||
auto const& tx = ctx_.tx;
|
||||
|
||||
auto const brokerID = tx[sfLoanBrokerID];
|
||||
auto const requestedAmount = tx[sfAmount];
|
||||
auto const amount = tx[sfAmount];
|
||||
auto const dstAcct = tx[~sfDestination].value_or(accountID_);
|
||||
|
||||
auto const broker = view().peek(keylet::loanBroker(brokerID));
|
||||
@@ -250,10 +223,6 @@ LoanBrokerCoverWithdraw::doApply()
|
||||
return tecINTERNAL; // LCOV_EXCL_LINE
|
||||
|
||||
auto const vaultAsset = vault->at(sfAsset);
|
||||
auto const amount = getVaultVersion(vault) == VaultVersion::FixedPrecision
|
||||
? debitToPosteriorBrokerCoverScale(
|
||||
vault, broker, requestedAmount, Number::RoundingMode::TowardsZero)
|
||||
: requestedAmount;
|
||||
|
||||
auto const brokerPseudoID = *broker->at(sfAccount);
|
||||
|
||||
@@ -270,7 +239,6 @@ LoanBrokerCoverWithdraw::doApply()
|
||||
brokerPseudoID,
|
||||
preFeeBalance_,
|
||||
amount,
|
||||
amount,
|
||||
j_);
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
|
||||
#include <xrpl/ledger/helpers/LendingHelpers.h>
|
||||
#include <xrpl/ledger/helpers/TokenHelpers.h>
|
||||
#include <xrpl/ledger/helpers/VaultHelpers.h>
|
||||
#include <xrpl/protocol/Asset.h>
|
||||
#include <xrpl/protocol/Feature.h>
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
@@ -78,7 +77,7 @@ LoanBrokerDelete::preclaim(PreclaimContext const& ctx)
|
||||
{
|
||||
// Any remaining debt should have been wiped out by the last Loan
|
||||
// Delete. This check is purely defensive.
|
||||
auto const scale = getVaultScale(vault);
|
||||
auto const scale = getAssetsTotalScale(vault);
|
||||
|
||||
auto const rounded =
|
||||
roundToAsset(asset, debtTotal, scale, Number::RoundingMode::TowardsZero);
|
||||
@@ -95,38 +94,21 @@ LoanBrokerDelete::preclaim(PreclaimContext const& ctx)
|
||||
|
||||
auto const coverAvailable = STAmount{asset, sleBroker->at(sfCoverAvailable)};
|
||||
// If there are assets in the cover, broker will receive them on deletion.
|
||||
// So we need to check if the broker owner is deep frozen for that asset.
|
||||
if (coverAvailable > beast::kZero)
|
||||
{
|
||||
auto const brokerPseudo = sleBroker->at(sfAccount);
|
||||
|
||||
// Pre-fixCleanup3_5_0: only freeze checks apply to the cover payout.
|
||||
// Post-fixCleanup3_5_0: apply the cover-withdraw transfer and authorization checks too.
|
||||
if (ctx.view.rules().enabled(fixCleanup3_5_0))
|
||||
{
|
||||
// Like a cover withdrawal, the payout skips the lsfMPTCanTransfer check so that an
|
||||
// issuer cannot trap the broker's cover.
|
||||
if (auto const ret = canTransfer(
|
||||
ctx.view, asset, brokerPseudo, brokerOwner, WaiveMPTCanTransfer::Yes))
|
||||
return ret;
|
||||
|
||||
if (auto const ret = requireAuth(ctx.view, asset, brokerOwner, AuthType::WeakAuth))
|
||||
return ret;
|
||||
|
||||
if (!holdingExists(ctx.view, brokerOwner, asset))
|
||||
{
|
||||
if (auto const ret = canAddHolding(ctx.view, asset); !isTesSuccess(ret))
|
||||
return ret;
|
||||
}
|
||||
}
|
||||
|
||||
if (auto const ret = checkDeepFrozen(ctx.view, brokerOwner, asset))
|
||||
{
|
||||
JLOG(ctx.j.warn()) << "Broker owner account is frozen.";
|
||||
return ret;
|
||||
}
|
||||
}
|
||||
|
||||
if (ctx.view.rules().enabled(fixCleanup3_2_0))
|
||||
if (ctx.view.rules().enabled(fixCleanup3_2_0))
|
||||
{
|
||||
if (coverAvailable > beast::kZero)
|
||||
{
|
||||
auto const brokerPseudo = sleBroker->at(sfAccount);
|
||||
if (auto const ret = checkFrozen(ctx.view, brokerPseudo, asset))
|
||||
{
|
||||
JLOG(ctx.j.warn()) << "Broker pseudo-account is frozen/locked.";
|
||||
@@ -169,36 +151,8 @@ LoanBrokerDelete::doApply()
|
||||
return tefBAD_LEDGER; // LCOV_EXCL_LINE
|
||||
}
|
||||
|
||||
auto const owner = view().peek(keylet::account(accountID_));
|
||||
if (!owner)
|
||||
return tefBAD_LEDGER; // LCOV_EXCL_LINE
|
||||
|
||||
// Pre-fixCleanup3_5_0: the owner count drops after the pseudo-account is erased.
|
||||
// Post-fixCleanup3_5_0: it drops first, so the owner's holding created below can use the
|
||||
// reserve that the broker frees.
|
||||
bool const fix350Enabled = view().rules().enabled(fixCleanup3_5_0);
|
||||
if (fix350Enabled)
|
||||
{
|
||||
// Decreases the owner count by two: one for the LoanBroker object, and one
|
||||
// for the pseudo-account.
|
||||
decreaseOwnerCountForObject(view(), owner, broker, 2, j_);
|
||||
}
|
||||
|
||||
{
|
||||
auto const coverAvailable = STAmount{vaultAsset, broker->at(sfCoverAvailable)};
|
||||
|
||||
// Pre-fixCleanup3_5_0: the cover goes straight to accountSend, so an MPT payout to an
|
||||
// owner with no MPToken fails with tecNO_AUTH.
|
||||
// Post-fixCleanup3_5_0: a nonzero cover first gets the owner a holding if it has none, as a
|
||||
// withdrawal to self does.
|
||||
if (fix350Enabled && coverAvailable > beast::kZero &&
|
||||
!holdingExists(view(), accountID_, vaultAsset))
|
||||
{
|
||||
if (auto const ter = addEmptyHolding(
|
||||
ctx_.getApplyViewContext(), accountID_, preFeeBalance_, vaultAsset, j_))
|
||||
return ter;
|
||||
}
|
||||
|
||||
if (auto const ter = accountSend(
|
||||
view(), brokerPseudoID, accountID_, coverAvailable, j_, {}, WaiveTransferFee::Yes))
|
||||
return ter;
|
||||
@@ -231,8 +185,15 @@ LoanBrokerDelete::doApply()
|
||||
|
||||
view().erase(brokerPseudoSLE);
|
||||
|
||||
if (!fix350Enabled)
|
||||
{
|
||||
auto const owner = view().peek(keylet::account(accountID_));
|
||||
if (!owner)
|
||||
return tefBAD_LEDGER; // LCOV_EXCL_LINE
|
||||
|
||||
// Decreases the owner count by two: one for the LoanBroker object, and
|
||||
// one for the pseudo-account.
|
||||
decreaseOwnerCountForObject(view(), owner, broker, 2, j_);
|
||||
}
|
||||
|
||||
view().erase(broker);
|
||||
|
||||
|
||||
Loaded 100 of 275 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user