mirror of
https://github.com/XRPLF/rippled.git
synced 2026-10-21 19:18:22 +00:00
Compare commits
21
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bda01e0575 | ||
|
|
4087861411 | ||
|
|
dc43faafed | ||
|
|
6af97c07d9 | ||
|
|
4c7d920e66 | ||
|
|
8b59d9d307 | ||
|
|
b0fa295595 | ||
|
|
9af432cb0e | ||
|
|
4435ca4cbf | ||
|
|
cd72a11786 | ||
|
|
b398b88385 | ||
|
|
31f6f90206 | ||
|
|
1d8c2f9172 | ||
|
|
f105418bdb | ||
|
|
4a134eb883 | ||
|
|
ec3f251ad8 | ||
|
|
5faa861226 | ||
|
|
88fd1222d6 | ||
|
|
f1ce9449f2 | ||
|
|
7237b3321c | ||
|
|
05c5998634 |
No files matched your search
@@ -103,6 +103,7 @@ words:
|
||||
- dearmor
|
||||
- decryptor
|
||||
- dedented
|
||||
- dedup
|
||||
- deleteme
|
||||
- demultiplexer
|
||||
- deserializaton
|
||||
@@ -344,6 +345,7 @@ words:
|
||||
- unambiguity
|
||||
- unauthorizes
|
||||
- unauthorizing
|
||||
- undeserializable
|
||||
- unergonomic
|
||||
- unfetched
|
||||
- unfindable
|
||||
|
||||
@@ -62,6 +62,7 @@ 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
|
||||
|
||||
@@ -123,13 +123,19 @@ 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: |
|
||||
pre-commit run --all-files || true
|
||||
git add --update
|
||||
pre-commit run || [ $? -eq 1 ]
|
||||
|
||||
- name: Generate git diff
|
||||
if: ${{ steps.files_changed.outcome != 'success' }}
|
||||
run: |
|
||||
git diff | tee "${DIFF_FILE}"
|
||||
git diff HEAD | tee "${DIFF_FILE}"
|
||||
|
||||
- name: Upload clang-tidy diff output
|
||||
if: ${{ github.event.repository.visibility == 'public' && steps.files_changed.outcome != 'success' }}
|
||||
|
||||
@@ -36,6 +36,7 @@ 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,33 +2,12 @@
|
||||
| ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| 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. |
|
||||
|
||||
## Minimum Requirements
|
||||
## Supported platforms
|
||||
|
||||
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.
|
||||
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).
|
||||
|
||||
## Steps
|
||||
|
||||
|
||||
+1
-1
@@ -332,7 +332,7 @@ This ensures that configuration changes don't introduce new warnings across the
|
||||
|
||||
### Installing clang-tidy
|
||||
|
||||
See the [environment setup guide](./docs/build/environment.md#clang-tidy) for how to get clang-tidy.
|
||||
The [Nix development shell](./docs/build/nix.md) provides `clang-tidy` at the [tested version](./docs/build/environment.md#tested-compiler-versions).
|
||||
|
||||
### Running clang-tidy locally
|
||||
|
||||
|
||||
@@ -1360,6 +1360,39 @@
|
||||
# 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
|
||||
|
||||
Vendored
+32
-77
@@ -4,6 +4,18 @@ 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
|
||||
@@ -35,20 +47,16 @@ compiler building. Treat support for anything outside the table as best-effort.
|
||||
|
||||
## Required tools
|
||||
|
||||
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 |
|
||||
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).
|
||||
|
||||
On Linux and macOS, the [Nix development shell](./nix.md) provides all of them
|
||||
(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.
|
||||
at the versions CI uses, recorded in [`nix/check-tools/`](../../nix/check-tools).
|
||||
|
||||
Once they are in place, verify that everything is installed and runnable with:
|
||||
|
||||
@@ -58,60 +66,19 @@ Once they are in place, verify that everything is installed and runnable with:
|
||||
|
||||
## Linux and macOS
|
||||
|
||||
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:
|
||||
Use the [Nix development shell](./nix.md):
|
||||
it provides the compiler and every tool CI uses,
|
||||
without installing anything system-wide.
|
||||
|
||||
```bash
|
||||
nix --experimental-features 'nix-command flakes' develop
|
||||
```
|
||||
### macOS: Apple Clang
|
||||
|
||||
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
|
||||
```
|
||||
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.
|
||||
|
||||
## Windows
|
||||
|
||||
@@ -123,8 +90,7 @@ 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)
|
||||
- Python, Conan, and CMake, at the versions listed in
|
||||
[Required tools](#required-tools).
|
||||
- recent releases of Python, Conan, and CMake
|
||||
- a [Rust toolchain](https://rustup.rs) — only needed to build with
|
||||
`-Drust=ON`, see [Rust](#rust)
|
||||
|
||||
@@ -145,16 +111,5 @@ 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
+2
-9
@@ -9,9 +9,7 @@ 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
|
||||
|
||||
@@ -27,13 +25,8 @@ From the root of the xrpld repository, enter the default development shell:
|
||||
nix --experimental-features 'nix-command flakes' develop
|
||||
```
|
||||
|
||||
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)
|
||||
|
||||
This downloads and sets up all required development tools (CMake, Ninja, Conan, etc.)
|
||||
and the compiler for your platform (see [Platform notes](#platform-notes)).
|
||||
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,6 +10,19 @@ 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:
|
||||
|
||||
@@ -94,6 +94,7 @@ 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";
|
||||
@@ -108,6 +109,8 @@ 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";
|
||||
|
||||
@@ -257,13 +257,42 @@ public:
|
||||
header_.validated = true;
|
||||
}
|
||||
|
||||
void
|
||||
/**
|
||||
* 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
|
||||
setAccepted(
|
||||
NetClock::time_point closeTime,
|
||||
NetClock::duration closeResolution,
|
||||
bool correctCloseTime);
|
||||
|
||||
void
|
||||
/**
|
||||
* 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
|
||||
setImmutable(bool rehash = true);
|
||||
|
||||
bool
|
||||
@@ -272,23 +301,28 @@ public:
|
||||
return immutable_;
|
||||
}
|
||||
|
||||
/* Mark this ledger as "should be full".
|
||||
/**
|
||||
* @return Whether both maps can still be the maps the header names. See
|
||||
* SHAMap::isValid().
|
||||
*/
|
||||
[[nodiscard]] bool
|
||||
mapsValid() const
|
||||
{
|
||||
return txMap_.isValid() && stateMap_.isValid();
|
||||
}
|
||||
|
||||
"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.
|
||||
*/
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
void
|
||||
setFull() const
|
||||
{
|
||||
txMap_.setFull();
|
||||
txMap_.setLedgerSeq(header_.seq);
|
||||
stateMap_.setFull();
|
||||
stateMap_.setLedgerSeq(header_.seq);
|
||||
txMap_.setFull(header_.seq);
|
||||
stateMap_.setFull(header_.seq);
|
||||
}
|
||||
|
||||
void
|
||||
@@ -418,6 +452,22 @@ 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,6 +265,13 @@ 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,
|
||||
@@ -272,7 +279,8 @@ doWithdraw(
|
||||
AccountID const& dstAcct,
|
||||
AccountID const& sourceAcct,
|
||||
XRPAmount priorBalance,
|
||||
STAmount const& amount,
|
||||
STAmount const& destinationAmount,
|
||||
STAmount const& sourceAmount,
|
||||
beast::Journal j);
|
||||
|
||||
/**
|
||||
|
||||
@@ -330,14 +330,6 @@ adjustImpreciseNumber(
|
||||
value = 0;
|
||||
}
|
||||
|
||||
inline int
|
||||
getAssetsTotalScale(SLE::ConstRef vaultSle)
|
||||
{
|
||||
if (!vaultSle)
|
||||
return Number::kMinExponent - 1; // LCOV_EXCL_LINE
|
||||
return scale(vaultSle->at(sfAssetsTotal), vaultSle->at(sfAsset));
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
|
||||
@@ -316,6 +316,20 @@ 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)
|
||||
|
||||
@@ -164,18 +164,20 @@ posteriorAssetScale(
|
||||
|
||||
/**
|
||||
* Shared core of creditToPosteriorAvailableScale and
|
||||
* creditToPosteriorBrokerCoverScale: floors reference + raw at scale 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.
|
||||
* 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 atScale,
|
||||
int baseScale,
|
||||
Number const& raw,
|
||||
Number::RoundingMode roundingMode);
|
||||
|
||||
|
||||
@@ -348,16 +348,19 @@ isAuditorMirrorCurrent(SLE const& issuance, SLE const& mptoken);
|
||||
areMirrorsCurrent(SLE const& issuance, SLE const& mptoken);
|
||||
|
||||
/**
|
||||
* @brief Set the holder's issuer mirror epoch to match the issuance's current issuer key epoch.
|
||||
* @brief Set the holder's issuer mirror epoch, and the issuer mirror key, to match the issuance's
|
||||
* current key.
|
||||
*
|
||||
* 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
|
||||
setIssuerMirrorEpoch(SLE const& issuance, SLE& mptoken);
|
||||
setIssuerMirrorCurrent(SLE const& issuance, SLE& mptoken);
|
||||
|
||||
/**
|
||||
* @brief Set the holder's auditor mirror epoch to match the issuance's current auditor key epoch.
|
||||
@@ -372,16 +375,18 @@ void
|
||||
setAuditorMirrorEpoch(SLE const& issuance, SLE& mptoken);
|
||||
|
||||
/**
|
||||
* @brief Set the holder's MPToken mirror epochs to match the issuance's current key epochs.
|
||||
* @brief Set the holder's MPToken mirror epochs, and the issuer mirror key, to match the issuance's
|
||||
* current keys.
|
||||
*
|
||||
* Call this after writing mirror ciphertexts under the issuance's currently
|
||||
* registered keys, so that the mirrors read as current afterwards.
|
||||
* registered keys, so that the mirrors read as current afterwards. Also records
|
||||
* sfIssuerMirrorEncryptionKey via setIssuerMirrorCurrent.
|
||||
*
|
||||
* @param issuance The MPTokenIssuance ledger object.
|
||||
* @param mptoken The holder's MPToken ledger entry to update.
|
||||
*/
|
||||
void
|
||||
setMirrorEpochs(SLE const& issuance, SLE& mptoken);
|
||||
setMirrorsCurrent(SLE const& issuance, SLE& mptoken);
|
||||
|
||||
/**
|
||||
* @brief Verifies revealed amount encryptions for all recipients.
|
||||
|
||||
@@ -10,6 +10,25 @@ 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.
|
||||
*
|
||||
@@ -33,13 +52,47 @@ 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)
|
||||
: 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}
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
@@ -696,6 +696,16 @@ 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
|
||||
|
||||
@@ -417,6 +417,7 @@ LEDGER_ENTRY(ltMPTOKEN_ISSUANCE, 0x007e, MPTokenIssuance, mpt_issuance, ({
|
||||
{sfAuditorEncryptionKey, SoeOptional},
|
||||
{sfIssuerKeyEpoch, SoeOptional},
|
||||
{sfAuditorKeyEpoch, SoeOptional},
|
||||
{sfInitialIssuerEncryptionKey, SoeOptional},
|
||||
{sfConfidentialOutstandingAmount, SoeDefault},
|
||||
}))
|
||||
|
||||
@@ -438,6 +439,7 @@ LEDGER_ENTRY(ltMPTOKEN, 0x007f, MPToken, mptoken, ({
|
||||
{sfAuditorEncryptedBalance, SoeOptional},
|
||||
{sfIssuerKeyMirrorEpoch, SoeOptional},
|
||||
{sfAuditorKeyMirrorEpoch, SoeOptional},
|
||||
{sfIssuerMirrorEncryptionKey, SoeOptional},
|
||||
{sfHolderEncryptionKey, SoeOptional},
|
||||
{sfRecoveryKey, SoeOptional},
|
||||
}))
|
||||
|
||||
@@ -333,6 +333,8 @@ 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)
|
||||
|
||||
@@ -252,6 +252,9 @@ 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
|
||||
|
||||
@@ -316,6 +316,30 @@ 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.
|
||||
@@ -565,6 +589,17 @@ 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,6 +399,30 @@ 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.
|
||||
@@ -670,6 +694,17 @@ 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.
|
||||
|
||||
+263
-35
@@ -2,6 +2,7 @@
|
||||
|
||||
#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>
|
||||
@@ -17,6 +18,7 @@
|
||||
#include <xrpl/shamap/SHAMapNodeID.h>
|
||||
#include <xrpl/shamap/SHAMapTreeNode.h>
|
||||
|
||||
#include <atomic>
|
||||
#include <condition_variable>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
@@ -40,7 +42,7 @@ class SHAMapSyncFilter;
|
||||
/**
|
||||
* Describes the current state of a given SHAMap
|
||||
*/
|
||||
enum class SHAMapState {
|
||||
enum class SHAMapState : std::uint8_t {
|
||||
/**
|
||||
* The map is in flux and objects can be added and removed.
|
||||
*
|
||||
@@ -120,16 +122,42 @@ 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::uint32_t ledgerSeq_ = 0;
|
||||
std::atomic<std::uint32_t> ledgerSeq_ = 0;
|
||||
|
||||
SHAMapTreeNodePtr root_;
|
||||
mutable SHAMapState state_;
|
||||
|
||||
/**
|
||||
* 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_;
|
||||
SHAMapType const type_;
|
||||
bool backed_ = true; // Map is backed by the database
|
||||
mutable bool full_ = false; // Map is believed complete in database
|
||||
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;
|
||||
|
||||
public:
|
||||
/**
|
||||
@@ -152,7 +180,14 @@ public:
|
||||
SHAMap&
|
||||
operator=(SHAMap const&) = delete;
|
||||
|
||||
// Take a snapshot of the given map:
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
SHAMap(SHAMap const& other, bool isMutable);
|
||||
|
||||
// build new map
|
||||
@@ -190,17 +225,33 @@ public:
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
// Returns a new map that's a snapshot of this one.
|
||||
// Handles copy on write for mutable snapshots.
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
std::shared_ptr<SHAMap>
|
||||
snapShot(bool isMutable) const;
|
||||
|
||||
/* Mark this SHAMap as "should be full", indicating
|
||||
that the local server wants all the corresponding nodes
|
||||
in durable storage.
|
||||
*/
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
void
|
||||
setFull();
|
||||
setFull(std::uint32_t ledgerSeq);
|
||||
|
||||
void
|
||||
setLedgerSeq(std::uint32_t lseq);
|
||||
@@ -261,12 +312,17 @@ public:
|
||||
lowerBound(UInt256 const& id) const;
|
||||
|
||||
/**
|
||||
* Visit every node in this SHAMap
|
||||
* Visit every node in this SHAMap.
|
||||
*
|
||||
* @param function called with every node visited.
|
||||
* If function returns false, visitNodes exits.
|
||||
* 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.
|
||||
*/
|
||||
void
|
||||
bool
|
||||
visitNodes(std::function<bool(SHAMapTreeNode&)> const& function) const;
|
||||
|
||||
/**
|
||||
@@ -280,11 +336,12 @@ 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.
|
||||
* @param function Called with every non-inner node visited.
|
||||
* @return What visitNodes reports about the walk.
|
||||
*/
|
||||
void
|
||||
bool
|
||||
visitLeaves(std::function<void(boost::intrusive_ptr<SHAMapItem const> const&)> const&) const;
|
||||
|
||||
// comparison/sync functions
|
||||
@@ -296,9 +353,13 @@ 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
|
||||
* @param return The nodes known to be missing
|
||||
* @return The nodes known to be missing, or empty if the map is Invalid
|
||||
*/
|
||||
std::vector<std::pair<SHAMapNodeID, UInt256>>
|
||||
getMissingNodes(int maxNodes, SHAMapSyncFilter const* filter);
|
||||
@@ -341,6 +402,9 @@ 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.
|
||||
@@ -360,6 +424,10 @@ 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.
|
||||
@@ -375,16 +443,57 @@ public:
|
||||
SHAMapTreeNodePtr treeNode,
|
||||
SHAMapSyncFilter const* filter);
|
||||
|
||||
// status functions
|
||||
void
|
||||
/**
|
||||
* 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
|
||||
setImmutable();
|
||||
bool
|
||||
|
||||
/**
|
||||
* @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
|
||||
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();
|
||||
bool
|
||||
|
||||
/**
|
||||
* 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
|
||||
isValid() const;
|
||||
|
||||
// caution: otherMap must be accessed only by this function
|
||||
@@ -524,6 +633,51 @@ 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;
|
||||
@@ -739,46 +893,120 @@ private:
|
||||
};
|
||||
|
||||
inline void
|
||||
SHAMap::setFull()
|
||||
SHAMap::setFull(std::uint32_t ledgerSeq)
|
||||
{
|
||||
full_ = true;
|
||||
ledgerSeq_.store(ledgerSeq, std::memory_order_relaxed);
|
||||
full_.store(true, std::memory_order_release);
|
||||
}
|
||||
|
||||
inline void
|
||||
SHAMap::setLedgerSeq(std::uint32_t lseq)
|
||||
{
|
||||
ledgerSeq_ = lseq;
|
||||
ledgerSeq_.store(lseq, std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
inline void
|
||||
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
|
||||
SHAMap::setImmutable()
|
||||
{
|
||||
XRPL_ASSERT(state_ != SHAMapState::Invalid, "xrpl::SHAMap::setImmutable : state is valid");
|
||||
state_ = SHAMapState::Immutable;
|
||||
SOMETIMES(!isValid(), "xrpl::SHAMap::setImmutable : map is invalid");
|
||||
return trySetState(SHAMapState::Immutable);
|
||||
}
|
||||
|
||||
inline bool
|
||||
SHAMap::isSynching() const
|
||||
{
|
||||
return state_ == SHAMapState::Synching;
|
||||
return state() == SHAMapState::Synching;
|
||||
}
|
||||
|
||||
inline bool
|
||||
SHAMap::isImmutable() const
|
||||
{
|
||||
return state() == SHAMapState::Immutable;
|
||||
}
|
||||
|
||||
inline void
|
||||
SHAMap::setSynching()
|
||||
{
|
||||
state_ = SHAMapState::Synching;
|
||||
// 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
|
||||
}
|
||||
}
|
||||
|
||||
inline void
|
||||
SHAMap::clearSynching()
|
||||
{
|
||||
state_ = SHAMapState::Modifying;
|
||||
// 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();
|
||||
}
|
||||
}
|
||||
|
||||
inline bool
|
||||
SHAMap::isValid() const
|
||||
{
|
||||
return state_ != SHAMapState::Invalid;
|
||||
return state() != SHAMapState::Invalid;
|
||||
}
|
||||
|
||||
inline void
|
||||
SHAMap::setInvalid()
|
||||
{
|
||||
// Through trySetState() like every other transition, so state_ has one writer funnel.
|
||||
trySetState(SHAMapState::Invalid);
|
||||
}
|
||||
|
||||
inline void
|
||||
|
||||
@@ -31,7 +31,19 @@ private:
|
||||
*/
|
||||
TaggedPointer hashesAndChildren_;
|
||||
|
||||
std::uint32_t fullBelowGen_ = 0;
|
||||
// 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::uint16_t isBranch_ = 0;
|
||||
|
||||
/**
|
||||
@@ -204,13 +216,13 @@ SHAMapInnerNode::getBranchCount() const
|
||||
inline bool
|
||||
SHAMapInnerNode::isFullBelow(std::uint32_t generation) const
|
||||
{
|
||||
return fullBelowGen_ == generation;
|
||||
return fullBelowGen_.load(std::memory_order_relaxed) == generation;
|
||||
}
|
||||
|
||||
inline void
|
||||
SHAMapInnerNode::setFullBelowGen(std::uint32_t gen)
|
||||
{
|
||||
fullBelowGen_ = gen;
|
||||
fullBelowGen_.store(gen, std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
} // namespace xrpl
|
||||
@@ -5,6 +5,7 @@
|
||||
#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>
|
||||
@@ -45,9 +46,29 @@ 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
|
||||
*/
|
||||
|
||||
@@ -52,6 +52,13 @@ private:
|
||||
TER
|
||||
applyFee();
|
||||
|
||||
/**
|
||||
* Write the default Feature Extension fee settings into FeeSettings on
|
||||
* `featureSmartEscrow` activation.
|
||||
*/
|
||||
void
|
||||
initializeVMFees();
|
||||
|
||||
TER
|
||||
applyUNLModify();
|
||||
};
|
||||
|
||||
@@ -200,11 +200,23 @@ 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);
|
||||
setImmutable();
|
||||
// Built locally. See Ledger::setImmutable().
|
||||
if (!setImmutable())
|
||||
{
|
||||
// LCOV_EXCL_START
|
||||
logicError("Ledger::Ledger(CreateGenesisT, ...): genesis ledger map is invalid");
|
||||
// LCOV_EXCL_STOP
|
||||
}
|
||||
}
|
||||
|
||||
Ledger::Ledger(
|
||||
@@ -215,7 +227,7 @@ Ledger::Ledger(
|
||||
Fees const& fees,
|
||||
Family& family,
|
||||
beast::Journal j)
|
||||
: immutable_(true)
|
||||
: immutable_(false)
|
||||
, txMap_(SHAMapType::TRANSACTION, info.txHash, family)
|
||||
, stateMap_(SHAMapType::STATE, info.accountHash, family)
|
||||
, fees_(fees)
|
||||
@@ -238,8 +250,20 @@ Ledger::Ledger(
|
||||
JLOG(j.warn()) << "Don't have state data root for ledger" << header_.seq;
|
||||
}
|
||||
|
||||
txMap_.setImmutable();
|
||||
stateMap_.setImmutable();
|
||||
// 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
|
||||
}
|
||||
|
||||
if (!setup())
|
||||
loaded = false;
|
||||
@@ -310,27 +334,50 @@ Ledger::Ledger(
|
||||
setup();
|
||||
}
|
||||
|
||||
void
|
||||
bool
|
||||
Ledger::setImmutable(bool rehash)
|
||||
{
|
||||
// Force update, since this is the only
|
||||
// place the hash transitions to valid
|
||||
if (!immutable_ && 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)
|
||||
{
|
||||
header_.txHash = txMap_.getHash().asUInt256();
|
||||
header_.accountHash = stateMap_.getHash().asUInt256();
|
||||
header_.txHash = txHash;
|
||||
header_.accountHash = accountHash;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
void
|
||||
bool
|
||||
Ledger::setAccepted(
|
||||
NetClock::time_point closeTime,
|
||||
NetClock::duration closeResolution,
|
||||
@@ -342,7 +389,17 @@ Ledger::setAccepted(
|
||||
header_.closeTime = closeTime;
|
||||
header_.closeTimeResolution = closeResolution;
|
||||
header_.closeFlags = correctCloseTime ? 0 : kSLcfNoConsensusTime;
|
||||
setImmutable();
|
||||
// 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;
|
||||
}
|
||||
|
||||
bool
|
||||
@@ -566,6 +623,7 @@ Ledger::setup()
|
||||
{
|
||||
bool oldFees = false;
|
||||
bool newFees = false;
|
||||
bool extensionFees = false;
|
||||
{
|
||||
auto const baseFee = sle->at(~sfBaseFee);
|
||||
auto const reserveBase = sle->at(~sfReserveBase);
|
||||
@@ -600,6 +658,20 @@ 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
|
||||
@@ -610,6 +682,12 @@ 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&)
|
||||
|
||||
@@ -511,9 +511,14 @@ doWithdraw(
|
||||
AccountID const& dstAcct,
|
||||
AccountID const& sourceAcct,
|
||||
XRPAmount priorBalance,
|
||||
STAmount const& amount,
|
||||
STAmount const& destinationAmount,
|
||||
STAmount const& sourceAmount,
|
||||
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
|
||||
@@ -523,9 +528,10 @@ doWithdraw(
|
||||
// create+delete MPTokens in the same transaction.
|
||||
if (dstAcct == senderAcct)
|
||||
{
|
||||
if (amount > beast::kZero || !ctx.view.rules().enabled(fixCleanup3_4_0))
|
||||
if (destinationAmount > beast::kZero || !ctx.view.rules().enabled(fixCleanup3_4_0))
|
||||
{
|
||||
if (auto const ter = addEmptyHolding(ctx, senderAcct, priorBalance, amount.asset(), j);
|
||||
if (auto const ter =
|
||||
addEmptyHolding(ctx, senderAcct, priorBalance, destinationAmount.asset(), j);
|
||||
!isTesSuccess(ter) && ter != tecDUPLICATE)
|
||||
return ter;
|
||||
}
|
||||
@@ -540,17 +546,33 @@ doWithdraw(
|
||||
if (accountHolds(
|
||||
ctx.view,
|
||||
sourceAcct,
|
||||
amount.asset(),
|
||||
destinationAmount.asset(),
|
||||
FreezeHandling::IgnoreFreeze,
|
||||
AuthHandling::IgnoreAuth,
|
||||
j) < amount)
|
||||
j) < sourceAmount)
|
||||
{
|
||||
// LCOV_EXCL_START
|
||||
JLOG(j.error()) << "doWithdraw: negative balance of broker cover assets.";
|
||||
JLOG(j.error()) << "doWithdraw: source account holds fewer assets than the withdrawal.";
|
||||
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
|
||||
@@ -562,7 +584,7 @@ doWithdraw(
|
||||
// Move the funds directly from the broker's pseudo-account to the
|
||||
// dstAcct
|
||||
return accountSend(
|
||||
ctx.view, sourceAcct, dstAcct, amount, j, *sponsorSle, WaiveTransferFee::Yes);
|
||||
ctx.view, sourceAcct, dstAcct, destinationAmount, j, *sponsorSle, WaiveTransferFee::Yes);
|
||||
}
|
||||
|
||||
TER
|
||||
|
||||
@@ -130,8 +130,8 @@ creditToPosteriorBrokerCoverScale(
|
||||
}
|
||||
|
||||
Number const reference = broker->at(sfCoverAvailable);
|
||||
int const scale = detail::getPosteriorBrokerCoverScale(vault, broker, raw);
|
||||
return detail::creditToPosteriorScale(asset, reference, scale, raw, roundingMode);
|
||||
return detail::creditToPosteriorScale(
|
||||
asset, reference, getVaultBaseScale(vault), raw, roundingMode);
|
||||
}
|
||||
|
||||
[[nodiscard]] STAmount
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
#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>
|
||||
@@ -551,6 +552,22 @@ 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
|
||||
@@ -1347,9 +1364,8 @@ directSendNoLimitMPT(
|
||||
{
|
||||
// Number math loses precision on large MPT amounts, which can
|
||||
// overcharge the sender. MPTs are integral, so compute the cost
|
||||
// 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());
|
||||
// exactly, matching the payment engine.
|
||||
saActual = addTransferFee(saAmount, rate);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -1474,9 +1490,8 @@ directSendNoLimitMultiMPT(
|
||||
{
|
||||
// Number math loses precision on large MPT amounts, which can
|
||||
// overcharge the sender. MPTs are integral, so compute the
|
||||
// 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());
|
||||
// cost exactly, matching the payment engine.
|
||||
actualSend = addTransferFee(amount, rate);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
@@ -48,7 +48,7 @@ fixedBaseScale(SLE::ConstRef vault)
|
||||
}
|
||||
|
||||
// Thin, vault-specific wrapper over posteriorAssetScale: used by
|
||||
// getPosteriorVaultScale and creditToPosteriorAvailableScale.
|
||||
// getPosteriorVaultScale and the cash outflow rounding below.
|
||||
[[nodiscard]] int
|
||||
posteriorScale(SLE::ConstRef vault, Number const& reference, Number const& delta)
|
||||
{
|
||||
@@ -195,11 +195,11 @@ posteriorAssetScale(
|
||||
creditToPosteriorScale(
|
||||
Asset const& asset,
|
||||
Number const& reference,
|
||||
int atScale,
|
||||
int baseScale,
|
||||
Number const& raw,
|
||||
Number::RoundingMode roundingMode)
|
||||
{
|
||||
// Floor the SUM (reference + raw), not just raw, at atScale. See
|
||||
// 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.
|
||||
@@ -208,9 +208,21 @@ creditToPosteriorScale(
|
||||
// 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 flooredSum = roundToAsset(asset, reference + raw, atScale, roundingMode);
|
||||
return STAmount{asset, flooredSum - reference};
|
||||
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
|
||||
@@ -399,8 +411,8 @@ creditToPosteriorAvailableScale(
|
||||
}
|
||||
|
||||
Number const reference = vault->at(sfAssetsAvailable);
|
||||
int const scale = posteriorScale(vault, reference, raw);
|
||||
return detail::creditToPosteriorScale(asset, reference, scale, raw, roundingMode);
|
||||
return detail::creditToPosteriorScale(
|
||||
asset, reference, fixedBaseScale(vault), raw, roundingMode);
|
||||
}
|
||||
|
||||
[[nodiscard]] Number
|
||||
|
||||
@@ -435,18 +435,25 @@ areMirrorsCurrent(SLE const& issuance, SLE const& mptoken)
|
||||
}
|
||||
|
||||
void
|
||||
setIssuerMirrorEpoch(SLE const& issuance, SLE& mptoken)
|
||||
setIssuerMirrorCurrent(SLE const& issuance, SLE& mptoken)
|
||||
{
|
||||
XRPL_ASSERT(
|
||||
issuance.getType() == ltMPTOKEN_ISSUANCE,
|
||||
"xrpl::setIssuerMirrorEpoch : issuance MPTokenIssuance object");
|
||||
"xrpl::setIssuerMirrorCurrent : issuance MPTokenIssuance object");
|
||||
XRPL_ASSERT(
|
||||
mptoken.getType() == ltMPTOKEN, "xrpl::setIssuerMirrorEpoch : mptoken MPToken object");
|
||||
mptoken.getType() == ltMPTOKEN, "xrpl::setIssuerMirrorCurrent : 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
|
||||
@@ -466,9 +473,9 @@ setAuditorMirrorEpoch(SLE const& issuance, SLE& mptoken)
|
||||
}
|
||||
|
||||
void
|
||||
setMirrorEpochs(SLE const& issuance, SLE& mptoken)
|
||||
setMirrorsCurrent(SLE const& issuance, SLE& mptoken)
|
||||
{
|
||||
setIssuerMirrorEpoch(issuance, mptoken);
|
||||
setIssuerMirrorCurrent(issuance, mptoken);
|
||||
setAuditorMirrorEpoch(issuance, mptoken);
|
||||
}
|
||||
|
||||
|
||||
@@ -1763,6 +1763,24 @@ 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
|
||||
{
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
|
||||
#include <boost/smart_ptr/intrusive_ptr.hpp>
|
||||
|
||||
#include <atomic>
|
||||
#include <cstdint>
|
||||
#include <exception>
|
||||
#include <functional>
|
||||
@@ -77,14 +78,23 @@ 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 ((state_ != SHAMapState::Immutable) || (other.state_ != SHAMapState::Immutable))
|
||||
if ((ownState != SHAMapState::Immutable) || (otherState != SHAMapState::Immutable))
|
||||
{
|
||||
unshare();
|
||||
}
|
||||
@@ -105,7 +115,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");
|
||||
|
||||
@@ -170,7 +180,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);
|
||||
}
|
||||
|
||||
@@ -183,10 +193,17 @@ SHAMap::finishFetch(SHAMapHash const& hash, std::shared_ptr<NodeObject> const& o
|
||||
{
|
||||
if (!object)
|
||||
{
|
||||
if (full_)
|
||||
// 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))
|
||||
{
|
||||
full_ = false;
|
||||
f_.missingNodeAcquireBySeq(ledgerSeq_, hash.asUInt256());
|
||||
f_.missingNodeAcquireBySeq(ledgerSeq(), hash.asUInt256());
|
||||
}
|
||||
return {};
|
||||
}
|
||||
@@ -219,7 +236,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);
|
||||
}
|
||||
@@ -326,9 +343,6 @@ 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;
|
||||
}
|
||||
@@ -340,7 +354,7 @@ SHAMap::descendNoStore(SHAMapInnerNode& parent, unsigned int branch) const
|
||||
{
|
||||
SHAMapTreeNodePtr ret = parent.getChild(branch);
|
||||
if (!ret && backed_)
|
||||
ret = fetchNode(parent.getChildHash(branch));
|
||||
ret = fetchNodeNT(parent.getChildHash(branch));
|
||||
return ret;
|
||||
}
|
||||
|
||||
@@ -399,7 +413,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);
|
||||
@@ -424,7 +438,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;
|
||||
@@ -637,7 +651,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);
|
||||
@@ -719,7 +733,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
|
||||
@@ -810,7 +824,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);
|
||||
@@ -901,7 +915,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,9 +13,11 @@
|
||||
#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>
|
||||
@@ -283,21 +285,36 @@ bool
|
||||
SHAMap::walkMapParallel(std::vector<SHAMapMissingNode>& missingNodes, int maxMissing) const
|
||||
{
|
||||
if (!root_->isInner()) // root_ is only node, and we have it
|
||||
return false;
|
||||
return true;
|
||||
|
||||
// Only the nodes this call records count towards the result, so remember what the
|
||||
// caller already had.
|
||||
auto const initialMissing = missingNodes.size();
|
||||
|
||||
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))
|
||||
topChildren[i] = descendNoStore(*innerRoot, 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
std::vector<std::thread> workers;
|
||||
workers.reserve(SHAMapInnerNode::kBranchFactor);
|
||||
std::vector<SHAMapMissingNode> exceptions;
|
||||
std::vector<std::string> exceptions;
|
||||
exceptions.reserve(SHAMapInnerNode::kBranchFactor);
|
||||
|
||||
std::array<std::stack<StackEntry, std::vector<StackEntry>>, SHAMapInnerNode::kBranchFactor>
|
||||
@@ -346,6 +363,14 @@ 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;
|
||||
@@ -353,10 +378,14 @@ SHAMap::walkMapParallel(std::vector<SHAMapMissingNode>& missingNodes, int maxMis
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (SHAMapMissingNode const& e)
|
||||
catch (std::exception 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.push_back(e);
|
||||
exceptions.emplace_back(e.what());
|
||||
// LCOV_EXCL_STOP
|
||||
}
|
||||
},
|
||||
std::move(nodeStacks[rootChildIndex]));
|
||||
@@ -366,14 +395,29 @@ SHAMap::walkMapParallel(std::vector<SHAMapMissingNode>& missingNodes, int maxMis
|
||||
worker.join();
|
||||
|
||||
std::scoped_lock const l(m);
|
||||
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;
|
||||
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;
|
||||
}
|
||||
|
||||
} // namespace xrpl
|
||||
@@ -16,6 +16,7 @@
|
||||
#include <xrpl/shamap/detail/TaggedPointer.h>
|
||||
#include <xrpl/shamap/detail/TaggedPointer.ipp>
|
||||
|
||||
#include <atomic>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <mutex>
|
||||
@@ -77,7 +78,9 @@ SHAMapInnerNode::clone(std::uint32_t cowid) const
|
||||
auto p = intr_ptr::makeShared<SHAMapInnerNode>(cowid, branchCount);
|
||||
p->hash_ = hash_;
|
||||
p->isBranch_ = isBranch_;
|
||||
p->fullBelowGen_ = fullBelowGen_;
|
||||
// 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);
|
||||
SHAMapHash* cloneHashes = nullptr;
|
||||
SHAMapHash* thisHashes = nullptr;
|
||||
SHAMapTreeNodePtr* cloneChildren = nullptr;
|
||||
|
||||
@@ -31,28 +31,55 @@
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
void
|
||||
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
|
||||
SHAMap::visitLeaves(
|
||||
std::function<void(boost::intrusive_ptr<SHAMapItem const> const& item)> const& leafFunction)
|
||||
const
|
||||
{
|
||||
visitNodes([&leafFunction](SHAMapTreeNode& node) {
|
||||
return visitNodes([&leafFunction](SHAMapTreeNode& node) {
|
||||
if (!node.isInner())
|
||||
leafFunction(safeDowncast<SHAMapLeafNode&>(node).peekItem());
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
void
|
||||
bool
|
||||
SHAMap::visitNodes(std::function<bool(SHAMapTreeNode&)> const& function) const
|
||||
{
|
||||
if (!root_)
|
||||
return;
|
||||
{
|
||||
// LCOV_EXCL_START
|
||||
UNREACHABLE("xrpl::SHAMap::visitNodes : non-null root");
|
||||
return true;
|
||||
// LCOV_EXCL_STOP
|
||||
}
|
||||
|
||||
function(*root_);
|
||||
// The root is readable by definition, so a stop here still reports a complete walk.
|
||||
if (!function(*root_))
|
||||
return true;
|
||||
|
||||
// A map whose root is a leaf holds one node, and this walk just visited it.
|
||||
if (!root_->isInner())
|
||||
return;
|
||||
return true;
|
||||
|
||||
using StackEntry = std::pair<unsigned int, intr_ptr::SharedPtr<SHAMapInnerNode>>;
|
||||
std::stack<StackEntry, std::vector<StackEntry>> stack;
|
||||
@@ -67,8 +94,17 @@ 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;
|
||||
return true;
|
||||
|
||||
if (child->isLeaf())
|
||||
{
|
||||
@@ -103,6 +139,8 @@ SHAMap::visitNodes(std::function<bool(SHAMapTreeNode&)> const& function) const
|
||||
std::tie(pos, node) = stack.top();
|
||||
stack.pop();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void
|
||||
@@ -143,13 +181,10 @@ SHAMap::visitDifferences(
|
||||
if (!function(*node))
|
||||
return;
|
||||
|
||||
// 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)
|
||||
// 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()))
|
||||
{
|
||||
// LCOV_EXCL_START
|
||||
UNREACHABLE("xrpl::SHAMap::visitDifferences : inner node at leaf depth");
|
||||
@@ -207,7 +242,12 @@ SHAMap::gmnProcessNodes(MissingNodes& mn, MissingNodes::StackEntry& se)
|
||||
// we already know this child node is missing
|
||||
fullBelow = false;
|
||||
}
|
||||
else if (!backed_ || !f_.getFullBelowCache()->touchIfExists(childHash.asUInt256()))
|
||||
// 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()))
|
||||
{
|
||||
bool pending = false;
|
||||
auto d = descendAsync(
|
||||
@@ -238,6 +278,17 @@ 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);
|
||||
@@ -301,6 +352,8 @@ 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;
|
||||
}
|
||||
@@ -311,17 +364,20 @@ 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,
|
||||
@@ -354,6 +410,11 @@ 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;
|
||||
|
||||
@@ -383,6 +444,11 @@ 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);
|
||||
|
||||
@@ -416,6 +482,11 @@ 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();
|
||||
|
||||
@@ -527,11 +598,19 @@ SHAMap::addRootNode(
|
||||
XRPL_ASSERT(cowid_ >= 1, "xrpl::SHAMap::addRootNode : valid cowid");
|
||||
XRPL_ASSERT(rootNode, "xrpl::SHAMap::addRootNode : non-null root node");
|
||||
|
||||
// we already have a 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.
|
||||
if (root_->getHash().isNonZero())
|
||||
{
|
||||
JLOG(journal_.trace()) << "Got root node, already have one";
|
||||
XRPL_ASSERT(root_->getHash() == hash, "xrpl::SHAMap::addRootNode : valid hash");
|
||||
|
||||
if (root_->getHash() != hash)
|
||||
{
|
||||
JLOG(journal_.warn()) << "Root node offered under hash " << hash
|
||||
<< ", but the map holds " << root_->getHash();
|
||||
return SHAMapAddNode::invalid();
|
||||
}
|
||||
|
||||
return SHAMapAddNode::duplicate();
|
||||
}
|
||||
|
||||
@@ -555,7 +634,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();
|
||||
@@ -598,7 +677,13 @@ SHAMap::addKnownNode(
|
||||
}
|
||||
|
||||
auto childHash = inner->getChildHash(branch);
|
||||
if (f_.getFullBelowCache()->touchIfExists(childHash.asUInt256()))
|
||||
|
||||
// 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()))
|
||||
{
|
||||
return SHAMapAddNode::duplicate();
|
||||
}
|
||||
@@ -616,25 +701,32 @@ SHAMap::addKnownNode(
|
||||
return SHAMapAddNode::invalid();
|
||||
}
|
||||
|
||||
// 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))
|
||||
// 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)
|
||||
{
|
||||
// Map is provably invalid
|
||||
state_ = SHAMapState::Invalid;
|
||||
return SHAMapAddNode::useful();
|
||||
JLOG(journal_.warn()) << "Node " << nodeID << " makes the map invalid at "
|
||||
<< currNodeID;
|
||||
setInvalid();
|
||||
return SHAMapAddNode::invalid();
|
||||
}
|
||||
|
||||
if (currNodeID != nodeID)
|
||||
// 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)
|
||||
{
|
||||
// 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();
|
||||
JLOG(journal_.warn()) << "Unable to hook node " << nodeID << ", stuck at "
|
||||
<< currNodeID;
|
||||
return SHAMapAddNode::invalid();
|
||||
}
|
||||
|
||||
if (backed_)
|
||||
@@ -647,7 +739,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();
|
||||
@@ -766,7 +858,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 (nodeID.getDepth() >= kLeafDepth)
|
||||
if (isLeafDepth(nodeID.getDepth()))
|
||||
{
|
||||
// LCOV_EXCL_START
|
||||
UNREACHABLE("xrpl::SHAMap::hasLeafNode : inner node at leaf depth");
|
||||
@@ -849,8 +941,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(
|
||||
depth >= kLeafDepth, "xrpl::SHAMap::verifyProofPath : inner at leaf depth");
|
||||
if (depth >= kLeafDepth)
|
||||
isLeafDepth(depth), "xrpl::SHAMap::verifyProofPath : inner at leaf depth");
|
||||
if (isLeafDepth(depth))
|
||||
return false;
|
||||
|
||||
auto const nodeId = SHAMapNodeID::createID(depth, key);
|
||||
@@ -859,10 +951,9 @@ SHAMap::verifyProofPath(UInt256 const& rootHash, UInt256 const& key, std::vector
|
||||
}
|
||||
else
|
||||
{
|
||||
// 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.
|
||||
// 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`.
|
||||
if (leafKey(*node) != key)
|
||||
return false;
|
||||
|
||||
|
||||
@@ -7,12 +7,14 @@
|
||||
#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>
|
||||
@@ -485,6 +487,7 @@ 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
|
||||
@@ -1295,6 +1298,18 @@ 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
|
||||
@@ -1303,7 +1318,8 @@ 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 = tolerateZeroDelta &&
|
||||
auto const destroyedIsSubUlp = !feeAdjustedWithdrawal &&
|
||||
tolerateZeroDelta &&
|
||||
roundToAsset(
|
||||
vaultAsset,
|
||||
vaultDeltaAssets.delta * -1 - destinationDelta.delta,
|
||||
@@ -1311,11 +1327,11 @@ ValidVault::finalize(
|
||||
Number::RoundingMode::Downward) == kZero;
|
||||
bool const withdrawAddsUp = fix340Enabled
|
||||
? agreesWithinOneUnit(
|
||||
localPseudoDeltaAssets * -1,
|
||||
expectedDestinationDelta,
|
||||
roundedDestinationDelta,
|
||||
vaultAsset,
|
||||
localMinScale)
|
||||
: localPseudoDeltaAssets * -1 == roundedDestinationDelta;
|
||||
: expectedDestinationDelta == roundedDestinationDelta;
|
||||
if (!destroyedIsSubUlp && !withdrawAddsUp)
|
||||
{
|
||||
JLOG(j.fatal()) << "Invariant failed: " << //
|
||||
|
||||
@@ -270,6 +270,7 @@ LoanBrokerCoverWithdraw::doApply()
|
||||
brokerPseudoID,
|
||||
preFeeBalance_,
|
||||
amount,
|
||||
amount,
|
||||
j_);
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#include <xrpl/basics/Log.h>
|
||||
#include <xrpl/basics/Number.h>
|
||||
#include <xrpl/beast/utility/Zero.h>
|
||||
#include <xrpl/beast/utility/instrumentation.h>
|
||||
#include <xrpl/core/ServiceRegistry.h>
|
||||
#include <xrpl/ledger/ApplyView.h>
|
||||
#include <xrpl/ledger/View.h>
|
||||
@@ -129,25 +130,133 @@ LoanManage::preclaim(PreclaimContext const& ctx)
|
||||
return tecNO_PERMISSION;
|
||||
}
|
||||
|
||||
if (auto const vault = ctx.view.read(keylet::vault(loanBrokerSle->at(sfVaultID)));
|
||||
vault && getVaultVersion(vault) == VaultVersion::FixedPrecision)
|
||||
{
|
||||
// FixedPrecision Vaults are not yet supported by LoanManage.
|
||||
return tecNO_PERMISSION;
|
||||
}
|
||||
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
STAmount
|
||||
LoanManage::calculateDefaultCover(SLE::Ref loanSle, SLE::Ref brokerSle, SLE::Ref vaultSle)
|
||||
{
|
||||
auto const vaultAsset = vaultSle->at(sfAsset);
|
||||
|
||||
Number const principalOutstanding = loanSle->at(sfPrincipalOutstanding);
|
||||
|
||||
TenthBips32 const coverRateMinimum{brokerSle->at(sfCoverRateMinimum)};
|
||||
TenthBips32 const coverRateLiquidation{brokerSle->at(sfCoverRateLiquidation)};
|
||||
|
||||
Number const rawCover = [&] {
|
||||
NumberRoundModeGuard const mg(Number::RoundingMode::Upward);
|
||||
// Compute minimum cover
|
||||
Number const minimumCover =
|
||||
tenthBipsOfValue(brokerSle->at(sfDebtTotal).value(), coverRateMinimum);
|
||||
|
||||
// Apply coverRateLiquidation
|
||||
return std::min(tenthBipsOfValue(minimumCover, coverRateLiquidation), principalOutstanding);
|
||||
}();
|
||||
|
||||
// rawCover is a product of multiplication and can carry more digits than STAmount's. Keep the
|
||||
// cap as an exact Number: rounding it to 16 digits first could drop digits that change
|
||||
// floor16(AssetsAvailable + cover).
|
||||
Number const cappedCover = std::min(rawCover, *brokerSle->at(sfCoverAvailable));
|
||||
|
||||
if (Asset{vaultAsset}.integral())
|
||||
{
|
||||
// Fix the mode so the STAmount conversion truncates the fractional part.
|
||||
NumberRoundModeGuard const mg(Number::RoundingMode::TowardsZero);
|
||||
return STAmount{vaultAsset, cappedCover};
|
||||
}
|
||||
|
||||
// Floor AssetsAvailable + cover at its posterior grid so the Vault side is exact, unless
|
||||
// AssetsAvailable has digits finer than the 16-digit cover can carry. The LoanBroker absorbs
|
||||
// the rounding: CoverAvailable - cover may need finer digits than its posterior grid and
|
||||
// rounds to nearest, as the broker's trust line does. Downward also keeps the 19-digit
|
||||
// AssetsAvailable + cover sum from rounding up past cappedCover.
|
||||
NumberRoundModeGuard const mg(Number::RoundingMode::Downward);
|
||||
return creditToPosteriorAvailableScale(vaultSle, cappedCover, Number::RoundingMode::Downward);
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
// Defaults a Loan on a FixedPrecision Vault. Cover is the raw XLS-66
|
||||
// First-Loss Capital amount, capped at the LoanBroker's CoverAvailable and
|
||||
// floored so AssetsAvailable + cover lands exactly on its posterior grid (a
|
||||
// no-op for integral assets). AssetsAvailable rises by cover through the cash writer;
|
||||
// AssetsDeployed drops by the Loan's full PrincipalOutstanding exactly, and
|
||||
// AssetsTotal is re-derived from AssetsAvailable + AssetsDeployed by the
|
||||
// writer's sync -- this function never touches sfAssetsTotal directly.
|
||||
[[nodiscard]] TER
|
||||
defaultLoanFixedPrecision(
|
||||
ApplyView& view,
|
||||
SLE::Ref loanSle,
|
||||
SLE::Ref brokerSle,
|
||||
SLE::Ref vaultSle,
|
||||
beast::Journal j)
|
||||
{
|
||||
Number const principalOutstanding = loanSle->at(sfPrincipalOutstanding);
|
||||
Number const scheduledInterest = loanSle->at(sfTotalValueOutstanding) - principalOutstanding -
|
||||
loanSle->at(sfManagementFeeOutstanding);
|
||||
|
||||
STAmount const coverAmount = LoanManage::calculateDefaultCover(loanSle, brokerSle, vaultSle);
|
||||
|
||||
// The broker's CoverAvailable decrease and the broker-to-vault transfer
|
||||
// both use this amount. AssetsAvailable + cover is exact unless
|
||||
// AssetsAvailable has digits finer than the 16-digit cover can carry; then
|
||||
// adjustVaultBalances rounds the sum. CoverAvailable - cover may need finer
|
||||
// digits than the broker's posterior grid; it rounds to nearest, as the
|
||||
// broker's trust line does.
|
||||
|
||||
Number const loss = loanSle->isFlag(lsfLoanImpaired) ? -principalOutstanding : 0;
|
||||
if (auto const ter = adjustVaultBalances(
|
||||
vaultSle,
|
||||
{
|
||||
.cash = coverAmount,
|
||||
.deployed = -principalOutstanding,
|
||||
.yield = -scheduledInterest,
|
||||
.loss = loss,
|
||||
},
|
||||
j);
|
||||
!isTesSuccess(ter))
|
||||
return ter; // LCOV_EXCL_LINE
|
||||
view.update(vaultSle);
|
||||
|
||||
brokerSle->at(sfDebtTotal) -= principalOutstanding;
|
||||
brokerSle->at(sfCoverAvailable) -= coverAmount;
|
||||
view.update(brokerSle);
|
||||
|
||||
loanSle->setFlag(lsfLoanDefault);
|
||||
loanSle->at(sfTotalValueOutstanding) = 0;
|
||||
loanSle->at(sfPaymentRemaining) = 0;
|
||||
loanSle->at(sfPrincipalOutstanding) = 0;
|
||||
loanSle->at(sfManagementFeeOutstanding) = 0;
|
||||
loanSle->at(sfNextPaymentDueDate) = 0;
|
||||
view.update(loanSle);
|
||||
|
||||
if (coverAmount == beast::kZero)
|
||||
return tesSUCCESS;
|
||||
|
||||
return accountSend(
|
||||
view,
|
||||
brokerSle->at(sfAccount),
|
||||
vaultSle->at(sfAccount),
|
||||
coverAmount,
|
||||
j,
|
||||
{},
|
||||
WaiveTransferFee::Yes);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
TER
|
||||
LoanManage::defaultLoan(
|
||||
ApplyView& view,
|
||||
SLE::Ref loanSle,
|
||||
SLE::Ref brokerSle,
|
||||
SLE::Ref vaultSle,
|
||||
Asset const& vaultAsset,
|
||||
beast::Journal j)
|
||||
{
|
||||
if (getVaultVersion(vaultSle) == VaultVersion::FixedPrecision)
|
||||
return defaultLoanFixedPrecision(view, loanSle, brokerSle, vaultSle, j);
|
||||
|
||||
auto const vaultAsset = *vaultSle->at(sfAsset);
|
||||
// Calculate the amount of the Default that First-Loss Capital covers:
|
||||
|
||||
std::int32_t const loanScale = loanSle->at(sfLoanScale);
|
||||
@@ -184,7 +293,7 @@ LoanManage::defaultLoan(
|
||||
// The vault may be at a different scale than the loan. Reduce rounding
|
||||
// errors during the accounting by rounding some of the values to that
|
||||
// scale.
|
||||
auto const vaultScale = getAssetsTotalScale(vaultSle);
|
||||
auto const vaultScale = getVaultScale(vaultSle);
|
||||
|
||||
{
|
||||
// Decrease the Total Value of the Vault:
|
||||
@@ -307,22 +416,39 @@ LoanManage::impairLoan(
|
||||
return tecTOO_SOON;
|
||||
}
|
||||
|
||||
bool const fixedPrecision = getVaultVersion(vaultSle) == VaultVersion::FixedPrecision;
|
||||
Number const lossUnrealized = loanVaultExposure(vaultSle, loanSle);
|
||||
|
||||
// The vault may be at a different scale than the loan. Reduce rounding
|
||||
// errors during the accounting by rounding some of the values to that
|
||||
// scale.
|
||||
auto const vaultScale = getAssetsTotalScale(vaultSle);
|
||||
|
||||
// Update the Vault object(set "paper loss")
|
||||
auto const vaultLossUnrealizedProxy = vaultSle->at(sfLossUnrealized);
|
||||
adjustImpreciseNumber(vaultLossUnrealizedProxy, lossUnrealized, vaultAsset, vaultScale);
|
||||
if (vaultLossUnrealizedProxy > vaultSle->at(sfAssetsTotal) - vaultSle->at(sfAssetsAvailable))
|
||||
// Update the Vault object (set "paper loss")
|
||||
if (fixedPrecision)
|
||||
{
|
||||
// Having a loss greater than the vault's unavailable assets
|
||||
// will leave the vault in an invalid / inconsistent state.
|
||||
JLOG(j.warn()) << "Vault unrealized loss is too large, and will corrupt the vault.";
|
||||
return tecLIMIT_EXCEEDED;
|
||||
// adjustVaultBalances's own LU' > AD' check covers the comparison
|
||||
// against AssetsDeployed.
|
||||
if (auto const ter = adjustVaultBalances(vaultSle, {.loss = lossUnrealized}, j);
|
||||
!isTesSuccess(ter))
|
||||
{
|
||||
// LCOV_EXCL_START
|
||||
JLOG(j.warn()) << "Vault unrealized loss is too large, and will corrupt the vault.";
|
||||
return ter;
|
||||
// LCOV_EXCL_STOP
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
auto const vaultLossUnrealizedProxy = vaultSle->at(sfLossUnrealized);
|
||||
// The vault may be at a different scale than the loan. Reduce rounding
|
||||
// errors during the accounting by rounding some of the values to that
|
||||
// scale.
|
||||
auto const vaultScale = getVaultScale(vaultSle);
|
||||
adjustImpreciseNumber(vaultLossUnrealizedProxy, lossUnrealized, vaultAsset, vaultScale);
|
||||
if (vaultLossUnrealizedProxy >
|
||||
vaultSle->at(sfAssetsTotal) - vaultSle->at(sfAssetsAvailable))
|
||||
{
|
||||
// Having a loss greater than the vault's unavailable assets
|
||||
// will leave the vault in an invalid / inconsistent state.
|
||||
JLOG(j.warn()) << "Vault unrealized loss is too large, and will corrupt the vault.";
|
||||
return tecLIMIT_EXCEEDED; // LCOV_EXCL_LINE
|
||||
}
|
||||
}
|
||||
view.update(vaultSle);
|
||||
|
||||
@@ -351,23 +477,32 @@ LoanManage::unimpairLoan(
|
||||
Asset const& vaultAsset,
|
||||
beast::Journal j)
|
||||
{
|
||||
// The vault may be at a different scale than the loan. Reduce rounding
|
||||
// errors during the accounting by rounding some of the values to that
|
||||
// scale.
|
||||
auto const vaultScale = getAssetsTotalScale(vaultSle);
|
||||
|
||||
// Update the Vault object(clear "paper loss")
|
||||
auto const vaultLossUnrealizedProxy = vaultSle->at(sfLossUnrealized);
|
||||
// Update the Vault object (clear "paper loss")
|
||||
bool const fixedPrecision = getVaultVersion(vaultSle) == VaultVersion::FixedPrecision;
|
||||
Number const lossReversed = loanVaultExposure(vaultSle, loanSle);
|
||||
if (vaultLossUnrealizedProxy < lossReversed)
|
||||
if (fixedPrecision)
|
||||
{
|
||||
// LCOV_EXCL_START
|
||||
JLOG(j.warn()) << "Vault unrealized loss is less than the amount to be cleared";
|
||||
return tefBAD_LEDGER;
|
||||
// LCOV_EXCL_STOP
|
||||
// adjustVaultBalances's own LU' < 0 check covers the comparison.
|
||||
if (auto const ter = adjustVaultBalances(vaultSle, {.loss = -lossReversed}, j);
|
||||
!isTesSuccess(ter))
|
||||
return ter; // LCOV_EXCL_LINE
|
||||
}
|
||||
else
|
||||
{
|
||||
auto const vaultLossUnrealizedProxy = vaultSle->at(sfLossUnrealized);
|
||||
if (vaultLossUnrealizedProxy < lossReversed)
|
||||
{
|
||||
// LCOV_EXCL_START
|
||||
JLOG(j.warn()) << "Vault unrealized loss is less than the amount to be cleared";
|
||||
return tefBAD_LEDGER;
|
||||
// LCOV_EXCL_STOP
|
||||
}
|
||||
// The vault may be at a different scale than the loan. Reduce rounding
|
||||
// errors during the accounting by rounding some of the values to that
|
||||
// scale.
|
||||
auto const vaultScale = getVaultScale(vaultSle);
|
||||
adjustImpreciseNumber(vaultLossUnrealizedProxy, -lossReversed, vaultAsset, vaultScale);
|
||||
}
|
||||
// Reverse the "paper loss"
|
||||
adjustImpreciseNumber(vaultLossUnrealizedProxy, -lossReversed, vaultAsset, vaultScale);
|
||||
|
||||
view.update(vaultSle);
|
||||
|
||||
@@ -422,7 +557,7 @@ LoanManage::doApply()
|
||||
// Valid flag combinations are checked in preflight. No flags is valid -
|
||||
// just a noop.
|
||||
if (tx.isFlag(tfLoanDefault))
|
||||
return defaultLoan(view, loanSle, brokerSle, vaultSle, vaultAsset, j_);
|
||||
return defaultLoan(view, loanSle, brokerSle, vaultSle, j_);
|
||||
if (tx.isFlag(tfLoanImpair))
|
||||
return impairLoan(view, loanSle, vaultSle, vaultAsset, j_);
|
||||
if (tx.isFlag(tfLoanUnimpair))
|
||||
@@ -435,9 +570,27 @@ LoanManage::doApply()
|
||||
// path. Post-amendment, we call associateAsset on all successful paths.
|
||||
if (view.rules().enabled(fixCleanup3_1_3) && isTesSuccess(result))
|
||||
{
|
||||
bool const fixedPrecision = getVaultVersion(vaultSle) == VaultVersion::FixedPrecision;
|
||||
[[maybe_unused]] bool const assetsEqualBeforeStore =
|
||||
fixedPrecision && vaultSle->at(sfAssetsAvailable) == vaultSle->at(sfAssetsTotal);
|
||||
|
||||
associateAsset(*loanSle, vaultAsset);
|
||||
associateAsset(*brokerSle, vaultAsset);
|
||||
associateAsset(*vaultSle, vaultAsset);
|
||||
|
||||
if (fixedPrecision)
|
||||
{
|
||||
[[maybe_unused]] Number const assetsAvailableAfter = *vaultSle->at(sfAssetsAvailable);
|
||||
[[maybe_unused]] Number const assetsTotalAfter = *vaultSle->at(sfAssetsTotal);
|
||||
XRPL_ASSERT_PARTS(
|
||||
assetsAvailableAfter <= assetsTotalAfter,
|
||||
"xrpl::LoanManage::doApply",
|
||||
"assets available must not be greater than assets outstanding");
|
||||
XRPL_ASSERT_PARTS(
|
||||
!assetsEqualBeforeStore || assetsAvailableAfter == assetsTotalAfter,
|
||||
"xrpl::LoanManage::doApply",
|
||||
"equal assets remain equal after storage");
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
|
||||
@@ -46,14 +46,12 @@ isPaymentAmountInvalid(STAmount const& amount)
|
||||
return amount <= beast::kZero;
|
||||
}
|
||||
|
||||
// Returns the account's true, unclamped balance in `asset`, for use only in
|
||||
// fund-conservation checks. accountHolds(..., SpendableHandling::FullBalance)
|
||||
// cannot be used for this: for XRP it always defers to xrpLiquid, which
|
||||
// subtracts the account's reserve, so a payee sitting below its own reserve
|
||||
// would appear to receive nothing even though its raw ledger balance grew.
|
||||
// That mismatch is exactly what a conservation check must not see.
|
||||
// Returns the balance stored on the ledger for a holder of `asset`: the raw
|
||||
// XRP balance (not xrpLiquid, which subtracts the reserve), or the signed
|
||||
// trust line or MPToken balance with no trust limit added. Not meaningful for
|
||||
// the issuer, which holds no balance of its own.
|
||||
STAmount
|
||||
conservationBalance(ReadView const& view, AccountID const& id, Asset const& asset, beast::Journal j)
|
||||
storedBalance(ReadView const& view, AccountID const& id, Asset const& asset, beast::Journal j)
|
||||
{
|
||||
if (isXRP(asset))
|
||||
{
|
||||
@@ -62,6 +60,25 @@ conservationBalance(ReadView const& view, AccountID const& id, Asset const& asse
|
||||
return STAmount{asset}; // LCOV_EXCL_LINE
|
||||
return view.balanceHookIOU(id, xrpAccount(), sle->getFieldAmount(sfBalance));
|
||||
}
|
||||
return accountHolds(
|
||||
view,
|
||||
id,
|
||||
asset,
|
||||
FreezeHandling::IgnoreFreeze,
|
||||
AuthHandling::IgnoreAuth,
|
||||
j,
|
||||
SpendableHandling::SimpleBalance);
|
||||
}
|
||||
|
||||
// Returns the account's balance in `asset`, for use only in fund-conservation
|
||||
// checks. A holder's is its stored balance: a spendable balance would add the
|
||||
// opposite trust limit, which can hide a small credit in rounding. An issuer
|
||||
// has no stored balance, so its spendable balance is used instead.
|
||||
STAmount
|
||||
conservationBalance(ReadView const& view, AccountID const& id, Asset const& asset, beast::Journal j)
|
||||
{
|
||||
if (id != asset.getIssuer())
|
||||
return storedBalance(view, id, asset, j);
|
||||
return accountHolds(
|
||||
view,
|
||||
id,
|
||||
@@ -283,11 +300,6 @@ LoanPay::preclaim(PreclaimContext const& ctx)
|
||||
return tefBAD_LEDGER;
|
||||
// LCOV_EXCL_STOP
|
||||
}
|
||||
if (getVaultVersion(vaultSle) == VaultVersion::FixedPrecision)
|
||||
{
|
||||
// FixedPrecision Vaults are not yet supported by LoanPay.
|
||||
return tecNO_PERMISSION;
|
||||
}
|
||||
auto const asset = vaultSle->at(sfAsset);
|
||||
auto const vaultPseudoAccount = vaultSle->at(sfAccount);
|
||||
|
||||
@@ -362,11 +374,11 @@ LoanPay::doApply()
|
||||
auto const asset = *vaultSle->at(sfAsset);
|
||||
|
||||
// Determine where to send the broker's fee
|
||||
auto coverAvailableProxy = brokerSle->at(sfCoverAvailable);
|
||||
TenthBips32 const coverRateMinimum{brokerSle->at(sfCoverRateMinimum)};
|
||||
auto debtTotalProxy = brokerSle->at(sfDebtTotal);
|
||||
|
||||
auto const vaultScale = getAssetsTotalScale(vaultSle);
|
||||
bool const fixedPrecision = getVaultVersion(vaultSle) == VaultVersion::FixedPrecision;
|
||||
auto const vaultScale = getVaultScale(vaultSle);
|
||||
|
||||
// Send the broker fee to the owner if they have sufficient cover available,
|
||||
// _and_ if the owner can receive funds
|
||||
@@ -392,7 +404,8 @@ LoanPay::doApply()
|
||||
return roundToAsset(
|
||||
asset, tenthBipsOfValue(debtTotalProxy.value(), coverRateMinimum), loanScale);
|
||||
}();
|
||||
return coverAvailableProxy >= minCover && !isDeepFrozen(view, brokerOwner, asset) &&
|
||||
return brokerSle->at(sfCoverAvailable) >= minCover &&
|
||||
!isDeepFrozen(view, brokerOwner, asset) &&
|
||||
!requireAuth(view, asset, brokerOwner, AuthType::StrongAuth);
|
||||
}();
|
||||
|
||||
@@ -425,6 +438,12 @@ LoanPay::doApply()
|
||||
}
|
||||
}
|
||||
|
||||
auto const scheduledInterest = [&loanSle] {
|
||||
return loanSle->at(sfTotalValueOutstanding) - loanSle->at(sfPrincipalOutstanding) -
|
||||
loanSle->at(sfManagementFeeOutstanding);
|
||||
};
|
||||
Number const scheduledInterestBefore = fixedPrecision ? scheduledInterest() : kNumZero;
|
||||
|
||||
LoanPaymentType const paymentType = [&tx] {
|
||||
// preflight already checked that at most one flag is set.
|
||||
if (tx.isFlag(tfLoanLatePayment))
|
||||
@@ -438,7 +457,6 @@ LoanPay::doApply()
|
||||
|
||||
std::expected<LoanPaymentParts, TER> const paymentParts =
|
||||
loanMakePayment(asset, view, loanSle, brokerSle, amount, paymentType, j_);
|
||||
|
||||
if (!paymentParts)
|
||||
{
|
||||
XRPL_ASSERT_PARTS(
|
||||
@@ -446,10 +464,12 @@ LoanPay::doApply()
|
||||
return paymentParts.error();
|
||||
}
|
||||
|
||||
// If the payment computation completed without error, the loanSle object
|
||||
// has been modified.
|
||||
// If the payment computation completed without error, the loanSle object has been modified.
|
||||
view.update(loanSle);
|
||||
|
||||
Number const scheduledInterestDelta =
|
||||
fixedPrecision ? scheduledInterest() - scheduledInterestBefore : kNumZero;
|
||||
|
||||
XRPL_ASSERT_PARTS(
|
||||
// It is possible to pay 0 principal
|
||||
paymentParts->principalPaid >= 0,
|
||||
@@ -476,35 +496,49 @@ LoanPay::doApply()
|
||||
// LCOV_EXCL_STOP
|
||||
}
|
||||
|
||||
auto const [assetsTotalDelta, debtTotalDelta] = loanPaymentDeltas(vaultSle, *paymentParts);
|
||||
//------------------------------------------------------
|
||||
// LoanBroker object state changes
|
||||
view.update(brokerSle);
|
||||
|
||||
Number const assetsAvailableBefore = *vaultSle->at(sfAssetsAvailable);
|
||||
Number const assetsTotalBefore = vaultSle->at(sfAssetsTotal);
|
||||
|
||||
auto const totalPaidToVaultRaw = paymentParts->principalPaid + paymentParts->interestPaid;
|
||||
auto const deltas = loanPaymentDeltas(vaultSle, *paymentParts);
|
||||
// FixedPrecision never writes sfAssetsTotal directly, so assetsTotalDelta
|
||||
// (cash_basis's interestPaid) is unused here and only meaningful for
|
||||
// Legacy/CashBasis below.
|
||||
Number const assetsTotalDelta = fixedPrecision ? kNumZero : deltas.assetsTotalDelta;
|
||||
Number const debtTotalDelta = deltas.debtTotalDelta;
|
||||
STAmount const totalPaidToVaultRounded = fixedPrecision
|
||||
// Pass the exact Number sum: rounding it to 16 digits first could drop
|
||||
// a tail that moves floor16(AssetsAvailable + sum).
|
||||
? creditToPosteriorAvailableScale(
|
||||
vaultSle, totalPaidToVaultRaw, Number::RoundingMode::Downward)
|
||||
: STAmount{
|
||||
asset,
|
||||
roundToAsset(asset, totalPaidToVaultRaw, vaultScale, Number::RoundingMode::Downward)};
|
||||
XRPL_ASSERT_PARTS(
|
||||
!asset.integral() || totalPaidToVaultRaw == Number(totalPaidToVaultRounded),
|
||||
"xrpl::LoanPay::doApply",
|
||||
"rounding does nothing for integral asset");
|
||||
Number const totalPaidToBrokerRaw = paymentParts->feePaid;
|
||||
// Fee redirect into cover is an inflow to CoverAvailable, so it must
|
||||
// floor the sum (see creditToPosteriorBrokerCoverScale's doc), not just
|
||||
// the delta. Legacy/CashBasis keeps the fee as the original, unrounded
|
||||
// Number: only the FixedPrecision path is 16-digit-rounded via STAmount.
|
||||
Number const totalPaidToBroker = fixedPrecision && !sendBrokerFeeToOwner
|
||||
// The exact fee, for the same reason as totalPaidToVaultRounded.
|
||||
? Number(creditToPosteriorBrokerCoverScale(
|
||||
vaultSle, brokerSle, totalPaidToBrokerRaw, Number::RoundingMode::Downward))
|
||||
: totalPaidToBrokerRaw;
|
||||
|
||||
JLOG(j_.debug()) << "Loan Pay: principal paid: " << paymentParts->principalPaid
|
||||
<< ", interest paid: " << paymentParts->interestPaid
|
||||
<< ", fee paid: " << paymentParts->feePaid
|
||||
<< ", assets total delta: " << assetsTotalDelta
|
||||
<< ", debt total delta: " << debtTotalDelta;
|
||||
|
||||
//------------------------------------------------------
|
||||
// LoanBroker object state changes
|
||||
view.update(brokerSle);
|
||||
|
||||
auto assetsAvailableProxy = vaultSle->at(sfAssetsAvailable);
|
||||
auto assetsTotalProxy = vaultSle->at(sfAssetsTotal);
|
||||
|
||||
auto const totalPaidToVaultRaw = paymentParts->principalPaid + paymentParts->interestPaid;
|
||||
auto const totalPaidToVaultRounded =
|
||||
roundToAsset(asset, totalPaidToVaultRaw, vaultScale, Number::RoundingMode::Downward);
|
||||
XRPL_ASSERT_PARTS(
|
||||
!asset.integral() || totalPaidToVaultRaw == totalPaidToVaultRounded,
|
||||
"xrpl::LoanPay::doApply",
|
||||
"rounding does nothing for integral asset");
|
||||
auto const totalPaidToBroker = paymentParts->feePaid;
|
||||
|
||||
XRPL_ASSERT_PARTS(
|
||||
(totalPaidToVaultRaw + totalPaidToBroker) ==
|
||||
(paymentParts->principalPaid + paymentParts->interestPaid + paymentParts->feePaid),
|
||||
"xrpl::LoanPay::doApply",
|
||||
"payments add up");
|
||||
<< ", debt total delta: " << debtTotalDelta
|
||||
<< ", scheduled interest delta: " << scheduledInterestDelta;
|
||||
|
||||
// Decrease LoanBroker Debt by the amount paid, add the Loan value change
|
||||
// (which might be negative). debtTotalDelta may be negative, increasing the
|
||||
@@ -514,45 +548,35 @@ LoanPay::doApply()
|
||||
"xrpl::LoanPay::doApply",
|
||||
"debtTotalDelta rounding good");
|
||||
// Despite our best efforts, it's possible for rounding errors to accumulate
|
||||
// in the loan broker's debt total. This is because the broker may have more
|
||||
// than one loan with significantly different scales.
|
||||
adjustImpreciseNumber(debtTotalProxy, -debtTotalDelta, asset, vaultScale);
|
||||
// in the loan broker's debt total on Legacy/CashBasis Vaults. This is because
|
||||
// the broker may have more than one loan with significantly different scales.
|
||||
adjustBrokerDebtTotal(brokerSle, vaultSle, -debtTotalDelta, vaultScale);
|
||||
|
||||
//------------------------------------------------------
|
||||
// Vault object state changes
|
||||
view.update(vaultSle);
|
||||
|
||||
Number const assetsAvailableBefore = *assetsAvailableProxy;
|
||||
Number const assetsTotalBefore = *assetsTotalProxy;
|
||||
#if !NDEBUG
|
||||
if (fixedPrecision)
|
||||
{
|
||||
Number const pseudoAccountBalanceBefore = accountHolds(
|
||||
view,
|
||||
vaultPseudoAccount,
|
||||
asset,
|
||||
FreezeHandling::IgnoreFreeze,
|
||||
AuthHandling::IgnoreAuth,
|
||||
j_);
|
||||
|
||||
XRPL_ASSERT_PARTS(
|
||||
assetsAvailableBefore == pseudoAccountBalanceBefore,
|
||||
"xrpl::LoanPay::doApply",
|
||||
"vault pseudo balance agrees before");
|
||||
// AssetsDeployed drops by exactly the principal paid this payment,
|
||||
// taken from the Loan's own payment split rather than the
|
||||
// broker-side debtTotalDelta.
|
||||
if (auto const ter = adjustVaultBalances(
|
||||
vaultSle,
|
||||
{
|
||||
.cash = totalPaidToVaultRounded,
|
||||
.deployed = -paymentParts->principalPaid,
|
||||
.yield = scheduledInterestDelta,
|
||||
},
|
||||
j_);
|
||||
!isTesSuccess(ter))
|
||||
return ter;
|
||||
}
|
||||
else
|
||||
{
|
||||
vaultSle->at(sfAssetsAvailable) += totalPaidToVaultRounded;
|
||||
vaultSle->at(sfAssetsTotal) += assetsTotalDelta;
|
||||
}
|
||||
#endif
|
||||
|
||||
assetsAvailableProxy += totalPaidToVaultRounded;
|
||||
assetsTotalProxy += assetsTotalDelta;
|
||||
|
||||
XRPL_ASSERT_PARTS(
|
||||
*assetsAvailableProxy <= *assetsTotalProxy,
|
||||
"xrpl::LoanPay::doApply",
|
||||
"assets available must not be greater than assets outstanding");
|
||||
|
||||
JLOG(j_.debug()) << "total paid to vault raw: " << totalPaidToVaultRaw
|
||||
<< ", total paid to vault rounded: " << totalPaidToVaultRounded
|
||||
<< ", total paid to broker: " << totalPaidToBroker
|
||||
<< ", amount from transaction: " << amount;
|
||||
|
||||
// Move funds
|
||||
XRPL_ASSERT_PARTS(
|
||||
@@ -563,10 +587,11 @@ LoanPay::doApply()
|
||||
if (!sendBrokerFeeToOwner)
|
||||
{
|
||||
// If there is not enough first-loss capital, add the fee to First Loss
|
||||
// Cover Pool. Note that this moves the entire fee - it does not attempt
|
||||
// to split it. The broker can Withdraw it later if they want, or leave
|
||||
// it for future needs.
|
||||
coverAvailableProxy += totalPaidToBroker;
|
||||
// Cover Pool. FixedPrecision rounds the redirected fee at the
|
||||
// posterior cover scale; any sub-unit remainder is forgiven. The
|
||||
// broker can Withdraw the credited amount later or leave it for future
|
||||
// needs.
|
||||
brokerSle->at(sfCoverAvailable) += totalPaidToBroker;
|
||||
}
|
||||
|
||||
associateAsset(*loanSle, asset);
|
||||
@@ -574,28 +599,48 @@ LoanPay::doApply()
|
||||
associateAsset(*vaultSle, asset);
|
||||
|
||||
// Duplicate some checks after rounding
|
||||
Number const assetsAvailableAfter = *assetsAvailableProxy;
|
||||
Number const assetsTotalAfter = *assetsTotalProxy;
|
||||
Number const assetsAvailableAfter = vaultSle->at(sfAssetsAvailable);
|
||||
Number const assetsTotalAfter = vaultSle->at(sfAssetsTotal);
|
||||
|
||||
XRPL_ASSERT_PARTS(
|
||||
assetsAvailableAfter <= assetsTotalAfter,
|
||||
"xrpl::LoanPay::doApply",
|
||||
"assets available must not be greater than assets outstanding");
|
||||
if (assetsAvailableAfter == assetsAvailableBefore)
|
||||
{
|
||||
// An unchanged assetsAvailable indicates that the amount paid to the
|
||||
// vault was zero, or rounded to zero. That should be impossible, but I
|
||||
// can't rule it out for extreme edge cases, so fail gracefully if it
|
||||
// happens.
|
||||
//
|
||||
// LCOV_EXCL_START
|
||||
JLOG(j_.warn()) << "LoanPay: Vault assets available unchanged after rounding: " //
|
||||
<< "Before: " << assetsAvailableBefore //
|
||||
<< ", After: " << assetsAvailableAfter;
|
||||
return tecPRECISION_LOSS;
|
||||
// LCOV_EXCL_STOP
|
||||
// Credit rounded to zero. On FixedPrecision, if this payment also
|
||||
// closes the Loan (XLS-66 section 3.2.8's terminal exception), let
|
||||
// it through: AssetsDeployed (vault) and DebtTotal (broker) already
|
||||
// dropped by the exact remaining principal, and YieldUnrealized was already
|
||||
// reduced by this payment's scheduled interest, above. No cash moves for the sub-unit
|
||||
// remainder; the Vault forgives it. A non-terminal zero-credit payment is still rejected:
|
||||
// letting it through would change the Loan's schedule with no cash movement at all.
|
||||
if (fixedPrecision && loanSle->at(sfPaymentRemaining) == 0)
|
||||
{
|
||||
JLOG(j_.debug()) << "LoanPay: terminal payment closed the Loan with a zero cash "
|
||||
"credit; the sub-unit remainder is forgiven. Principal: "
|
||||
<< paymentParts->principalPaid
|
||||
<< ", Interest: " << paymentParts->interestPaid;
|
||||
|
||||
// Nothing moves for this terminal close when the sub-unit remainder is forgiven and
|
||||
// either no fee is owed or the fee goes to the broker owner who is also the borrower,
|
||||
// i.e. the owner would pay itself. Return before the transfer and its conservation
|
||||
// checks, since they assume some transfer happens.
|
||||
if (totalPaidToVaultRounded == beast::kZero &&
|
||||
(totalPaidToBroker == beast::kZero || brokerPayee == accountID_))
|
||||
{
|
||||
return tesSUCCESS;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
JLOG(j_.warn()) << "LoanPay: Vault assets available unchanged after rounding: " //
|
||||
<< "Before: " << assetsAvailableBefore //
|
||||
<< ", After: " << assetsAvailableAfter //
|
||||
<< ", Credit: " << totalPaidToVaultRounded //
|
||||
<< ", Principal: " << paymentParts->principalPaid //
|
||||
<< ", Interest: " << paymentParts->interestPaid;
|
||||
return tecPRECISION_LOSS;
|
||||
}
|
||||
}
|
||||
if (assetsTotalDelta != beast::kZero && assetsTotalAfter == assetsTotalBefore)
|
||||
if (!fixedPrecision && assetsTotalDelta != beast::kZero &&
|
||||
assetsTotalAfter == assetsTotalBefore)
|
||||
{
|
||||
// Non-zero assetsTotalDelta with an unchanged assetsTotal indicates that
|
||||
// the actual value change rounded to zero. That should be impossible, but
|
||||
@@ -611,7 +656,8 @@ LoanPay::doApply()
|
||||
return tecPRECISION_LOSS;
|
||||
// LCOV_EXCL_STOP
|
||||
}
|
||||
if (assetsTotalDelta == beast::kZero && assetsTotalAfter != assetsTotalBefore)
|
||||
if (!fixedPrecision && assetsTotalDelta == beast::kZero &&
|
||||
assetsTotalAfter != assetsTotalBefore)
|
||||
{
|
||||
// A change in assetsTotal when there was no assetsTotalDelta indicates
|
||||
// that something really weird happened. That should be flat out
|
||||
@@ -690,22 +736,6 @@ LoanPay::doApply()
|
||||
WaiveTransferFee::Yes))
|
||||
return ter;
|
||||
|
||||
#if !NDEBUG
|
||||
{
|
||||
Number const pseudoAccountBalanceAfter = accountHolds(
|
||||
view,
|
||||
vaultPseudoAccount,
|
||||
asset,
|
||||
FreezeHandling::IgnoreFreeze,
|
||||
AuthHandling::IgnoreAuth,
|
||||
j_);
|
||||
XRPL_ASSERT_PARTS(
|
||||
assetsAvailableAfter == pseudoAccountBalanceAfter,
|
||||
"xrpl::LoanPay::doApply",
|
||||
"vault pseudo balance agrees after");
|
||||
}
|
||||
#endif
|
||||
|
||||
// Check that funds are conserved
|
||||
auto const accountBalanceAfter = conservationBalance(view, accountID_, asset, j_);
|
||||
auto const vaultBalanceAfter = accountID_ == vaultPseudoAccount
|
||||
@@ -754,10 +784,6 @@ LoanPay::doApply()
|
||||
}();
|
||||
|
||||
// No object changes are made below this point
|
||||
XRPL_ASSERT_PARTS(
|
||||
Number::getround() == Number::RoundingMode::ToNearest,
|
||||
"xrpl::LoanPay::doApply",
|
||||
"Number rounding ToNearest");
|
||||
NumberRoundModeGuard const mg(Number::RoundingMode::ToNearest);
|
||||
|
||||
auto const accountBalanceBeforeRounded = roundToScale(accountBalanceBefore, balanceScale);
|
||||
@@ -808,8 +834,26 @@ LoanPay::doApply()
|
||||
<< ", total " << to_string(totalBalanceChangeRounded) << " ("
|
||||
<< Number(totalBalanceChange) << ")";
|
||||
|
||||
// Each trust line rounds its own change to 16 digits, so a side can move by up to one unit
|
||||
// of its balance less or more than was sent. Integral assets move exactly.
|
||||
auto const roundingUnit = [&](STAmount const& before, STAmount const& after) {
|
||||
Number unit{0};
|
||||
if (asset.integral())
|
||||
return unit;
|
||||
for (auto const& balance : {before, after})
|
||||
{
|
||||
if (balance != beast::kZero)
|
||||
unit = std::max(unit, Number{1, balance.exponent()});
|
||||
}
|
||||
return unit;
|
||||
};
|
||||
Number const accountUnit = roundingUnit(accountBalanceBefore, accountBalanceAfter);
|
||||
Number const vaultUnit = roundingUnit(vaultBalanceBefore, vaultBalanceAfter);
|
||||
Number const brokerUnit = roundingUnit(brokerBalanceBefore, brokerBalanceAfter);
|
||||
|
||||
bool const goodRounding = totalBalanceBeforeRounded == totalBalanceAfterRounded ||
|
||||
totalBalanceChangeRounded == beast::kZero;
|
||||
totalBalanceChangeRounded == beast::kZero ||
|
||||
abs(Number(totalBalanceChange)) <= accountUnit + vaultUnit + brokerUnit;
|
||||
if (totalBalanceBeforeRounded != totalBalanceAfterRounded)
|
||||
{
|
||||
JLOG((goodRounding ? j_.debug() : j_.warn()))
|
||||
@@ -830,11 +874,20 @@ LoanPay::doApply()
|
||||
goodRounding, "xrpl::LoanPay::doApply", "funds are conserved (with rounding)");
|
||||
|
||||
XRPL_ASSERT_PARTS(
|
||||
accountBalanceAfter < accountBalanceBefore || accountID_ == asset.getIssuer(),
|
||||
// A payment below one unit of the payer's balance can round away on its trust line. A fee
|
||||
// the owner-borrower pays to itself does not leave its balance.
|
||||
accountBalanceAfter < accountBalanceBefore || accountID_ == asset.getIssuer() ||
|
||||
(accountBalanceAfter == accountBalanceBefore &&
|
||||
totalPaidToVaultRounded +
|
||||
(brokerPayee == accountID_ ? Number{0} : totalPaidToBroker) <=
|
||||
accountUnit),
|
||||
"xrpl::LoanPay::doApply",
|
||||
"account balance decreased");
|
||||
XRPL_ASSERT_PARTS(
|
||||
vaultBalanceAfter >= beast::kZero && brokerBalanceAfter >= beast::kZero,
|
||||
// A broker owner may owe the issuer on a reciprocal IOU trust line, so
|
||||
// its stored balance can be negative. Pseudo-accounts cannot.
|
||||
vaultBalanceAfter >= beast::kZero &&
|
||||
(brokerBalanceAfter >= beast::kZero || (sendBrokerFeeToOwner && !asset.integral())),
|
||||
"xrpl::LoanPay::doApply",
|
||||
"non-negative vault and broker balances");
|
||||
XRPL_ASSERT_PARTS(
|
||||
@@ -846,7 +899,11 @@ LoanPay::doApply()
|
||||
"xrpl::LoanPay::doApply",
|
||||
"broker balance did not decrease");
|
||||
XRPL_ASSERT_PARTS(
|
||||
vaultBalanceAfter > vaultBalanceBefore || brokerBalanceAfter > brokerBalanceBefore,
|
||||
// An IOU issuer's spendable balance does not change when it is paid, and a credit below
|
||||
// one unit of the payee's balance can round away on its trust line.
|
||||
vaultBalanceAfter > vaultBalanceBefore || brokerBalanceAfter > brokerBalanceBefore ||
|
||||
brokerPayee == asset.getIssuer() ||
|
||||
(Number(totalPaidToVaultRounded) <= vaultUnit && totalPaidToBroker <= brokerUnit),
|
||||
"xrpl::LoanPay::doApply",
|
||||
"vault and/or broker balance increased");
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
#include <xrpl/ledger/entries/FeeSettingsEntry.h>
|
||||
#include <xrpl/ledger/entries/NegativeUNLEntry.h>
|
||||
#include <xrpl/protocol/Feature.h>
|
||||
#include <xrpl/protocol/Fees.h>
|
||||
#include <xrpl/protocol/Protocol.h>
|
||||
#include <xrpl/protocol/PublicKey.h>
|
||||
#include <xrpl/protocol/SField.h>
|
||||
@@ -124,12 +125,23 @@ Change::preclaim(PreclaimContext const& ctx)
|
||||
ctx.tx.isFieldPresent(sfReserveIncrementDrops))
|
||||
return temDISABLED;
|
||||
}
|
||||
// The ttFEE transaction format defines these fields as optional,
|
||||
// but they are unconditionally forbidden until FeeVoteImpl is
|
||||
// updated to populate them (SmartEscrow behavioral port).
|
||||
if (ctx.tx.isFieldPresent(sfGasLimit) || ctx.tx.isFieldPresent(sfBytecodeSizeLimit) ||
|
||||
ctx.tx.isFieldPresent(sfGasPrice))
|
||||
return temDISABLED;
|
||||
if (ctx.view.rules().enabled(featureSmartEscrow))
|
||||
{
|
||||
if (!ctx.tx.isFieldPresent(sfGasLimit) ||
|
||||
!ctx.tx.isFieldPresent(sfBytecodeSizeLimit) ||
|
||||
!ctx.tx.isFieldPresent(sfGasPrice))
|
||||
return temMALFORMED;
|
||||
if (ctx.tx[sfGasLimit] > kMaxGasLimit ||
|
||||
ctx.tx[sfBytecodeSizeLimit] > kMaxBytecodeSizeLimit ||
|
||||
ctx.tx[sfGasPrice] < kMinGasPrice)
|
||||
return temBAD_FEE;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (ctx.tx.isFieldPresent(sfGasLimit) ||
|
||||
ctx.tx.isFieldPresent(sfBytecodeSizeLimit) || ctx.tx.isFieldPresent(sfGasPrice))
|
||||
return temDISABLED;
|
||||
}
|
||||
return tesSUCCESS;
|
||||
case ttAMENDMENT:
|
||||
case ttUNL_MODIFY:
|
||||
@@ -164,6 +176,28 @@ Change::preCompute()
|
||||
XRPL_ASSERT(accountID_ == beast::kZero, "xrpl::Change::preCompute : zero account");
|
||||
}
|
||||
|
||||
void
|
||||
Change::initializeVMFees()
|
||||
{
|
||||
FeeSettingsEntryW feeObject(view());
|
||||
|
||||
if (!feeObject)
|
||||
{
|
||||
feeObject.newSLE();
|
||||
feeObject.insert();
|
||||
}
|
||||
|
||||
// Compile-time constants, never `FeeSetup`: every node applies this
|
||||
// pseudo-transaction, so reading local config here would diverge.
|
||||
feeObject->at(sfGasLimit) = kDefaultGasLimit;
|
||||
feeObject->at(sfBytecodeSizeLimit) = kDefaultBytecodeSizeLimit;
|
||||
feeObject->at(sfGasPrice) = kDefaultGasPrice;
|
||||
|
||||
feeObject.update();
|
||||
|
||||
JLOG(j_.info()) << "Feature Extension fees initialized on SmartEscrow activation";
|
||||
}
|
||||
|
||||
TER
|
||||
Change::applyAmendment()
|
||||
{
|
||||
@@ -240,6 +274,9 @@ Change::applyAmendment()
|
||||
<< " activated: server blocked.";
|
||||
ctx_.registry.get().getOPs().setAmendmentBlocked();
|
||||
}
|
||||
|
||||
if (amendment == featureSmartEscrow)
|
||||
initializeVMFees();
|
||||
}
|
||||
|
||||
if (newMajorities.empty())
|
||||
@@ -266,14 +303,12 @@ Change::applyFee()
|
||||
feeObject.newSLE();
|
||||
feeObject.insert();
|
||||
}
|
||||
auto const set = [](FeeSettingsEntryW& feeObject, STTx const& tx, auto const& field) {
|
||||
feeObject->at(field) = tx[field];
|
||||
auto const set = [](FeeSettingsEntryW& feeObject, STTx const& tx, auto const&... fields) {
|
||||
((feeObject->at(fields) = tx[fields]), ...);
|
||||
};
|
||||
if (view().rules().enabled(featureXRPFees))
|
||||
{
|
||||
set(feeObject, ctx_.tx, sfBaseFeeDrops);
|
||||
set(feeObject, ctx_.tx, sfReserveBaseDrops);
|
||||
set(feeObject, ctx_.tx, sfReserveIncrementDrops);
|
||||
set(feeObject, ctx_.tx, sfBaseFeeDrops, sfReserveBaseDrops, sfReserveIncrementDrops);
|
||||
// Ensure the old fields are removed
|
||||
feeObject->makeFieldAbsent(sfBaseFee);
|
||||
feeObject->makeFieldAbsent(sfReferenceFeeUnits);
|
||||
@@ -282,10 +317,11 @@ Change::applyFee()
|
||||
}
|
||||
else
|
||||
{
|
||||
set(feeObject, ctx_.tx, sfBaseFee);
|
||||
set(feeObject, ctx_.tx, sfReferenceFeeUnits);
|
||||
set(feeObject, ctx_.tx, sfReserveBase);
|
||||
set(feeObject, ctx_.tx, sfReserveIncrement);
|
||||
set(feeObject, ctx_.tx, sfBaseFee, sfReferenceFeeUnits, sfReserveBase, sfReserveIncrement);
|
||||
}
|
||||
if (view().rules().enabled(featureSmartEscrow))
|
||||
{
|
||||
set(feeObject, ctx_.tx, sfGasLimit, sfBytecodeSizeLimit, sfGasPrice);
|
||||
}
|
||||
|
||||
feeObject.update();
|
||||
|
||||
@@ -214,7 +214,7 @@ ConfidentialMPTClawback::doApply()
|
||||
// proof using the corresponding stale private key. The mirrors are updated
|
||||
// to the current epoch during execution.
|
||||
if (view().rules().enabled(featureConfidentialMPTKeyRotation))
|
||||
setMirrorEpochs(*sleIssuance, *sleHolderMPToken);
|
||||
setMirrorsCurrent(*sleIssuance, *sleHolderMPToken);
|
||||
|
||||
// Decrease Global Confidential Outstanding Amount
|
||||
auto const oldCOA = (*sleIssuance)[sfConfidentialOutstandingAmount];
|
||||
|
||||
@@ -345,7 +345,7 @@ ConfidentialMPTConvert::doApply()
|
||||
|
||||
// Initialize key epochs when registering the keys.
|
||||
if (view().rules().enabled(featureConfidentialMPTKeyRotation))
|
||||
setMirrorEpochs(*sleIssuance, *sleMptoken);
|
||||
setMirrorsCurrent(*sleIssuance, *sleMptoken);
|
||||
|
||||
// Spending balance starts at zero. Must use canonical zero encryption
|
||||
// (deterministic ciphertext) so the ledger state is reproducible.
|
||||
|
||||
@@ -85,6 +85,9 @@ ConfidentialMPTHolderKeyUpdate::preflight(PreflightContext const& ctx)
|
||||
if (hasInbox && ctx.tx[sfConfidentialBalanceInbox].length() != kEcGamalEncryptedTotalLength)
|
||||
return temBAD_CIPHERTEXT;
|
||||
|
||||
// TODO: Check the sfZKProof length for each mode once the proof sizes are available from
|
||||
// mpt-crypto.
|
||||
|
||||
if (!isValidCompressedECPoint(ctx.tx[sfHolderEncryptionKey]))
|
||||
return temMALFORMED;
|
||||
|
||||
@@ -143,6 +146,9 @@ ConfidentialMPTHolderKeyUpdate::preclaim(PreclaimContext const& ctx)
|
||||
if (ctx.tx.isFlag(tfHolderKeyRecovery) && sleMptoken->isFieldPresent(sfRecoveryKey))
|
||||
return tecNO_PERMISSION;
|
||||
|
||||
// TODO: Verify sfZKProof. Rotation mode proves the new spending and inbox ciphertexts hold the
|
||||
// same amounts as the current ones under the new key; Recovery mode proves knowledge of the
|
||||
// private key for the new sfHolderEncryptionKey.
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
|
||||
@@ -201,6 +201,9 @@ ConfidentialMPTMirrorUpdate::preclaim(PreclaimContext const& ctx)
|
||||
return tecNO_PERMISSION;
|
||||
}
|
||||
|
||||
// TODO: Verify sfZKProof. Issuer mode must anchor on sfIssuerEncryptedBalance under the
|
||||
// key that encrypted it; Holder mode anchors on sfConfidentialBalanceSpending under
|
||||
// sfHolderEncryptionKey.
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
@@ -241,7 +244,7 @@ ConfidentialMPTMirrorUpdate::doApply()
|
||||
if (ctx_.tx.isFieldPresent(sfIssuerEncryptedAmount))
|
||||
{
|
||||
(*sleMptoken)[sfIssuerEncryptedBalance] = ctx_.tx[sfIssuerEncryptedAmount];
|
||||
setIssuerMirrorEpoch(*sleIssuance, *sleMptoken);
|
||||
setIssuerMirrorCurrent(*sleIssuance, *sleMptoken);
|
||||
}
|
||||
|
||||
if (ctx_.tx.isFieldPresent(sfAuditorEncryptedAmount))
|
||||
|
||||
@@ -453,10 +453,7 @@ MPTokenIssuanceSet::doApply()
|
||||
return tecINTERNAL; // LCOV_EXCL_LINE
|
||||
|
||||
// NOTE: presence must be checked before the key is overwritten below.
|
||||
bool const isRotation = sle->isFieldPresent(keyField);
|
||||
sle->setFieldVL(keyField, *pubKey);
|
||||
|
||||
if (isRotation)
|
||||
if (sle->isFieldPresent(keyField))
|
||||
{
|
||||
// Preclaim rejects overwriting an existing key unless the amendment is
|
||||
// enabled.
|
||||
@@ -479,9 +476,20 @@ MPTokenIssuanceSet::doApply()
|
||||
// LCOV_EXCL_STOP
|
||||
}
|
||||
|
||||
// Proofs over sfIssuerEncryptedBalance must be verified against the issuer key
|
||||
// that encrypted the mirror balance.
|
||||
//
|
||||
// - Mirrors migrated after an issuer key rotation store their new key in
|
||||
// MPToken's sfIssuerMirrorEncryptionKey.
|
||||
// - Mirrors that were never migrated carry no record and remain under the original key.
|
||||
//
|
||||
// Save the original key here in MPTokenIssuance object during the first key rotation
|
||||
if (keyField == sfIssuerEncryptionKey && !sle->isFieldPresent(epochField))
|
||||
(*sle)[sfInitialIssuerEncryptionKey] = (*sle)[keyField];
|
||||
|
||||
(*sle)[epochField] = epoch + 1;
|
||||
}
|
||||
|
||||
(*sle)[keyField] = *pubKey;
|
||||
return tesSUCCESS;
|
||||
};
|
||||
|
||||
|
||||
@@ -12,12 +12,15 @@
|
||||
#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/Feature.h>
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
#include <xrpl/protocol/LedgerFormats.h> // IWYU pragma: keep
|
||||
#include <xrpl/protocol/MPTIssue.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>
|
||||
#include <xrpl/protocol/STNumber.h> // IWYU pragma: keep
|
||||
#include <xrpl/protocol/STTakesAsset.h>
|
||||
@@ -49,6 +52,26 @@ shouldWaiveWithdrawal(ReadView const& view, AccountID const& account, SLE::Const
|
||||
: WaiveUnrealizedLoss::No;
|
||||
}
|
||||
|
||||
static Rate
|
||||
withdrawalTransferRate(
|
||||
ReadView const& view,
|
||||
AccountID const& account,
|
||||
AccountID const& destination,
|
||||
Asset const& asset)
|
||||
{
|
||||
// Pre-fixCleanup3_5_0: every vault withdrawal waives the transfer fee.
|
||||
// Post-fixCleanup3_5_0: the fee applies only when a holder other than the issuer withdraws to
|
||||
// another such holder.
|
||||
if (!view.rules().enabled(fixCleanup3_5_0))
|
||||
return kParityRate;
|
||||
|
||||
if (asset.native() || destination == account || account == asset.getIssuer() ||
|
||||
destination == asset.getIssuer())
|
||||
return kParityRate;
|
||||
|
||||
return transferRate(view, asset);
|
||||
}
|
||||
|
||||
NotTEC
|
||||
VaultWithdraw::preflight(PreflightContext const& ctx)
|
||||
{
|
||||
@@ -170,11 +193,13 @@ VaultWithdraw::preclaim(PreclaimContext const& ctx)
|
||||
if (!maybeAssets)
|
||||
return tefINTERNAL; // LCOV_EXCL_LINE
|
||||
|
||||
auto const rate = withdrawalTransferRate(ctx.view, account, dstAcct, vaultAsset);
|
||||
auto const amountToReceive = subtractTransferFee(*maybeAssets, rate);
|
||||
if (auto const ret = canWithdraw(
|
||||
ctx.view,
|
||||
account,
|
||||
dstAcct,
|
||||
*maybeAssets,
|
||||
amountToReceive,
|
||||
ctx.tx.isFieldPresent(sfDestinationTag),
|
||||
ctx.tx[~sfCredentialIDs]))
|
||||
return ret;
|
||||
@@ -302,6 +327,8 @@ VaultWithdraw::doApply()
|
||||
|
||||
auto const amount = ctx_.tx[sfAmount];
|
||||
Asset const vaultAsset = vault->at(sfAsset);
|
||||
auto const dstAcct = ctx_.tx[~sfDestination].value_or(accountID_);
|
||||
auto const rate = withdrawalTransferRate(view(), accountID_, dstAcct, vaultAsset);
|
||||
|
||||
MPTIssue const share{mptIssuanceID};
|
||||
STAmount sharesRedeemed = {share};
|
||||
@@ -330,7 +357,11 @@ VaultWithdraw::doApply()
|
||||
view().rules().enabled(fixCleanup3_4_0) ? TruncateShares::Yes : TruncateShares::No;
|
||||
{
|
||||
auto const maybeShares = assetsToSharesWithdraw(
|
||||
vault, sleIssuance, amount, truncate, waiveUnrealizedLoss);
|
||||
vault,
|
||||
sleIssuance,
|
||||
addTransferFee(amount, rate),
|
||||
truncate,
|
||||
waiveUnrealizedLoss);
|
||||
if (!maybeShares)
|
||||
return tecINTERNAL; // LCOV_EXCL_LINE
|
||||
sharesRedeemed = *maybeShares;
|
||||
@@ -523,6 +554,13 @@ VaultWithdraw::doApply()
|
||||
assetsWithdrawn = allAvailable;
|
||||
}
|
||||
|
||||
auto const assetsDelivered = subtractTransferFee(assetsWithdrawn, rate);
|
||||
if (assetsWithdrawn > beast::kZero && assetsDelivered == beast::kZero)
|
||||
{
|
||||
JLOG(j_.debug()) << "VaultWithdraw: transfer fee reduces the payout to zero";
|
||||
return tecPRECISION_LOSS;
|
||||
}
|
||||
|
||||
if (finalPayout && getVaultVersion(vault) != VaultVersion::FixedPrecision)
|
||||
{
|
||||
// Do not let dust accumulate in the Vault.
|
||||
@@ -573,9 +611,15 @@ VaultWithdraw::doApply()
|
||||
|
||||
associateAsset(*vault, vaultAsset);
|
||||
|
||||
auto const dstAcct = ctx_.tx[~sfDestination].value_or(accountID_);
|
||||
return doWithdraw(
|
||||
applyViewContext, accountID_, dstAcct, vaultAccount, preFeeBalance_, assetsWithdrawn, j_);
|
||||
applyViewContext,
|
||||
accountID_,
|
||||
dstAcct,
|
||||
vaultAccount,
|
||||
preFeeBalance_,
|
||||
assetsDelivered,
|
||||
assetsWithdrawn,
|
||||
j_);
|
||||
}
|
||||
|
||||
void
|
||||
|
||||
@@ -0,0 +1,365 @@
|
||||
#pragma once
|
||||
|
||||
#include <test/jtx/PeerStub.h>
|
||||
|
||||
#include <xrpld/app/ledger/ConsensusTransSetSF.h>
|
||||
#include <xrpld/overlay/Peer.h>
|
||||
#include <xrpld/overlay/PeerSet.h>
|
||||
|
||||
#include <xrpl/basics/SHAMapHash.h>
|
||||
#include <xrpl/basics/base_uint.h>
|
||||
#include <xrpl/protocol/Serializer.h>
|
||||
#include <xrpl/resource/Charge.h>
|
||||
#include <xrpl/shamap/SHAMapAddNode.h>
|
||||
#include <xrpl/shamap/SHAMapNodeID.h>
|
||||
#include <xrpl/shamap/SHAMapTreeNode.h>
|
||||
|
||||
#include <tests/libxrpl/shamap/DeepChain.h>
|
||||
|
||||
#include <xrpl.pb.h>
|
||||
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <optional>
|
||||
#include <set>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace xrpl::test {
|
||||
|
||||
// The chain builder needs only libxrpl, so it is shared with the gtest suites; see the header for
|
||||
// what keeps the protobuf reply builder below in this tree.
|
||||
using tests::DeepChain;
|
||||
|
||||
// A smallest-possible leaf plus its 4-byte HashPrefix lands one byte short of the floor
|
||||
// ConsensusTransSetSF::gotNode() parses at, so a chain's leaf stays below the parse threshold.
|
||||
static_assert(
|
||||
sizeof(std::uint32_t) + DeepChain::kLeafItemBytes < ConsensusTransSetSF::kMinTxNodeBytesToParse,
|
||||
"a smallest-possible leaf must stay below the resubmission floor");
|
||||
|
||||
/**
|
||||
* A peer that records what it was charged. Every other method comes from
|
||||
* PeerStub.
|
||||
*
|
||||
* One instance per packet keeps charges() unambiguous about which packet was
|
||||
* charged what.
|
||||
*
|
||||
* charges_ is unguarded: charging happens on the packet path, so every charge
|
||||
* lands on the thread that fed the packet in.
|
||||
*/
|
||||
class ChargeRecordingPeer : public PeerStub
|
||||
{
|
||||
public:
|
||||
/**
|
||||
* @param hasTxSet What hasTxSet() reports, which is how an acquisition
|
||||
* decides whether this peer is worth asking. Defaults to true, so a
|
||||
* peer handed straight to takeNodes() needs no argument.
|
||||
*/
|
||||
explicit ChargeRecordingPeer(bool hasTxSet = true) : PeerStub(nextId()), hasTxSet_(hasTxSet)
|
||||
{
|
||||
}
|
||||
|
||||
void
|
||||
charge(resource::Charge const& fee, std::string const&) override
|
||||
{
|
||||
charges_.push_back(fee);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Every fee this peer has been charged, in the order charged.
|
||||
*/
|
||||
[[nodiscard]] std::vector<resource::Charge> const&
|
||||
charges() const
|
||||
{
|
||||
return charges_;
|
||||
}
|
||||
|
||||
// PeerStub returns false for both, and an acquisition asks the peers reporting true.
|
||||
|
||||
[[nodiscard]] bool
|
||||
hasTxSet(UInt256 const&) const override
|
||||
{
|
||||
return hasTxSet_;
|
||||
}
|
||||
|
||||
[[nodiscard]] bool
|
||||
hasLedger(UInt256 const&, std::uint32_t) const override
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
private:
|
||||
/**
|
||||
* The next id to hand out, distinct per instance because
|
||||
* RequestCountingPeerSet dedups by tracked id and PeerStub's own default is
|
||||
* zero for every instance.
|
||||
*
|
||||
* @return The id.
|
||||
*/
|
||||
[[nodiscard]] static ID
|
||||
nextId()
|
||||
{
|
||||
static std::atomic<ID> next{1};
|
||||
return next++;
|
||||
}
|
||||
|
||||
std::vector<resource::Charge> charges_;
|
||||
bool hasTxSet_;
|
||||
};
|
||||
|
||||
/**
|
||||
* A peer set that counts the requests an acquisition makes through it. Offers
|
||||
* peers to a hasItem/onPeerAdded callback pair, hard-filtered by hasItem (which
|
||||
* only scores in the real peer set) and deduped by tracked id.
|
||||
*
|
||||
* A count is a call, not a delivery: a request naming no peer reaches nobody
|
||||
* while this set tracks none. requests() and broadcasts() are counted apart.
|
||||
*
|
||||
* Every write here is guarded, since the retry timer drives addPeers() and
|
||||
* sendRequest() from a job thread while the test reads the results.
|
||||
*/
|
||||
class RequestCountingPeerSet : public PeerSet
|
||||
{
|
||||
public:
|
||||
/**
|
||||
* @param candidates The peers addPeers() may offer, in the order they are
|
||||
* considered. Fixed at construction. Empty offers no one.
|
||||
*/
|
||||
explicit RequestCountingPeerSet(std::vector<std::shared_ptr<Peer>> candidates = {})
|
||||
: candidates_(std::move(candidates))
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Offer the candidates to the caller, the way the real peer set offers the
|
||||
* peers the overlay is tracking.
|
||||
*
|
||||
* @param limit The most peers to add, recorded for firstLimit().
|
||||
* @param hasItem Hard-filters the candidates worth asking, where the real
|
||||
* peer set only scores with it.
|
||||
* @param onPeerAdded Called for each selected candidate.
|
||||
*/
|
||||
void
|
||||
addPeers(
|
||||
std::size_t limit,
|
||||
std::function<bool(std::shared_ptr<Peer> const&)> hasItem,
|
||||
std::function<void(std::shared_ptr<Peer> const&)> onPeerAdded) override
|
||||
{
|
||||
std::vector<std::shared_ptr<Peer>> selected;
|
||||
{
|
||||
std::scoped_lock const lock(mutex_);
|
||||
|
||||
if (!firstLimit_)
|
||||
firstLimit_ = limit;
|
||||
|
||||
for (auto const& candidate : candidates_)
|
||||
{
|
||||
if (selected.size() >= limit)
|
||||
break;
|
||||
// Dedup by tracked id, like the real peer set: onPeerAdded runs once per candidate.
|
||||
if (hasItem(candidate) && addedPeers_.insert(candidate->id()).second)
|
||||
selected.push_back(candidate);
|
||||
}
|
||||
}
|
||||
|
||||
// Outside the lock: onPeerAdded() reenters this object through sendRequest().
|
||||
for (auto const& peer : selected)
|
||||
onPeerAdded(peer);
|
||||
}
|
||||
|
||||
/**
|
||||
* Record the request, and record it in the broadcast count as well when it
|
||||
* names no peer.
|
||||
*
|
||||
* @param peer The peer to ask, or null to ask every tracked peer.
|
||||
*/
|
||||
void
|
||||
sendRequest(
|
||||
::google::protobuf::Message const&,
|
||||
protocol::MessageType,
|
||||
std::shared_ptr<Peer> const& peer) override
|
||||
{
|
||||
std::scoped_lock const lock(mutex_);
|
||||
++requests_;
|
||||
if (!peer)
|
||||
++broadcasts_;
|
||||
}
|
||||
|
||||
/**
|
||||
* The ids of every peer addPeers() has selected, which is what an
|
||||
* acquisition takes for the peers it is tracking.
|
||||
*
|
||||
* Unguarded: every caller of this and of addPeers() is an acquisition
|
||||
* holding its own mtx_, so the ids stay fixed while a caller iterates. A
|
||||
* test thread reads addedPeers() instead.
|
||||
* InboundLedger::getPeerCount() reports zero for these, since it resolves
|
||||
* ids through the overlay, while these peers live only in this harness.
|
||||
*
|
||||
* @return The ids.
|
||||
*/
|
||||
[[nodiscard]] std::set<Peer::ID> const&
|
||||
getPeerIds() const override
|
||||
{
|
||||
return addedPeers_;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return How many requests have been sent through this peer set, counting
|
||||
* a broadcast as one.
|
||||
*/
|
||||
[[nodiscard]] int
|
||||
requests() const
|
||||
{
|
||||
std::scoped_lock const lock(mutex_);
|
||||
return requests_;
|
||||
}
|
||||
|
||||
/**
|
||||
* How many of those requests carried no peer of their own.
|
||||
*
|
||||
* @return The count.
|
||||
*/
|
||||
[[nodiscard]] int
|
||||
broadcasts() const
|
||||
{
|
||||
std::scoped_lock const lock(mutex_);
|
||||
return broadcasts_;
|
||||
}
|
||||
|
||||
/**
|
||||
* The limit the first addPeers() call asked for, which is init()'s, since
|
||||
* onTimer() keeps calling addPeers(1) for as long as an acquisition runs.
|
||||
*
|
||||
* @return The limit, or nullopt if addPeers() has not been called.
|
||||
*/
|
||||
[[nodiscard]] std::optional<std::size_t>
|
||||
firstLimit() const
|
||||
{
|
||||
std::scoped_lock const lock(mutex_);
|
||||
return firstLimit_;
|
||||
}
|
||||
|
||||
/**
|
||||
* A set, because onTimer() keeps re-offering the same candidates.
|
||||
*
|
||||
* @return The ids of every peer addPeers() has selected so far.
|
||||
*/
|
||||
[[nodiscard]] std::set<Peer::ID>
|
||||
addedPeers() const
|
||||
{
|
||||
std::scoped_lock const lock(mutex_);
|
||||
return addedPeers_;
|
||||
}
|
||||
|
||||
private:
|
||||
std::vector<std::shared_ptr<Peer>> const candidates_;
|
||||
|
||||
mutable std::mutex mutex_;
|
||||
int requests_{0};
|
||||
int broadcasts_{0};
|
||||
std::optional<std::size_t> firstLimit_;
|
||||
std::set<Peer::ID> addedPeers_;
|
||||
};
|
||||
|
||||
/**
|
||||
* The given nodes of a chain as a TMLedgerData, so a test can go through the
|
||||
* real dispatch. Not in DeepChain, since the protobuf types are xrpld and that
|
||||
* header is shared with the libxrpl-only gtest binary.
|
||||
*
|
||||
* @param chain The chain the nodes came from, which names the reply by default.
|
||||
* @param data The nodes to include, each with its claimed position.
|
||||
* @param type The reply type, which selects which map the receiver applies it
|
||||
* to.
|
||||
* @param ledgerHash The hash the reply claims to be about, defaulting to the
|
||||
* chain root for a TX set. A ledger acquisition wants its header hash
|
||||
* here, since the chain root is only that ledger's account hash.
|
||||
* @param ledgerSeq The sequence to name in the reply.
|
||||
* @return The reply packet.
|
||||
*/
|
||||
[[nodiscard]] inline std::shared_ptr<protocol::TMLedgerData>
|
||||
packetFor(
|
||||
DeepChain const& chain,
|
||||
std::vector<std::pair<SHAMapNodeID, SHAMapTreeNodePtr>> const& data,
|
||||
protocol::TMLedgerInfoType type = protocol::liTS_CANDIDATE,
|
||||
std::optional<UInt256> const& ledgerHash = std::nullopt,
|
||||
std::uint32_t ledgerSeq = 0)
|
||||
{
|
||||
auto packet = std::make_shared<protocol::TMLedgerData>();
|
||||
auto const hash = ledgerHash.value_or(chain.rootHash.asUInt256());
|
||||
packet->set_ledgerhash(hash.data(), UInt256::size());
|
||||
packet->set_ledgerseq(ledgerSeq);
|
||||
packet->set_type(type);
|
||||
|
||||
for (auto const& [nodeID, node] : data)
|
||||
{
|
||||
Serializer s;
|
||||
node->serializeForWire(s);
|
||||
|
||||
auto* const ledgerNode = packet->add_nodes();
|
||||
ledgerNode->set_nodedata(s.peekData().data(), s.peekData().size());
|
||||
|
||||
// A leaf carries its own key, so the receiver rebuilds its position from that plus a
|
||||
// depth. An inner node has no key and needs the full ID. The two fields are a oneof.
|
||||
if (node->isLeaf())
|
||||
{
|
||||
ledgerNode->set_depth(nodeID.getDepth());
|
||||
}
|
||||
else
|
||||
{
|
||||
ledgerNode->set_id(nodeID.getRawString());
|
||||
}
|
||||
}
|
||||
|
||||
return packet;
|
||||
}
|
||||
|
||||
/**
|
||||
* Poll until the condition holds, or give up. An acquisition's own timer and
|
||||
* the jobs it hands finished work to both run on other threads.
|
||||
*
|
||||
* @param condition What to wait for.
|
||||
* @param deadline The longest to wait.
|
||||
* @return Whether the condition held before the deadline.
|
||||
*/
|
||||
[[nodiscard]] inline bool
|
||||
waitFor(
|
||||
std::function<bool()> const& condition,
|
||||
std::chrono::steady_clock::duration deadline = std::chrono::seconds{10})
|
||||
{
|
||||
auto const giveUp = std::chrono::steady_clock::now() + deadline;
|
||||
while (std::chrono::steady_clock::now() < giveUp)
|
||||
{
|
||||
if (condition())
|
||||
return true;
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds{10});
|
||||
}
|
||||
return condition();
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether a batch verdict carries exactly the given counts.
|
||||
*
|
||||
* The counts, since get() is a log format. It is pinned once, in the
|
||||
* SHAMapAddNode tests, and is what to pass BEAST_EXPECTS() as the reason a
|
||||
* check here failed.
|
||||
*
|
||||
* @param san The verdict to check.
|
||||
* @param good How many nodes the batch should have hooked in.
|
||||
* @param bad How many it should have rejected.
|
||||
* @param duplicate How many it should have already held.
|
||||
* @return Whether the verdict matches.
|
||||
*/
|
||||
[[nodiscard]] inline bool
|
||||
tallyIs(SHAMapAddNode const& san, int good, int bad, int duplicate)
|
||||
{
|
||||
return san.getGood() == good && san.getBad() == bad && san.getDuplicate() == duplicate;
|
||||
}
|
||||
|
||||
} // namespace xrpl::test
|
||||
@@ -3309,8 +3309,6 @@ class Batch_test : public beast::unit_test::Suite
|
||||
{
|
||||
testcase("loan");
|
||||
|
||||
// Temporary: FixedPrecision lending is incomplete until the LoanManage PR.
|
||||
features = features - featureLendingProtocolV1_2;
|
||||
bool const lendingBatchEnabled = features[featureLendingProtocolV1_2];
|
||||
|
||||
using namespace test::jtx;
|
||||
|
||||
@@ -3156,6 +3156,174 @@ class ConfidentialMPTKeyRotation_test : public ConfidentialTransferTestBase
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testInitialIssuerEncryptionKey(FeatureBitset features)
|
||||
{
|
||||
testcase("MPTokenIssuanceSet initial issuer encryption key");
|
||||
using namespace test::jtx;
|
||||
|
||||
Env env{*this, features};
|
||||
Account const alice("alice");
|
||||
Account const bob("bob");
|
||||
Account const auditor("auditor");
|
||||
MPTTester mptAlice(env, alice, {.holders = {bob}});
|
||||
|
||||
mptAlice.create({
|
||||
.ownerCount = 1,
|
||||
.flags = tfMPTCanTransfer | tfMPTCanHoldConfidentialBalance,
|
||||
});
|
||||
|
||||
mptAlice.generateKeyPair(alice);
|
||||
mptAlice.generateKeyPair(auditor);
|
||||
|
||||
// First-time registration of both keys is not a rotation, so no
|
||||
// initial issuer key is captured.
|
||||
mptAlice.set({
|
||||
.account = alice,
|
||||
.issuerPubKey = mptAlice.getPubKey(alice),
|
||||
.auditorPubKey = mptAlice.getPubKey(auditor),
|
||||
});
|
||||
BEAST_EXPECT(mptAlice.checkKeyEpochs(std::nullopt, std::nullopt));
|
||||
BEAST_EXPECT(mptAlice.checkInitialIssuerKey(std::nullopt));
|
||||
|
||||
// Rotating the auditor key alone must not capture an initial issuer
|
||||
// key: only an issuer key rotation writes sfInitialIssuerEncryptionKey.
|
||||
mptAlice.generateKeyPair(auditor);
|
||||
mptAlice.set({.account = alice, .auditorPubKey = mptAlice.getPubKey(auditor)});
|
||||
BEAST_EXPECT(mptAlice.checkKeyEpochs(std::nullopt, 1u));
|
||||
BEAST_EXPECT(mptAlice.checkInitialIssuerKey(std::nullopt));
|
||||
|
||||
// The first issuer rotation preserves the epoch 0 issuer key (alice's)
|
||||
// as sfInitialIssuerEncryptionKey and bumps the issuer epoch to 1.
|
||||
mptAlice.generateKeyPair(alice);
|
||||
mptAlice.set({.account = alice, .issuerPubKey = mptAlice.getPubKey(alice)});
|
||||
BEAST_EXPECT(mptAlice.checkKeyEpochs(1u, 1u));
|
||||
BEAST_EXPECT(mptAlice.checkInitialIssuerKey(mptAlice.getPubKey(alice, 0)));
|
||||
|
||||
// A second issuer rotation bumps the epoch again but leaves the initial
|
||||
// issuer key unchanged: it remains alice's epoch 0 key.
|
||||
mptAlice.generateKeyPair(alice);
|
||||
mptAlice.set({.account = alice, .issuerPubKey = mptAlice.getPubKey(alice)});
|
||||
BEAST_EXPECT(mptAlice.checkKeyEpochs(2u, 1u));
|
||||
BEAST_EXPECT(mptAlice.checkInitialIssuerKey(mptAlice.getPubKey(alice, 0)));
|
||||
|
||||
// Rotating both keys in one transaction bumps both epochs, and the
|
||||
// initial issuer key still remains alice's epoch 0 key.
|
||||
mptAlice.generateKeyPair(alice);
|
||||
mptAlice.generateKeyPair(auditor);
|
||||
mptAlice.set({
|
||||
.account = alice,
|
||||
.issuerPubKey = mptAlice.getPubKey(alice),
|
||||
.auditorPubKey = mptAlice.getPubKey(auditor),
|
||||
});
|
||||
BEAST_EXPECT(mptAlice.checkKeyEpochs(3u, 2u));
|
||||
BEAST_EXPECT(mptAlice.checkInitialIssuerKey(mptAlice.getPubKey(alice, 0)));
|
||||
}
|
||||
|
||||
void
|
||||
testIssuerMirrorEncryptionKey(FeatureBitset features)
|
||||
{
|
||||
testcase("ConfidentialMPT issuer mirror encryption key");
|
||||
using namespace test::jtx;
|
||||
|
||||
std::uint64_t const amount = 100;
|
||||
|
||||
Account const alice("alice");
|
||||
Account const bob("bob");
|
||||
Account const carol("carol");
|
||||
Account const auditor("auditor");
|
||||
|
||||
// Convert path: setIssuerMirrorCurrent runs inside ConfidentialMPTConvert.
|
||||
{
|
||||
Env env{*this, features};
|
||||
MPTTester mptAlice(env, alice, {.holders = {bob, carol}, .auditor = auditor});
|
||||
setupConfidentialIssuance(mptAlice, alice, {bob, carol}, {auditor});
|
||||
|
||||
mptAlice.set({
|
||||
.account = alice,
|
||||
.issuerPubKey = mptAlice.getPubKey(alice),
|
||||
.auditorPubKey = mptAlice.getPubKey(auditor),
|
||||
});
|
||||
|
||||
// bob converts while the issuer key sits at epoch 0, so his issuer
|
||||
// mirror epoch and key are both left absent.
|
||||
mptAlice.convert({
|
||||
.account = bob,
|
||||
.amt = 50,
|
||||
.holderPubKey = mptAlice.getPubKey(bob),
|
||||
});
|
||||
BEAST_EXPECT(mptAlice.checkMirrorEpochs(bob, std::nullopt, std::nullopt));
|
||||
BEAST_EXPECT(mptAlice.checkIssuerMirrorEncryptionKey(std::nullopt, bob));
|
||||
|
||||
// Rotate the issuer key to epoch 3, then carol converts: her issuer
|
||||
// mirror is stamped at epoch 3 under the issuance's current issuer
|
||||
// key.
|
||||
for (int i = 0; i < 3; ++i)
|
||||
{
|
||||
mptAlice.generateKeyPair(alice);
|
||||
mptAlice.set({.account = alice, .issuerPubKey = mptAlice.getPubKey(alice)});
|
||||
}
|
||||
BEAST_EXPECT(mptAlice.checkKeyEpochs(3u, std::nullopt));
|
||||
|
||||
mptAlice.convert({
|
||||
.account = carol,
|
||||
.amt = 50,
|
||||
.holderPubKey = mptAlice.getPubKey(carol),
|
||||
});
|
||||
BEAST_EXPECT(mptAlice.checkMirrorEpochs(carol, 3u, std::nullopt));
|
||||
BEAST_EXPECT(mptAlice.checkIssuerMirrorEncryptionKey(mptAlice.getPubKey(alice), carol));
|
||||
}
|
||||
|
||||
// Migration path: setIssuerMirrorCurrent runs inside
|
||||
// ConfidentialMPTMirrorUpdate, refreshing the mirror key on each
|
||||
// migration to the issuance's then-current issuer key.
|
||||
{
|
||||
Env env{*this, features};
|
||||
ConfidentialEnv ct{
|
||||
env,
|
||||
alice,
|
||||
{{.account = bob}},
|
||||
tfMPTCanHoldConfidentialBalance | tfMPTCanTransfer,
|
||||
auditor};
|
||||
|
||||
// bob converted at issuer epoch 0 during setup, so his issuer
|
||||
// mirror key starts absent.
|
||||
BEAST_EXPECT(ct.mpt.checkMirrorEpochs(bob, std::nullopt, std::nullopt));
|
||||
BEAST_EXPECT(ct.mpt.checkIssuerMirrorEncryptionKey(std::nullopt, bob));
|
||||
|
||||
// Rotate the issuer key twice (epoch 0 -> 2), leaving bob stale.
|
||||
for (int i = 0; i < 2; ++i)
|
||||
{
|
||||
ct.mpt.generateKeyPair(alice);
|
||||
ct.mpt.set({.account = alice, .issuerPubKey = ct.mpt.getPubKey(alice)});
|
||||
}
|
||||
|
||||
// Migrate bob's issuer mirror to epoch 2 under alice's epoch 2 key.
|
||||
Buffer const cipher2 = ct.mpt.encryptAmount(alice, amount, generateBlindingFactor());
|
||||
ct.mpt.mirrorUpdate({
|
||||
.account = alice,
|
||||
.holder = bob,
|
||||
.issuerEncryptedAmount = cipher2,
|
||||
});
|
||||
BEAST_EXPECT(ct.mpt.checkMirrorEpochs(bob, 2u, std::nullopt));
|
||||
BEAST_EXPECT(ct.mpt.checkIssuerMirrorEncryptionKey(ct.mpt.getPubKey(alice, 2), bob));
|
||||
|
||||
// Rotate once more (epoch 3) and migrate again: the mirror key
|
||||
// refreshes to the new issuance issuer key.
|
||||
ct.mpt.generateKeyPair(alice);
|
||||
ct.mpt.set({.account = alice, .issuerPubKey = ct.mpt.getPubKey(alice)});
|
||||
|
||||
Buffer const cipher3 = ct.mpt.encryptAmount(alice, amount, generateBlindingFactor());
|
||||
ct.mpt.mirrorUpdate({
|
||||
.account = alice,
|
||||
.holder = bob,
|
||||
.issuerEncryptedAmount = cipher3,
|
||||
});
|
||||
BEAST_EXPECT(ct.mpt.checkMirrorEpochs(bob, 3u, std::nullopt));
|
||||
BEAST_EXPECT(ct.mpt.checkIssuerMirrorEncryptionKey(ct.mpt.getPubKey(alice, 3), bob));
|
||||
}
|
||||
}
|
||||
|
||||
public:
|
||||
void
|
||||
testMPTokenIssuanceSetWithFeats(FeatureBitset features)
|
||||
@@ -3179,6 +3347,9 @@ public:
|
||||
testMPTokenIssuanceSetWithFeats(all);
|
||||
testMPTokenIssuanceSetWithFeats(all - featureConfidentialMPTKeyRotation);
|
||||
|
||||
testInitialIssuerEncryptionKey(all);
|
||||
testIssuerMirrorEncryptionKey(all);
|
||||
|
||||
testConfidentialMPTConvertEpoch(all);
|
||||
testConfidentialMPTConvertEpoch(all - featureConfidentialMPTKeyRotation);
|
||||
testConfidentialMPTSendEpoch(all);
|
||||
|
||||
+804
-35
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,694 @@
|
||||
#include <test/app/AcquireTestHelpers.h>
|
||||
#include <test/jtx/Env.h>
|
||||
|
||||
#include <xrpld/app/ledger/InboundLedger.h>
|
||||
#include <xrpld/app/ledger/InboundLedgers.h>
|
||||
#include <xrpld/app/ledger/LedgerMaster.h>
|
||||
|
||||
#include <xrpl/basics/base_uint.h>
|
||||
#include <xrpl/basics/chrono.h>
|
||||
#include <xrpl/beast/unit_test/suite.h>
|
||||
#include <xrpl/ledger/Ledger.h>
|
||||
#include <xrpl/nodestore/NodeObject.h>
|
||||
#include <xrpl/protocol/HashPrefix.h>
|
||||
#include <xrpl/protocol/LedgerHeader.h>
|
||||
#include <xrpl/protocol/Serializer.h>
|
||||
#include <xrpl/protocol/jss.h>
|
||||
#include <xrpl/shamap/SHAMapNodeID.h>
|
||||
|
||||
#include <xrpl.pb.h>
|
||||
|
||||
#include <chrono>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <set>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace xrpl::test {
|
||||
|
||||
/**
|
||||
* An acquisition that exposes the entry points its bases keep protected, so a
|
||||
* case can reach them through this subclass alone.
|
||||
*/
|
||||
struct TestableInboundLedger final : InboundLedger
|
||||
{
|
||||
using InboundLedger::InboundLedger;
|
||||
|
||||
/**
|
||||
* Look for the ledger locally, ask the peers being tracked for the rest,
|
||||
* and arm the timer, as InboundLedgers::acquire() does. That caller holds
|
||||
* its collection lock across init(), which releases it, so this stands in
|
||||
* with a lock of its own. The mutex is declared first, so it outlives the
|
||||
* lock.
|
||||
*/
|
||||
void
|
||||
startAcquire()
|
||||
{
|
||||
std::recursive_mutex collectionMutex;
|
||||
ScopedLockType collectionLock(collectionMutex);
|
||||
init(collectionLock);
|
||||
}
|
||||
|
||||
/**
|
||||
* Hold the acquisition's own lock. The timer chain takes this lock in
|
||||
* invokeOnTimer(), so holding it keeps the chain from running until the
|
||||
* lock is released. The lock is recursive, so init() can take it again.
|
||||
*
|
||||
* @return The held lock.
|
||||
*/
|
||||
[[nodiscard]] ScopedLockType
|
||||
lock() const
|
||||
{
|
||||
return ScopedLockType(mtx_);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ask for more nodes, or judge what has been collected, as a fresh
|
||||
* acquisition does.
|
||||
*/
|
||||
void
|
||||
triggerAdded()
|
||||
{
|
||||
trigger(nullptr, TriggerReason::Added);
|
||||
}
|
||||
|
||||
/**
|
||||
* Record that every part has been fetched.
|
||||
*/
|
||||
void
|
||||
markComplete()
|
||||
{
|
||||
ScopedLockType const sl(mtx_);
|
||||
complete_ = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Settle the acquisition and signal whatever is waiting on it.
|
||||
*/
|
||||
void
|
||||
signalDone()
|
||||
{
|
||||
ScopedLockType const sl(mtx_);
|
||||
done();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* The ledger an acquisition is assembling, as a pointer that can modify it.
|
||||
*
|
||||
* @param acquire The acquisition to read from.
|
||||
* @return The ledger, or nullptr if there is none to report.
|
||||
*/
|
||||
[[nodiscard]] static std::shared_ptr<Ledger>
|
||||
mutableLedger(InboundLedger const& acquire)
|
||||
{
|
||||
// Sound because the acquisition holds a non-const ledger and only hands out a const view.
|
||||
return std::const_pointer_cast<Ledger>(acquire.getLedger());
|
||||
}
|
||||
|
||||
struct InboundLedger_test : public beast::unit_test::Suite
|
||||
{
|
||||
/**
|
||||
* A retry interval short enough that a whole timeout chain costs a fraction
|
||||
* of a second. TimeoutCounter refuses anything at or below 10ms.
|
||||
*/
|
||||
static constexpr auto kFastRetry = std::chrono::milliseconds{20};
|
||||
|
||||
/**
|
||||
* A seed unique to this chain within the suite.
|
||||
*
|
||||
* The Env below is shared, and its node store, fetch packs and remembered
|
||||
* failures are all keyed by hash. A fresh seed per chain gives every chain
|
||||
* distinct hashes, so each case resolves only its own nodes.
|
||||
*
|
||||
* @return The seed.
|
||||
*/
|
||||
[[nodiscard]] unsigned int
|
||||
nextSeed()
|
||||
{
|
||||
return ++seed_;
|
||||
}
|
||||
|
||||
/**
|
||||
* A ledger header naming the given map roots, with a hash derived from the
|
||||
* fields, so an acquisition accepts the header as its own however the roots
|
||||
* are chosen.
|
||||
*
|
||||
* @param txHash The transaction map root. Zero means no transactions.
|
||||
* @param accountHash The state map root. Zero is a ledger no acquisition
|
||||
* can finish.
|
||||
* @return The header, with its hash filled in.
|
||||
*/
|
||||
static LedgerHeader
|
||||
makeHeader(UInt256 const& txHash, UInt256 const& accountHash)
|
||||
{
|
||||
LedgerHeader header;
|
||||
header.seq = 2;
|
||||
header.parentCloseTime = NetClock::time_point{};
|
||||
header.closeTime = NetClock::time_point{};
|
||||
header.closeTimeResolution = NetClock::duration{10};
|
||||
header.closeFlags = 0;
|
||||
header.txHash = txHash;
|
||||
header.accountHash = accountHash;
|
||||
header.hash = calculateLedgerHash(header);
|
||||
return header;
|
||||
}
|
||||
|
||||
/**
|
||||
* The common shape: no transactions, so only the state map is in play.
|
||||
*
|
||||
* @param chain The chain whose root to name as the state hash.
|
||||
* @return The header, with its hash filled in.
|
||||
*/
|
||||
static LedgerHeader
|
||||
makeHeader(DeepChain const& chain)
|
||||
{
|
||||
return makeHeader(UInt256{}, chain.rootHash.asUInt256());
|
||||
}
|
||||
|
||||
/**
|
||||
* Put the header in the local store, which is the first place tryDB()
|
||||
* looks. The store keeps its entries, where a fetch pack hands each out
|
||||
* once, so more than one acquisition of the same ledger can find it.
|
||||
*
|
||||
* @param env The environment whose node store to seed.
|
||||
* @param header The header to store, keyed by its own hash.
|
||||
*/
|
||||
static void
|
||||
storeHeader(jtx::Env& env, LedgerHeader const& header)
|
||||
{
|
||||
Serializer s;
|
||||
s.add32(HashPrefix::LedgerMaster);
|
||||
addRaw(header, s);
|
||||
|
||||
env.app().getNodeFamily().db().store(
|
||||
NodeObjectType::Ledger, std::move(s.modData()), header.hash, header.seq);
|
||||
}
|
||||
|
||||
/**
|
||||
* Put every node of a chain in the local store, so a state-map walk
|
||||
* resolves the whole map from the store.
|
||||
*
|
||||
* @param env The environment whose node store to seed.
|
||||
* @param header The header whose sequence the nodes are stored under.
|
||||
* @param chain The chain supplying the nodes.
|
||||
* @param maxDepth The deepest node to store, so a caller can leave a walk
|
||||
* something to ask for.
|
||||
*/
|
||||
static void
|
||||
storeStateNodes(
|
||||
jtx::Env& env,
|
||||
LedgerHeader const& header,
|
||||
DeepChain const& chain,
|
||||
unsigned int maxDepth)
|
||||
{
|
||||
auto& db = env.app().getNodeFamily().db();
|
||||
|
||||
for (auto depth = 0u; depth <= maxDepth; ++depth)
|
||||
{
|
||||
db.store(
|
||||
NodeObjectType::AccountNode,
|
||||
chain.prefixedNodeAt(depth),
|
||||
chain.nodeAt(depth)->getHash().asUInt256(),
|
||||
header.seq);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The header alone as a liBASE reply, so takeHeader() alone judges the
|
||||
* packet.
|
||||
*
|
||||
* @param header The header to carry, serialized without a hash prefix, as
|
||||
* takeHeader() expects.
|
||||
* @return The reply packet.
|
||||
*/
|
||||
static std::shared_ptr<protocol::TMLedgerData>
|
||||
headerPacket(LedgerHeader const& header)
|
||||
{
|
||||
Serializer s;
|
||||
addRaw(header, s);
|
||||
|
||||
auto packet = std::make_shared<protocol::TMLedgerData>();
|
||||
packet->set_ledgerhash(header.hash.data(), UInt256::size());
|
||||
packet->set_ledgerseq(header.seq);
|
||||
packet->set_type(protocol::liBASE);
|
||||
packet->add_nodes()->set_nodedata(s.peekData().data(), s.peekData().size());
|
||||
return packet;
|
||||
}
|
||||
|
||||
/**
|
||||
* An acquisition started with nothing in the store for its hash, so it
|
||||
* waits on its peers.
|
||||
*
|
||||
* @param env The environment to run in.
|
||||
* @param header The header of the ledger to acquire.
|
||||
* @return The acquisition, started.
|
||||
*/
|
||||
static std::shared_ptr<TestableInboundLedger>
|
||||
startPeerAcquire(jtx::Env& env, LedgerHeader const& header)
|
||||
{
|
||||
auto acquire = std::make_shared<TestableInboundLedger>(
|
||||
env.app(),
|
||||
header.hash,
|
||||
header.seq,
|
||||
InboundLedger::Reason::GENERIC,
|
||||
stopwatch(),
|
||||
std::make_unique<RequestCountingPeerSet>());
|
||||
acquire->startAcquire();
|
||||
return acquire;
|
||||
}
|
||||
|
||||
/**
|
||||
* A ledger whose maps all resolve locally finishes on the spot, and the
|
||||
* finished ledger is immutable and handed on.
|
||||
*
|
||||
* Both entry points into tryDB() are driven: InboundLedgers::acquire(), the
|
||||
* only caller of init(), and checkLocal(), which reaches done().
|
||||
*
|
||||
* @param env The environment to run in.
|
||||
*/
|
||||
void
|
||||
testLocalLedgerCompletesAcquire(jtx::Env& env)
|
||||
{
|
||||
testcase("A ledger found locally completes the acquire");
|
||||
|
||||
// A chain ending in a real leaf, so the state map is complete rather than merely rooted.
|
||||
// No transactions, so only the state map is in play.
|
||||
auto const chain = DeepChain::toLeaf(2, nextSeed());
|
||||
auto const header = makeHeader(chain);
|
||||
|
||||
storeHeader(env, header);
|
||||
storeStateNodes(env, header, chain, chain.deepestDepth);
|
||||
|
||||
// acquire() returns the ledger only once the acquisition is complete and unfailed, so a
|
||||
// non-null result shows tryDB() found it in the store.
|
||||
auto const acquired = env.app().getInboundLedgers().acquire(
|
||||
header.hash, header.seq, InboundLedger::Reason::GENERIC);
|
||||
|
||||
BEAST_EXPECT(acquired != nullptr);
|
||||
if (acquired)
|
||||
{
|
||||
BEAST_EXPECT(acquired->isImmutable());
|
||||
BEAST_EXPECT(acquired->header().hash == header.hash);
|
||||
}
|
||||
|
||||
// init() hands a ledger it completed to LedgerMaster itself.
|
||||
BEAST_EXPECT(env.app().getLedgerMaster().getLedgerByHash(header.hash) != nullptr);
|
||||
|
||||
// Nothing on init()'s path records a failure, since init() never reaches done(). This is
|
||||
// the baseline for the check after checkLocal() below: an entry found there came from
|
||||
// done().
|
||||
BEAST_EXPECT(!env.app().getInboundLedgers().isFailure(header.hash));
|
||||
|
||||
// The reason decides where a settled ledger goes: HISTORY counts it toward the fetch rate
|
||||
// instead of handing it to LedgerMaster.
|
||||
{
|
||||
auto const historyChain = DeepChain::toLeaf(2, nextSeed());
|
||||
auto const historyHeader = makeHeader(historyChain);
|
||||
|
||||
storeHeader(env, historyHeader);
|
||||
storeStateNodes(env, historyHeader, historyChain, historyChain.deepestDepth);
|
||||
|
||||
// onLedgerFetched() is the only writer of this rate, and done() calls it before it
|
||||
// posts any work, so the read below is not racing the job queue.
|
||||
auto const rateBefore = env.app().getInboundLedgers().fetchRate();
|
||||
|
||||
auto const history = std::make_shared<InboundLedger>(
|
||||
env.app(),
|
||||
historyHeader.hash,
|
||||
historyHeader.seq,
|
||||
InboundLedger::Reason::HISTORY,
|
||||
stopwatch(),
|
||||
std::make_unique<RequestCountingPeerSet>());
|
||||
|
||||
BEAST_EXPECT(history->checkLocal());
|
||||
BEAST_EXPECT(history->isComplete());
|
||||
BEAST_EXPECT(!history->isFailed());
|
||||
|
||||
// The switch the rate check reads is reached only once the ledger has been settled.
|
||||
auto const historyLedger = history->getLedger();
|
||||
BEAST_EXPECT(historyLedger != nullptr);
|
||||
if (historyLedger)
|
||||
BEAST_EXPECT(historyLedger->isImmutable());
|
||||
|
||||
BEAST_EXPECT(env.app().getInboundLedgers().fetchRate() > rateBefore);
|
||||
}
|
||||
|
||||
// The same ledger through checkLocal(), which unlike init() reaches done().
|
||||
auto const again = std::make_shared<InboundLedger>(
|
||||
env.app(),
|
||||
header.hash,
|
||||
header.seq,
|
||||
InboundLedger::Reason::GENERIC,
|
||||
stopwatch(),
|
||||
std::make_unique<RequestCountingPeerSet>());
|
||||
|
||||
// True because the acquisition ended, which it reports only after done() has run.
|
||||
BEAST_EXPECT(again->checkLocal());
|
||||
BEAST_EXPECT(again->isComplete());
|
||||
BEAST_EXPECT(!again->isFailed());
|
||||
|
||||
auto const settled = again->getLedger();
|
||||
BEAST_EXPECT(settled != nullptr);
|
||||
if (settled)
|
||||
BEAST_EXPECT(settled->isImmutable());
|
||||
|
||||
// done() records a failure only inside the AcqDone job it queues, and checkLocal() returns
|
||||
// before that job runs. Wait for the job, so the list is read after done() has had its say.
|
||||
env.app().getJobQueue().rendezvous();
|
||||
BEAST_EXPECT(!env.app().getInboundLedgers().isFailure(header.hash));
|
||||
}
|
||||
|
||||
/**
|
||||
* A ledger whose map goes invalid on the way to being settled must be
|
||||
* discarded rather than delivered.
|
||||
*
|
||||
* done() settles the ledger before it logs or acts on the outcome, and an
|
||||
* abandoned map makes settling refuse, so the acquisition records a
|
||||
* failure.
|
||||
*
|
||||
* @param env The environment to run in.
|
||||
*/
|
||||
void
|
||||
testInvalidatedLedgerFailsInDone(jtx::Env& env)
|
||||
{
|
||||
testcase("A ledger invalidated on its way to being settled fails");
|
||||
|
||||
// The fabricated chain, so feeding it to the state map invalidates the map.
|
||||
DeepChain const chain{nextSeed()};
|
||||
|
||||
// Only the header is local, so the acquisition holds a ledger with an empty state map.
|
||||
auto const header = makeHeader(chain);
|
||||
storeHeader(env, header);
|
||||
|
||||
auto const acquire = std::make_shared<TestableInboundLedger>(
|
||||
env.app(),
|
||||
header.hash,
|
||||
header.seq,
|
||||
InboundLedger::Reason::GENERIC,
|
||||
stopwatch(),
|
||||
std::make_unique<RequestCountingPeerSet>());
|
||||
|
||||
BEAST_EXPECT(!acquire->checkLocal());
|
||||
BEAST_EXPECT(!acquire->isFailed());
|
||||
BEAST_EXPECT(!acquire->isComplete());
|
||||
|
||||
auto const ledger = mutableLedger(*acquire);
|
||||
BEAST_EXPECT(ledger != nullptr);
|
||||
if (!ledger)
|
||||
return;
|
||||
|
||||
// The state of affairs done() is handed: every part fetched, as far as the caller can tell.
|
||||
acquire->markComplete();
|
||||
|
||||
// And the walk that has since reached the verdict.
|
||||
auto& stateMap = ledger->stateMap();
|
||||
BEAST_EXPECT(stateMap.addRootNode(chain.rootHash, chain.nodeAt(0), nullptr).isGood());
|
||||
for (auto const& [nodeID, node] : chain.nodesBelowRoot())
|
||||
stateMap.addKnownNode(nodeID, node, nullptr);
|
||||
BEAST_EXPECT(!stateMap.isValid());
|
||||
|
||||
acquire->signalDone();
|
||||
|
||||
// complete_ is withdrawn alongside the failure, or every guard that checks it before
|
||||
// failed_ keeps treating this ledger as delivered.
|
||||
BEAST_EXPECT(!acquire->isComplete());
|
||||
BEAST_EXPECT(acquire->isFailed());
|
||||
|
||||
// This failure leaves ledger_ set, so only the failed_ guard in getLedger() withholds it.
|
||||
BEAST_EXPECT(acquire->getLedger() == nullptr);
|
||||
|
||||
// getLedgerByHash answers null for the hash, and the hash is remembered as a failure,
|
||||
// which defers re-acquisition.
|
||||
BEAST_EXPECT(env.app().getLedgerMaster().getLedgerByHash(header.hash) == nullptr);
|
||||
BEAST_EXPECT(waitFor([&] { return env.app().getInboundLedgers().isFailure(header.hash); }));
|
||||
}
|
||||
|
||||
/**
|
||||
* An acquisition that fails on local data must still signal.
|
||||
*
|
||||
* Both entry points that reach tryDB() are covered. done() is what
|
||||
* signals, runs logFailure(), and lands the hash in recentFailures_,
|
||||
* which stops the next round asking for the same ledger.
|
||||
* recentFailures_ is what the assertions watch, since it is the
|
||||
* caller-visible consequence of having signaled.
|
||||
*
|
||||
* @param env The environment to run in.
|
||||
*/
|
||||
void
|
||||
testLocalFailureSignalsDone(jtx::Env& env)
|
||||
{
|
||||
testcase("An acquisition that fails locally still signals");
|
||||
|
||||
// A zero account hash is a ledger no acquisition can ever finish, and tryDB() says so as
|
||||
// soon as it has the header.
|
||||
auto const header = makeHeader(UInt256{}, UInt256{});
|
||||
storeHeader(env, header);
|
||||
|
||||
BEAST_EXPECT(!env.app().getInboundLedgers().isFailure(header.hash));
|
||||
|
||||
// acquire() is the only caller of init(), and returns nullptr for a failed acquisition.
|
||||
BEAST_EXPECT(
|
||||
env.app().getInboundLedgers().acquire(
|
||||
header.hash, header.seq, InboundLedger::Reason::GENERIC) == nullptr);
|
||||
|
||||
// The failure reached recentFailures_, which is what stops the next round asking again.
|
||||
BEAST_EXPECT(waitFor([&] { return env.app().getInboundLedgers().isFailure(header.hash); }));
|
||||
|
||||
// The other way tryDB() judges a ledger unobtainable: the header it found is not the one
|
||||
// asked for. A non-zero account hash, so the route above cannot be what fails this one.
|
||||
auto const strayHeader = makeHeader(UInt256{}, UInt256{2});
|
||||
storeHeader(env, strayHeader);
|
||||
|
||||
BEAST_EXPECT(!env.app().getInboundLedgers().isFailure(strayHeader.hash));
|
||||
|
||||
// acquire() hands the sequence to the acquisition unscreened, so one past the stored
|
||||
// header's is enough to make tryDB() reject what it reads.
|
||||
auto const wrongSeq = strayHeader.seq + 1;
|
||||
|
||||
BEAST_EXPECT(
|
||||
env.app().getInboundLedgers().acquire(
|
||||
strayHeader.hash, wrongSeq, InboundLedger::Reason::GENERIC) == nullptr);
|
||||
|
||||
// tryDB() discards the ledger it built before it gives up, so done() runs here with none
|
||||
// to report. The failure is recorded all the same, which is what the call is for.
|
||||
BEAST_EXPECT(
|
||||
waitFor([&] { return env.app().getInboundLedgers().isFailure(strayHeader.hash); }));
|
||||
|
||||
// The other route into tryDB(): a trigger() on an acquisition that has no header yet. A
|
||||
// hash of its own, so this drives a fresh entry.
|
||||
auto const otherHeader = makeHeader(UInt256{1}, UInt256{});
|
||||
storeHeader(env, otherHeader);
|
||||
|
||||
auto const viaTrigger = std::make_shared<TestableInboundLedger>(
|
||||
env.app(),
|
||||
otherHeader.hash,
|
||||
otherHeader.seq,
|
||||
InboundLedger::Reason::GENERIC,
|
||||
stopwatch(),
|
||||
std::make_unique<RequestCountingPeerSet>());
|
||||
|
||||
BEAST_EXPECT(!env.app().getInboundLedgers().isFailure(otherHeader.hash));
|
||||
|
||||
viaTrigger->triggerAdded();
|
||||
|
||||
// Failed before any header state was published: no ledger, and have_header false.
|
||||
BEAST_EXPECT(viaTrigger->isFailed());
|
||||
BEAST_EXPECT(!viaTrigger->isComplete());
|
||||
BEAST_EXPECT(viaTrigger->getLedger() == nullptr);
|
||||
BEAST_EXPECT(!viaTrigger->getJson(0)[jss::have_header].asBool());
|
||||
BEAST_EXPECT(
|
||||
waitFor([&] { return env.app().getInboundLedgers().isFailure(otherHeader.hash); }));
|
||||
}
|
||||
|
||||
/**
|
||||
* A peer-supplied header whose account hash is zero fails the acquisition
|
||||
* and signals, as a local one does. The peer is not charged, and
|
||||
* recentFailures_ is what shows done() ran.
|
||||
*
|
||||
* @param env The environment to run in.
|
||||
*/
|
||||
void
|
||||
testPeerZeroAccountHashFails(jtx::Env& env)
|
||||
{
|
||||
testcase("A peer header with a zero account hash fails the acquire");
|
||||
|
||||
// A real transaction root and a zero state root. Nothing is stored for this hash, so only a
|
||||
// peer can supply the header.
|
||||
auto const chain = DeepChain::toLeaf(2, nextSeed());
|
||||
auto const header = makeHeader(chain.rootHash.asUInt256(), UInt256{});
|
||||
|
||||
BEAST_EXPECT(!env.app().getInboundLedgers().isFailure(header.hash));
|
||||
|
||||
auto const acquire = startPeerAcquire(env, header);
|
||||
BEAST_EXPECT(!acquire->isFailed());
|
||||
BEAST_EXPECT(!acquire->getJson(0)[jss::have_header].asBool());
|
||||
|
||||
auto const peer = std::make_shared<ChargeRecordingPeer>();
|
||||
BEAST_EXPECT(acquire->gotData(peer, headerPacket(header)));
|
||||
acquire->runData();
|
||||
|
||||
// Failed, not complete, and the header is dropped, as tryDB() leaves one.
|
||||
BEAST_EXPECT(acquire->isFailed());
|
||||
BEAST_EXPECT(!acquire->isComplete());
|
||||
BEAST_EXPECT(acquire->getLedger() == nullptr);
|
||||
BEAST_EXPECT(!acquire->getJson(0)[jss::have_header].asBool());
|
||||
|
||||
// The header is the one asked for, so the sender is not to blame.
|
||||
BEAST_EXPECT(peer->charges().empty());
|
||||
|
||||
// done() ran, so the hash reached recentFailures_, which stops the next round asking again.
|
||||
BEAST_EXPECT(waitFor([&] { return env.app().getInboundLedgers().isFailure(header.hash); }));
|
||||
}
|
||||
|
||||
/**
|
||||
* A peer-supplied header with a zero transaction hash is taken, since that
|
||||
* is an empty transaction set, and the acquisition finishes once the peer
|
||||
* supplies the state map.
|
||||
*
|
||||
* @param env The environment to run in.
|
||||
*/
|
||||
void
|
||||
testPeerHeaderWithoutTransactionsCompletes(jtx::Env& env)
|
||||
{
|
||||
testcase("A peer header with no transactions completes the acquire");
|
||||
|
||||
// No transactions, and a state map that ends in a real leaf, so the acquisition can finish.
|
||||
auto const chain = DeepChain::toLeaf(2, nextSeed());
|
||||
auto const header = makeHeader(chain);
|
||||
|
||||
auto const acquire = startPeerAcquire(env, header);
|
||||
|
||||
auto const peer = std::make_shared<ChargeRecordingPeer>();
|
||||
BEAST_EXPECT(acquire->gotData(peer, headerPacket(header)));
|
||||
acquire->runData();
|
||||
|
||||
// The header is taken, the empty transaction map counts as fetched, and the state map is
|
||||
// what remains.
|
||||
BEAST_EXPECT(!acquire->isFailed());
|
||||
BEAST_EXPECT(!acquire->isComplete());
|
||||
auto const json = acquire->getJson(0);
|
||||
BEAST_EXPECT(json[jss::have_header].asBool());
|
||||
BEAST_EXPECT(json[jss::have_transactions].asBool());
|
||||
BEAST_EXPECT(!json[jss::have_state].asBool());
|
||||
|
||||
// The whole state map as state-node replies, root first, which completes the acquisition.
|
||||
// The second gotData() answers false, since the first already asked for a dispatch.
|
||||
BEAST_EXPECT(acquire->gotData(
|
||||
peer,
|
||||
packetFor(
|
||||
chain,
|
||||
{{SHAMapNodeID{}, chain.nodeAt(0)}},
|
||||
protocol::liAS_NODE,
|
||||
header.hash,
|
||||
header.seq)));
|
||||
BEAST_EXPECT(!acquire->gotData(
|
||||
peer,
|
||||
packetFor(
|
||||
chain, chain.nodesBelowRoot(), protocol::liAS_NODE, header.hash, header.seq)));
|
||||
acquire->runData();
|
||||
|
||||
BEAST_EXPECT(acquire->isComplete());
|
||||
BEAST_EXPECT(!acquire->isFailed());
|
||||
BEAST_EXPECT(peer->charges().empty());
|
||||
|
||||
auto const settled = acquire->getLedger();
|
||||
BEAST_EXPECT(settled != nullptr);
|
||||
if (settled)
|
||||
BEAST_EXPECT(settled->isImmutable());
|
||||
|
||||
BEAST_EXPECT(!env.app().getInboundLedgers().isFailure(header.hash));
|
||||
}
|
||||
|
||||
/**
|
||||
* The retry timer re-asks, then gives up and signals.
|
||||
*
|
||||
* The only case that drives onTimer() rather than trigger() directly,
|
||||
* which is what covers the give-up: past kLedgerTimeoutRetriesMax the
|
||||
* acquisition fails itself and done() records that, so the same
|
||||
* doomed ledger is not asked for again on the next round. It is also
|
||||
* what the retry interval is a constructor parameter for, since the
|
||||
* chain runs past kLedgerTimeoutRetriesMax ticks of three seconds
|
||||
* apiece in production.
|
||||
*
|
||||
* The store is empty for this hash and no reply arrives, so every tick
|
||||
* counts a timeout and the count climbs to the limit. A hash of its own,
|
||||
* so this case owns its entry in the failure list.
|
||||
*
|
||||
* @param env The environment to run in.
|
||||
*/
|
||||
void
|
||||
testTimerRetriesThenGivesUp(jtx::Env& env)
|
||||
{
|
||||
testcase("The retry timer re-asks, then gives up");
|
||||
|
||||
UInt256 const kUnknownLedger{8};
|
||||
|
||||
// One candidate, which onTimer() re-offers on every tick.
|
||||
auto const candidate = std::make_shared<ChargeRecordingPeer>();
|
||||
auto peerSet =
|
||||
std::make_unique<RequestCountingPeerSet>(std::vector<std::shared_ptr<Peer>>{candidate});
|
||||
auto const* const peerSetPtr = peerSet.get();
|
||||
|
||||
auto acquire = std::make_shared<TestableInboundLedger>(
|
||||
env.app(),
|
||||
kUnknownLedger,
|
||||
0,
|
||||
InboundLedger::Reason::GENERIC,
|
||||
stopwatch(),
|
||||
std::move(peerSet),
|
||||
kFastRetry);
|
||||
|
||||
BEAST_EXPECT(!env.app().getInboundLedgers().isFailure(kUnknownLedger));
|
||||
|
||||
// init() finds the hash absent from the store, so it asks the candidate and queues the
|
||||
// first tick as a job at once rather than arming the timer. The case counts from here, past
|
||||
// those first requests. The lock keeps the chain from running until the baseline is
|
||||
// recorded. Without it the whole chain (kLedgerTimeoutRetriesMax + 1 ticks, the later ones
|
||||
// at kFastRetry) can finish first, and the count below would have nothing to climb from.
|
||||
int requestsFromInit = 0;
|
||||
{
|
||||
auto const held = acquire->lock();
|
||||
acquire->startAcquire();
|
||||
BEAST_EXPECT(!acquire->isFailed());
|
||||
requestsFromInit = peerSetPtr->requests();
|
||||
}
|
||||
BEAST_EXPECT(requestsFromInit > 0);
|
||||
BEAST_EXPECT(peerSetPtr->addedPeers() == std::set<Peer::ID>{candidate->id()});
|
||||
|
||||
// Every tick asks again, and past kLedgerTimeoutRetriesMax (6) the chain gives up.
|
||||
BEAST_EXPECT(waitFor([&] { return acquire->isFailed(); }));
|
||||
BEAST_EXPECT(!acquire->isComplete());
|
||||
BEAST_EXPECT(peerSetPtr->requests() > requestsFromInit);
|
||||
|
||||
// done() remembered the hash, which is what stops the next round asking again.
|
||||
BEAST_EXPECT(
|
||||
waitFor([&] { return env.app().getInboundLedgers().isFailure(kUnknownLedger); }));
|
||||
}
|
||||
|
||||
void
|
||||
run() override
|
||||
{
|
||||
// One Env for the suite, since building one costs far more than any case here. Safe
|
||||
// because every chain is seeded through nextSeed(): the node store, the fetch packs and
|
||||
// the remembered failures are all shared, and all three are keyed by hash.
|
||||
jtx::Env env{*this};
|
||||
|
||||
testLocalLedgerCompletesAcquire(env);
|
||||
testInvalidatedLedgerFailsInDone(env);
|
||||
testLocalFailureSignalsDone(env);
|
||||
testPeerZeroAccountHashFails(env);
|
||||
testPeerHeaderWithoutTransactionsCompletes(env);
|
||||
|
||||
// Last: the only case that waits out a whole timeout chain.
|
||||
testTimerRetriesThenGivesUp(env);
|
||||
}
|
||||
|
||||
private:
|
||||
unsigned int seed_{0};
|
||||
};
|
||||
|
||||
BEAST_DEFINE_TESTSUITE(InboundLedger, app, xrpl);
|
||||
|
||||
} // namespace xrpl::test
|
||||
@@ -11,6 +11,7 @@
|
||||
|
||||
#include <xrpl/basics/base_uint.h>
|
||||
#include <xrpl/basics/chrono.h>
|
||||
#include <xrpl/basics/contract.h>
|
||||
#include <xrpl/beast/insight/NullCollector.h>
|
||||
#include <xrpl/beast/unit_test/suite.h>
|
||||
#include <xrpl/beast/utility/Journal.h>
|
||||
@@ -24,6 +25,7 @@
|
||||
|
||||
#include <cassert>
|
||||
#include <memory>
|
||||
#include <stdexcept>
|
||||
#include <vector>
|
||||
|
||||
namespace xrpl::test {
|
||||
@@ -72,11 +74,15 @@ public:
|
||||
}
|
||||
res->unshare();
|
||||
|
||||
// Accept ledger
|
||||
res->setAccepted(
|
||||
res->header().closeTime,
|
||||
res->header().closeTimeResolution,
|
||||
true /* close time correct*/);
|
||||
// Accept the ledger. Thrown rather than asserted because a refusal leaves res unusable,
|
||||
// and this helper is static, so BEAST_EXPECT is out of reach.
|
||||
if (!res->setAccepted(
|
||||
res->header().closeTime,
|
||||
res->header().closeTimeResolution,
|
||||
true /* close time correct*/))
|
||||
{
|
||||
Throw<std::runtime_error>("makeLedger: ledger could not be accepted");
|
||||
}
|
||||
lh.insert(res, false);
|
||||
return res;
|
||||
}
|
||||
|
||||
@@ -28,17 +28,28 @@
|
||||
|
||||
#include <xrpl/basics/Slice.h>
|
||||
#include <xrpl/basics/base_uint.h>
|
||||
#include <xrpl/basics/random.h>
|
||||
#include <xrpl/beast/net/IPAddress.h>
|
||||
#include <xrpl/beast/unit_test/suite.h>
|
||||
#include <xrpl/beast/utility/Journal.h>
|
||||
#include <xrpl/beast/xor_shift_engine.h>
|
||||
#include <xrpl/json/json_value.h>
|
||||
#include <xrpl/ledger/ApplyView.h>
|
||||
#include <xrpl/ledger/Ledger.h>
|
||||
#include <xrpl/nodestore/Database.h>
|
||||
#include <xrpl/nodestore/NodeObject.h>
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
#include <xrpl/protocol/LedgerHeader.h>
|
||||
#include <xrpl/protocol/RippleLedgerHash.h>
|
||||
#include <xrpl/protocol/Serializer.h>
|
||||
#include <xrpl/protocol/TER.h>
|
||||
#include <xrpl/protocol/TxFlags.h>
|
||||
#include <xrpl/server/Handoff.h>
|
||||
#include <xrpl/shamap/Family.h>
|
||||
#include <xrpl/shamap/SHAMap.h>
|
||||
#include <xrpl/shamap/SHAMapItem.h>
|
||||
#include <xrpl/shamap/SHAMapMissingNode.h> // SHAMapType
|
||||
#include <xrpl/shamap/SHAMapTreeNode.h>
|
||||
|
||||
#include <boost/asio/ip/address.hpp>
|
||||
|
||||
@@ -64,6 +75,7 @@
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <unordered_set>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace xrpl::test {
|
||||
@@ -1090,6 +1102,111 @@ struct LedgerReplayer_test : public beast::unit_test::Suite
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A ledger whose transaction map cannot be read in full must draw an error reply
|
||||
* rather than a short transaction list.
|
||||
*
|
||||
* The two maps are built in memory and only part of them is written to the store: the
|
||||
* whole state map, so the ledger loads, and the transaction map's root alone. The
|
||||
* nodes below that root are never written, so the ledger the handler reads back from
|
||||
* the store cannot reach them.
|
||||
*/
|
||||
void
|
||||
testReplayDeltaIncompleteTxMap()
|
||||
{
|
||||
static constexpr auto kItems = 200;
|
||||
static constexpr auto kSeed = 0xdeadbeefU;
|
||||
|
||||
// Far enough ahead of the server's own history that the missing-node handler finds
|
||||
// no hash to acquire and returns at once.
|
||||
static constexpr auto kSeqAhead = 1000;
|
||||
|
||||
testcase("ReplayDelta incomplete tx map");
|
||||
|
||||
LedgerServer server(*this, {.initLedgers = 1});
|
||||
auto& family = server.app.getNodeFamily();
|
||||
|
||||
// The items only have to be well formed, because the walk stops before the handler
|
||||
// reads any of them.
|
||||
beast::XorShiftEngine engine{kSeed};
|
||||
auto const makeItem = [&engine] {
|
||||
static constexpr auto kWordsPerItem = 3;
|
||||
|
||||
Serializer s;
|
||||
for (auto word = 0; word < kWordsPerItem; ++word)
|
||||
s.add32(randInt<std::uint32_t>(engine));
|
||||
return makeShamapitem(s.getSHA512Half(), s.slice());
|
||||
};
|
||||
|
||||
SHAMap stateMap{SHAMapType::STATE, family};
|
||||
SHAMap txMap{SHAMapType::TRANSACTION, family};
|
||||
for (auto i = 0; i < kItems; ++i)
|
||||
{
|
||||
stateMap.addItem(SHAMapNodeType::TnAccountState, makeItem());
|
||||
txMap.addItem(SHAMapNodeType::TnTransactionNm, makeItem());
|
||||
}
|
||||
BEAST_EXPECT(stateMap.setImmutable());
|
||||
BEAST_EXPECT(txMap.setImmutable());
|
||||
|
||||
// Read both hashes before storing anything. getHash() is what computes the node
|
||||
// hashes, through unshare(), so storing first would write every node under a stale
|
||||
// hash and nothing would be findable afterwards.
|
||||
LedgerHeader header = server.ledgerMaster.getClosedLedger()->header();
|
||||
header.seq += kSeqAhead;
|
||||
header.accountHash = stateMap.getHash().asUInt256();
|
||||
header.txHash = txMap.getHash().asUInt256();
|
||||
header.hash = calculateLedgerHash(header);
|
||||
|
||||
// Write a map's nodes into the store. visitNodes reports the root first, so
|
||||
// rootOnly lets that one call through and stops at the next.
|
||||
auto const storeMap = [&family](SHAMap const& map, bool rootOnly) {
|
||||
auto stored = 0;
|
||||
map.visitNodes([&family, rootOnly, &stored](SHAMapTreeNode& node) {
|
||||
if (rootOnly && stored > 0)
|
||||
return false;
|
||||
|
||||
Serializer s;
|
||||
node.serializeWithPrefix(s);
|
||||
family.db().store(
|
||||
NodeObjectType::AccountNode,
|
||||
std::move(s.modData()),
|
||||
node.getHash().asUInt256(),
|
||||
0);
|
||||
++stored;
|
||||
return true;
|
||||
});
|
||||
return stored;
|
||||
};
|
||||
|
||||
BEAST_EXPECT(storeMap(stateMap, false) > 1);
|
||||
BEAST_EXPECT(storeMap(txMap, true) == 1);
|
||||
|
||||
bool loaded = false;
|
||||
auto const holed = std::make_shared<Ledger const>(
|
||||
header,
|
||||
loaded,
|
||||
false,
|
||||
server.ledgerMaster.getClosedLedger()->rules(),
|
||||
server.ledgerMaster.getClosedLedger()->fees(),
|
||||
family,
|
||||
server.app.getLogs().journal("Ledger"));
|
||||
|
||||
// Both roots are readable, so the ledger loads and the handler accepts it.
|
||||
BEAST_EXPECT(loaded);
|
||||
BEAST_EXPECT(holed->isImmutable());
|
||||
server.ledgerMaster.storeLedger(holed);
|
||||
|
||||
auto const request = std::make_shared<protocol::TMReplayDeltaRequest>();
|
||||
request->set_ledgerhash(header.hash.data(), header.hash.size());
|
||||
auto const reply = server.msgHandler.processReplayDeltaRequest(request);
|
||||
|
||||
// The reply names the missing nodes and carries no partial payload.
|
||||
BEAST_EXPECT(reply.has_error());
|
||||
BEAST_EXPECT(reply.error() == protocol::TMReplyError::reNO_NODE);
|
||||
BEAST_EXPECT(reply.transaction_size() == 0);
|
||||
BEAST_EXPECT(!reply.has_ledgerheader());
|
||||
}
|
||||
|
||||
void
|
||||
testTaskParameter()
|
||||
{
|
||||
@@ -1492,6 +1609,7 @@ struct LedgerReplayer_test : public beast::unit_test::Suite
|
||||
{
|
||||
testProofPath();
|
||||
testReplayDelta();
|
||||
testReplayDeltaIncompleteTxMap();
|
||||
testTruncatedHeader();
|
||||
testTaskParameter();
|
||||
testConfig();
|
||||
|
||||
@@ -43,6 +43,12 @@ struct PseudoTx_test : public beast::unit_test::Suite
|
||||
obj[sfReserveIncrement] = 0;
|
||||
obj[sfReferenceFeeUnits] = 0;
|
||||
}
|
||||
if (rules.enabled(featureSmartEscrow))
|
||||
{
|
||||
obj[sfGasLimit] = 0;
|
||||
obj[sfBytecodeSizeLimit] = 0;
|
||||
obj[sfGasPrice] = 0;
|
||||
}
|
||||
});
|
||||
|
||||
res.emplace_back(ttAMENDMENT, [&](auto& obj) {
|
||||
@@ -107,7 +113,9 @@ struct PseudoTx_test : public beast::unit_test::Suite
|
||||
FeatureBitset const all{testableAmendments()};
|
||||
FeatureBitset const xrpFees{featureXRPFees};
|
||||
|
||||
testPrevented(all - featureXRPFees - featureSmartEscrow);
|
||||
testPrevented(all - featureXRPFees);
|
||||
testPrevented(all - featureSmartEscrow);
|
||||
testPrevented(all);
|
||||
testAllowed();
|
||||
}
|
||||
|
||||
@@ -102,7 +102,7 @@ class RCLValidations_test : public beast::unit_test::Suite
|
||||
BEAST_EXPECT(next->read(keylet::feeSettings()));
|
||||
if (forceHash)
|
||||
{
|
||||
next->setImmutable();
|
||||
BEAST_EXPECT(next->setImmutable());
|
||||
forceHash = false;
|
||||
}
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
#include <test/jtx/paths.h>
|
||||
#include <test/jtx/pay.h>
|
||||
#include <test/jtx/paychan.h>
|
||||
#include <test/jtx/regkey.h>
|
||||
#include <test/jtx/sendmax.h>
|
||||
#include <test/jtx/seq.h>
|
||||
#include <test/jtx/sig.h>
|
||||
@@ -414,6 +415,59 @@ public:
|
||||
sponsor::As(sponsor, spfSponsorReserve),
|
||||
Sig(sfSponsorSignature, sponsor),
|
||||
Ter(tesSUCCESS));
|
||||
|
||||
// Sponsor authorizes with its regular key -> tesSUCCESS
|
||||
Account const regKey("regKey");
|
||||
Account const other("other");
|
||||
env.fund(XRP(10000), regKey, other);
|
||||
env.close();
|
||||
env(regkey(sponsor, regKey));
|
||||
env.close();
|
||||
env(noop(alice),
|
||||
Fee(XRP(1)),
|
||||
sponsor::As(sponsor, spfSponsorReserve),
|
||||
Sig(sfSponsorSignature, regKey),
|
||||
Ter(tesSUCCESS));
|
||||
|
||||
// Sponsor slot signed by an unrelated funded account's key -> tefBAD_AUTH
|
||||
env(noop(alice),
|
||||
Fee(XRP(1)),
|
||||
sponsor::As(sponsor, spfSponsorReserve),
|
||||
Sig(sfSponsorSignature, other),
|
||||
Ter(tefBAD_AUTH));
|
||||
|
||||
// Sponsor with master key disabled signing with its master key
|
||||
// -> tefMASTER_DISABLED
|
||||
Account const sponsorDM("sponsorDM");
|
||||
env.fund(XRP(10000), sponsorDM);
|
||||
env.close();
|
||||
env(regkey(sponsorDM, regKey));
|
||||
env(fset(sponsorDM, asfDisableMaster), Sig(sponsorDM));
|
||||
env.close();
|
||||
env(noop(alice),
|
||||
Fee(XRP(1)),
|
||||
sponsor::As(sponsorDM, spfSponsorReserve),
|
||||
Sig(sfSponsorSignature, sponsorDM),
|
||||
Ter(tefMASTER_DISABLED));
|
||||
|
||||
// Sponsor keyed by a pseudo-account that carries a SponsorSignature
|
||||
// -> tefBAD_AUTH (pseudo-accounts can't authorize signatures)
|
||||
Account const gw("gw");
|
||||
env.fund(XRP(1000000), gw);
|
||||
env.close();
|
||||
Vault const vault{env};
|
||||
auto [vtx, vkey] = vault.create({.owner = alice, .asset = gw["IOU"].asset()});
|
||||
env(vtx);
|
||||
env.close();
|
||||
auto const vaultSle = env.le(vkey);
|
||||
BEAST_EXPECT(vaultSle);
|
||||
Account const pseudoAcc("vault", vaultSle->getAccountID(sfAccount));
|
||||
env.memoize(pseudoAcc);
|
||||
env(noop(alice),
|
||||
Fee(XRP(1)),
|
||||
sponsor::As(pseudoAcc, spfSponsorReserve),
|
||||
Sig(sfSponsorSignature, other),
|
||||
Ter(tefBAD_AUTH));
|
||||
}
|
||||
|
||||
void
|
||||
@@ -472,6 +526,24 @@ public:
|
||||
sponsor::As(sponsor, spfSponsorReserve),
|
||||
Msig(sfSponsorSignature, {signer1, signer2}),
|
||||
Ter(tesSUCCESS));
|
||||
env.close();
|
||||
|
||||
// Quorum is 2 but only one signer signs -> tefBAD_QUORUM
|
||||
env(noop(alice),
|
||||
Fee(XRP(1)),
|
||||
sponsor::As(sponsor, spfSponsorReserve),
|
||||
Msig(sfSponsorSignature, {signer1}),
|
||||
Ter(tefBAD_QUORUM));
|
||||
|
||||
// Signer that is not on the sponsor's signer list -> tefBAD_SIGNATURE
|
||||
Account const stranger("stranger");
|
||||
env.fund(XRP(10000), stranger);
|
||||
env.close();
|
||||
env(noop(alice),
|
||||
Fee(XRP(1)),
|
||||
sponsor::As(sponsor, spfSponsorReserve),
|
||||
Msig(sfSponsorSignature, {stranger}),
|
||||
Ter(tefBAD_SIGNATURE));
|
||||
}
|
||||
|
||||
void
|
||||
@@ -1859,8 +1931,7 @@ public:
|
||||
{
|
||||
// existing owner objects that are outside the v1 SponsorshipTransfer
|
||||
// object allow-list
|
||||
// Temporary: FixedPrecision lending is incomplete until the LoanManage PR.
|
||||
Env env{*this, features - featureLendingProtocolV1_2};
|
||||
Env env{*this, features};
|
||||
Account const alice("alice");
|
||||
Account const sponsor("sponsor");
|
||||
env.fund(XRP(10000), alice, sponsor);
|
||||
|
||||
@@ -0,0 +1,525 @@
|
||||
#include <test/app/AcquireTestHelpers.h>
|
||||
#include <test/jtx/Env.h>
|
||||
|
||||
#include <xrpld/app/ledger/InboundTransactions.h>
|
||||
#include <xrpld/app/ledger/detail/TransactionAcquire.h>
|
||||
#include <xrpld/overlay/Peer.h>
|
||||
|
||||
#include <xrpl/basics/base_uint.h>
|
||||
#include <xrpl/beast/unit_test/suite.h>
|
||||
#include <xrpl/resource/Fees.h>
|
||||
#include <xrpl/shamap/SHAMap.h>
|
||||
#include <xrpl/shamap/SHAMapAddNode.h>
|
||||
#include <xrpl/shamap/SHAMapNodeID.h>
|
||||
|
||||
#include <xrpl.pb.h>
|
||||
|
||||
#include <chrono>
|
||||
#include <memory>
|
||||
#include <set>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace xrpl::test {
|
||||
|
||||
/**
|
||||
* An acquisition that exposes state its bases keep protected.
|
||||
*/
|
||||
struct TestableTransactionAcquire final : TransactionAcquire
|
||||
{
|
||||
using TransactionAcquire::TransactionAcquire;
|
||||
|
||||
/**
|
||||
* Whether the set being acquired is still structurally coherent.
|
||||
*
|
||||
* @return Whether the map is still valid.
|
||||
*/
|
||||
[[nodiscard]] bool
|
||||
isMapValid() const
|
||||
{
|
||||
// Under the lock: a batch on another thread can reach the verdict.
|
||||
ScopedLockType const sl(mtx_);
|
||||
return map_->isValid();
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether a batch has advanced the set since the flag was last cleared.
|
||||
*
|
||||
* @return Whether progress has been recorded.
|
||||
*/
|
||||
[[nodiscard]] bool
|
||||
madeProgress() const
|
||||
{
|
||||
// Under the lock: a timer tick clears the flag on a job thread.
|
||||
ScopedLockType const sl(mtx_);
|
||||
return progress_;
|
||||
}
|
||||
|
||||
/**
|
||||
* Forget any recorded progress.
|
||||
*/
|
||||
void
|
||||
clearProgress()
|
||||
{
|
||||
ScopedLockType const sl(mtx_);
|
||||
progress_ = false;
|
||||
}
|
||||
};
|
||||
|
||||
struct TransactionAcquire_test : public beast::unit_test::Suite
|
||||
{
|
||||
/**
|
||||
* A retry interval short enough that a whole timeout chain costs a fraction
|
||||
* of a second.
|
||||
*
|
||||
* TimeoutCounter refuses anything at or below 10ms. At this interval the
|
||||
* window between the first retry (four timeouts in) and giving up (twenty)
|
||||
* is still a third of a second, which is what the one case that watches
|
||||
* both needs.
|
||||
*/
|
||||
static constexpr auto kFastRetry = std::chrono::milliseconds{20};
|
||||
|
||||
/**
|
||||
* A seed no other chain in this suite has used.
|
||||
*
|
||||
* The Env below is shared, and ConsensusTransSetSF::gotNode() puts
|
||||
* every node it accepts into the application-wide NodeCache while
|
||||
* InboundTransactions keys its acquisitions by set hash. A fresh seed
|
||||
* per chain gives every chain distinct hashes, so each case resolves
|
||||
* and revives only its own.
|
||||
*
|
||||
* @return The seed.
|
||||
*/
|
||||
[[nodiscard]] unsigned int
|
||||
nextSeed()
|
||||
{
|
||||
return ++seed_;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether takeNodes() declined to look at the data at all.
|
||||
*
|
||||
* TimeoutCounter::complete_ and failed_ are both protected, so this
|
||||
* stands in for either: a done acquisition returns a verdict with every
|
||||
* count at zero.
|
||||
*
|
||||
* @param san The verdict a takeNodes() call returned.
|
||||
* @return Whether that verdict shows the data was never looked at.
|
||||
*/
|
||||
static bool
|
||||
wasIgnored(SHAMapAddNode const& san)
|
||||
{
|
||||
return tallyIs(san, 0, 0, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait for a finished set to reach InboundTransactions.
|
||||
*
|
||||
* done() hands the map over through a job, so a delivered set is what
|
||||
* separates completion from a stop.
|
||||
*
|
||||
* @param env The environment whose InboundTransactions to watch.
|
||||
* @param setHash The set to wait for.
|
||||
* @return The delivered map, or nullptr if none arrived.
|
||||
*/
|
||||
[[nodiscard]] static std::shared_ptr<SHAMap>
|
||||
waitForDeliveredSet(jtx::Env& env, UInt256 const& setHash)
|
||||
{
|
||||
auto& inbound = env.app().getInboundTransactions();
|
||||
|
||||
// acquire=false keeps this to a lookup rather than registering an acquisition. The job is
|
||||
// queued before takeNodes() returns, so a few seconds is a generous deadline.
|
||||
std::shared_ptr<SHAMap> delivered;
|
||||
if (!waitFor(
|
||||
[&] { return (delivered = inbound.getSet(setHash, false)) != nullptr; },
|
||||
std::chrono::seconds{5}))
|
||||
return nullptr;
|
||||
return delivered;
|
||||
}
|
||||
|
||||
/**
|
||||
* A chain ending in a leaf completes the acquisition, which stops the
|
||||
* asking and hands the map to InboundTransactions. Later replies for it
|
||||
* are then left alone.
|
||||
*
|
||||
* @param env The environment to run in.
|
||||
*/
|
||||
void
|
||||
testHappyPathCompletesAcquisition(jtx::Env& env)
|
||||
{
|
||||
testcase("A chain ending in a leaf completes the acquire");
|
||||
|
||||
auto const chain = DeepChain::toLeaf(3, nextSeed());
|
||||
|
||||
auto peerSet = std::make_unique<RequestCountingPeerSet>();
|
||||
auto const* const peerSetPtr = peerSet.get();
|
||||
|
||||
UInt256 const setHash = chain.rootHash.asUInt256();
|
||||
auto const acquire =
|
||||
std::make_shared<TransactionAcquire>(env.app(), setHash, std::move(peerSet));
|
||||
auto const peer = std::make_shared<ChargeRecordingPeer>();
|
||||
|
||||
// The root alone leaves the set incomplete, so accepting it asks for the level below.
|
||||
auto const rootResult = acquire->takeNodes({{SHAMapNodeID{}, chain.nodeAt(0)}}, peer);
|
||||
BEAST_EXPECT(rootResult.isUseful());
|
||||
int const requestsWhileIncomplete = peerSetPtr->requests();
|
||||
BEAST_EXPECT(requestsWhileIncomplete > 0);
|
||||
|
||||
// The rest of the chain, ending in the leaf.
|
||||
auto const result = acquire->takeNodes(chain.nodesBelowRoot(), peer);
|
||||
BEAST_EXPECT(result.isUseful());
|
||||
BEAST_EXPECT(!result.isInvalid());
|
||||
|
||||
// The request count holds steady, so the asking has stopped.
|
||||
BEAST_EXPECT(peerSetPtr->requests() == requestsWhileIncomplete);
|
||||
|
||||
auto const delivered = waitForDeliveredSet(env, setHash);
|
||||
BEAST_EXPECT(delivered != nullptr);
|
||||
if (delivered)
|
||||
{
|
||||
BEAST_EXPECT(delivered->getHash() == chain.rootHash);
|
||||
BEAST_EXPECT(delivered->isValid());
|
||||
}
|
||||
|
||||
// A reply arriving after the set is finished is left alone, and the asking stays stopped.
|
||||
// init() stayed uncalled, so the set above is what settled the acquisition.
|
||||
BEAST_EXPECT(wasIgnored(acquire->takeNodes({{SHAMapNodeID{}, chain.nodeAt(0)}}, peer)));
|
||||
BEAST_EXPECT(peerSetPtr->requests() == requestsWhileIncomplete);
|
||||
}
|
||||
|
||||
/**
|
||||
* Two peers each answering with a different missing piece are both accepted
|
||||
* without penalty, and the set completes from their combined replies.
|
||||
*
|
||||
* Driven through InboundTransactions::gotData(), so the leaf goes over the
|
||||
* real dispatch, which derives a leaf's position from its own key.
|
||||
*
|
||||
* @param env The environment to run in.
|
||||
*/
|
||||
void
|
||||
testTwoPeersEachSupplyPartOfTheSet(jtx::Env& env)
|
||||
{
|
||||
testcase("Two peers each supplying part of a set are both accepted without penalty");
|
||||
|
||||
auto const chain = DeepChain::toLeaf(3, nextSeed());
|
||||
auto& inbound = env.app().getInboundTransactions();
|
||||
|
||||
// acquire=true registers the TransactionAcquire that gotData() looks up by hash.
|
||||
UInt256 const setHash = chain.rootHash.asUInt256();
|
||||
BEAST_EXPECT(inbound.getSet(setHash, true) == nullptr);
|
||||
|
||||
// The first peer answers with the root only.
|
||||
auto const peerA = std::make_shared<ChargeRecordingPeer>();
|
||||
inbound.gotData(setHash, peerA, packetFor(chain, {{SHAMapNodeID{}, chain.nodeAt(0)}}));
|
||||
BEAST_EXPECT(peerA->charges().empty());
|
||||
|
||||
// The second answers with everything the first left out, and finishes the set.
|
||||
auto const peerB = std::make_shared<ChargeRecordingPeer>();
|
||||
inbound.gotData(setHash, peerB, packetFor(chain, chain.nodesBelowRoot()));
|
||||
BEAST_EXPECT(peerB->charges().empty());
|
||||
|
||||
auto const delivered = waitForDeliveredSet(env, setHash);
|
||||
BEAST_EXPECT(delivered != nullptr);
|
||||
if (delivered)
|
||||
BEAST_EXPECT(delivered->getHash() == chain.rootHash);
|
||||
}
|
||||
|
||||
/**
|
||||
* A root that does not hash to the set we asked for is a plain mismatch,
|
||||
* and has to leave the acquisition able to try another peer.
|
||||
*
|
||||
* @param env The environment to run in.
|
||||
*/
|
||||
void
|
||||
testBadRootKeepsAcquireAlive(jtx::Env& env)
|
||||
{
|
||||
testcase("A mismatched root leaves the acquire recoverable");
|
||||
|
||||
DeepChain const chain{nextSeed()};
|
||||
|
||||
// Acquire an unrelated hash, so the chain's root cannot match it.
|
||||
auto const acquire = std::make_shared<TestableTransactionAcquire>(
|
||||
env.app(), UInt256{42}, std::make_unique<RequestCountingPeerSet>());
|
||||
auto const peer = std::make_shared<ChargeRecordingPeer>();
|
||||
|
||||
auto const result = acquire->takeNodes({{SHAMapNodeID{}, chain.nodeAt(0)}}, peer);
|
||||
BEAST_EXPECTS(tallyIs(result, 0, 1, 0), result.get());
|
||||
|
||||
// A mismatched root tells the acquisition only that this peer's answer is wrong, so the
|
||||
// map keeps its state.
|
||||
BEAST_EXPECT(acquire->isMapValid());
|
||||
|
||||
// Still alive: the next packet is examined rather than waved through.
|
||||
BEAST_EXPECT(!wasIgnored(acquire->takeNodes({{SHAMapNodeID{}, chain.nodeAt(0)}}, peer)));
|
||||
}
|
||||
|
||||
/**
|
||||
* A second reply carrying a root we already have must stay free.
|
||||
*
|
||||
* This is what an honest second responder to the initial fan-out sends:
|
||||
* trigger() broadcasts to every tracked peer, so several answer the same
|
||||
* request and all but the first carry data the map already holds, so they
|
||||
* go uncharged.
|
||||
*
|
||||
* Covers the root specifically, which takeNodes() short-circuits on
|
||||
* haveRoot_ without consulting the map. A repeated non-root node takes the
|
||||
* other route, through addKnownNode() - see
|
||||
* testDuplicateNonRootReplyIsFree().
|
||||
*
|
||||
* @param env The environment to run in.
|
||||
*/
|
||||
void
|
||||
testDuplicateRootReplyIsFree(jtx::Env& env)
|
||||
{
|
||||
testcase("A reply of a root we already have is free");
|
||||
|
||||
DeepChain const chain{nextSeed()};
|
||||
auto& inbound = env.app().getInboundTransactions();
|
||||
|
||||
// acquire=true registers the TransactionAcquire that gotData() looks up by hash.
|
||||
UInt256 const setHash = chain.rootHash.asUInt256();
|
||||
BEAST_EXPECT(inbound.getSet(setHash, true) == nullptr);
|
||||
|
||||
auto const rootPacket = packetFor(chain, {{SHAMapNodeID{}, chain.nodeAt(0)}});
|
||||
|
||||
// The first responder supplies the root, which is genuinely useful.
|
||||
auto const firstPeer = std::make_shared<ChargeRecordingPeer>();
|
||||
inbound.gotData(setHash, firstPeer, rootPacket);
|
||||
BEAST_EXPECT(firstPeer->charges().empty());
|
||||
|
||||
// The second sends the same root. It answered what we asked, so it goes uncharged.
|
||||
auto const secondPeer = std::make_shared<ChargeRecordingPeer>();
|
||||
inbound.gotData(setHash, secondPeer, rootPacket);
|
||||
BEAST_EXPECT(secondPeer->charges().empty());
|
||||
}
|
||||
|
||||
/**
|
||||
* A repeated non-root node stays free too.
|
||||
*
|
||||
* The counterpart to testDuplicateRootReplyIsFree(), covering the route
|
||||
* that consults the map: addKnownNode() reports a node it already holds as
|
||||
* a duplicate, which isGood() counts as success.
|
||||
*
|
||||
* @param env The environment to run in.
|
||||
*/
|
||||
void
|
||||
testDuplicateNonRootReplyIsFree(jtx::Env& env)
|
||||
{
|
||||
testcase("A repeated non-root node is free");
|
||||
|
||||
auto const chain = DeepChain::toLeaf(3, nextSeed());
|
||||
auto& inbound = env.app().getInboundTransactions();
|
||||
|
||||
UInt256 const setHash = chain.rootHash.asUInt256();
|
||||
BEAST_EXPECT(inbound.getSet(setHash, true) == nullptr);
|
||||
|
||||
auto const rootPeer = std::make_shared<ChargeRecordingPeer>();
|
||||
inbound.gotData(setHash, rootPeer, packetFor(chain, {{SHAMapNodeID{}, chain.nodeAt(0)}}));
|
||||
BEAST_EXPECT(rootPeer->charges().empty());
|
||||
|
||||
// Depth 1 alone, so the set stays incomplete and the acquisition keeps examining data
|
||||
// rather than waving the second copy through as a late reply.
|
||||
auto const level1 = packetFor(chain, {{chain.idAt(1), chain.nodeAt(1)}});
|
||||
|
||||
auto const firstPeer = std::make_shared<ChargeRecordingPeer>();
|
||||
inbound.gotData(setHash, firstPeer, level1);
|
||||
BEAST_EXPECT(firstPeer->charges().empty());
|
||||
|
||||
auto const secondPeer = std::make_shared<ChargeRecordingPeer>();
|
||||
inbound.gotData(setHash, secondPeer, level1);
|
||||
BEAST_EXPECT(secondPeer->charges().empty());
|
||||
}
|
||||
|
||||
/**
|
||||
* A reply whose node data cannot be deserialized is charged for.
|
||||
*
|
||||
* gotData() rejects the packet before the acquisition is handed
|
||||
* anything, so this pins the charge on the dispatch layer rather than
|
||||
* on takeNodes(). It also gives this suite's "was not charged"
|
||||
* assertions their teeth: a harness that recorded no charge at all
|
||||
* would satisfy all of them and fail only here.
|
||||
*
|
||||
* @param env The environment to run in.
|
||||
*/
|
||||
void
|
||||
testUndeserializableNodeIsCharged(jtx::Env& env)
|
||||
{
|
||||
testcase("A reply with undeserializable node data is charged");
|
||||
|
||||
DeepChain const chain{nextSeed()};
|
||||
auto& inbound = env.app().getInboundTransactions();
|
||||
|
||||
UInt256 const setHash = chain.rootHash.asUInt256();
|
||||
BEAST_EXPECT(inbound.getSet(setHash, true) == nullptr);
|
||||
|
||||
// getTreeNode() rejects this before the acquisition is handed anything.
|
||||
auto const packet = std::make_shared<protocol::TMLedgerData>();
|
||||
packet->set_ledgerhash(setHash.data(), UInt256::size());
|
||||
packet->set_ledgerseq(0);
|
||||
packet->set_type(protocol::liTS_CANDIDATE);
|
||||
|
||||
auto* const node = packet->add_nodes();
|
||||
node->set_nodedata("\xff", 1);
|
||||
node->set_id(SHAMapNodeID{}.getRawString());
|
||||
|
||||
auto const peer = std::make_shared<ChargeRecordingPeer>();
|
||||
inbound.gotData(setHash, peer, packet);
|
||||
|
||||
BEAST_EXPECT(peer->charges() == std::vector{resource::kFeeInvalidData});
|
||||
}
|
||||
|
||||
/**
|
||||
* A well-formed reply whose root hashes to a different set is charged for.
|
||||
*
|
||||
* The counterpart to testUndeserializableNodeIsCharged(), which gotData()
|
||||
* rejects before the acquisition sees it. This reply deserializes cleanly
|
||||
* and reaches takeNodes(), whose invalid verdict gotData() turns into the
|
||||
* charge. testBadRootKeepsAcquireAlive() reads that verdict directly, so
|
||||
* this is the case that pins the charge made from it.
|
||||
*
|
||||
* @param env The environment to run in.
|
||||
*/
|
||||
void
|
||||
testMismatchedRootThroughGotDataIsCharged(jtx::Env& env)
|
||||
{
|
||||
testcase("A well-formed reply with a mismatched root is charged");
|
||||
|
||||
// The set being acquired, and an unrelated chain whose root cannot hash to it.
|
||||
DeepChain const chain{nextSeed()};
|
||||
DeepChain const stranger{nextSeed()};
|
||||
auto& inbound = env.app().getInboundTransactions();
|
||||
|
||||
// acquire=true registers the TransactionAcquire that gotData() looks up by hash.
|
||||
UInt256 const setHash = chain.rootHash.asUInt256();
|
||||
BEAST_EXPECT(inbound.getSet(setHash, true) == nullptr);
|
||||
|
||||
// The reply names the set we asked for, but carries the stranger's root.
|
||||
auto const packet = packetFor(
|
||||
stranger, {{SHAMapNodeID{}, stranger.nodeAt(0)}}, protocol::liTS_CANDIDATE, setHash);
|
||||
|
||||
auto const peer = std::make_shared<ChargeRecordingPeer>();
|
||||
inbound.gotData(setHash, peer, packet);
|
||||
|
||||
BEAST_EXPECT(peer->charges() == std::vector{resource::kFeeInvalidData});
|
||||
}
|
||||
|
||||
/**
|
||||
* init() passes hasTxSet() to addPeers() as its candidate filter.
|
||||
*
|
||||
* init() hands addPeers() hasTxSet(hash_) as its hasItem callback and
|
||||
* trigger() as its onPeerAdded callback. The harness selects on that
|
||||
* callback while the real peer set only scores with it, so this pins which
|
||||
* callbacks the acquisition supplies, not how many peers production asks.
|
||||
*
|
||||
* @param env The environment to run in.
|
||||
*/
|
||||
void
|
||||
testInitFiltersCandidatesByHasTxSet(jtx::Env& env)
|
||||
{
|
||||
testcase("init() passes hasTxSet as its candidate filter");
|
||||
|
||||
DeepChain const chain{nextSeed()};
|
||||
|
||||
// Ordered with the useless peer first, so a filter that is ignored altogether shows up as
|
||||
// the wrong peer being asked rather than as one extra request.
|
||||
auto const withoutSet = std::make_shared<ChargeRecordingPeer>(false);
|
||||
auto const withSet = std::make_shared<ChargeRecordingPeer>(true);
|
||||
|
||||
auto peerSet = std::make_unique<RequestCountingPeerSet>(
|
||||
std::vector<std::shared_ptr<Peer>>{withoutSet, withSet});
|
||||
auto const* const peerSetPtr = peerSet.get();
|
||||
|
||||
auto const acquire = std::make_shared<TransactionAcquire>(
|
||||
env.app(), chain.rootHash.asUInt256(), std::move(peerSet));
|
||||
|
||||
static constexpr int kStartPeers = 2;
|
||||
acquire->init(kStartPeers);
|
||||
|
||||
// Stop the retry loop, which keeps offering the same candidates while the acquisition runs.
|
||||
acquire->cancel();
|
||||
|
||||
BEAST_EXPECT(peerSetPtr->firstLimit() == kStartPeers);
|
||||
BEAST_EXPECT(peerSetPtr->addedPeers() == std::set<Peer::ID>{withSet->id()});
|
||||
|
||||
// The peer that was added is also asked, rather than merely tracked.
|
||||
BEAST_EXPECT(peerSetPtr->requests() >= 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* The retry timer broadcasts with no peer of its own, then gives up on
|
||||
* its own.
|
||||
*
|
||||
* The only case that reaches onTimer(). Pins the two behaviors, not the
|
||||
* thresholds they trip at: bounding those means asserting on wall clock.
|
||||
* The broadcast count is read after the give-up, which the failed_ guard
|
||||
* in trigger() makes safe.
|
||||
*
|
||||
* The acquisition tracks no peer, so the broadcast this case counts reaches
|
||||
* nobody in production either. What is pinned is that onTimer() issues it.
|
||||
*
|
||||
* @param env The environment to run in.
|
||||
*/
|
||||
void
|
||||
testTimerBroadcastsThenGivesUp(jtx::Env& env)
|
||||
{
|
||||
testcase("The retry timer broadcasts, then gives up");
|
||||
|
||||
DeepChain const chain{nextSeed()};
|
||||
|
||||
auto peerSet = std::make_unique<RequestCountingPeerSet>();
|
||||
auto const* const peerSetPtr = peerSet.get();
|
||||
|
||||
// An unrelated hash, so the probe below is always rejected.
|
||||
auto const acquire = std::make_shared<TransactionAcquire>(
|
||||
env.app(), UInt256{42}, std::move(peerSet), kFastRetry);
|
||||
|
||||
// No candidates, so nothing goes out until onTimer() decides to broadcast.
|
||||
acquire->init(1);
|
||||
BEAST_EXPECT(peerSetPtr->requests() == 0);
|
||||
|
||||
// A root that cannot hash to this acquisition's set is rejected without recording progress,
|
||||
// so polling with it does not postpone the timeout. A fresh peer each time keeps the
|
||||
// rejections from piling up on one.
|
||||
auto const probe = [&] {
|
||||
return wasIgnored(acquire->takeNodes(
|
||||
{{SHAMapNodeID{}, chain.nodeAt(0)}}, std::make_shared<ChargeRecordingPeer>()));
|
||||
};
|
||||
|
||||
// kNormTimeouts (4) intervals in, onTimer() broadcasts with no peer of its own to ask. Past
|
||||
// kMaxTimeouts (20) it fails itself, and stops examining data.
|
||||
BEAST_EXPECT(waitFor(probe));
|
||||
|
||||
// Read as a broadcast rather than as a request, since a request here would mean a peer was
|
||||
// named. Reading it after the failure is safe. trigger() returns before sending once
|
||||
// failed_ is set, and both run under the acquisition's mutex, so any counted broadcast
|
||||
// went out while the acquisition was still alive.
|
||||
BEAST_EXPECT(peerSetPtr->broadcasts() > 0);
|
||||
}
|
||||
|
||||
void
|
||||
run() override
|
||||
{
|
||||
// One Env for the suite, which is safe only because every chain is seeded through
|
||||
// nextSeed(): cases sharing an Env share a NodeCache, so they must not share a hash.
|
||||
jtx::Env env{*this};
|
||||
|
||||
testHappyPathCompletesAcquisition(env);
|
||||
testTwoPeersEachSupplyPartOfTheSet(env);
|
||||
testBadRootKeepsAcquireAlive(env);
|
||||
testDuplicateRootReplyIsFree(env);
|
||||
testDuplicateNonRootReplyIsFree(env);
|
||||
testUndeserializableNodeIsCharged(env);
|
||||
testMismatchedRootThroughGotDataIsCharged(env);
|
||||
testInitFiltersCandidatesByHasTxSet(env);
|
||||
|
||||
// Last: the only case that waits out a whole timeout chain.
|
||||
testTimerBroadcastsThenGivesUp(env);
|
||||
}
|
||||
|
||||
private:
|
||||
unsigned int seed_{0};
|
||||
};
|
||||
|
||||
BEAST_DEFINE_TESTSUITE(TransactionAcquire, app, xrpl);
|
||||
|
||||
} // namespace xrpl::test
|
||||
@@ -62,10 +62,7 @@ namespace xrpl::test {
|
||||
|
||||
class InvariantsVault_test : public InvariantsBase
|
||||
{
|
||||
FeatureBitset const all_{test::jtx::testableAmendments()};
|
||||
// testableAmendments() leaves featureLendingProtocolV1_2 off until the
|
||||
// LoanManage PR; enable it explicitly for FixedPrecision rows.
|
||||
FeatureBitset const fixedPrecision_{all_ | featureLendingProtocolV1_2};
|
||||
FeatureBitset const all_{test::jtx::testableAmendments() | featureLendingProtocolV1_2};
|
||||
|
||||
void
|
||||
testVault() // NOLINT(readability-function-size)
|
||||
@@ -1018,8 +1015,11 @@ class InvariantsVault_test : public InvariantsBase
|
||||
// circuits and returns success without inspecting the loan or the
|
||||
// vault. The same state that trips the principal-outstanding check
|
||||
// under V1_1 must be silently accepted here.
|
||||
//
|
||||
// V1_2 is disabled too: it alone makes VaultCreate pick FixedPrecision,
|
||||
// whose AssetsTotal-sync invariant would fire on this scenario.
|
||||
doInvariantCheck(
|
||||
makeEnv(all_ - featureLendingProtocolV1_1),
|
||||
makeEnv(all_ - featureLendingProtocolV1_1 - featureLendingProtocolV1_2),
|
||||
{},
|
||||
[&](Account const& a1, Account const& a2, ApplyContext& ac) {
|
||||
auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq()));
|
||||
@@ -1446,6 +1446,7 @@ class InvariantsVault_test : public InvariantsBase
|
||||
// valid positive dust while moving the posterior AssetsTotal to a much
|
||||
// finer scale. The dust must be bounded by the former scale rather than
|
||||
// compared with one unit at the posterior scale.
|
||||
//
|
||||
// CashBasis, not FixedPrecision: AssetsTotal is set independently of
|
||||
// AssetsAvailable plus the vault's AssetsDeployed, which the FixedPrecision
|
||||
// sync invariant rejects.
|
||||
@@ -1597,9 +1598,7 @@ class InvariantsVault_test : public InvariantsBase
|
||||
// (here the neutral ttACCOUNT_SET) is a violation, even while the
|
||||
// loan still has outstanding obligations: the transaction-type check
|
||||
// fires before the not-fully-paid-off check.
|
||||
// Temporary: FixedPrecision lending is incomplete until the LoanManage PR.
|
||||
doInvariantCheck(
|
||||
makeEnv(all_ - featureLendingProtocolV1_2),
|
||||
{"Loan deleted by a transaction other than LoanDelete"},
|
||||
eraseLoan,
|
||||
XRPAmount{},
|
||||
@@ -1943,7 +1942,6 @@ class InvariantsVault_test : public InvariantsBase
|
||||
|
||||
// FixedPrecision-only: a new vault must have AssetsDeployed == 0.
|
||||
doInvariantCheck(
|
||||
makeEnv(fixedPrecision_),
|
||||
{
|
||||
"created vault must be empty",
|
||||
"updated zero sized vault must have no AssetsDeployed",
|
||||
@@ -1970,7 +1968,6 @@ class InvariantsVault_test : public InvariantsBase
|
||||
|
||||
// FixedPrecision-only: a new vault must have YieldUnrealized == 0.
|
||||
doInvariantCheck(
|
||||
makeEnv(fixedPrecision_),
|
||||
{
|
||||
"created vault must be empty",
|
||||
"create operation must not have updated a vault",
|
||||
@@ -2299,14 +2296,13 @@ class InvariantsVault_test : public InvariantsBase
|
||||
{tecINVARIANT_FAILED, tefINVARIANT_FAILED},
|
||||
precloseXrp);
|
||||
|
||||
// Temporary: FixedPrecision lending is incomplete until the LoanManage PR.
|
||||
doInvariantCheck(
|
||||
makeEnv(all_ - featureLendingProtocolV1_2),
|
||||
{
|
||||
"deposit must increase vault balance",
|
||||
"deposit must decrease depositor balance",
|
||||
"deposit must change vault and depositor balance by equal amount",
|
||||
"deposit and assets outstanding must add up",
|
||||
// FixedPrecision: only AssetsAvailable's delta is compared; the
|
||||
// stored AssetsTotal is a derived cache.
|
||||
"deposit and assets available must add up",
|
||||
},
|
||||
[&](Account const& a1, Account const& a2, ApplyContext& ac) {
|
||||
@@ -2401,7 +2397,8 @@ class InvariantsVault_test : public InvariantsBase
|
||||
precloseXrp,
|
||||
TxAccount::A2);
|
||||
|
||||
// Temporary: FixedPrecision lending is incomplete until the LoanManage PR.
|
||||
// AssetsTotal alone is wrong. FixedPrecision does not compare it per
|
||||
// transaction, so this exercises the Legacy/CashBasis path.
|
||||
doInvariantCheck(
|
||||
makeEnv(all_ - featureLendingProtocolV1_2),
|
||||
{"deposit and assets outstanding must add up"},
|
||||
@@ -2427,11 +2424,9 @@ class InvariantsVault_test : public InvariantsBase
|
||||
precloseXrp,
|
||||
TxAccount::A2);
|
||||
|
||||
// Temporary: FixedPrecision lending is incomplete until the LoanManage PR.
|
||||
doInvariantCheck(
|
||||
makeEnv(all_ - featureLendingProtocolV1_2),
|
||||
// FixedPrecision does not compare AssetsTotal's delta.
|
||||
{
|
||||
"deposit and assets outstanding must add up",
|
||||
"deposit and assets available must add up",
|
||||
},
|
||||
[&](Account const& a1, Account const& a2, ApplyContext& ac) {
|
||||
@@ -2493,14 +2488,12 @@ class InvariantsVault_test : public InvariantsBase
|
||||
{tecINVARIANT_FAILED, tefINVARIANT_FAILED},
|
||||
precloseXrp);
|
||||
|
||||
// Temporary: FixedPrecision lending is incomplete until the LoanManage PR.
|
||||
doInvariantCheck(
|
||||
makeEnv(all_ - featureLendingProtocolV1_2),
|
||||
{
|
||||
"withdrawal must change vault and destination balance by equal amount",
|
||||
"withdrawal must decrease vault balance",
|
||||
"withdrawal must increase destination balance",
|
||||
"withdrawal and assets outstanding must add up",
|
||||
// FixedPrecision does not compare AssetsTotal's delta.
|
||||
"withdrawal and assets available must add up",
|
||||
},
|
||||
[&](Account const& a1, Account const& a2, ApplyContext& ac) {
|
||||
@@ -2592,11 +2585,9 @@ class InvariantsVault_test : public InvariantsBase
|
||||
precloseXrp,
|
||||
TxAccount::A2);
|
||||
|
||||
// Temporary: FixedPrecision lending is incomplete until the LoanManage PR.
|
||||
doInvariantCheck(
|
||||
makeEnv(all_ - featureLendingProtocolV1_2),
|
||||
// FixedPrecision does not compare AssetsTotal's delta.
|
||||
{
|
||||
"withdrawal and assets outstanding must add up",
|
||||
"withdrawal and assets available must add up",
|
||||
},
|
||||
[&](Account const& a1, Account const& a2, ApplyContext& ac) {
|
||||
@@ -2612,7 +2603,8 @@ class InvariantsVault_test : public InvariantsBase
|
||||
precloseXrp,
|
||||
TxAccount::A2);
|
||||
|
||||
// Temporary: FixedPrecision lending is incomplete until the LoanManage PR.
|
||||
// AssetsTotal alone is wrong. FixedPrecision does not compare it per
|
||||
// transaction, so this exercises the Legacy/CashBasis path.
|
||||
doInvariantCheck(
|
||||
makeEnv(all_ - featureLendingProtocolV1_2),
|
||||
{"withdrawal and assets outstanding must add up"},
|
||||
@@ -2785,12 +2777,10 @@ class InvariantsVault_test : public InvariantsBase
|
||||
{tecINVARIANT_FAILED, tefINVARIANT_FAILED},
|
||||
precloseMpt);
|
||||
|
||||
// Temporary: FixedPrecision lending is incomplete until the LoanManage PR.
|
||||
doInvariantCheck(
|
||||
makeEnv(all_ - featureLendingProtocolV1_2),
|
||||
{
|
||||
"clawback must change holder and vault shares by equal amount",
|
||||
"clawback and assets outstanding must add up",
|
||||
// FixedPrecision does not compare AssetsTotal's delta.
|
||||
"clawback and assets available must add up",
|
||||
},
|
||||
[&](Account const& a1, Account const& a2, ApplyContext& ac) {
|
||||
@@ -3222,8 +3212,7 @@ class InvariantsVault_test : public InvariantsBase
|
||||
|
||||
for (auto const withFix : {false, true})
|
||||
{
|
||||
// Temporary: FixedPrecision lending is incomplete until the LoanManage PR.
|
||||
FeatureBitset amendments = all_ - featureLendingProtocolV1_2;
|
||||
FeatureBitset amendments = all_;
|
||||
if (!withFix)
|
||||
amendments = amendments - fixCleanup3_4_0;
|
||||
|
||||
@@ -3443,6 +3432,71 @@ class InvariantsVault_test : public InvariantsBase
|
||||
}
|
||||
}
|
||||
|
||||
// Closed-ended FixedPrecision vault with one open loan. Closed-ended
|
||||
// because LoanBrokerSet::preclaim only accepts those under
|
||||
// featureLendingProtocolV1_1.
|
||||
static Keylet
|
||||
makeFixedPrecisionVaultWithLoan(
|
||||
test::jtx::Account const& owner,
|
||||
test::jtx::Account const& borrower,
|
||||
test::jtx::Account const& issuer,
|
||||
test::jtx::Env& env)
|
||||
{
|
||||
using namespace test::jtx;
|
||||
|
||||
env.fund(XRP(1'000'000), issuer, borrower);
|
||||
env.close();
|
||||
|
||||
PrettyAsset const usd = issuer["USD"];
|
||||
env(trust(owner, usd(1'000'000)));
|
||||
env(trust(borrower, usd(1'000'000)));
|
||||
env.close();
|
||||
|
||||
env(pay(issuer, owner, usd(100'000)));
|
||||
env(pay(issuer, borrower, usd(1'000)));
|
||||
env.close();
|
||||
|
||||
Vault const vault{env};
|
||||
auto [vaultTx, vaultKeylet, subscriptionDate] = vault.createClosedEnded({
|
||||
.owner = owner,
|
||||
.asset = usd,
|
||||
.subscriptionOffset = std::chrono::seconds{60},
|
||||
.investmentWindow = std::chrono::seconds{10ull * 365ull * 24ull * 60ull * 60ull},
|
||||
});
|
||||
env(vaultTx);
|
||||
env.close();
|
||||
|
||||
env(vault.deposit(
|
||||
{.depositor = owner, .id = vaultKeylet.key, .amount = usd(1'000).value()}));
|
||||
env.close();
|
||||
|
||||
auto const brokerKeylet =
|
||||
keylet::loanBroker(owner.id(), SeqProxy::rawSequence(env.seq(owner)));
|
||||
{
|
||||
using namespace loan_broker;
|
||||
env(set(owner, vaultKeylet.key), Fee(env.current()->fees().base * 2));
|
||||
env.close();
|
||||
}
|
||||
|
||||
// LoanSet is gated on Investment; advance out of Subscription.
|
||||
vault.closePastSubscription(subscriptionDate);
|
||||
|
||||
{
|
||||
using namespace loan;
|
||||
env(set(borrower, brokerKeylet.key, usd(100).value()),
|
||||
kCounterparty(owner),
|
||||
kInterestRate(TenthBips32{0}),
|
||||
kPaymentTotal(3),
|
||||
kPaymentInterval(86400u),
|
||||
kGracePeriod(3600u),
|
||||
Sig(sfCounterpartySignature, owner),
|
||||
Fee(env.current()->fees().base * 2));
|
||||
env.close();
|
||||
}
|
||||
|
||||
return vaultKeylet;
|
||||
}
|
||||
|
||||
static void
|
||||
addToVaultField(std::shared_ptr<SLE> const& sle, SF_NUMBER const& field, Number const& delta)
|
||||
{
|
||||
@@ -3463,6 +3517,53 @@ class InvariantsVault_test : public InvariantsBase
|
||||
TER secondTer = tecINVARIANT_FAILED;
|
||||
};
|
||||
|
||||
void
|
||||
checkLoanVaultRow(AssetsDeployedRow const& row)
|
||||
{
|
||||
using namespace test::jtx;
|
||||
|
||||
testcase(row.name);
|
||||
|
||||
// Create the vault before building the transaction, so the transaction
|
||||
// carries the real VaultID.
|
||||
auto env = makeEnv(all_);
|
||||
Account const owner{"A1"};
|
||||
Account const other{"A2"};
|
||||
Account const issuer{"issuer"};
|
||||
Account const borrower{"borrower"};
|
||||
env.fund(XRP(1000), owner, other);
|
||||
auto const vaultKeylet = makeFixedPrecisionVaultWithLoan(owner, borrower, issuer, env);
|
||||
if (!BEAST_EXPECT(env.le(vaultKeylet)))
|
||||
return;
|
||||
env.close();
|
||||
|
||||
STTx tx{row.txType, [&](STObject& obj) {
|
||||
if (row.withVaultId)
|
||||
{
|
||||
obj.setFieldH256(sfVaultID, vaultKeylet.key);
|
||||
}
|
||||
}};
|
||||
tx.setAccountID(sfAccount, owner.id());
|
||||
|
||||
doInvariantCheck(
|
||||
std::move(env),
|
||||
owner,
|
||||
other,
|
||||
row.logs,
|
||||
[&](Account const&, Account const&, ApplyContext& ac) -> bool {
|
||||
auto const sle = ac.view().peek(vaultKeylet);
|
||||
if (!sle || !row.mutate(ac, sle))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
ac.view().update(sle);
|
||||
return true;
|
||||
},
|
||||
XRPAmount{},
|
||||
tx,
|
||||
{tecINVARIANT_FAILED, row.secondTer});
|
||||
}
|
||||
|
||||
void
|
||||
checkXrpVaultRow(AssetsDeployedRow const& row)
|
||||
{
|
||||
@@ -3484,7 +3585,7 @@ class InvariantsVault_test : public InvariantsBase
|
||||
};
|
||||
|
||||
doInvariantCheck(
|
||||
makeEnv(fixedPrecision_),
|
||||
makeEnv(all_),
|
||||
row.logs,
|
||||
[&](Account const& a1, Account const&, ApplyContext& ac) {
|
||||
auto const sle =
|
||||
@@ -3506,6 +3607,94 @@ class InvariantsVault_test : public InvariantsBase
|
||||
void
|
||||
testVaultAssetsDeployedInvariants()
|
||||
{
|
||||
// ttLOAN_PAY and ttLOAN_MANAGE may change AssetsDeployed and LossUnrealized
|
||||
// respectively, which isolates each check from the "vault transaction
|
||||
// must not change ..." checks.
|
||||
AssetsDeployedRow const loanRows[] = {
|
||||
{
|
||||
.name = "FixedPrecision vault: AssetsDeployed drift from AssetsTotal",
|
||||
.logs =
|
||||
{
|
||||
"stored AssetsTotal must equal AssetsAvailable plus AssetsDeployed, "
|
||||
"rounded "
|
||||
"Downward",
|
||||
},
|
||||
.mutate =
|
||||
[](ApplyContext&, std::shared_ptr<SLE> const& sle) {
|
||||
addToVaultField(sle, sfAssetsDeployed, Number{1});
|
||||
return true;
|
||||
},
|
||||
.txType = ttLOAN_PAY,
|
||||
},
|
||||
{
|
||||
.name = "FixedPrecision vault: stored AssetsTotal != Downward16(AA + AD)",
|
||||
.logs =
|
||||
{
|
||||
"stored AssetsTotal must equal AssetsAvailable plus AssetsDeployed, "
|
||||
"rounded "
|
||||
"Downward",
|
||||
},
|
||||
.mutate =
|
||||
[](ApplyContext&, std::shared_ptr<SLE> const& sle) {
|
||||
addToVaultField(sle, sfAssetsTotal, Number{1});
|
||||
return true;
|
||||
},
|
||||
.txType = ttLOAN_PAY,
|
||||
},
|
||||
// FixedPrecision compares LU with AssetsDeployed directly; the message
|
||||
// is shared with the Legacy/CashBasis "LU <= AT - AA" check.
|
||||
{
|
||||
.name = "FixedPrecision vault: LossUnrealized exceeds AssetsDeployed",
|
||||
.logs =
|
||||
{
|
||||
"loss unrealized must not exceed the difference between assets "
|
||||
"outstanding and available",
|
||||
},
|
||||
.mutate =
|
||||
[](ApplyContext&, std::shared_ptr<SLE> const& sle) {
|
||||
Number const assetsDeployed = sle->at(sfAssetsDeployed);
|
||||
(*sle)[sfLossUnrealized] = assetsDeployed + Number{1};
|
||||
return true;
|
||||
},
|
||||
.txType = ttLOAN_MANAGE,
|
||||
},
|
||||
// AssetsTotal moves too so the sync check stays quiet.
|
||||
{
|
||||
.name = "FixedPrecision vault: AssetsDeployed changed by a non-lending transaction",
|
||||
.logs = {"vault transaction must not change AssetsDeployed"},
|
||||
.mutate =
|
||||
[](ApplyContext&, std::shared_ptr<SLE> const& sle) {
|
||||
addToVaultField(sle, sfAssetsDeployed, Number{1});
|
||||
addToVaultField(sle, sfAssetsTotal, Number{1});
|
||||
return true;
|
||||
},
|
||||
.withVaultId = true,
|
||||
},
|
||||
// On-ledger mirror of VaultWithdraw's tecHAS_OBLIGATIONS guard.
|
||||
{
|
||||
.name = "FixedPrecision vault: non-zero AssetsDeployed after shares return to zero",
|
||||
.logs = {"updated zero sized vault must have no AssetsDeployed"},
|
||||
.mutate =
|
||||
[](ApplyContext& ac, std::shared_ptr<SLE> const& sle) {
|
||||
auto const issuance =
|
||||
ac.view().peek(keylet::mptokenIssuance(sle->at(sfShareMPTID)));
|
||||
if (!issuance)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
issuance->setFieldU64(sfOutstandingAmount, 0);
|
||||
ac.view().update(issuance);
|
||||
return true;
|
||||
},
|
||||
.withVaultId = true,
|
||||
.secondTer = tefINVARIANT_FAILED,
|
||||
},
|
||||
};
|
||||
for (auto const& row : loanRows)
|
||||
{
|
||||
checkLoanVaultRow(row);
|
||||
}
|
||||
|
||||
AssetsDeployedRow const xrpRows[] = {
|
||||
{
|
||||
.name = "FixedPrecision vault: negative AssetsDeployed",
|
||||
@@ -3607,7 +3796,7 @@ class InvariantsVault_test : public InvariantsBase
|
||||
};
|
||||
|
||||
doInvariantCheck(
|
||||
makeEnv(fixedPrecision_),
|
||||
makeEnv(all_),
|
||||
{"withdrawal and assets available must add up"},
|
||||
[&vaultKeylet](Account const& a1, Account const&, ApplyContext& ac) -> bool {
|
||||
auto const sleVault = ac.view().peek(vaultKeylet);
|
||||
|
||||
@@ -837,23 +837,19 @@ public:
|
||||
{
|
||||
// Batch rejects Vault and Lending inners before LendingProtocolV1_2.
|
||||
FeatureBitset const lendingBatch{all_ | featureLendingProtocolV1_2};
|
||||
testVaultLifecycle(lendingBatch);
|
||||
testArbitrage(lendingBatch);
|
||||
testArbitrageRollback(lendingBatch);
|
||||
testImpairAndCoverDeposit(lendingBatch);
|
||||
testDefaultAndCoverWithdraw(lendingBatch);
|
||||
testIndependentVaultChain(lendingBatch);
|
||||
testIndependentLoanChain(lendingBatch);
|
||||
for (auto const& features : {lendingBatch, lendingBatch | featureLendingProtocolV1_1})
|
||||
{
|
||||
testVaultLifecycle(features);
|
||||
testArbitrage(features);
|
||||
testArbitrageRollback(features);
|
||||
testImpairAndCoverDeposit(features);
|
||||
testDefaultAndCoverWithdraw(features);
|
||||
testIndependentVaultChain(features);
|
||||
testIndependentLoanChain(features);
|
||||
}
|
||||
testClosedEndedVaultLifecycle(lendingBatch | featureLendingProtocolV1_1);
|
||||
testLoanLifecycleOpenEndedVault(lendingBatch);
|
||||
|
||||
// Temporary while the FixedPrecision Vault stack lands: with
|
||||
// featureLendingProtocolV1_1 these Vaults are FixedPrecision, whose
|
||||
// lending transactions are enabled in later PRs. Only the Vault-only
|
||||
// cases run here; the rest are restored in the LoanManage PR.
|
||||
FeatureBitset const fixedPrecision{lendingBatch | featureLendingProtocolV1_1};
|
||||
testVaultLifecycle(fixedPrecision);
|
||||
testIndependentVaultChain(fixedPrecision);
|
||||
testClosedEndedVaultLifecycle(fixedPrecision);
|
||||
testLoanLifecycleClosedEndedVault(lendingBatch | featureLendingProtocolV1_1);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -1964,8 +1964,7 @@ public:
|
||||
Account const lender{"lender"};
|
||||
Account const borrower{"borrower"};
|
||||
|
||||
// Temporary: FixedPrecision lending is incomplete until the LoanManage PR.
|
||||
Env env{*this, testableAmendments() - featureLendingProtocolV1_2};
|
||||
Env env{*this};
|
||||
Vault const vault{env};
|
||||
env.fund(XRP(10'000), lender, borrower);
|
||||
env.close();
|
||||
|
||||
@@ -301,8 +301,7 @@ private:
|
||||
|
||||
for (bool const deepFreeze : {true, false})
|
||||
{
|
||||
// Temporary: FixedPrecision lending is incomplete until the LoanManage PR.
|
||||
Env env(*this, testableAmendments() - featureLendingProtocolV1_2);
|
||||
Env env(*this);
|
||||
|
||||
auto const getCoverBalance = [&](BrokerInfo const& brokerInfo,
|
||||
auto const& accountField) {
|
||||
@@ -386,8 +385,7 @@ private:
|
||||
Account const borrower{"borrower"};
|
||||
auto const iou = issuer["IOU"];
|
||||
|
||||
// Temporary: FixedPrecision lending is incomplete until the LoanManage PR.
|
||||
Env env(*this, testableAmendments() - featureLendingProtocolV1_2);
|
||||
Env env(*this);
|
||||
env.fund(XRP(1'000), lender, issuer, borrower);
|
||||
env(trust(lender, iou(10'000'000)));
|
||||
env(pay(issuer, lender, iou(5'000'000)));
|
||||
@@ -442,8 +440,7 @@ private:
|
||||
Account const lender{"lender"};
|
||||
Account const borrower{"borrower"};
|
||||
|
||||
// Temporary: FixedPrecision lending is incomplete until the LoanManage PR.
|
||||
Env env(*this, testableAmendments() - featureLendingProtocolV1_2);
|
||||
Env env(*this);
|
||||
env.fund(XRP(1'000'000), issuer, lender, borrower);
|
||||
env.close();
|
||||
|
||||
@@ -514,8 +511,7 @@ private:
|
||||
Account const borrower{"borrower"};
|
||||
auto const iou = issuer["IOU"];
|
||||
|
||||
// Temporary: FixedPrecision lending is incomplete until the LoanManage PR.
|
||||
Env env(*this, testableAmendments() - featureLendingProtocolV1_2);
|
||||
Env env(*this);
|
||||
env.fund(XRP(1'000), lender, issuer, borrower);
|
||||
env(trust(lender, iou(10'000'000)));
|
||||
env(pay(issuer, lender, iou(5'000'000)));
|
||||
|
||||
@@ -289,8 +289,7 @@ private:
|
||||
Account const issuer("issuer");
|
||||
Account const borrower = issuer;
|
||||
Account const lender("lender");
|
||||
// Temporary: FixedPrecision lending is incomplete until the LoanManage PR.
|
||||
Env env(*this, testableAmendments() - featureLendingProtocolV1_2);
|
||||
Env env(*this);
|
||||
|
||||
env.fund(XRP(1'000), issuer, lender);
|
||||
|
||||
@@ -330,8 +329,7 @@ private:
|
||||
Account const depositor{"depositor"};
|
||||
|
||||
auto const testLoanAsset = [&](auto&& getMaxDebt, auto const& borrower) {
|
||||
// Temporary: FixedPrecision lending is incomplete until the LoanManage PR.
|
||||
Env env(*this, testableAmendments() - featureLendingProtocolV1_2);
|
||||
Env env(*this);
|
||||
Vault const vault(env);
|
||||
|
||||
if (borrower == broker)
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
#pragma once
|
||||
|
||||
#include <test/app/lending/LoanPayFixedPrecisionBase.h>
|
||||
#include <test/jtx/Account.h>
|
||||
#include <test/jtx/Env.h>
|
||||
#include <test/jtx/TestHelpers.h>
|
||||
|
||||
#include <xrpl/basics/chrono.h>
|
||||
#include <xrpl/beast/unit_test/suite.h>
|
||||
#include <xrpl/protocol/Keylet.h>
|
||||
#include <xrpl/protocol/SField.h>
|
||||
#include <xrpl/protocol/TxFlags.h>
|
||||
|
||||
#include <chrono>
|
||||
#include <cstdint>
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
// LoanManage-tier scaffolding on top of LoanPayFixedPrecisionBase: the
|
||||
// default/impair helpers needed by LoanManageFixedPrecision only.
|
||||
class LoanManageFixedPrecisionBase : public LoanPayFixedPrecisionBase
|
||||
{
|
||||
protected:
|
||||
// Advance the clock one second past the loan's due date (late, in grace) or
|
||||
// past its grace period (defaultable).
|
||||
void
|
||||
closePastDue(test::jtx::Env& env, Keylet const& loanKeylet)
|
||||
{
|
||||
auto const loanSle = env.le(loanKeylet);
|
||||
if (!BEAST_EXPECT(loanSle))
|
||||
return;
|
||||
std::uint32_t const dueDate = loanSle->at(sfNextPaymentDueDate);
|
||||
env.close(NetClock::time_point{NetClock::duration{dueDate}} + std::chrono::seconds{1});
|
||||
}
|
||||
|
||||
void
|
||||
closePastGrace(test::jtx::Env& env, Keylet const& loanKeylet)
|
||||
{
|
||||
auto const loanSle = env.le(loanKeylet);
|
||||
if (!BEAST_EXPECT(loanSle))
|
||||
return;
|
||||
std::uint32_t const dueDate = loanSle->at(sfNextPaymentDueDate);
|
||||
std::uint32_t const grace = loanSle->at(sfGracePeriod);
|
||||
env.close(
|
||||
NetClock::time_point{NetClock::duration{dueDate + grace}} + std::chrono::seconds{1});
|
||||
}
|
||||
|
||||
void
|
||||
impairWhenLate(test::jtx::Env& env, test::jtx::Account const& owner, Keylet const& loanKeylet)
|
||||
{
|
||||
closePastDue(env, loanKeylet);
|
||||
env(test::jtx::loan::manage(owner, loanKeylet.key, tfLoanImpair));
|
||||
env.close();
|
||||
}
|
||||
|
||||
void
|
||||
defaultAfterGrace(
|
||||
test::jtx::Env& env,
|
||||
test::jtx::Account const& owner,
|
||||
Keylet const& loanKeylet)
|
||||
{
|
||||
closePastGrace(env, loanKeylet);
|
||||
env(test::jtx::loan::manage(owner, loanKeylet.key, tfLoanDefault));
|
||||
env.close();
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace xrpl
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,340 @@
|
||||
#pragma once
|
||||
|
||||
#include <test/app/lending/LoanFixedPrecisionBase.h>
|
||||
#include <test/app/vault/VaultFixedPrecisionBase.h>
|
||||
#include <test/jtx/Account.h>
|
||||
#include <test/jtx/Env.h>
|
||||
#include <test/jtx/TestHelpers.h>
|
||||
#include <test/jtx/amount.h>
|
||||
#include <test/jtx/fee.h>
|
||||
#include <test/jtx/sig.h>
|
||||
#include <test/jtx/vault.h>
|
||||
|
||||
#include <xrpl/basics/Number.h>
|
||||
#include <xrpl/basics/chrono.h>
|
||||
#include <xrpl/beast/unit_test/suite.h>
|
||||
#include <xrpl/beast/utility/Zero.h>
|
||||
#include <xrpl/ledger/helpers/LendingHelpers.h>
|
||||
#include <xrpl/protocol/Asset.h>
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
#include <xrpl/protocol/Keylet.h>
|
||||
#include <xrpl/protocol/Protocol.h>
|
||||
#include <xrpl/protocol/SField.h>
|
||||
#include <xrpl/protocol/STAmount.h>
|
||||
#include <xrpl/protocol/STLedgerEntry.h>
|
||||
#include <xrpl/protocol/SeqProxy.h>
|
||||
#include <xrpl/protocol/TxFlags.h>
|
||||
#include <xrpl/protocol/Units.h>
|
||||
|
||||
#include <cassert>
|
||||
#include <chrono>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
// LoanPay-tier scaffolding on top of LoanFixedPrecisionBase: the
|
||||
// coarsened-vault fixture (repeated late interest pushes a FixedPrecision
|
||||
// vault's live Scale past its base grid) and the helpers built on it. Shared
|
||||
// by LoanPayFixedPrecision and, via LoanManageFixedPrecisionBase, by
|
||||
// LoanManageFixedPrecision.
|
||||
class LoanPayFixedPrecisionBase : public LoanFixedPrecisionBase
|
||||
{
|
||||
protected:
|
||||
// Brings the Keylet overload into scope alongside the CoarsenedVault
|
||||
// overload added below; otherwise the derived overload would hide it.
|
||||
using LoanFixedPrecisionBase::checkVaultLoanSums;
|
||||
using VaultFixedPrecisionBase::expectCoarsened;
|
||||
|
||||
// Coarsens a FixedPrecision vault through paid late interest, which the
|
||||
// LoanSet Open-zone guard does not bound. Deposits near the Open-zone ceiling,
|
||||
// originates a zero-interest loan for most of it, lets the first payment go
|
||||
// very late at the maximum LateInterestRate, and pays it late.
|
||||
//
|
||||
// extraLoans are originated before the coarsening loan goes late, because a
|
||||
// coarsened vault refuses further LoanSet. loanKeylets[0] is the coarsening
|
||||
// loan; loanKeylets[1..] are the extras in order.
|
||||
struct CoarsenedVault
|
||||
{
|
||||
LendingFixture fixture;
|
||||
std::vector<Keylet> loanKeylets;
|
||||
};
|
||||
|
||||
// An extra loan originated before the vault coarsens, on top of the shared
|
||||
// zero-interest / 1-day-interval / 60s-grace defaults.
|
||||
struct ExtraLoan
|
||||
{
|
||||
Number principal;
|
||||
std::uint32_t flags = 0;
|
||||
std::uint32_t paymentTotal = 5;
|
||||
// Override with a long interval to keep the first payment from being overdue
|
||||
// once the clock advances to coarsen the vault.
|
||||
std::uint32_t paymentInterval = 24 * 60 * 60;
|
||||
// Defaults to the shared depositor. A dust-sized loan needs its own borrower
|
||||
// with a small balance: a sub-1e-9 debit from a multi-million balance does
|
||||
// not fit STAmount's 16 digits and rounds away.
|
||||
std::optional<test::jtx::Account> borrower = std::nullopt;
|
||||
// A flat per-payment service fee to the broker owner, giving a terminal
|
||||
// payment a zero vault-credit leg but a non-zero fee leg.
|
||||
Number serviceFee{0};
|
||||
};
|
||||
|
||||
void
|
||||
checkVaultLoanSums(
|
||||
test::jtx::Env& env,
|
||||
CoarsenedVault const& coarsened,
|
||||
std::string const& label = {})
|
||||
{
|
||||
checkVaultLoanSums(env, coarsened.fixture, coarsened.loanKeylets, label);
|
||||
}
|
||||
|
||||
struct CoarsenParams
|
||||
{
|
||||
std::uint8_t scale = 10;
|
||||
Number depositAmount{899'999};
|
||||
Number loanPrincipal{700'000};
|
||||
// The explicit initializer avoids the missing-field-initializers warning when a
|
||||
// designated initializer omits it.
|
||||
std::vector<ExtraLoan> extraLoans = // NOLINT(readability-redundant-member-init)
|
||||
{};
|
||||
TenthBips32 coverRateMinimum = TenthBips32(0);
|
||||
TenthBips32 coverRateLiquidation = TenthBips32(0);
|
||||
Number coverDeposit{0};
|
||||
// 800 days pushes AssetsAvailable itself past its 16-digit budget, so cash
|
||||
// flows floor at a coarser grid. 200 days coarsens only getVaultScale()
|
||||
// (AA + AssetsDeployed), leaving AA's own grid at the base grid.
|
||||
int overdueDays = 800;
|
||||
};
|
||||
|
||||
void
|
||||
expectCoarsened(test::jtx::Env const& env, CoarsenedVault const& coarsened)
|
||||
{
|
||||
expectCoarsened(env, coarsened.fixture.vaultKeylet);
|
||||
}
|
||||
|
||||
// IOU setup for the coarsened-vault scenarios: the depositor holds 10M USD.
|
||||
static AssetAccounts
|
||||
setupCoarsenIou(test::jtx::Env& env, bool clawback = false)
|
||||
{
|
||||
return setupIou(
|
||||
env,
|
||||
{.clawback = clawback, .depositorTrust = 30'000'000, .depositorFunds = 10'000'000});
|
||||
}
|
||||
|
||||
enum class Debit { Withdraw, Clawback };
|
||||
|
||||
// Submits a VaultWithdraw (depositor) or VaultClawback (issuer) of amount and
|
||||
// returns how much AssetsAvailable dropped.
|
||||
Number
|
||||
debitAvailable(
|
||||
test::jtx::Env& env,
|
||||
CoarsenedVault const& coarsened,
|
||||
Debit kind,
|
||||
test::jtx::Account const& issuer,
|
||||
test::jtx::Account const& depositor,
|
||||
PrettyAsset const& asset,
|
||||
Number const& amount)
|
||||
{
|
||||
auto const& vault = coarsened.fixture.vault;
|
||||
auto const& vaultKeylet = coarsened.fixture.vaultKeylet;
|
||||
auto const before = snapshotVault(env, vaultKeylet, asset);
|
||||
if (kind == Debit::Withdraw)
|
||||
{
|
||||
env(vault.withdraw(
|
||||
{.depositor = depositor, .id = vaultKeylet.key, .amount = asset(amount)}));
|
||||
}
|
||||
else
|
||||
{
|
||||
env(vault.clawback({
|
||||
.issuer = issuer,
|
||||
.id = vaultKeylet.key,
|
||||
.holder = depositor,
|
||||
.amount = asset(amount).value(),
|
||||
}));
|
||||
}
|
||||
env.close();
|
||||
return before.available - snapshotVault(env, vaultKeylet, asset).available;
|
||||
}
|
||||
|
||||
CoarsenedVault
|
||||
coarsenVault(
|
||||
test::jtx::Env& env,
|
||||
test::jtx::Account const& owner,
|
||||
test::jtx::Account const& depositor,
|
||||
PrettyAsset const& asset)
|
||||
{
|
||||
return coarsenVault(env, owner, depositor, asset, CoarsenParams{});
|
||||
}
|
||||
|
||||
CoarsenedVault
|
||||
coarsenVault(
|
||||
test::jtx::Env& env,
|
||||
test::jtx::Account const& owner,
|
||||
test::jtx::Account const& depositor,
|
||||
PrettyAsset const& asset,
|
||||
CoarsenParams const& params)
|
||||
{
|
||||
using namespace test::jtx;
|
||||
using namespace loan;
|
||||
using namespace loan_broker;
|
||||
using namespace lending;
|
||||
|
||||
// A 15-year investment window, so extra loans with long payment intervals
|
||||
// still fit before RedemptionDate.
|
||||
std::chrono::seconds const investmentWindow{15 * 365 * 24 * 60 * 60};
|
||||
auto fixture = setupLendingVault(
|
||||
env,
|
||||
owner,
|
||||
depositor,
|
||||
asset,
|
||||
params.depositAmount,
|
||||
investmentWindow,
|
||||
params.scale,
|
||||
params.coverRateMinimum,
|
||||
params.coverRateLiquidation);
|
||||
|
||||
if (params.coverDeposit != beast::kZero)
|
||||
{
|
||||
env(coverDeposit(owner, fixture.brokerKeylet.key, asset(params.coverDeposit).value()));
|
||||
env.close();
|
||||
}
|
||||
|
||||
std::uint32_t const gracePeriod = 60;
|
||||
|
||||
std::vector<Keylet> loanKeylets;
|
||||
std::vector<ExtraLoan> allLoans{ExtraLoan{.principal = params.loanPrincipal}};
|
||||
allLoans.insert(allLoans.end(), params.extraLoans.begin(), params.extraLoans.end());
|
||||
for (std::size_t i = 0; i < allLoans.size(); ++i)
|
||||
{
|
||||
auto const loanKeylet =
|
||||
keylet::loan(fixture.brokerKeylet.key, SeqProxy::rawSequence(i + 1));
|
||||
auto const& loan = allLoans[i];
|
||||
auto const submit = [&](auto&&... extra) {
|
||||
env(set(loan.borrower.value_or(depositor),
|
||||
fixture.brokerKeylet.key,
|
||||
loan.principal,
|
||||
loan.flags),
|
||||
kInterestRate(TenthBips32(0)),
|
||||
kLoanServiceFee(loan.serviceFee),
|
||||
kGracePeriod(gracePeriod),
|
||||
kPaymentInterval(loan.paymentInterval),
|
||||
kPaymentTotal(loan.paymentTotal),
|
||||
Sig(sfCounterpartySignature, owner),
|
||||
Fee(env.current()->fees().base * 2),
|
||||
extra...);
|
||||
};
|
||||
if (i == 0)
|
||||
{
|
||||
// Only the coarsening loan needs a non-zero LateInterestRate.
|
||||
submit(kLateInterestRate(kMaxLateInterestRate));
|
||||
}
|
||||
else
|
||||
{
|
||||
submit();
|
||||
}
|
||||
env.close();
|
||||
loanKeylets.push_back(loanKeylet);
|
||||
}
|
||||
|
||||
auto const loanSle = env.le(loanKeylets[0]);
|
||||
BEAST_EXPECT(loanSle);
|
||||
std::uint32_t const dueDate = loanSle ? loanSle->at(sfNextPaymentDueDate) : 0;
|
||||
|
||||
// Leave the first payment overdue long enough that penalty interest pushes
|
||||
// AssetsAvailable itself, not just AA + AssetsDeployed, past its 16-digit budget at
|
||||
// the base scale; only then do cash credits floor.
|
||||
env.close(
|
||||
NetClock::time_point{NetClock::duration{dueDate + gracePeriod}} +
|
||||
std::chrono::seconds{params.overdueDays * 24 * 60 * 60});
|
||||
|
||||
// Generously covers the coarsening loan's principal plus accrued late
|
||||
// interest.
|
||||
Number const latePaymentAmount = params.loanPrincipal * 10;
|
||||
env(pay(
|
||||
depositor, loanKeylets[0].key, asset(latePaymentAmount).value(), tfLoanLatePayment));
|
||||
env.close();
|
||||
|
||||
return {.fixture = fixture, .loanKeylets = loanKeylets};
|
||||
}
|
||||
|
||||
// Reads an integral-asset vault Number field (AssetsAvailable, AssetsDeployed,
|
||||
// LossUnrealized, ... are STNumber fields, not plain UInt64 SFields) as a
|
||||
// std::uint64_t for plain integer arithmetic and search loops. Number
|
||||
// itself does not normalize to the asset's own zero exponent, so the value
|
||||
// is round-tripped through an STAmount of the vault's (integral) asset
|
||||
// first, matching how STAmount::mpt()/xrp() extract an exact int64.
|
||||
template <class T>
|
||||
static std::uint64_t
|
||||
toU64(SLE::ConstRef vault, TypedField<T> const& field)
|
||||
{
|
||||
Asset const asset = vault->at(sfAsset);
|
||||
STAmount const amount{asset, Number(vault->at(field))};
|
||||
assert(amount.exponent() == 0);
|
||||
return static_cast<std::uint64_t>(amount.mantissa());
|
||||
}
|
||||
|
||||
// Doubles AssetsAvailable roughly cycles times via an origination/
|
||||
// repayment loop: originate a single-payment, kMaxInterestRate (100%),
|
||||
// one-year loan for the vault's entire current AssetsAvailable, then pay
|
||||
// it off in full at the due date. AssetsDeployed returns to zero after every
|
||||
// cycle, and the depositor's share count is never touched, so this grows
|
||||
// the per-share NAV purely through externally-funded interest -- the
|
||||
// "originate, repay, and delete...loans that grow the vault" step shared
|
||||
// by the sole-holder clawback and final-withdrawal scenarios below. The
|
||||
// depositor needs enough of their own funds to cover the interest leg of
|
||||
// every cycle (the principal itself round-trips: it is lent back out,
|
||||
// then repaid).
|
||||
void
|
||||
compoundVaultBySoleHolderLoanCycles(
|
||||
test::jtx::Env& env,
|
||||
LendingFixture const& fixture,
|
||||
test::jtx::Account const& depositor,
|
||||
PrettyAsset const& asset,
|
||||
int cycles)
|
||||
{
|
||||
using namespace test::jtx;
|
||||
using namespace loan;
|
||||
|
||||
for (int i = 0; i < cycles; ++i)
|
||||
{
|
||||
auto const vaultSle = env.le(fixture.vaultKeylet);
|
||||
if (!BEAST_EXPECT(vaultSle))
|
||||
return;
|
||||
Number const principal = vaultSle->at(sfAssetsAvailable);
|
||||
if (!BEAST_EXPECT(principal > beast::kZero))
|
||||
return;
|
||||
|
||||
std::uint32_t const oneYearInterval = 365 * 24 * 60 * 60;
|
||||
auto const loanKeylet = openLoan(
|
||||
env,
|
||||
fixture,
|
||||
principal,
|
||||
/* paymentTotal */ 1,
|
||||
percentageToTenthBips(100),
|
||||
/* flags */ 0,
|
||||
oneYearInterval);
|
||||
|
||||
auto const loanSle = env.le(loanKeylet);
|
||||
if (!BEAST_EXPECT(loanSle))
|
||||
return;
|
||||
std::uint32_t const dueDate = loanSle->at(sfNextPaymentDueDate);
|
||||
env.close(NetClock::time_point{NetClock::duration{dueDate}} + std::chrono::seconds{1});
|
||||
|
||||
Number const periodicPayment = loanSle->at(sfPeriodicPayment);
|
||||
std::int32_t const loanScale = loanSle->at(sfLoanScale);
|
||||
Number const paymentDue = roundPeriodicPayment(asset, periodicPayment, loanScale);
|
||||
|
||||
// The clock is advanced to just past the due date, so the payment
|
||||
// must be flagged late (LateInterestRate is 0 by default, so this
|
||||
// adds no penalty -- it only satisfies the overdue check in
|
||||
// LendingHelpers' loanMakePayment).
|
||||
env(pay(depositor, loanKeylet.key, asset(paymentDue).value(), tfLoanLatePayment));
|
||||
env.close();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace xrpl
|
||||
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
@@ -452,8 +452,7 @@ private:
|
||||
// Under featureLendingProtocolV1_1 LoanBrokerSet::preclaim only
|
||||
// accepts closed-ended vaults, so build one and advance past
|
||||
// SubscriptionDate before creating the broker and the loan.
|
||||
// Temporary: FixedPrecision lending is incomplete until the LoanManage PR.
|
||||
Env env(*this, testableAmendments() - featureLendingProtocolV1_2);
|
||||
Env env(*this);
|
||||
Vault const vault(env);
|
||||
|
||||
env.fund(XRP(10'000), lender, issuer, borrower, depositor);
|
||||
|
||||
@@ -817,10 +817,7 @@ private:
|
||||
// `all_`), so callers must opt in. Closed-ended vaults are gated on this amendment; without
|
||||
// it VaultCreate returns temDISABLED and every follow-on txn sees tecNO_ENTRY.
|
||||
auto const withEnv = [&, this](auto&& body) {
|
||||
// Temporary: FixedPrecision lending is incomplete until the LoanManage PR.
|
||||
Env env(
|
||||
*this,
|
||||
(testableAmendments() | featureLendingProtocolV1_1) - featureLendingProtocolV1_2);
|
||||
Env env(*this, testableAmendments() | featureLendingProtocolV1_1);
|
||||
env.fund(XRP(1'000'000'000), issuer, lender, borrower);
|
||||
env.close();
|
||||
PrettyAsset const asset{xrpIssue(), 1'000'000};
|
||||
|
||||
@@ -195,10 +195,7 @@ protected:
|
||||
// most of this file's tests assert instant-interest-recognition-specific expected values
|
||||
// for those fields. Tests that specifically exercise the amendment opt
|
||||
// it back in explicitly (e.g. `all_ | featureLendingProtocolV1_1`).
|
||||
// featureLendingProtocolV1_2 is also excluded: it changes vault precision.
|
||||
FeatureBitset const all_{
|
||||
jtx::testableAmendments() - featureLendingProtocolV1_1 - featureLendingProtocolV1_2,
|
||||
};
|
||||
FeatureBitset const all_{jtx::testableAmendments() - featureLendingProtocolV1_1};
|
||||
std::string const iouCurrency_{"IOU"};
|
||||
|
||||
struct BrokerParameters
|
||||
|
||||
@@ -105,8 +105,7 @@ private:
|
||||
auto const iou = issuer["IOU"];
|
||||
|
||||
auto const testWrapper = [&](auto&& test) {
|
||||
// Temporary: FixedPrecision lending is incomplete until the LoanManage PR.
|
||||
Env env(*this, testableAmendments() - featureLendingProtocolV1_2);
|
||||
Env env(*this);
|
||||
env.fund(XRP(1'000), lender, issuer, borrower, sponsor);
|
||||
env(trust(lender, iou(10'000'000)));
|
||||
env(pay(issuer, lender, iou(5'000'000)));
|
||||
@@ -291,8 +290,7 @@ private:
|
||||
auto const iou = issuer["IOU"];
|
||||
|
||||
// preclaim
|
||||
// Temporary: FixedPrecision lending is incomplete until the LoanManage PR.
|
||||
Env env(*this, testableAmendments() - featureLendingProtocolV1_2);
|
||||
Env env(*this);
|
||||
env.fund(XRP(1'000), lender, issuer, borrower);
|
||||
env(trust(lender, iou(10'000'000)));
|
||||
env(pay(issuer, lender, iou(5'000'000)));
|
||||
@@ -373,8 +371,7 @@ private:
|
||||
Account const lender{"lender"};
|
||||
Account const issuer{"issuer"};
|
||||
Account const borrower{"borrower"};
|
||||
// Temporary: FixedPrecision lending is incomplete until the LoanManage PR.
|
||||
Env env(*this, testableAmendments() - featureLendingProtocolV1_2);
|
||||
Env env(*this);
|
||||
|
||||
env.fund(XRP(100'000), issuer, lender, borrower);
|
||||
env.close();
|
||||
|
||||
@@ -0,0 +1,311 @@
|
||||
#include <test/app/lending/LoanPayFixedPrecisionBase.h>
|
||||
#include <test/jtx/Env.h>
|
||||
#include <test/jtx/amount.h>
|
||||
#include <test/jtx/ter.h>
|
||||
#include <test/jtx/vault.h>
|
||||
|
||||
#include <xrpl/basics/Number.h>
|
||||
#include <xrpl/basics/chrono.h>
|
||||
#include <xrpl/beast/unit_test/suite.h>
|
||||
#include <xrpl/beast/utility/Zero.h>
|
||||
#include <xrpl/ledger/helpers/VaultHelpers.h>
|
||||
#include <xrpl/protocol/Keylet.h>
|
||||
#include <xrpl/protocol/STAmount.h>
|
||||
#include <xrpl/protocol/TER.h>
|
||||
|
||||
#include <chrono>
|
||||
#include <string>
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
// Vault transactions (withdraw, clawback, deposit, ValidVault) on a
|
||||
// FixedPrecision Vault that LoanPay late interest has coarsened past its base
|
||||
// grid. The coarsening needs FixedPrecision LoanPay, so these live with it.
|
||||
class VaultCoarsenedFixedPrecision_test : public LoanPayFixedPrecisionBase
|
||||
{
|
||||
void
|
||||
testCoarsenedVaultWithdrawClawbackKeepAvailableGridDigit()
|
||||
{
|
||||
using namespace test::jtx;
|
||||
|
||||
testcase(
|
||||
"Lending: VaultWithdraw and VaultClawback of an amount with a digit on AA's grid "
|
||||
"but not AT's succeed exactly");
|
||||
|
||||
Env env(*this, features());
|
||||
auto const [issuer, owner, depositor, asset] = setupCoarsenIou(env, true);
|
||||
|
||||
auto const coarsened = coarsenVault(env, owner, depositor, asset, {.overdueDays = 200});
|
||||
auto const& vaultKeylet = coarsened.fixture.vaultKeylet;
|
||||
expectCoarsened(env, coarsened);
|
||||
|
||||
// Withdrawals need the Redemption phase.
|
||||
env.close(coarsened.fixture.redemptionDate + std::chrono::seconds{1});
|
||||
|
||||
struct Row
|
||||
{
|
||||
Debit kind = Debit::Withdraw;
|
||||
char const* label = nullptr;
|
||||
// Off-grid at AA's base grid (1e-10).
|
||||
Number amount;
|
||||
};
|
||||
Row const rows[] = {
|
||||
{.kind = Debit::Withdraw, .label = "withdraw", .amount = Number{123'0000000007LL, -10}},
|
||||
{.kind = Debit::Clawback, .label = "clawback", .amount = Number{111'0000000003LL, -10}},
|
||||
};
|
||||
for (auto const& row : rows)
|
||||
{
|
||||
Number const delta =
|
||||
debitAvailable(env, coarsened, row.kind, issuer, depositor, asset, row.amount);
|
||||
// The share round-trip can trim the delta, but it must keep AA's finer grid
|
||||
// precision, not collapse to the AssetsTotal grid (-9).
|
||||
BEAST_EXPECT(delta > beast::kZero);
|
||||
BEAST_EXPECT(delta <= row.amount);
|
||||
BEAST_EXPECT(delta != roundToAsset(asset, delta, -9, Number::RoundingMode::Downward));
|
||||
auto const after = snapshotVault(env, vaultKeylet, asset);
|
||||
BEAST_EXPECT(after.available <= after.total);
|
||||
|
||||
checkVaultLoanSums(
|
||||
env,
|
||||
coarsened,
|
||||
std::string{"testCoarsenedVaultWithdrawClawbackKeepAvailableGridDigit ("} +
|
||||
row.label + ")");
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testCoarsenedVaultSubUnitWithdrawClawbackSucceeds()
|
||||
{
|
||||
using namespace test::jtx;
|
||||
|
||||
testcase(
|
||||
"Lending: VaultWithdraw and VaultClawback smaller than half a unit of the "
|
||||
"AssetsTotal cache's grid still succeed");
|
||||
|
||||
Env env(*this, features());
|
||||
auto const [issuer, owner, depositor, asset] = setupCoarsenIou(env, true);
|
||||
|
||||
auto const coarsened = coarsenVault(env, owner, depositor, asset, {.overdueDays = 200});
|
||||
auto const vaultSle0 = env.le(coarsened.fixture.vaultKeylet);
|
||||
if (!BEAST_EXPECT(vaultSle0))
|
||||
return;
|
||||
// The AssetsTotal cache grid (-9) is coarser than the base grid, and AA
|
||||
// itself still sits on the base grid (-10).
|
||||
BEAST_EXPECT(getVaultScale(vaultSle0) == -9);
|
||||
BEAST_EXPECT(getVaultBaseScale(vaultSle0) == -10);
|
||||
|
||||
// Withdrawals need the Redemption phase.
|
||||
env.close(coarsened.fixture.redemptionDate + std::chrono::seconds{1});
|
||||
|
||||
struct Row
|
||||
{
|
||||
Debit kind = Debit::Withdraw;
|
||||
char const* label = nullptr;
|
||||
// On AA's base grid (-10) but under half a unit of AssetsTotal's grid (5e-10).
|
||||
Number amount;
|
||||
};
|
||||
Row const rows[] = {
|
||||
{.kind = Debit::Withdraw, .label = "withdraw", .amount = Number{2, -10}},
|
||||
{.kind = Debit::Clawback, .label = "clawback", .amount = Number{3, -10}},
|
||||
};
|
||||
for (auto const& row : rows)
|
||||
{
|
||||
// Rounding Downward at -9 gives exactly zero.
|
||||
BEAST_EXPECT(
|
||||
roundToAsset(asset, row.amount, -9, Number::RoundingMode::Downward) ==
|
||||
beast::kZero);
|
||||
|
||||
Number const delta =
|
||||
debitAvailable(env, coarsened, row.kind, issuer, depositor, asset, row.amount);
|
||||
// The delta may be trimmed by the share round-trip. It must stay strictly
|
||||
// positive and sub-(AT-grid); the bug rejected it outright.
|
||||
BEAST_EXPECT(delta > beast::kZero);
|
||||
BEAST_EXPECT(delta <= row.amount);
|
||||
BEAST_EXPECT(
|
||||
roundToAsset(asset, delta, -9, Number::RoundingMode::Downward) == beast::kZero);
|
||||
|
||||
checkVaultLoanSums(
|
||||
env,
|
||||
coarsened,
|
||||
std::string{"testCoarsenedVaultSubUnitWithdrawClawbackSucceeds ("} + row.label +
|
||||
")");
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testCoarsenedVaultDebitCrossesPowerOfTenDownward()
|
||||
{
|
||||
using namespace test::jtx;
|
||||
|
||||
testcase(
|
||||
"Lending: VaultWithdraw and VaultClawback debits that cross a power of ten keep "
|
||||
"AssetsAvailable at 16 digits");
|
||||
|
||||
Env env(*this, features());
|
||||
auto const [issuer, owner, depositor, asset] = setupCoarsenIou(env, true);
|
||||
|
||||
auto const coarsened = coarsenVault(env, owner, depositor, asset, {.overdueDays = 200});
|
||||
auto const& vaultKeylet = coarsened.fixture.vaultKeylet;
|
||||
expectCoarsened(env, coarsened);
|
||||
|
||||
// Withdrawals need the Redemption phase.
|
||||
env.close(coarsened.fixture.redemptionDate + std::chrono::seconds{1});
|
||||
|
||||
// A margin, since the share round-trip can trim the delta short of the
|
||||
// requested amount.
|
||||
Number const margin{100};
|
||||
|
||||
// The clawback runs on the post-withdraw vault, crossing a fresh boundary.
|
||||
struct Row
|
||||
{
|
||||
Debit kind = Debit::Withdraw;
|
||||
char const* label = nullptr;
|
||||
};
|
||||
Row const rows[] = {
|
||||
{.kind = Debit::Withdraw, .label = "withdraw"},
|
||||
{.kind = Debit::Clawback, .label = "clawback"},
|
||||
};
|
||||
for (auto const& row : rows)
|
||||
{
|
||||
Number const availableBefore = snapshotVault(env, vaultKeylet, asset).available;
|
||||
|
||||
// The largest power of ten below AssetsAvailable; crossing it downward drops
|
||||
// a digit from the integer part.
|
||||
Number const powerOfTen = largestPowerOfTenAtMost(availableBefore);
|
||||
|
||||
// One transaction spans the whole gap; the AssetsAvailable-delta check is
|
||||
// exact against the real transfer.
|
||||
Number const amount = availableBefore - powerOfTen + margin;
|
||||
BEAST_EXPECT(amount > beast::kZero);
|
||||
BEAST_EXPECT(amount < availableBefore);
|
||||
|
||||
debitAvailable(env, coarsened, row.kind, issuer, depositor, asset, amount);
|
||||
|
||||
BEAST_EXPECT(expectAvailableAtSixteenDigits(env, vaultKeylet, asset) < powerOfTen);
|
||||
|
||||
checkVaultLoanSums(
|
||||
env,
|
||||
coarsened,
|
||||
std::string{"testCoarsenedVaultDebitCrossesPowerOfTenDownward ("} + row.label +
|
||||
")");
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testValidVaultLargeSingleDebitAcrossAssetsTotalGridDoesNotFalsePositive()
|
||||
{
|
||||
using namespace test::jtx;
|
||||
|
||||
testcase(
|
||||
"Lending: a single large VaultWithdraw/VaultClawback across the AssetsTotal grid "
|
||||
"boundary succeeds (ValidVault false-positive regression)");
|
||||
|
||||
Number const margin{1'000};
|
||||
Number const powerOfTen{100'000};
|
||||
|
||||
struct Row
|
||||
{
|
||||
Debit kind = Debit::Withdraw;
|
||||
char const* label = nullptr;
|
||||
};
|
||||
// Each debit runs on a fresh vault so AssetsTotal starts above its boundary
|
||||
// (after a shrink it would be dominated by AssetsDeployed).
|
||||
Row const rows[] = {
|
||||
{.kind = Debit::Withdraw, .label = "withdraw"},
|
||||
{.kind = Debit::Clawback, .label = "clawback"},
|
||||
};
|
||||
for (auto const& row : rows)
|
||||
{
|
||||
Env env(*this, features());
|
||||
auto const [issuer, owner, depositor, asset] = setupCoarsenIou(env, true);
|
||||
|
||||
// Setup from the original report: AssetsAvailable 723562.1976154236,
|
||||
// AssetsDeployed 560000.
|
||||
auto const coarsened = coarsenVault(env, owner, depositor, asset, {.overdueDays = 200});
|
||||
auto const& vaultKeylet = coarsened.fixture.vaultKeylet;
|
||||
expectCoarsened(env, coarsened);
|
||||
|
||||
env.close(coarsened.fixture.redemptionDate + std::chrono::seconds{1});
|
||||
|
||||
auto const before = snapshotVault(env, vaultKeylet, asset);
|
||||
// The stored AssetsTotal crosses its own power-of-ten boundary, not
|
||||
// necessarily AssetsAvailable's, since AssetsDeployed adds magnitude.
|
||||
Number const powerOfTenAT = largestPowerOfTenAtMost(before.total);
|
||||
BEAST_EXPECT(before.total >= powerOfTenAT);
|
||||
|
||||
// One debit takes AssetsAvailable below its boundary and carries AssetsTotal
|
||||
// across its own boundary in the same transaction.
|
||||
debitAvailable(
|
||||
env,
|
||||
coarsened,
|
||||
row.kind,
|
||||
issuer,
|
||||
depositor,
|
||||
asset,
|
||||
before.available - powerOfTen + margin);
|
||||
|
||||
BEAST_EXPECT(snapshotVault(env, vaultKeylet, asset).total < powerOfTenAT);
|
||||
checkVaultLoanSums(
|
||||
env,
|
||||
coarsened,
|
||||
std::string{
|
||||
"testValidVaultLargeSingleDebitAcrossAssetsTotalGridDoesNotFalsePositive ("} +
|
||||
row.label + ")");
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testDepositRefusedOnDebtCoarsenedVault()
|
||||
{
|
||||
using namespace test::jtx;
|
||||
|
||||
testcase(
|
||||
"Lending: VaultDeposit is refused on a vault coarsened via AssetsDeployed while "
|
||||
"AssetsAvailable is still uncoarsened -- always via the Investment-phase gate, "
|
||||
"not the precision-loss check");
|
||||
|
||||
Env env(*this, features());
|
||||
auto const [issuer, owner, depositor, asset] = setupCoarsenIou(env);
|
||||
|
||||
auto const coarsened = coarsenVault(env, owner, depositor, asset, {.overdueDays = 200});
|
||||
auto const& vaultKeylet = coarsened.fixture.vaultKeylet;
|
||||
|
||||
auto const vaultSle = env.le(vaultKeylet);
|
||||
if (!BEAST_EXPECT(vaultSle))
|
||||
return;
|
||||
BEAST_EXPECT(getVaultScale(vaultSle) == -9);
|
||||
BEAST_EXPECT(getVaultBaseScale(vaultSle) == -10);
|
||||
|
||||
// Off-grid at the coarsened live scale (-9) but on AA's base grid (-10).
|
||||
Number const depositAmount{2, -10};
|
||||
BEAST_EXPECT(
|
||||
roundToAsset(asset, depositAmount, -9, Number::RoundingMode::Downward) == beast::kZero);
|
||||
|
||||
// An AssetsDeployed-coarsened vault always has a LoanBroker, so it is closed-ended
|
||||
// and in the Investment phase while debt is outstanding. VaultDeposit refuses
|
||||
// there before the precision-loss check, so this amount fails with tecEXPIRED,
|
||||
// not tecPRECISION_LOSS. See the VaultBalance.* gtest cases for the bare-SLE
|
||||
// precision check.
|
||||
env(coarsened.fixture.vault.deposit(
|
||||
{.depositor = depositor, .id = vaultKeylet.key, .amount = asset(depositAmount)}),
|
||||
Ter(tecEXPIRED));
|
||||
env.close();
|
||||
|
||||
checkVaultLoanSums(env, coarsened, "testDepositRefusedOnDebtCoarsenedVault");
|
||||
}
|
||||
|
||||
public:
|
||||
void
|
||||
run() override
|
||||
{
|
||||
testCoarsenedVaultWithdrawClawbackKeepAvailableGridDigit();
|
||||
testCoarsenedVaultSubUnitWithdrawClawbackSucceeds();
|
||||
testCoarsenedVaultDebitCrossesPowerOfTenDownward();
|
||||
testValidVaultLargeSingleDebitAcrossAssetsTotalGridDoesNotFalsePositive();
|
||||
testDepositRefusedOnDebtCoarsenedVault();
|
||||
}
|
||||
};
|
||||
|
||||
BEAST_DEFINE_TESTSUITE(VaultCoarsenedFixedPrecision, app, xrpl);
|
||||
|
||||
} // namespace xrpl
|
||||
@@ -50,6 +50,12 @@ namespace xrpl {
|
||||
|
||||
class VaultBugs_test : public VaultTestBase
|
||||
{
|
||||
// The IOU precision-boundary bugs in this suite probe the STAmount
|
||||
// 16-digit mantissa cliff (~1e16). FixedPrecision's Open-zone cap
|
||||
// (9e(15-Scale)) makes that value unreachable at any Scale, so these
|
||||
// scenarios cannot be reproduced under V1.2 by construction.
|
||||
FeatureBitset const cashBasisAll_{all_ - featureLendingProtocolV1_2};
|
||||
|
||||
private:
|
||||
// Bug: the equality check (vault outflow == destination inflow) was
|
||||
// skipped whenever the destination delta rounded to zero at localMinScale,
|
||||
@@ -127,13 +133,13 @@ private:
|
||||
testcase(
|
||||
"bug: VaultWithdraw to destination at IOU precision boundary fires "
|
||||
"invariant (pre-fixCleanup3_2_0)");
|
||||
runScenario(all_ - fixCleanup3_2_0, tecINVARIANT_FAILED);
|
||||
runScenario(cashBasisAll_ - fixCleanup3_2_0, tecINVARIANT_FAILED);
|
||||
}
|
||||
{
|
||||
testcase(
|
||||
"bug: VaultWithdraw to destination at IOU precision boundary succeeds "
|
||||
"when destroyed amount is sub-ULP (post-fixCleanup3_2_0)");
|
||||
runScenario(all_, tesSUCCESS);
|
||||
runScenario(cashBasisAll_, tesSUCCESS);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -192,13 +198,13 @@ private:
|
||||
testcase(
|
||||
"bug: VaultDeposit by issuer at IOU edge fires "
|
||||
"tecINVARIANT_FAILED at finalize (pre-fixCleanup3_2_0)");
|
||||
runScenario(all_ - fixCleanup3_2_0, tecINVARIANT_FAILED);
|
||||
runScenario(cashBasisAll_ - fixCleanup3_2_0, tecINVARIANT_FAILED);
|
||||
}
|
||||
{
|
||||
testcase(
|
||||
"bug: VaultDeposit by issuer at IOU edge rejects with "
|
||||
"tecPRECISION_LOSS proactively (post-fixCleanup3_2_0)");
|
||||
runScenario(all_, tecPRECISION_LOSS);
|
||||
runScenario(cashBasisAll_, tecPRECISION_LOSS);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -271,13 +277,13 @@ private:
|
||||
testcase(
|
||||
"bug: VaultDeposit across IOU scale boundary fires invariant "
|
||||
"(pre-fixCleanup3_2_0)");
|
||||
runScenario(all_ - fixCleanup3_2_0, tecINVARIANT_FAILED);
|
||||
runScenario(cashBasisAll_ - fixCleanup3_2_0, tecINVARIANT_FAILED);
|
||||
}
|
||||
{
|
||||
testcase(
|
||||
"bug: VaultDeposit across IOU scale boundary succeeds "
|
||||
"(post-fixCleanup3_2_0)");
|
||||
runScenario(all_, tecPRECISION_LOSS);
|
||||
runScenario(cashBasisAll_, tecPRECISION_LOSS);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -344,13 +350,13 @@ private:
|
||||
testcase(
|
||||
"bug: VaultWithdraw across IOU scale boundary fires invariant "
|
||||
"(pre-fixCleanup3_2_0)");
|
||||
runScenario(all_ - fixCleanup3_2_0, tecINVARIANT_FAILED);
|
||||
runScenario(cashBasisAll_ - fixCleanup3_2_0, tecINVARIANT_FAILED);
|
||||
}
|
||||
{
|
||||
testcase(
|
||||
"bug: VaultWithdraw across IOU scale boundary succeeds "
|
||||
"(post-fixCleanup3_2_0)");
|
||||
runScenario(all_, tesSUCCESS);
|
||||
runScenario(cashBasisAll_, tesSUCCESS);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -436,13 +442,13 @@ private:
|
||||
// Also remove fixCleanup3_4_0 so the VaultDeposit clamp
|
||||
// introduced by that amendment does not short-circuit this
|
||||
// pre-fixCleanup3_2_0 scenario with tecPRECISION_LOSS.
|
||||
runScenario(all_ - fixCleanup3_2_0 - fixCleanup3_4_0, tecINVARIANT_FAILED);
|
||||
runScenario(cashBasisAll_ - fixCleanup3_2_0 - fixCleanup3_4_0, tecINVARIANT_FAILED);
|
||||
}
|
||||
{
|
||||
testcase(
|
||||
"bug: VaultDeposit below Vault precision canonicalized to zero "
|
||||
"(post-fixCleanup3_2_0)");
|
||||
runScenario(all_, tecPRECISION_LOSS);
|
||||
runScenario(cashBasisAll_, tecPRECISION_LOSS);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -580,7 +586,7 @@ private:
|
||||
// pattern that only makes sense on open-ended vaults. The gate
|
||||
// added by LP V1.1 is unrelated to the truncation bug asserted
|
||||
// here.
|
||||
auto const legacy = all_ - featureLendingProtocolV1_1;
|
||||
auto const legacy = cashBasisAll_ - featureLendingProtocolV1_1;
|
||||
{
|
||||
testcase(
|
||||
"bug: VaultDeposit share truncation lets depositor debit "
|
||||
@@ -696,25 +702,25 @@ private:
|
||||
testcase(
|
||||
"bug: VaultWithdraw to third-party at IOU edge fires invariant "
|
||||
"(pre-fixCleanup3_2_0)");
|
||||
runScenario(all_ - fixCleanup3_2_0, DestKind::ThirdParty, tecINVARIANT_FAILED);
|
||||
runScenario(cashBasisAll_ - fixCleanup3_2_0, DestKind::ThirdParty, tecINVARIANT_FAILED);
|
||||
}
|
||||
{
|
||||
testcase(
|
||||
"bug: VaultWithdraw to third-party at IOU edge succeeds "
|
||||
"(post-fixCleanup3_2_0)");
|
||||
runScenario(all_, DestKind::ThirdParty, tesSUCCESS);
|
||||
runScenario(cashBasisAll_, DestKind::ThirdParty, tesSUCCESS);
|
||||
}
|
||||
{
|
||||
testcase(
|
||||
"bug: VaultWithdraw to self at IOU edge fires invariant "
|
||||
"(pre-fixCleanup3_2_0)");
|
||||
runScenario(all_ - fixCleanup3_2_0, DestKind::Self, tecINVARIANT_FAILED);
|
||||
runScenario(cashBasisAll_ - fixCleanup3_2_0, DestKind::Self, tecINVARIANT_FAILED);
|
||||
}
|
||||
{
|
||||
testcase(
|
||||
"bug: VaultWithdraw to self at IOU edge succeeds "
|
||||
"(post-fixCleanup3_2_0)");
|
||||
runScenario(all_, DestKind::Self, tesSUCCESS);
|
||||
runScenario(cashBasisAll_, DestKind::Self, tesSUCCESS);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -786,9 +792,9 @@ private:
|
||||
};
|
||||
|
||||
testcase("bug: VaultClawback dust debit fires invariant (pre-fixCleanup3_4_0)");
|
||||
runScenario(all_ - fixCleanup3_4_0, tecINVARIANT_FAILED);
|
||||
runScenario(cashBasisAll_ - fixCleanup3_4_0, tecINVARIANT_FAILED);
|
||||
testcase("bug: VaultClawback dust debit rejected cleanly (post-fixCleanup3_4_0)");
|
||||
runScenario(all_, tecPRECISION_LOSS);
|
||||
runScenario(cashBasisAll_, tecPRECISION_LOSS);
|
||||
}
|
||||
|
||||
{
|
||||
@@ -810,9 +816,9 @@ private:
|
||||
};
|
||||
|
||||
testcase("bug: VaultWithdraw dust debit fires invariant (pre-fixCleanup3_4_0)");
|
||||
runScenario(all_ - fixCleanup3_4_0, tecINVARIANT_FAILED);
|
||||
runScenario(cashBasisAll_ - fixCleanup3_4_0, tecINVARIANT_FAILED);
|
||||
testcase("bug: VaultWithdraw dust debit rejected cleanly (post-fixCleanup3_4_0)");
|
||||
runScenario(all_, tecPRECISION_LOSS);
|
||||
runScenario(cashBasisAll_, tecPRECISION_LOSS);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -878,12 +884,12 @@ private:
|
||||
testcase(
|
||||
"bug: VaultDeposit overcredits across an IOU scale boundary "
|
||||
"(pre-fixCleanup3_4_0)");
|
||||
runScenario(all_ - fixCleanup3_2_0 - fixCleanup3_4_0, true);
|
||||
runScenario(cashBasisAll_ - fixCleanup3_2_0 - fixCleanup3_4_0, true);
|
||||
|
||||
testcase(
|
||||
"bug: VaultDeposit no longer overcredits across an IOU scale boundary "
|
||||
"(post-fixCleanup3_4_0)");
|
||||
runScenario(all_, false);
|
||||
runScenario(cashBasisAll_, false);
|
||||
}
|
||||
|
||||
// 1e17 IOU at scale 0. Withdraw all-but-one, then the last share:
|
||||
@@ -931,11 +937,11 @@ private:
|
||||
testcase(
|
||||
"bug: VaultWithdraw permanently locks a large IOU vault "
|
||||
"(pre-fixCleanup3_4_0)");
|
||||
runScenario(all_ - fixCleanup3_4_0, tecINVARIANT_FAILED);
|
||||
runScenario(cashBasisAll_ - fixCleanup3_4_0, tecINVARIANT_FAILED);
|
||||
testcase(
|
||||
"bug: VaultWithdraw no longer locks a large IOU vault "
|
||||
"(post-fixCleanup3_4_0)");
|
||||
runScenario(all_, tesSUCCESS);
|
||||
runScenario(cashBasisAll_, tesSUCCESS);
|
||||
}
|
||||
|
||||
// VaultDeposit::preclaim uses accountHolds(..., SpendableHandling::
|
||||
@@ -1006,14 +1012,14 @@ private:
|
||||
"IOU vault deposit exceeding depositor's balance but "
|
||||
"within counterparty's trust limit, pre-fixCleanup3_2_0 "
|
||||
"(tefINTERNAL)");
|
||||
runTest(all_ - fixCleanup3_2_0, tefINTERNAL);
|
||||
runTest(cashBasisAll_ - fixCleanup3_2_0, tefINTERNAL);
|
||||
}
|
||||
{
|
||||
testcase(
|
||||
"IOU vault deposit exceeding depositor's balance but "
|
||||
"within counterparty's trust limit, post-fixCleanup3_2_0 "
|
||||
"(tesSUCCESS)");
|
||||
runTest(all_, tesSUCCESS);
|
||||
runTest(cashBasisAll_, tesSUCCESS);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1025,7 +1031,7 @@ private:
|
||||
using namespace test::jtx;
|
||||
testcase("Bug6 - limit bypass with share-denominated withdrawal");
|
||||
|
||||
auto const allAmendments = all_ | featureSingleAssetVault;
|
||||
auto const allAmendments = cashBasisAll_ | featureSingleAssetVault;
|
||||
|
||||
for (auto const& features : {allAmendments, allAmendments - fixCleanup3_1_3})
|
||||
{
|
||||
@@ -1285,13 +1291,13 @@ private:
|
||||
testcase(
|
||||
"bug: VaultClawback round-trip overshoot lets issuer recover "
|
||||
"more than requested (pre-fixCleanup3_4_0)");
|
||||
runScenario(all_ - fixCleanup3_4_0, false);
|
||||
runScenario(cashBasisAll_ - fixCleanup3_4_0, false);
|
||||
}
|
||||
{
|
||||
testcase(
|
||||
"bug: VaultClawback round-trip overshoot is clamped so "
|
||||
"assetsRecovered <= clawbackAmount (post-fixCleanup3_4_0)");
|
||||
runScenario(all_, true);
|
||||
runScenario(cashBasisAll_, true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1349,13 +1355,13 @@ private:
|
||||
testcase(
|
||||
"bug: VaultWithdraw round-trip overshoot delivers more than "
|
||||
"requested (pre-fixCleanup3_4_0)");
|
||||
runScenario(all_ - fixCleanup3_4_0, false);
|
||||
runScenario(cashBasisAll_ - fixCleanup3_4_0, false);
|
||||
}
|
||||
{
|
||||
testcase(
|
||||
"bug: VaultWithdraw round-trip overshoot is clamped so "
|
||||
"assetsWithdrawn <= requested (post-fixCleanup3_4_0)");
|
||||
runScenario(all_, true);
|
||||
runScenario(cashBasisAll_, true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1553,12 +1559,12 @@ private:
|
||||
BEAST_EXPECT(tokenAfter->getFieldU64(sfMPTAmount) == expectedSharesAfter);
|
||||
};
|
||||
|
||||
runSole(all_ - fixCleanup3_4_0, tecINVARIANT_FAILED);
|
||||
runSole(all_, tesSUCCESS);
|
||||
runSole(cashBasisAll_ - fixCleanup3_4_0, tecINVARIANT_FAILED);
|
||||
runSole(cashBasisAll_, tesSUCCESS);
|
||||
|
||||
testcase("VaultClawback after impaired loan, non-sole holder");
|
||||
{
|
||||
Env env(*this, all_);
|
||||
Env env(*this, cashBasisAll_);
|
||||
auto const maybeSetup = makeImpairedLoanVault(env, 1'000);
|
||||
if (!maybeSetup)
|
||||
{
|
||||
@@ -1770,30 +1776,42 @@ private:
|
||||
};
|
||||
|
||||
runScenario(
|
||||
all_, false /* removeAssetToken */, false /* withdrawAllAliceShares */, tesSUCCESS);
|
||||
cashBasisAll_,
|
||||
false /* removeAssetToken */,
|
||||
false /* withdrawAllAliceShares */,
|
||||
tesSUCCESS);
|
||||
runScenario(
|
||||
all_, false /* removeAssetToken */, true /* withdrawAllAliceShares */, tesSUCCESS);
|
||||
cashBasisAll_,
|
||||
false /* removeAssetToken */,
|
||||
true /* withdrawAllAliceShares */,
|
||||
tesSUCCESS);
|
||||
runScenario(
|
||||
all_, true /* removeAssetToken */, false /* withdrawAllAliceShares */, tesSUCCESS);
|
||||
cashBasisAll_,
|
||||
true /* removeAssetToken */,
|
||||
false /* withdrawAllAliceShares */,
|
||||
tesSUCCESS);
|
||||
runScenario(
|
||||
all_, true /* removeAssetToken */, true /* withdrawAllAliceShares */, tesSUCCESS);
|
||||
cashBasisAll_,
|
||||
true /* removeAssetToken */,
|
||||
true /* withdrawAllAliceShares */,
|
||||
tesSUCCESS);
|
||||
runScenario(
|
||||
all_ - fixCleanup3_4_0,
|
||||
cashBasisAll_ - fixCleanup3_4_0,
|
||||
false /* removeAssetToken */,
|
||||
false /* withdrawAllAliceShares */,
|
||||
tecINVARIANT_FAILED);
|
||||
runScenario(
|
||||
all_ - fixCleanup3_4_0,
|
||||
cashBasisAll_ - fixCleanup3_4_0,
|
||||
false /* removeAssetToken */,
|
||||
true /* withdrawAllAliceShares */,
|
||||
tecINVARIANT_FAILED);
|
||||
runScenario(
|
||||
all_ - fixCleanup3_4_0,
|
||||
cashBasisAll_ - fixCleanup3_4_0,
|
||||
true /* removeAssetToken */,
|
||||
false /* withdrawAllAliceShares */,
|
||||
tecINVARIANT_FAILED);
|
||||
runScenario(
|
||||
all_ - fixCleanup3_4_0,
|
||||
cashBasisAll_ - fixCleanup3_4_0,
|
||||
true /* removeAssetToken */,
|
||||
true /* withdrawAllAliceShares */,
|
||||
tecINVARIANT_FAILED);
|
||||
@@ -1813,7 +1831,7 @@ private:
|
||||
using namespace loan;
|
||||
using namespace std::chrono_literals;
|
||||
|
||||
Env env(*this, all_);
|
||||
Env env(*this, cashBasisAll_);
|
||||
|
||||
Account const issuer{"issuer"};
|
||||
Account const owner{"owner"};
|
||||
@@ -2053,8 +2071,8 @@ private:
|
||||
BEAST_EXPECT(vaultAfter->at(sfAssetsAvailable) == asset(0).value());
|
||||
};
|
||||
|
||||
runScenario(all_, tesSUCCESS);
|
||||
runScenario(all_ - fixCleanup3_4_0, tecINVARIANT_FAILED);
|
||||
runScenario(cashBasisAll_, tesSUCCESS);
|
||||
runScenario(cashBasisAll_ - fixCleanup3_4_0, tecINVARIANT_FAILED);
|
||||
}
|
||||
|
||||
// addEmptyHolding() used to check isGlobalFrozen(issuer) and
|
||||
@@ -2349,63 +2367,63 @@ private:
|
||||
"bug: VaultWithdraw to self fails with tecINTERNAL after issuer "
|
||||
"clears asfDefaultRipple even though the trust line exists "
|
||||
"(pre-fixCleanup3_4_0)");
|
||||
runExistingLine(all_ - fixCleanup3_4_0, tecINTERNAL);
|
||||
runExistingLine(cashBasisAll_ - fixCleanup3_4_0, tecINTERNAL);
|
||||
|
||||
testcase(
|
||||
"bug: VaultWithdraw to self succeeds after issuer clears "
|
||||
"asfDefaultRipple when the trust line exists (post-fixCleanup3_4_0)");
|
||||
runExistingLine(all_, tesSUCCESS);
|
||||
runExistingLine(cashBasisAll_, tesSUCCESS);
|
||||
|
||||
testcase(
|
||||
"bug: VaultWithdraw to self with an existing line still gets "
|
||||
"tecFROZEN under asfGlobalFreeze (post-fixCleanup3_4_0)");
|
||||
runExistingLine(all_, tecFROZEN, true);
|
||||
runExistingLine(cashBasisAll_, tecFROZEN, true);
|
||||
|
||||
testcase(
|
||||
"bug: VaultWithdraw to self fails with tecINTERNAL after issuer "
|
||||
"clears asfDefaultRipple and the trust line was deleted "
|
||||
"(pre-fixCleanup3_4_0)");
|
||||
runDeletedLine(all_ - fixCleanup3_4_0, tecINTERNAL);
|
||||
runDeletedLine(cashBasisAll_ - fixCleanup3_4_0, tecINTERNAL);
|
||||
|
||||
testcase(
|
||||
"bug: VaultWithdraw to self fails with terNO_RIPPLE after issuer "
|
||||
"clears asfDefaultRipple and the trust line was deleted "
|
||||
"(post-fixCleanup3_4_0)");
|
||||
runDeletedLine(all_, terNO_RIPPLE);
|
||||
runDeletedLine(cashBasisAll_, terNO_RIPPLE);
|
||||
|
||||
testcase(
|
||||
"bug: LoanBrokerCoverWithdraw to self fails with tecINTERNAL after "
|
||||
"issuer clears asfDefaultRipple even though the trust line exists "
|
||||
"(pre-fixCleanup3_4_0)");
|
||||
runCoverWithdraw(all_ - fixCleanup3_4_0, tecINTERNAL);
|
||||
runCoverWithdraw(cashBasisAll_ - fixCleanup3_4_0, tecINTERNAL);
|
||||
|
||||
testcase(
|
||||
"bug: LoanBrokerCoverWithdraw to self succeeds after issuer clears "
|
||||
"asfDefaultRipple when the trust line exists (post-fixCleanup3_4_0)");
|
||||
runCoverWithdraw(all_, tesSUCCESS);
|
||||
runCoverWithdraw(cashBasisAll_, tesSUCCESS);
|
||||
|
||||
testcase(
|
||||
"bug: LoanBrokerCoverWithdraw to self fails with tecINTERNAL after "
|
||||
"issuer clears asfDefaultRipple and the trust line was deleted "
|
||||
"(pre-fixCleanup3_4_0)");
|
||||
runDeletedCoverWithdraw(all_ - fixCleanup3_4_0, tecINTERNAL);
|
||||
runDeletedCoverWithdraw(cashBasisAll_ - fixCleanup3_4_0, tecINTERNAL);
|
||||
|
||||
testcase(
|
||||
"bug: LoanBrokerCoverWithdraw to self fails with terNO_RIPPLE after "
|
||||
"issuer clears asfDefaultRipple and the trust line was deleted "
|
||||
"(post-fixCleanup3_4_0)");
|
||||
runDeletedCoverWithdraw(all_, terNO_RIPPLE);
|
||||
runDeletedCoverWithdraw(cashBasisAll_, terNO_RIPPLE);
|
||||
|
||||
testcase(
|
||||
"bug: private VaultWithdraw to self fails with tecINTERNAL after "
|
||||
"issuer clears asfDefaultRipple even though the trust line exists "
|
||||
"(pre-fixCleanup3_4_0)");
|
||||
runPrivateVault(all_ - fixCleanup3_4_0, tecINTERNAL);
|
||||
runPrivateVault(cashBasisAll_ - fixCleanup3_4_0, tecINTERNAL);
|
||||
|
||||
testcase(
|
||||
"bug: private VaultWithdraw to self succeeds after issuer clears "
|
||||
"asfDefaultRipple when the trust line exists (post-fixCleanup3_4_0)");
|
||||
runPrivateVault(all_, tesSUCCESS);
|
||||
runPrivateVault(cashBasisAll_, tesSUCCESS);
|
||||
}
|
||||
|
||||
// Bug 1: a sponsored XRP VaultWithdraw to a distinct destination is
|
||||
@@ -2497,12 +2515,12 @@ private:
|
||||
"bug: sponsored XRP withdrawal to a distinct destination misreads a "
|
||||
"touched-but-zero sender delta as a second recipient "
|
||||
"(pre-fixCleanup3_4_0)");
|
||||
runScenario(all_ - fixCleanup3_4_0, tecINVARIANT_FAILED);
|
||||
runScenario(cashBasisAll_ - fixCleanup3_4_0, tecINVARIANT_FAILED);
|
||||
|
||||
testcase(
|
||||
"bug: sponsored XRP withdrawal to a distinct destination succeeds "
|
||||
"(post-fixCleanup3_4_0)");
|
||||
runScenario(all_, tesSUCCESS);
|
||||
runScenario(cashBasisAll_, tesSUCCESS);
|
||||
}
|
||||
|
||||
// Bug 2: a co-signed fee sponsor named as the withdrawal's own
|
||||
@@ -2576,12 +2594,12 @@ private:
|
||||
"bug: co-signed sponsor named as withdrawal destination has its "
|
||||
"own fee debit misread as breaking the payout equality "
|
||||
"(pre-fixCleanup3_4_0)");
|
||||
runScenario(all_ - fixCleanup3_4_0, tecINVARIANT_FAILED);
|
||||
runScenario(cashBasisAll_ - fixCleanup3_4_0, tecINVARIANT_FAILED);
|
||||
|
||||
testcase(
|
||||
"bug: co-signed sponsor named as withdrawal destination succeeds "
|
||||
"(post-fixCleanup3_4_0)");
|
||||
runScenario(all_, tesSUCCESS);
|
||||
runScenario(cashBasisAll_, tesSUCCESS);
|
||||
}
|
||||
|
||||
// Pre-funded fee sponsorship draws the fee from ltSponsorship.sfFeeAmount,
|
||||
@@ -2678,18 +2696,18 @@ private:
|
||||
testcase(
|
||||
"pre-funded fee XRP withdrawal to a distinct destination succeeds "
|
||||
"(post-fixCleanup3_4_0)");
|
||||
runScenario(all_, tesSUCCESS, false);
|
||||
runScenario(cashBasisAll_, tesSUCCESS, false);
|
||||
|
||||
testcase(
|
||||
"bug: pre-funded sponsor named as withdrawal destination misreads "
|
||||
"the sender's touched-but-zero delta as a second recipient "
|
||||
"(pre-fixCleanup3_4_0)");
|
||||
runScenario(all_ - fixCleanup3_4_0, tecINVARIANT_FAILED, true);
|
||||
runScenario(cashBasisAll_ - fixCleanup3_4_0, tecINVARIANT_FAILED, true);
|
||||
|
||||
testcase(
|
||||
"bug: pre-funded sponsor named as withdrawal destination receives "
|
||||
"the full payout (post-fixCleanup3_4_0)");
|
||||
runScenario(all_, tesSUCCESS, true);
|
||||
runScenario(cashBasisAll_, tesSUCCESS, true);
|
||||
}
|
||||
|
||||
// Unsponsored third-party XRP withdrawal: the sender's AccountRoot moves
|
||||
@@ -2704,7 +2722,7 @@ private:
|
||||
"unsponsored XRP withdrawal to a distinct destination succeeds "
|
||||
"(pre-fixCleanup3_4_0)");
|
||||
|
||||
Env env{*this, all_ - fixCleanup3_4_0};
|
||||
Env env{*this, cashBasisAll_ - fixCleanup3_4_0};
|
||||
Account const owner{"owner"};
|
||||
Account const holder{"holder"};
|
||||
Account const destination{"destination"};
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
#include <test/jtx/Env.h>
|
||||
#include <test/jtx/TestHelpers.h>
|
||||
#include <test/jtx/amount.h>
|
||||
#include <test/jtx/envconfig.h>
|
||||
#include <test/jtx/escrow.h>
|
||||
#include <test/jtx/fee.h>
|
||||
#include <test/jtx/flags.h>
|
||||
@@ -51,13 +50,7 @@ private:
|
||||
using namespace test::jtx;
|
||||
using namespace loan_broker;
|
||||
using namespace loan;
|
||||
// Temporary: FixedPrecision lending is incomplete until the LoanManage PR.
|
||||
Env env(
|
||||
*this,
|
||||
envconfig(),
|
||||
testableAmendments() - featureLendingProtocolV1_2,
|
||||
nullptr,
|
||||
beast::Severity::Warning);
|
||||
Env env(*this, beast::Severity::Warning);
|
||||
|
||||
auto const vaultAssetBalance = [&](Keylet const& vaultKeylet) {
|
||||
auto const sleVault = env.le(vaultKeylet);
|
||||
@@ -332,8 +325,7 @@ private:
|
||||
using namespace test::jtx;
|
||||
using namespace loan_broker;
|
||||
using namespace loan;
|
||||
// Temporary: FixedPrecision lending is incomplete until the LoanManage PR.
|
||||
Env env(*this, testableAmendments() - featureLendingProtocolV1_2);
|
||||
Env env(*this);
|
||||
env.enableFeature(fixCleanup3_1_3);
|
||||
|
||||
// Under featureLendingProtocolV1_1 LoanBrokerSet::preclaim only
|
||||
@@ -849,6 +841,7 @@ private:
|
||||
|
||||
Env legacyEnv(*this, all_);
|
||||
legacyEnv.disableFeature(fixCleanup3_1_3);
|
||||
legacyEnv.disableFeature(featureLendingProtocolV1_2);
|
||||
legacyEnv.fund(XRP(10000), issuer, owner, depositor);
|
||||
legacyEnv.close();
|
||||
|
||||
|
||||
@@ -555,8 +555,7 @@ private:
|
||||
using namespace loan_broker;
|
||||
using namespace loan;
|
||||
|
||||
// Temporary: FixedPrecision lending is incomplete until the LoanManage PR.
|
||||
Env env{*this, testableAmendments() - featureLendingProtocolV1_2};
|
||||
Env env{*this, testableAmendments()};
|
||||
Account const owner{"owner"};
|
||||
Account const depositor{"depositor"};
|
||||
Account const borrower{"borrower"};
|
||||
@@ -634,8 +633,7 @@ private:
|
||||
using namespace loan_broker;
|
||||
using namespace loan;
|
||||
|
||||
// Temporary: FixedPrecision lending is incomplete until the LoanManage PR.
|
||||
Env env{*this, testableAmendments() - featureLendingProtocolV1_2};
|
||||
Env env{*this, testableAmendments()};
|
||||
Account const owner{"owner"};
|
||||
Account const alice{"alice"};
|
||||
Account const bob{"bob"};
|
||||
@@ -762,12 +760,13 @@ private:
|
||||
balancesEq(XRP(140).value(), XRP(200).value());
|
||||
|
||||
// bob has 200 XRP-worth of shares but only 140 XRP is available (the remaining 60 XRP
|
||||
// sits in the outstanding loan). A full 200 XRP withdrawal fails against the
|
||||
// AssetsAvailable cap; bob redeems 140 XRP instead and is left holding 60M shares backed
|
||||
// by the loan receivable — the realistic outcome when capital is still deployed at
|
||||
// Redemption.
|
||||
// sits in the outstanding loan). Requesting the full 200 XRP is a final withdrawal
|
||||
// (it would burn all of bob's remaining shares), and FixedPrecision refuses to empty
|
||||
// the vault while AssetsDeployed is non-zero, ahead of the ordinary insufficient-funds
|
||||
// guard; bob redeems 140 XRP instead and is left holding 60M shares backed by the loan
|
||||
// receivable — the realistic outcome when capital is still deployed at Redemption.
|
||||
env(vault.withdraw({.depositor = bob, .id = keylet.key, .amount = XRP(200).value()}),
|
||||
Ter{tecINSUFFICIENT_FUNDS});
|
||||
Ter{tecHAS_OBLIGATIONS});
|
||||
env.close();
|
||||
env(vault.withdraw({.depositor = bob, .id = keylet.key, .amount = XRP(140).value()}));
|
||||
env.close();
|
||||
@@ -796,8 +795,7 @@ private:
|
||||
using namespace loan_broker;
|
||||
using namespace loan;
|
||||
|
||||
// Temporary: FixedPrecision lending is incomplete until the LoanManage PR.
|
||||
Env env{*this, testableAmendments() - featureLendingProtocolV1_2};
|
||||
Env env{*this, testableAmendments()};
|
||||
Account const owner{"owner"};
|
||||
Account const alice{"alice"};
|
||||
Account const borrower{"borrower"};
|
||||
@@ -862,8 +860,7 @@ private:
|
||||
using namespace loan_broker;
|
||||
using namespace loan;
|
||||
|
||||
// Temporary: FixedPrecision lending is incomplete until the LoanManage PR.
|
||||
Env env{*this, testableAmendments() - featureLendingProtocolV1_2};
|
||||
Env env{*this, testableAmendments()};
|
||||
Account const owner{"owner"};
|
||||
Account const alice{"alice"};
|
||||
Account const bob{"bob"};
|
||||
|
||||
@@ -50,6 +50,39 @@ namespace xrpl {
|
||||
class VaultLifecycle_test : public VaultTestBase
|
||||
{
|
||||
private:
|
||||
struct FundedVault
|
||||
{
|
||||
Keylet keylet;
|
||||
test::jtx::Account account;
|
||||
test::jtx::PrettyAsset shares;
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates a vault owned by `owner` and deposits `amount` into it from `depositor`.
|
||||
*/
|
||||
static FundedVault
|
||||
createFundedVault(
|
||||
test::jtx::Env& env,
|
||||
test::jtx::Vault& vault,
|
||||
test::jtx::Account const& owner,
|
||||
test::jtx::Account const& depositor,
|
||||
STAmount const& amount)
|
||||
{
|
||||
auto [tx, keylet] = vault.create({.owner = owner, .asset = amount.asset()});
|
||||
env(tx);
|
||||
env.close();
|
||||
env(vault.deposit({.depositor = depositor, .id = keylet.key, .amount = amount}));
|
||||
env.close();
|
||||
|
||||
auto const sle = env.le(keylet);
|
||||
test::jtx::Account const account{"vault", sle->at(sfAccount)};
|
||||
env.memoize(account);
|
||||
return {
|
||||
.keylet = keylet,
|
||||
.account = account,
|
||||
.shares = test::jtx::PrettyAsset{MPTIssue{sle->at(sfShareMPTID)}},
|
||||
};
|
||||
}
|
||||
void
|
||||
testSequences()
|
||||
{
|
||||
@@ -573,6 +606,7 @@ private:
|
||||
bool enableClawback = true;
|
||||
bool requireAuth = true;
|
||||
int initialXRP = 1000;
|
||||
std::uint16_t transferFee = 0;
|
||||
FeatureBitset features = testableAmendments();
|
||||
};
|
||||
|
||||
@@ -597,6 +631,7 @@ private:
|
||||
MPTTester mptt{env, issuer, kMptInitNoFund};
|
||||
auto const kNone = LedgerSpecificFlags(0);
|
||||
mptt.create({
|
||||
.transferFee = args.transferFee,
|
||||
.flags = tfMPTCanTransfer | tfMPTCanLock |
|
||||
(args.enableClawback ? tfMPTCanClawback : kNone) |
|
||||
(args.requireAuth ? tfMPTRequireAuth : kNone),
|
||||
@@ -616,6 +651,324 @@ private:
|
||||
test(env, issuer, owner, depositor, asset, vault, mptt);
|
||||
};
|
||||
|
||||
// The MPT charges a 25% transfer fee, so 100 gross delivers 80 net.
|
||||
auto const feeArgs = CaseArgs{.transferFee = 25'000};
|
||||
|
||||
auto const testTransferFeeGate = [&](FeatureBitset const& features) {
|
||||
testCase(
|
||||
[this, features](
|
||||
Env& env,
|
||||
Account const&,
|
||||
Account const& owner,
|
||||
Account const& depositor,
|
||||
PrettyAsset const& asset,
|
||||
Vault& vault,
|
||||
MPTTester& mptt) {
|
||||
bool const feeCharged = features[fixCleanup3_5_0];
|
||||
testcase(
|
||||
feeCharged ? "MPT transfer fee on third-party withdrawal"
|
||||
: "MPT transfer fee waived pre-fixCleanup3_5_0");
|
||||
|
||||
auto const funded = createFundedVault(env, vault, owner, depositor, asset(100));
|
||||
|
||||
auto tx = vault.withdraw({
|
||||
.depositor = depositor,
|
||||
.id = funded.keylet.key,
|
||||
.amount = funded.shares(100),
|
||||
});
|
||||
tx[sfDestination] = owner.human();
|
||||
env(tx);
|
||||
env.close();
|
||||
|
||||
auto const expected = feeCharged ? 80 : 100;
|
||||
BEAST_EXPECT(mptt.checkMPTokenAmount(depositor, 900));
|
||||
BEAST_EXPECT(mptt.checkMPTokenAmount(owner, expected));
|
||||
BEAST_EXPECT(mptt.checkMPTokenOutstandingAmount(900 + expected));
|
||||
BEAST_EXPECT(
|
||||
env.balance(funded.account, asset.raw().get<MPTIssue>()) == asset(0));
|
||||
|
||||
env(vault.del({.owner = owner, .id = funded.keylet.key}));
|
||||
env.close();
|
||||
},
|
||||
CaseArgs{.transferFee = 25'000, .features = features});
|
||||
};
|
||||
testTransferFeeGate(testableAmendments() - fixCleanup3_5_0);
|
||||
testTransferFeeGate(testableAmendments());
|
||||
|
||||
testCase(
|
||||
[this](
|
||||
Env& env,
|
||||
Account const&,
|
||||
Account const& owner,
|
||||
Account const& depositor,
|
||||
PrettyAsset const& asset,
|
||||
Vault& vault,
|
||||
MPTTester& mptt) {
|
||||
testcase("MPT transfer fee on fixed-asset third-party withdrawal");
|
||||
|
||||
auto const funded = createFundedVault(env, vault, owner, depositor, asset(100));
|
||||
auto const mptIssue = asset.raw().get<MPTIssue>();
|
||||
auto const shareIssue = funded.shares.raw().get<MPTIssue>();
|
||||
|
||||
auto tx = vault.withdraw(
|
||||
{.depositor = depositor, .id = funded.keylet.key, .amount = asset(40)});
|
||||
tx[sfDestination] = owner.human();
|
||||
env(tx);
|
||||
env.close();
|
||||
|
||||
// The destination receives the requested 40; the gross 50 leaves the vault.
|
||||
BEAST_EXPECT(mptt.checkMPTokenAmount(owner, 40));
|
||||
BEAST_EXPECT(env.balance(funded.account, mptIssue) == asset(50));
|
||||
BEAST_EXPECT(env.balance(depositor, shareIssue) == funded.shares(50));
|
||||
BEAST_EXPECT(mptt.checkMPTokenOutstandingAmount(990));
|
||||
},
|
||||
feeArgs);
|
||||
|
||||
testCase(
|
||||
[this](
|
||||
Env& env,
|
||||
Account const&,
|
||||
Account const& owner,
|
||||
Account const& depositor,
|
||||
PrettyAsset const& asset,
|
||||
Vault& vault,
|
||||
MPTTester& mptt) {
|
||||
testcase("MPT transfer fee on fixed-share third-party withdrawal");
|
||||
|
||||
auto const funded = createFundedVault(env, vault, owner, depositor, asset(100));
|
||||
auto const mptIssue = asset.raw().get<MPTIssue>();
|
||||
auto const shareIssue = funded.shares.raw().get<MPTIssue>();
|
||||
|
||||
auto tx = vault.withdraw(
|
||||
{.depositor = depositor, .id = funded.keylet.key, .amount = funded.shares(25)});
|
||||
tx[sfDestination] = owner.human();
|
||||
env(tx);
|
||||
env.close();
|
||||
|
||||
// The 25 shares redeem 25 assets; the destination receives 20 net.
|
||||
BEAST_EXPECT(mptt.checkMPTokenAmount(owner, 20));
|
||||
BEAST_EXPECT(env.balance(funded.account, mptIssue) == asset(75));
|
||||
BEAST_EXPECT(env.balance(depositor, shareIssue) == funded.shares(75));
|
||||
BEAST_EXPECT(mptt.checkMPTokenOutstandingAmount(995));
|
||||
},
|
||||
feeArgs);
|
||||
|
||||
testCase(
|
||||
[this](
|
||||
Env& env,
|
||||
Account const& issuer,
|
||||
Account const& owner,
|
||||
Account const& depositor,
|
||||
PrettyAsset const& asset,
|
||||
Vault& vault,
|
||||
MPTTester& mptt) {
|
||||
testcase("MPT no transfer fee on withdrawal to issuer");
|
||||
|
||||
auto const funded = createFundedVault(env, vault, owner, depositor, asset(100));
|
||||
auto const mptIssue = asset.raw().get<MPTIssue>();
|
||||
auto const shareIssue = funded.shares.raw().get<MPTIssue>();
|
||||
|
||||
auto tx = vault.withdraw(
|
||||
{.depositor = depositor, .id = funded.keylet.key, .amount = asset(10)});
|
||||
tx[sfDestination] = issuer.human();
|
||||
env(tx);
|
||||
env.close();
|
||||
|
||||
BEAST_EXPECT(env.balance(funded.account, mptIssue) == asset(90));
|
||||
BEAST_EXPECT(env.balance(depositor, shareIssue) == funded.shares(90));
|
||||
BEAST_EXPECT(mptt.checkMPTokenOutstandingAmount(990));
|
||||
},
|
||||
feeArgs);
|
||||
|
||||
testCase(
|
||||
[this](
|
||||
Env& env,
|
||||
Account const&,
|
||||
Account const& owner,
|
||||
Account const& depositor,
|
||||
PrettyAsset const& asset,
|
||||
Vault& vault,
|
||||
MPTTester& mptt) {
|
||||
testcase("MPT no transfer fee on self-withdrawal");
|
||||
|
||||
auto const funded = createFundedVault(env, vault, owner, depositor, asset(100));
|
||||
auto const mptIssue = asset.raw().get<MPTIssue>();
|
||||
|
||||
env(vault.withdraw(
|
||||
{.depositor = depositor, .id = funded.keylet.key, .amount = asset(10)}));
|
||||
env.close();
|
||||
|
||||
BEAST_EXPECT(mptt.checkMPTokenAmount(depositor, 910));
|
||||
BEAST_EXPECT(env.balance(funded.account, mptIssue) == asset(90));
|
||||
BEAST_EXPECT(mptt.checkMPTokenOutstandingAmount(1000));
|
||||
},
|
||||
feeArgs);
|
||||
|
||||
testCase(
|
||||
[this](
|
||||
Env& env,
|
||||
Account const&,
|
||||
Account const& owner,
|
||||
Account const& depositor,
|
||||
PrettyAsset const& asset,
|
||||
Vault& vault,
|
||||
MPTTester& mptt) {
|
||||
testcase("MPT transfer fee must fit in the vault position");
|
||||
|
||||
auto const funded = createFundedVault(env, vault, owner, depositor, asset(100));
|
||||
auto const mptIssue = asset.raw().get<MPTIssue>();
|
||||
auto const shareIssue = funded.shares.raw().get<MPTIssue>();
|
||||
|
||||
// Delivering 81 costs 102 gross, more than the 100 shares held.
|
||||
auto tx = vault.withdraw(
|
||||
{.depositor = depositor, .id = funded.keylet.key, .amount = asset(81)});
|
||||
tx[sfDestination] = owner.human();
|
||||
env(tx, Ter{tecINSUFFICIENT_FUNDS});
|
||||
env.close();
|
||||
|
||||
// Delivering 80 costs exactly the 100 shares held.
|
||||
tx = vault.withdraw(
|
||||
{.depositor = depositor, .id = funded.keylet.key, .amount = asset(80)});
|
||||
tx[sfDestination] = owner.human();
|
||||
env(tx);
|
||||
env.close();
|
||||
|
||||
BEAST_EXPECT(mptt.checkMPTokenAmount(owner, 80));
|
||||
BEAST_EXPECT(env.balance(funded.account, mptIssue) == asset(0));
|
||||
BEAST_EXPECT(env.balance(depositor, shareIssue) == funded.shares(0));
|
||||
BEAST_EXPECT(mptt.checkMPTokenOutstandingAmount(980));
|
||||
|
||||
env(vault.del({.owner = owner, .id = funded.keylet.key}));
|
||||
env.close();
|
||||
},
|
||||
feeArgs);
|
||||
|
||||
// The payout rounds down whether or not MPTokensV2 is enabled.
|
||||
auto const testTransferFeeRoundsDown = [&](FeatureBitset const& features) {
|
||||
testCase(
|
||||
[this, features](
|
||||
Env& env,
|
||||
Account const&,
|
||||
Account const& owner,
|
||||
Account const& depositor,
|
||||
PrettyAsset const& asset,
|
||||
Vault& vault,
|
||||
MPTTester& mptt) {
|
||||
testcase(
|
||||
features[featureMPTokensV2]
|
||||
? "MPT transfer fee rounds the payout down to zero"
|
||||
: "MPT transfer fee rounds the payout down to zero without "
|
||||
"MPTokensV2");
|
||||
|
||||
auto const funded = createFundedVault(env, vault, owner, depositor, asset(1));
|
||||
auto const mptIssue = asset.raw().get<MPTIssue>();
|
||||
|
||||
auto tx = vault.withdraw({
|
||||
.depositor = depositor,
|
||||
.id = funded.keylet.key,
|
||||
.amount = funded.shares(1),
|
||||
});
|
||||
tx[sfDestination] = owner.human();
|
||||
env(tx, Ter{tecPRECISION_LOSS});
|
||||
env.close();
|
||||
|
||||
// A self-withdrawal pays no fee, so the same share still redeems.
|
||||
env(vault.withdraw({
|
||||
.depositor = depositor,
|
||||
.id = funded.keylet.key,
|
||||
.amount = funded.shares(1),
|
||||
}));
|
||||
env.close();
|
||||
|
||||
BEAST_EXPECT(mptt.checkMPTokenAmount(depositor, 1000));
|
||||
BEAST_EXPECT(env.balance(funded.account, mptIssue) == asset(0));
|
||||
|
||||
env(vault.del({.owner = owner, .id = funded.keylet.key}));
|
||||
env.close();
|
||||
},
|
||||
CaseArgs{.transferFee = 25'000, .features = features});
|
||||
};
|
||||
testTransferFeeRoundsDown(testableAmendments());
|
||||
testTransferFeeRoundsDown(testableAmendments() - featureMPTokensV2);
|
||||
|
||||
testCase(
|
||||
[this](
|
||||
Env& env,
|
||||
Account const& issuer,
|
||||
Account const& owner,
|
||||
Account const& depositor,
|
||||
PrettyAsset const& asset,
|
||||
Vault& vault,
|
||||
MPTTester& mptt) {
|
||||
testcase("MPT transfer fee leaves other depositors whole");
|
||||
|
||||
Account const charlie{"charlie"};
|
||||
env.fund(XRP(1000), charlie);
|
||||
env.close();
|
||||
mptt.authorize({.account = charlie});
|
||||
mptt.authorize({.account = issuer, .holder = charlie});
|
||||
env(pay(issuer, owner, asset(100)));
|
||||
env.close();
|
||||
|
||||
auto const funded = createFundedVault(env, vault, owner, depositor, asset(100));
|
||||
env(vault.deposit(
|
||||
{.depositor = owner, .id = funded.keylet.key, .amount = asset(100)}));
|
||||
env.close();
|
||||
auto const shareIssue = funded.shares.raw().get<MPTIssue>();
|
||||
|
||||
auto const vaultBefore = env.le(funded.keylet);
|
||||
Number const assetsTotalBefore = vaultBefore->at(sfAssetsTotal);
|
||||
Number const assetsAvailableBefore = vaultBefore->at(sfAssetsAvailable);
|
||||
|
||||
// Delivering 40 to charlie costs the depositor 50 gross.
|
||||
auto tx = vault.withdraw(
|
||||
{.depositor = depositor, .id = funded.keylet.key, .amount = asset(40)});
|
||||
tx[sfDestination] = charlie.human();
|
||||
env(tx);
|
||||
env.close();
|
||||
|
||||
BEAST_EXPECT(mptt.checkMPTokenAmount(charlie, 40));
|
||||
auto const vaultAfter = env.le(funded.keylet);
|
||||
BEAST_EXPECT(vaultAfter->at(sfAssetsTotal) == assetsTotalBefore - 50);
|
||||
BEAST_EXPECT(vaultAfter->at(sfAssetsAvailable) == assetsAvailableBefore - 50);
|
||||
|
||||
// The owner's 100 shares still redeem for the 100 it deposited.
|
||||
BEAST_EXPECT(env.balance(owner, shareIssue) == funded.shares(100));
|
||||
env(vault.withdraw(
|
||||
{.depositor = owner, .id = funded.keylet.key, .amount = funded.shares(100)}));
|
||||
env.close();
|
||||
BEAST_EXPECT(mptt.checkMPTokenAmount(owner, 100));
|
||||
},
|
||||
feeArgs);
|
||||
|
||||
testCase(
|
||||
[this](
|
||||
Env& env,
|
||||
Account const& issuer,
|
||||
Account const& owner,
|
||||
Account const& depositor,
|
||||
PrettyAsset const& asset,
|
||||
Vault& vault,
|
||||
MPTTester& mptt) {
|
||||
testcase("MPT no transfer fee on issuer withdrawal to a third party");
|
||||
|
||||
auto const funded = createFundedVault(env, vault, owner, issuer, asset(100));
|
||||
auto const mptIssue = asset.raw().get<MPTIssue>();
|
||||
auto const shareIssue = funded.shares.raw().get<MPTIssue>();
|
||||
|
||||
auto tx = vault.withdraw(
|
||||
{.depositor = issuer, .id = funded.keylet.key, .amount = asset(40)});
|
||||
tx[sfDestination] = depositor.human();
|
||||
env(tx);
|
||||
env.close();
|
||||
|
||||
BEAST_EXPECT(mptt.checkMPTokenAmount(depositor, 1040));
|
||||
BEAST_EXPECT(env.balance(funded.account, mptIssue) == asset(60));
|
||||
BEAST_EXPECT(env.balance(issuer, shareIssue) == funded.shares(60));
|
||||
BEAST_EXPECT(mptt.checkMPTokenOutstandingAmount(1100));
|
||||
},
|
||||
feeArgs);
|
||||
|
||||
testCase([this](
|
||||
Env& env,
|
||||
Account const& issuer,
|
||||
@@ -1499,66 +1852,281 @@ private:
|
||||
env.close();
|
||||
});
|
||||
|
||||
// The issuer charges a 25% transfer fee, so 100 gross delivers 80 net.
|
||||
auto const feeArgs = CaseArgs{.transferRate = 1.25};
|
||||
|
||||
auto const testTransferFeeGate = [&](FeatureBitset const& features) {
|
||||
testCase(
|
||||
[this, features](
|
||||
Env& env,
|
||||
Account const& owner,
|
||||
Account const& issuer,
|
||||
Account const& charlie,
|
||||
auto,
|
||||
Vault& vault,
|
||||
PrettyAsset const& asset,
|
||||
auto&&...) {
|
||||
bool const feeCharged = features[fixCleanup3_5_0];
|
||||
testcase(
|
||||
feeCharged ? "IOU transfer fee on third-party withdrawal"
|
||||
: "IOU transfer fee waived pre-fixCleanup3_5_0");
|
||||
|
||||
auto const funded = createFundedVault(env, vault, owner, owner, asset(100));
|
||||
auto const issue = asset.raw().get<Issue>();
|
||||
|
||||
// Deposits are fee-free.
|
||||
BEAST_EXPECT(env.balance(owner, issue) == asset(100));
|
||||
BEAST_EXPECT(env.balance(funded.account, issue) == asset(100));
|
||||
|
||||
env(vault.clawback({
|
||||
.issuer = issuer,
|
||||
.id = funded.keylet.key,
|
||||
.holder = owner,
|
||||
.amount = asset(50),
|
||||
}));
|
||||
env.close();
|
||||
|
||||
// Clawbacks are fee-free.
|
||||
BEAST_EXPECT(env.balance(owner, issue) == asset(100));
|
||||
BEAST_EXPECT(env.balance(funded.account, issue) == asset(50));
|
||||
|
||||
env(vault.withdraw({
|
||||
.depositor = owner,
|
||||
.id = funded.keylet.key,
|
||||
.amount = funded.shares(20'000'000),
|
||||
}));
|
||||
env.close();
|
||||
|
||||
// Self-withdrawals are fee-free.
|
||||
BEAST_EXPECT(env.balance(owner, issue) == asset(120));
|
||||
BEAST_EXPECT(env.balance(funded.account, issue) == asset(30));
|
||||
|
||||
auto tx = vault.withdraw({
|
||||
.depositor = owner,
|
||||
.id = funded.keylet.key,
|
||||
.amount = funded.shares(30'000'000),
|
||||
});
|
||||
tx[sfDestination] = charlie.human();
|
||||
env(tx);
|
||||
env.close();
|
||||
|
||||
auto const expected = feeCharged ? asset(24) : asset(30);
|
||||
BEAST_EXPECT(env.balance(owner, issue) == asset(120));
|
||||
BEAST_EXPECT(env.balance(charlie, issue) == expected);
|
||||
BEAST_EXPECT(env.balance(funded.account, issue) == asset(0));
|
||||
|
||||
env(vault.del({.owner = owner, .id = funded.keylet.key}));
|
||||
env.close();
|
||||
},
|
||||
CaseArgs{.transferRate = 1.25, .features = features});
|
||||
};
|
||||
testTransferFeeGate(testableAmendments() - fixCleanup3_5_0);
|
||||
testTransferFeeGate(testableAmendments());
|
||||
|
||||
testCase(
|
||||
[&, this](
|
||||
[this](
|
||||
Env& env,
|
||||
Account const& owner,
|
||||
Account const&,
|
||||
Account const& charlie,
|
||||
auto,
|
||||
Vault& vault,
|
||||
PrettyAsset const& asset,
|
||||
auto&&...) {
|
||||
testcase("IOU transfer fee on fixed-asset third-party withdrawal");
|
||||
|
||||
auto const funded = createFundedVault(env, vault, owner, owner, asset(100));
|
||||
auto const issue = asset.raw().get<Issue>();
|
||||
auto const shareIssue = funded.shares.raw().get<MPTIssue>();
|
||||
|
||||
auto tx = vault.withdraw(
|
||||
{.depositor = owner, .id = funded.keylet.key, .amount = asset(40)});
|
||||
tx[sfDestination] = charlie.human();
|
||||
env(tx);
|
||||
env.close();
|
||||
|
||||
// The destination receives the requested 40; the gross 50 leaves the vault.
|
||||
BEAST_EXPECT(env.balance(charlie, issue) == asset(40));
|
||||
BEAST_EXPECT(env.balance(funded.account, issue) == asset(50));
|
||||
BEAST_EXPECT(env.balance(owner, shareIssue) == funded.shares(50'000'000));
|
||||
BEAST_EXPECT(env.balance(owner, issue) == asset(100));
|
||||
},
|
||||
feeArgs);
|
||||
|
||||
testCase(
|
||||
[this](
|
||||
Env& env,
|
||||
Account const& owner,
|
||||
Account const&,
|
||||
Account const& charlie,
|
||||
auto,
|
||||
Vault& vault,
|
||||
PrettyAsset const& asset,
|
||||
auto&&...) {
|
||||
testcase("IOU transfer fee on fixed-share third-party withdrawal");
|
||||
|
||||
auto const funded = createFundedVault(env, vault, owner, owner, asset(100));
|
||||
auto const issue = asset.raw().get<Issue>();
|
||||
auto const shareIssue = funded.shares.raw().get<MPTIssue>();
|
||||
|
||||
auto tx = vault.withdraw({
|
||||
.depositor = owner,
|
||||
.id = funded.keylet.key,
|
||||
.amount = funded.shares(25'000'000),
|
||||
});
|
||||
tx[sfDestination] = charlie.human();
|
||||
env(tx);
|
||||
env.close();
|
||||
|
||||
// The shares redeem 25 assets; the destination receives 20 net.
|
||||
BEAST_EXPECT(env.balance(charlie, issue) == asset(20));
|
||||
BEAST_EXPECT(env.balance(funded.account, issue) == asset(75));
|
||||
BEAST_EXPECT(env.balance(owner, shareIssue) == funded.shares(75'000'000));
|
||||
},
|
||||
feeArgs);
|
||||
|
||||
testCase(
|
||||
[this](
|
||||
Env& env,
|
||||
Account const& owner,
|
||||
Account const& issuer,
|
||||
Account const&,
|
||||
auto,
|
||||
Vault& vault,
|
||||
PrettyAsset const& asset,
|
||||
auto&&...) {
|
||||
testcase("IOU no transfer fee on withdrawal to issuer");
|
||||
|
||||
auto const funded = createFundedVault(env, vault, owner, owner, asset(100));
|
||||
auto const issue = asset.raw().get<Issue>();
|
||||
|
||||
auto tx = vault.withdraw(
|
||||
{.depositor = owner, .id = funded.keylet.key, .amount = asset(10)});
|
||||
tx[sfDestination] = issuer.human();
|
||||
env(tx);
|
||||
env.close();
|
||||
|
||||
BEAST_EXPECT(env.balance(funded.account, issue) == asset(90));
|
||||
BEAST_EXPECT(env.balance(owner, issue) == asset(100));
|
||||
},
|
||||
feeArgs);
|
||||
|
||||
testCase(
|
||||
[this](
|
||||
Env& env,
|
||||
Account const& owner,
|
||||
Account const&,
|
||||
Account const&,
|
||||
auto,
|
||||
Vault& vault,
|
||||
PrettyAsset const& asset,
|
||||
auto&&...) {
|
||||
testcase("IOU no transfer fee on self-withdrawal");
|
||||
|
||||
auto const funded = createFundedVault(env, vault, owner, owner, asset(100));
|
||||
auto const issue = asset.raw().get<Issue>();
|
||||
|
||||
env(vault.withdraw(
|
||||
{.depositor = owner, .id = funded.keylet.key, .amount = asset(10)}));
|
||||
env.close();
|
||||
|
||||
BEAST_EXPECT(env.balance(owner, issue) == asset(110));
|
||||
BEAST_EXPECT(env.balance(funded.account, issue) == asset(90));
|
||||
},
|
||||
feeArgs);
|
||||
|
||||
testCase(
|
||||
[this](
|
||||
Env& env,
|
||||
Account const& owner,
|
||||
Account const&,
|
||||
Account const& charlie,
|
||||
auto,
|
||||
Vault& vault,
|
||||
PrettyAsset const& asset,
|
||||
auto&&...) {
|
||||
testcase("IOU transfer fee must fit in the vault position");
|
||||
|
||||
auto const funded = createFundedVault(env, vault, owner, owner, asset(100));
|
||||
auto const issue = asset.raw().get<Issue>();
|
||||
|
||||
// Delivering 81 costs 101.25 gross, more than the vault position.
|
||||
auto tx = vault.withdraw(
|
||||
{.depositor = owner, .id = funded.keylet.key, .amount = asset(81)});
|
||||
tx[sfDestination] = charlie.human();
|
||||
env(tx, Ter{tecINSUFFICIENT_FUNDS});
|
||||
env.close();
|
||||
|
||||
// Delivering 80 costs exactly the 100 held.
|
||||
tx = vault.withdraw(
|
||||
{.depositor = owner, .id = funded.keylet.key, .amount = asset(80)});
|
||||
tx[sfDestination] = charlie.human();
|
||||
env(tx);
|
||||
env.close();
|
||||
|
||||
BEAST_EXPECT(env.balance(charlie, issue) == asset(80));
|
||||
BEAST_EXPECT(env.balance(funded.account, issue) == asset(0));
|
||||
|
||||
env(vault.del({.owner = owner, .id = funded.keylet.key}));
|
||||
env.close();
|
||||
},
|
||||
feeArgs);
|
||||
|
||||
testCase(
|
||||
[this](
|
||||
Env& env,
|
||||
Account const& owner,
|
||||
Account const& issuer,
|
||||
Account const& charlie,
|
||||
auto vaultAccount,
|
||||
auto,
|
||||
Vault& vault,
|
||||
PrettyAsset const& asset,
|
||||
auto issuanceId) {
|
||||
testcase("IOU transfer fees not applied");
|
||||
auto&&...) {
|
||||
testcase("IOU no transfer fee on issuer withdrawal to a third party");
|
||||
|
||||
auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
|
||||
auto const funded = createFundedVault(env, vault, owner, issuer, asset(100));
|
||||
auto const issue = asset.raw().get<Issue>();
|
||||
|
||||
auto tx = vault.withdraw(
|
||||
{.depositor = issuer, .id = funded.keylet.key, .amount = asset(40)});
|
||||
tx[sfDestination] = charlie.human();
|
||||
env(tx);
|
||||
env.close();
|
||||
|
||||
env(vault.deposit({.depositor = owner, .id = keylet.key, .amount = asset(100)}));
|
||||
env.close();
|
||||
|
||||
auto const issue = asset.raw().get<Issue>();
|
||||
Asset const share = Asset(issuanceId(keylet));
|
||||
|
||||
// transfer fees ignored on deposit
|
||||
BEAST_EXPECT(env.balance(owner, issue) == asset(100));
|
||||
BEAST_EXPECT(env.balance(vaultAccount(keylet), issue) == asset(100));
|
||||
|
||||
{
|
||||
auto const tx = vault.clawback(
|
||||
{.issuer = issuer, .id = keylet.key, .holder = owner, .amount = asset(50)});
|
||||
env(tx);
|
||||
env.close();
|
||||
}
|
||||
|
||||
// transfer fees ignored on clawback
|
||||
BEAST_EXPECT(env.balance(owner, issue) == asset(100));
|
||||
BEAST_EXPECT(env.balance(vaultAccount(keylet), issue) == asset(50));
|
||||
|
||||
env(vault.withdraw(
|
||||
{.depositor = owner, .id = keylet.key, .amount = share(20'000'000)}));
|
||||
|
||||
// transfer fees ignored on withdraw
|
||||
BEAST_EXPECT(env.balance(owner, issue) == asset(120));
|
||||
BEAST_EXPECT(env.balance(vaultAccount(keylet), issue) == asset(30));
|
||||
|
||||
{
|
||||
auto tx = vault.withdraw(
|
||||
{.depositor = owner, .id = keylet.key, .amount = share(30'000'000)});
|
||||
tx[sfDestination] = charlie.human();
|
||||
env(tx);
|
||||
}
|
||||
|
||||
// transfer fees ignored on withdraw to 3rd party
|
||||
BEAST_EXPECT(env.balance(owner, issue) == asset(120));
|
||||
BEAST_EXPECT(env.balance(charlie, issue) == asset(30));
|
||||
BEAST_EXPECT(env.balance(vaultAccount(keylet), issue) == asset(0));
|
||||
|
||||
env(vault.del({.owner = owner, .id = keylet.key}));
|
||||
env.close();
|
||||
BEAST_EXPECT(env.balance(charlie, issue) == asset(40));
|
||||
BEAST_EXPECT(env.balance(funded.account, issue) == asset(60));
|
||||
},
|
||||
CaseArgs{.transferRate = 1.25});
|
||||
feeArgs);
|
||||
|
||||
testCase(
|
||||
[this](
|
||||
Env& env,
|
||||
Account const& owner,
|
||||
Account const&,
|
||||
Account const& charlie,
|
||||
auto,
|
||||
Vault& vault,
|
||||
PrettyAsset const& asset,
|
||||
auto&&...) {
|
||||
testcase("IOU receiver limit applies to the net amount");
|
||||
|
||||
// The limit fits the 40 delivered but not the 50 gross.
|
||||
env(trust(charlie, asset(40)));
|
||||
env.close();
|
||||
|
||||
auto const funded = createFundedVault(env, vault, owner, owner, asset(100));
|
||||
auto const issue = asset.raw().get<Issue>();
|
||||
|
||||
auto tx = vault.withdraw(
|
||||
{.depositor = owner, .id = funded.keylet.key, .amount = asset(40)});
|
||||
tx[sfDestination] = charlie.human();
|
||||
env(tx);
|
||||
env.close();
|
||||
|
||||
BEAST_EXPECT(env.balance(charlie, issue) == asset(40));
|
||||
BEAST_EXPECT(env.balance(funded.account, issue) == asset(50));
|
||||
},
|
||||
feeArgs);
|
||||
|
||||
testCase([&, this](
|
||||
Env& env,
|
||||
|
||||
@@ -49,6 +49,12 @@ namespace xrpl {
|
||||
|
||||
class VaultScale_test : public VaultTestBase
|
||||
{
|
||||
// The IOU precision-boundary bugs in this suite probe the STAmount
|
||||
// 16-digit mantissa cliff (~1e16). FixedPrecision's Open-zone cap
|
||||
// (9e(15-Scale)) makes that value unreachable at any Scale, so these
|
||||
// scenarios cannot be reproduced under V1.2 by construction.
|
||||
FeatureBitset const cashBasisAll_{all_ - featureLendingProtocolV1_2};
|
||||
|
||||
private:
|
||||
void
|
||||
testScaleIOU()
|
||||
@@ -77,7 +83,7 @@ private:
|
||||
// attaching a loan broker). featureLendingProtocolV1_1 adds a
|
||||
// closed-ended vault gate on LoanBrokerSet::preclaim and is
|
||||
// orthogonal to what this suite asserts, so strip it here.
|
||||
Env env{*this, all_ - featureLendingProtocolV1_1};
|
||||
Env env{*this, cashBasisAll_ - featureLendingProtocolV1_1};
|
||||
Account const owner{"owner"};
|
||||
Account const issuer{"issuer"};
|
||||
Account const depositor{"depositor"};
|
||||
@@ -1377,8 +1383,8 @@ public:
|
||||
run() override
|
||||
{
|
||||
testScaleIOU();
|
||||
testAssetsMaximum(all_ - fixCleanup3_5_0);
|
||||
testAssetsMaximum(all_);
|
||||
testAssetsMaximum(cashBasisAll_ - fixCleanup3_5_0);
|
||||
testAssetsMaximum(cashBasisAll_);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -114,13 +114,7 @@ protected:
|
||||
return {.vault = vault, .keylet = keylet, .sub = sub, .red = red};
|
||||
}
|
||||
|
||||
// The IOU precision-boundary bugs in VaultBugs_test.cpp probe the
|
||||
// STAmount 16-digit mantissa cliff (~1e16). FixedPrecision's Open-zone
|
||||
// cap (9e(15-Scale)) makes that value unreachable at any Scale, so
|
||||
// these scenarios cannot be reproduced under V1.2 by construction.
|
||||
// Tests for the fixed-precision protocol enable featureLendingProtocolV1_2
|
||||
// explicitly.
|
||||
FeatureBitset const all_{test::jtx::testableAmendments() - featureLendingProtocolV1_2};
|
||||
FeatureBitset const all_{test::jtx::testableAmendments()};
|
||||
std::string const iouCurrency_{"IOU"};
|
||||
};
|
||||
|
||||
|
||||
@@ -1202,9 +1202,13 @@ private:
|
||||
Vault const vault{env};
|
||||
|
||||
// Without the fix, rounding an out-of-range AssetsMaximum to the vault
|
||||
// asset throws.
|
||||
TER const createErr = withFix ? TER{temMALFORMED} : TER{tefEXCEPTION};
|
||||
TER const setErr = withFix ? TER{tecPRECISION_LOSS} : TER{tefEXCEPTION};
|
||||
// asset throws. FixedPrecision Vaults check AssetsMaximum against the
|
||||
// base grid in preclaim first, which rejects it without throwing.
|
||||
bool const fixedPrecision = features[featureLendingProtocolV1_1] &&
|
||||
features[featureLendingProtocolV1_2] && features[fixCleanup3_4_0];
|
||||
TER const preFixErr = fixedPrecision ? TER{tecPRECISION_LOSS} : TER{tefEXCEPTION};
|
||||
TER const createErr = withFix ? TER{temMALFORMED} : preFixErr;
|
||||
TER const setErr = withFix ? TER{tecPRECISION_LOSS} : preFixErr;
|
||||
|
||||
auto const test = [&](Asset const& asset, Number const& maxValid, Number const& overMax) {
|
||||
{
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
#include <xrpl/config/BasicConfig.h>
|
||||
#include <xrpl/config/Constants.h>
|
||||
#include <xrpl/protocol/SystemParameters.h> // IWYU pragma: keep
|
||||
#include <xrpl/protocol/XRPAmount.h>
|
||||
#include <xrpl/server/Port.h>
|
||||
|
||||
#include <boost/lexical_cast/bad_lexical_cast.hpp>
|
||||
@@ -1753,6 +1754,30 @@ r.ripple.com:51235
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testToFees()
|
||||
{
|
||||
testcase("toFees");
|
||||
|
||||
FeeSetup setup;
|
||||
|
||||
setup.referenceFee = XRPAmount{17};
|
||||
setup.accountReserve = XRPAmount{3'000'017};
|
||||
setup.ownerReserve = XRPAmount{700'017};
|
||||
setup.gasLimit = 123'457;
|
||||
setup.bytecodeSizeLimit = 23'457;
|
||||
setup.gasPrice = 7'654'321;
|
||||
|
||||
auto const fees = setup.toFees();
|
||||
|
||||
BEAST_EXPECT(fees.base == setup.referenceFee);
|
||||
BEAST_EXPECT(fees.reserve == setup.accountReserve);
|
||||
BEAST_EXPECT(fees.increment == setup.ownerReserve);
|
||||
BEAST_EXPECT(fees.gasLimit == setup.gasLimit);
|
||||
BEAST_EXPECT(fees.bytecodeSizeLimit == setup.bytecodeSizeLimit);
|
||||
BEAST_EXPECT(fees.gasPrice == setup.gasPrice);
|
||||
}
|
||||
|
||||
void
|
||||
run() override
|
||||
{
|
||||
@@ -1772,6 +1797,7 @@ r.ripple.com:51235
|
||||
testAmendment();
|
||||
testOverlay();
|
||||
testNetworkID();
|
||||
testToFees();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -21,8 +21,9 @@ void
|
||||
setupConfigForUnitTests(Config& cfg)
|
||||
{
|
||||
using namespace jtx;
|
||||
// Default fees to old values, so tests don't have to worry about changes in
|
||||
// Config.h
|
||||
// Reserves held at pre-2021 mainnet values; many tests carry balance
|
||||
// expectations built on them. Extension limits are deliberately unset --
|
||||
// tests inherit the FeeSetup defaults, or set [voting].
|
||||
cfg.fees.referenceFee = UNIT_TEST_REFERENCE_FEE;
|
||||
cfg.fees.accountReserve = XRP(200).value().xrp().drops();
|
||||
cfg.fees.ownerReserve = XRP(50).value().xrp().drops();
|
||||
|
||||
@@ -827,6 +827,36 @@ MPTTester::checkEncryptionKeys(
|
||||
});
|
||||
}
|
||||
|
||||
bool
|
||||
MPTTester::checkEncryptionKey(
|
||||
std::optional<Buffer> const& expected,
|
||||
SF_VL const& field,
|
||||
std::optional<Account> const& holder) const
|
||||
{
|
||||
return forObject(
|
||||
[&](SLEP const& sle) -> bool {
|
||||
auto const encryptionKey = (*sle)[~field];
|
||||
if (!expected)
|
||||
return !encryptionKey;
|
||||
return encryptionKey && Slice(*encryptionKey) == Slice(*expected);
|
||||
},
|
||||
holder);
|
||||
}
|
||||
|
||||
bool
|
||||
MPTTester::checkInitialIssuerKey(std::optional<Buffer> const& expected) const
|
||||
{
|
||||
return checkEncryptionKey(expected, sfInitialIssuerEncryptionKey);
|
||||
}
|
||||
|
||||
bool
|
||||
MPTTester::checkIssuerMirrorEncryptionKey(
|
||||
std::optional<Buffer> const& expected,
|
||||
Account const& holder) const
|
||||
{
|
||||
return checkEncryptionKey(expected, sfIssuerMirrorEncryptionKey, holder);
|
||||
}
|
||||
|
||||
void
|
||||
MPTTester::pay(
|
||||
Account const& src,
|
||||
|
||||
@@ -739,6 +739,13 @@ public:
|
||||
std::optional<Account> const& issuerKeyOwner,
|
||||
std::optional<Account> const& auditorKeyOwner) const;
|
||||
|
||||
[[nodiscard]] bool
|
||||
checkInitialIssuerKey(std::optional<Buffer> const& expected) const;
|
||||
|
||||
[[nodiscard]] bool
|
||||
checkIssuerMirrorEncryptionKey(std::optional<Buffer> const& expected, Account const& holder)
|
||||
const;
|
||||
|
||||
[[nodiscard]] Account const&
|
||||
issuer() const
|
||||
{
|
||||
@@ -965,6 +972,12 @@ private:
|
||||
json::Value& jv,
|
||||
Account const& account,
|
||||
std::uint64_t const amount) const;
|
||||
|
||||
[[nodiscard]] bool
|
||||
checkEncryptionKey(
|
||||
std::optional<Buffer> const& expected,
|
||||
SF_VL const& field,
|
||||
std::optional<Account> const& holder = std::nullopt) const;
|
||||
};
|
||||
|
||||
} // namespace xrpl::test::jtx
|
||||
@@ -13,6 +13,7 @@
|
||||
|
||||
#include <xrpl/basics/base_uint.h>
|
||||
#include <xrpl/basics/chrono.h>
|
||||
#include <xrpl/basics/contract.h>
|
||||
#include <xrpl/beast/hash/uhash.h>
|
||||
#include <xrpl/beast/unit_test/suite.h>
|
||||
#include <xrpl/json/json_value.h>
|
||||
@@ -194,7 +195,7 @@ public:
|
||||
metadata->add(*metaSerializer);
|
||||
|
||||
ledger->rawTxInsert(UInt256{1}, txSerializer, metaSerializer);
|
||||
ledger->setImmutable();
|
||||
BEAST_EXPECT(ledger->setImmutable());
|
||||
ledger->setValidated();
|
||||
|
||||
try
|
||||
@@ -277,7 +278,12 @@ public:
|
||||
ledger->rawTxInsert(UInt256{seq}, txSerializer, metaSerializer);
|
||||
}
|
||||
|
||||
ledger->setImmutable();
|
||||
// Thrown rather than asserted because a refusal leaves the ledger unusable, and this
|
||||
// helper is static, so BEAST_EXPECT is out of reach.
|
||||
if (!ledger->setImmutable())
|
||||
{
|
||||
Throw<std::runtime_error>("bookChangesFor: ledger could not be made immutable");
|
||||
}
|
||||
ledger->setValidated();
|
||||
|
||||
return xrpl::rpc::computeBookChanges(std::static_pointer_cast<Ledger const>(ledger));
|
||||
|
||||
@@ -392,6 +392,21 @@ public:
|
||||
"badCredentials");
|
||||
}
|
||||
|
||||
{
|
||||
testcase(
|
||||
"deposit_authorized with credentials failure: zero "
|
||||
"credential index");
|
||||
|
||||
for (auto const& zero : {std::string("0"), std::string(64, '0')})
|
||||
{
|
||||
auto const args = depositAuthArgs(alice, becky, "validated", {zero});
|
||||
|
||||
auto const jv = env.rpc("json", "deposit_authorized", args.toStyledString());
|
||||
checkCredentialsResponse(
|
||||
jv[jss::result], alice, becky, false, {zero}, "badCredentials");
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
testcase(
|
||||
"deposit_authorized with credentials not authorized: "
|
||||
|
||||
@@ -32,6 +32,7 @@
|
||||
#include <xrpl/json/to_string.h>
|
||||
#include <xrpl/protocol/AccountID.h>
|
||||
#include <xrpl/protocol/Feature.h>
|
||||
#include <xrpl/protocol/Fees.h>
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
#include <xrpl/protocol/KeyType.h>
|
||||
#include <xrpl/protocol/PublicKey.h>
|
||||
@@ -437,7 +438,19 @@ public:
|
||||
{
|
||||
using namespace jtx;
|
||||
|
||||
Env env{*this, singleThreadIo(envconfig(validator, "")), features};
|
||||
// doValidation only writes a fee field when its target differs from the
|
||||
// ledger's current value, so a node with nothing to change emits
|
||||
// nothing. Vote for limits other than the ones already in force, so
|
||||
// the extension fields actually appear on the flag ledger.
|
||||
auto const voteDifferentLimits = [](std::unique_ptr<Config> cfg) {
|
||||
auto& voting = cfg->section(Sections::kVoting);
|
||||
voting.set(Keys::kGasLimit, std::to_string(kDefaultGasLimit - 1000));
|
||||
voting.set(Keys::kBytecodeSizeLimit, std::to_string(kDefaultBytecodeSizeLimit - 1000));
|
||||
voting.set(Keys::kGasPrice, std::to_string(kDefaultGasPrice - 1000));
|
||||
|
||||
return cfg;
|
||||
};
|
||||
Env env{*this, singleThreadIo(voteDifferentLimits(envconfig(validator, ""))), features};
|
||||
auto& cfg = env.app().config();
|
||||
if (!BEAST_EXPECT(cfg.section(Sections::kValidationSeed).empty()))
|
||||
return;
|
||||
@@ -520,6 +533,28 @@ public:
|
||||
if (jv.isMember(jss::reserve_inc) != isFlagLedger)
|
||||
return false;
|
||||
|
||||
if (env.closed()->rules().enabled(featureSmartEscrow))
|
||||
{
|
||||
if (jv.isMember(jss::gas_limit) != isFlagLedger)
|
||||
return false;
|
||||
|
||||
if (jv.isMember(jss::bytecode_size_limit) != isFlagLedger)
|
||||
return false;
|
||||
|
||||
if (jv.isMember(jss::gas_price) != isFlagLedger)
|
||||
return false;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (jv.isMember(jss::gas_limit))
|
||||
return false;
|
||||
|
||||
if (jv.isMember(jss::bytecode_size_limit))
|
||||
return false;
|
||||
|
||||
if (jv.isMember(jss::gas_price))
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
@@ -2466,7 +2501,8 @@ public:
|
||||
testTransactionsAPIv1();
|
||||
testTransactionsAPIv2();
|
||||
testManifests();
|
||||
testValidations(all - xrpFees);
|
||||
testValidations(all - featureXRPFees - featureSmartEscrow);
|
||||
testValidations(all - featureSmartEscrow);
|
||||
testValidations(all);
|
||||
testSubErrors(true);
|
||||
testSubErrors(false);
|
||||
|
||||
@@ -1,123 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <xrpl/basics/ByteUtilities.h>
|
||||
#include <xrpl/basics/base_uint.h>
|
||||
#include <xrpl/basics/chrono.h>
|
||||
#include <xrpl/basics/contract.h>
|
||||
#include <xrpl/beast/utility/Journal.h>
|
||||
#include <xrpl/config/BasicConfig.h>
|
||||
#include <xrpl/config/Constants.h>
|
||||
#include <xrpl/nodestore/Database.h>
|
||||
#include <xrpl/nodestore/DummyScheduler.h>
|
||||
#include <xrpl/nodestore/Manager.h>
|
||||
#include <xrpl/shamap/Family.h>
|
||||
#include <xrpl/shamap/FullBelowCache.h>
|
||||
#include <xrpl/shamap/TreeNodeCache.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <stdexcept>
|
||||
|
||||
namespace xrpl::test {
|
||||
|
||||
/**
|
||||
* Test implementation of Family for unit tests.
|
||||
*
|
||||
* Uses an in-memory NodeStore database and simple caches.
|
||||
* The missingNode methods throw since tests shouldn't encounter missing nodes.
|
||||
*/
|
||||
class TestFamily : public Family
|
||||
{
|
||||
private:
|
||||
std::unique_ptr<node_store::Database> db_;
|
||||
TestStopwatch clock_;
|
||||
std::shared_ptr<FullBelowCache> fbCache_;
|
||||
std::shared_ptr<TreeNodeCache> tnCache_;
|
||||
node_store::DummyScheduler scheduler_;
|
||||
beast::Journal j_;
|
||||
|
||||
public:
|
||||
explicit TestFamily(beast::Journal j)
|
||||
: fbCache_(std::make_shared<FullBelowCache>("TestFamily full below cache", clock_, j))
|
||||
, tnCache_(
|
||||
std::make_shared<TreeNodeCache>(
|
||||
"TestFamily tree node cache",
|
||||
65536,
|
||||
std::chrono::minutes{1},
|
||||
clock_,
|
||||
j))
|
||||
, j_(j)
|
||||
{
|
||||
Section config;
|
||||
config.set(Keys::kType, "memory");
|
||||
config.set(Keys::kPath, "TestFamily");
|
||||
db_ = node_store::Manager::instance().makeDatabase(megabytes(4), scheduler_, 1, config, j);
|
||||
}
|
||||
|
||||
node_store::Database&
|
||||
db() override
|
||||
{
|
||||
return *db_;
|
||||
}
|
||||
|
||||
[[nodiscard]] node_store::Database const&
|
||||
db() const override
|
||||
{
|
||||
return *db_;
|
||||
}
|
||||
|
||||
beast::Journal const&
|
||||
journal() override
|
||||
{
|
||||
return j_;
|
||||
}
|
||||
|
||||
std::shared_ptr<FullBelowCache>
|
||||
getFullBelowCache() override
|
||||
{
|
||||
return fbCache_;
|
||||
}
|
||||
|
||||
std::shared_ptr<TreeNodeCache>
|
||||
getTreeNodeCache() override
|
||||
{
|
||||
return tnCache_;
|
||||
}
|
||||
|
||||
void
|
||||
sweep() override
|
||||
{
|
||||
fbCache_->sweep();
|
||||
tnCache_->sweep();
|
||||
}
|
||||
|
||||
void
|
||||
missingNodeAcquireBySeq(std::uint32_t refNum, UInt256 const& nodeHash) override
|
||||
{
|
||||
Throw<std::runtime_error>("TestFamily: missing node (by seq)");
|
||||
}
|
||||
|
||||
void
|
||||
missingNodeAcquireByHash(UInt256 const& refHash, std::uint32_t refNum) override
|
||||
{
|
||||
Throw<std::runtime_error>("TestFamily: missing node (by hash)");
|
||||
}
|
||||
|
||||
void
|
||||
reset() override
|
||||
{
|
||||
(*fbCache_).reset();
|
||||
(*tnCache_).reset();
|
||||
}
|
||||
|
||||
/**
|
||||
* Access the test clock for time manipulation in tests.
|
||||
*/
|
||||
TestStopwatch&
|
||||
clock()
|
||||
{
|
||||
return clock_;
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace xrpl::test
|
||||
@@ -12,8 +12,8 @@
|
||||
|
||||
#include <boost/asio/io_context.hpp>
|
||||
|
||||
#include <helpers/TestFamily.h>
|
||||
#include <helpers/TestSink.h>
|
||||
#include <shamap/common.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
@@ -73,7 +73,7 @@ class TestServiceRegistry : public ServiceRegistry
|
||||
{
|
||||
TestLogs logs_{beast::Severity::Warning};
|
||||
boost::asio::io_context ioContext_;
|
||||
TestFamily family_{logs_.journal("TestFamily")};
|
||||
tests::TestNodeFamily family_{logs_.journal("TestNodeFamily")};
|
||||
LoadFeeTrack feeTrack_{logs_.journal("LoadFeeTrack")};
|
||||
TestNetworkIDService networkIDService_;
|
||||
HashRouter hashRouter_{HashRouter::Setup{}, stopwatch()};
|
||||
|
||||
@@ -69,7 +69,13 @@ TxTest::TxTest(std::optional<FeatureBitset> features)
|
||||
rules_.emplace(featureSet_);
|
||||
|
||||
// Default fees for testing
|
||||
Fees const fees{XRPAmount{10}, XRPAmount{10000000}, XRPAmount{2000000}};
|
||||
Fees const fees{
|
||||
XRPAmount{10},
|
||||
XRPAmount{10000000},
|
||||
XRPAmount{2000000},
|
||||
kDefaultGasLimit,
|
||||
kDefaultBytecodeSizeLimit,
|
||||
kDefaultGasPrice};
|
||||
|
||||
// Create a genesis ledger as the base
|
||||
closedLedger_ = std::make_shared<Ledger>(
|
||||
@@ -209,7 +215,10 @@ TxTest::close()
|
||||
accum.apply(*newLedger);
|
||||
}
|
||||
|
||||
newLedger->setAccepted(ledgerCloseTime, newLedger->header().closeTimeResolution, true);
|
||||
if (!newLedger->setAccepted(ledgerCloseTime, newLedger->header().closeTimeResolution, true))
|
||||
{
|
||||
Throw<std::runtime_error>("TxTest::close: ledger has an invalid map");
|
||||
}
|
||||
|
||||
closedLedger_ = newLedger;
|
||||
|
||||
|
||||
@@ -0,0 +1,186 @@
|
||||
#include <xrpl/basics/base_uint.h>
|
||||
#include <xrpl/basics/chrono.h>
|
||||
#include <xrpl/beast/hash/uhash.h>
|
||||
#include <xrpl/beast/utility/Journal.h>
|
||||
#include <xrpl/ledger/Ledger.h>
|
||||
#include <xrpl/nodestore/NodeObject.h>
|
||||
#include <xrpl/protocol/Feature.h>
|
||||
#include <xrpl/protocol/Fees.h>
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
#include <xrpl/protocol/Rules.h>
|
||||
#include <xrpl/protocol/SField.h>
|
||||
#include <xrpl/protocol/STLedgerEntry.h>
|
||||
#include <xrpl/protocol/XRPAmount.h>
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
#include <shamap/common.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <unordered_set>
|
||||
#include <vector>
|
||||
|
||||
namespace xrpl::test {
|
||||
|
||||
namespace {
|
||||
|
||||
// Unlike every protocol constant, so an assertion cannot pass by chance.
|
||||
constexpr std::uint32_t kLocalGasLimit{123'457};
|
||||
constexpr std::uint32_t kLocalBytecodeSizeLimit{23'457};
|
||||
constexpr std::uint32_t kLocalGasPrice{7'654'321};
|
||||
|
||||
// Stands in for FeeSetup::toFees(): local configuration.
|
||||
Fees
|
||||
localFees()
|
||||
{
|
||||
return Fees{
|
||||
XRPAmount{10},
|
||||
XRPAmount{10'000'000},
|
||||
XRPAmount{2'000'000},
|
||||
kLocalGasLimit,
|
||||
kLocalBytecodeSizeLimit,
|
||||
kLocalGasPrice};
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
struct ExtensionFees : ::testing::Test
|
||||
{
|
||||
std::unordered_set<UInt256, beast::Uhash<>> presets;
|
||||
tests::TestNodeFamily family{beast::Journal{beast::Journal::getNullSink()}};
|
||||
|
||||
std::shared_ptr<Ledger>
|
||||
makeGenesis(std::vector<UInt256> const& amendments)
|
||||
{
|
||||
presets.insert(amendments.begin(), amendments.end());
|
||||
return std::make_shared<Ledger>(
|
||||
kCreateGenesis, Rules{presets}, localFees(), amendments, family);
|
||||
}
|
||||
|
||||
// Closed successor with FeeSettings rewritten by `edit`; setImmutable runs
|
||||
// Ledger::setup, so fees() is what the resolution produced. Its state is
|
||||
// flushed to the node store, so it can be reloaded.
|
||||
template <typename F>
|
||||
std::shared_ptr<Ledger>
|
||||
successorWithFeeSettings(Ledger const& parent, F&& edit)
|
||||
{
|
||||
auto ledger = std::make_shared<Ledger>(parent, NetClock::time_point{});
|
||||
auto const sle = std::make_shared<SLE>(*ledger->read(keylet::feeSettings()));
|
||||
edit(*sle);
|
||||
ledger->rawReplace(sle);
|
||||
ledger->stateMap().flushDirty(NodeObjectType::AccountNode);
|
||||
EXPECT_TRUE(ledger->setImmutable());
|
||||
return ledger;
|
||||
}
|
||||
|
||||
// Whether `ledger` loads back from the node store. Loading is the one
|
||||
// place the result of Ledger::setup is reported.
|
||||
bool
|
||||
reloads(Ledger const& ledger)
|
||||
{
|
||||
bool loaded = false;
|
||||
Ledger const reloaded(
|
||||
ledger.header(),
|
||||
loaded,
|
||||
false,
|
||||
Rules{presets},
|
||||
localFees(),
|
||||
family,
|
||||
beast::Journal{beast::Journal::getNullSink()});
|
||||
return loaded;
|
||||
}
|
||||
};
|
||||
|
||||
// test how Ledger::setup decides the three extension fee fields
|
||||
|
||||
// Genesis is the one place local configuration legitimately reaches the ledger.
|
||||
TEST_F(ExtensionFees, genesis_writes_the_configured_values)
|
||||
{
|
||||
auto const ledger = makeGenesis({featureSmartEscrow});
|
||||
|
||||
EXPECT_EQ(ledger->fees().gasLimit, kLocalGasLimit);
|
||||
EXPECT_EQ(ledger->fees().bytecodeSizeLimit, kLocalBytecodeSizeLimit);
|
||||
EXPECT_EQ(ledger->fees().gasPrice, kLocalGasPrice);
|
||||
}
|
||||
|
||||
// Absent must resolve to the protocol constant, never to the constructed Fees,
|
||||
// which every caller builds from local configuration.
|
||||
TEST_F(ExtensionFees, absent_fields_resolve_to_the_protocol_constants)
|
||||
{
|
||||
auto const genesis = makeGenesis({featureSmartEscrow});
|
||||
|
||||
auto const ledger = successorWithFeeSettings(*genesis, [](SLE& sle) {
|
||||
sle.makeFieldAbsent(sfGasLimit);
|
||||
sle.makeFieldAbsent(sfBytecodeSizeLimit);
|
||||
sle.makeFieldAbsent(sfGasPrice);
|
||||
});
|
||||
|
||||
ASSERT_TRUE(ledger->rules().enabled(featureSmartEscrow));
|
||||
|
||||
EXPECT_EQ(ledger->fees().gasLimit, kDefaultGasLimit);
|
||||
EXPECT_EQ(ledger->fees().bytecodeSizeLimit, kDefaultBytecodeSizeLimit);
|
||||
EXPECT_EQ(ledger->fees().gasPrice, kDefaultGasPrice);
|
||||
|
||||
EXPECT_NE(ledger->fees().gasLimit, kLocalGasLimit);
|
||||
EXPECT_NE(ledger->fees().bytecodeSizeLimit, kLocalBytecodeSizeLimit);
|
||||
EXPECT_NE(ledger->fees().gasPrice, kLocalGasPrice);
|
||||
}
|
||||
|
||||
// The fallback only fills gaps; a present field still governs.
|
||||
TEST_F(ExtensionFees, present_fields_are_taken_from_the_ledger)
|
||||
{
|
||||
constexpr std::uint32_t kVotedGasLimit{987'654};
|
||||
constexpr std::uint32_t kVotedBytecodeSizeLimit{87'654};
|
||||
constexpr std::uint32_t kVotedGasPrice{76'543};
|
||||
|
||||
auto const genesis = makeGenesis({featureSmartEscrow});
|
||||
|
||||
auto const ledger = successorWithFeeSettings(*genesis, [&](SLE& sle) {
|
||||
sle.at(sfGasLimit) = kVotedGasLimit;
|
||||
sle.at(sfBytecodeSizeLimit) = kVotedBytecodeSizeLimit;
|
||||
sle.at(sfGasPrice) = kVotedGasPrice;
|
||||
});
|
||||
|
||||
EXPECT_EQ(ledger->fees().gasLimit, kVotedGasLimit);
|
||||
EXPECT_EQ(ledger->fees().bytecodeSizeLimit, kVotedBytecodeSizeLimit);
|
||||
EXPECT_EQ(ledger->fees().gasPrice, kVotedGasPrice);
|
||||
}
|
||||
|
||||
// An explicit zero is the kill switch, not absence.
|
||||
TEST_F(ExtensionFees, an_explicit_zero_is_not_treated_as_absent)
|
||||
{
|
||||
auto const genesis = makeGenesis({featureSmartEscrow});
|
||||
|
||||
auto const ledger = successorWithFeeSettings(*genesis, [](SLE& sle) {
|
||||
sle.at(sfGasLimit) = 0u;
|
||||
sle.at(sfBytecodeSizeLimit) = 0u;
|
||||
});
|
||||
|
||||
EXPECT_EQ(ledger->fees().gasLimit, 0u);
|
||||
EXPECT_EQ(ledger->fees().bytecodeSizeLimit, 0u);
|
||||
}
|
||||
|
||||
// FeeSettings may carry the gas settings only once the amendment is enabled.
|
||||
TEST_F(ExtensionFees, gas_settings_before_the_amendment_fail_to_load)
|
||||
{
|
||||
auto const genesis = makeGenesis({});
|
||||
|
||||
auto const ledger =
|
||||
successorWithFeeSettings(*genesis, [](SLE& sle) { sle.at(sfGasLimit) = kDefaultGasLimit; });
|
||||
|
||||
ASSERT_FALSE(ledger->rules().enabled(featureSmartEscrow));
|
||||
EXPECT_FALSE(reloads(*ledger));
|
||||
}
|
||||
|
||||
TEST_F(ExtensionFees, gas_settings_after_the_amendment_load)
|
||||
{
|
||||
auto const genesis = makeGenesis({featureSmartEscrow});
|
||||
|
||||
auto const ledger =
|
||||
successorWithFeeSettings(*genesis, [](SLE& sle) { sle.at(sfGasLimit) = kDefaultGasLimit; });
|
||||
|
||||
ASSERT_TRUE(ledger->rules().enabled(featureSmartEscrow));
|
||||
EXPECT_TRUE(reloads(*ledger));
|
||||
}
|
||||
|
||||
} // namespace xrpl::test
|
||||
@@ -0,0 +1,50 @@
|
||||
#include <xrpl/ledger/helpers/TokenHelpers.h>
|
||||
|
||||
#include <xrpl/protocol/AccountID.h>
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
#include <xrpl/protocol/Issue.h>
|
||||
#include <xrpl/protocol/MPTIssue.h>
|
||||
#include <xrpl/protocol/Rate.h>
|
||||
#include <xrpl/protocol/STAmount.h>
|
||||
#include <xrpl/protocol/UintTypes.h>
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
using namespace xrpl;
|
||||
|
||||
namespace {
|
||||
|
||||
// A 25% transfer fee.
|
||||
Rate const kRate125{1'250'000'000};
|
||||
|
||||
AccountID const kIssuer{1};
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST(TokenHelpers, add_transfer_fee)
|
||||
{
|
||||
Issue const usd{toCurrency("USD"), kIssuer};
|
||||
MPTIssue const mpt{makeMptID(1, kIssuer)};
|
||||
|
||||
EXPECT_EQ(addTransferFee(STAmount{usd, 80}, kRate125), STAmount(usd, 100));
|
||||
EXPECT_EQ(addTransferFee(STAmount{usd, 80}, kParityRate), STAmount(usd, 80));
|
||||
|
||||
// Integral MPT: the fee on 1 unit rounds up to a whole unit.
|
||||
EXPECT_EQ(addTransferFee(STAmount{mpt, 80}, kRate125), STAmount(mpt, 100));
|
||||
EXPECT_EQ(addTransferFee(STAmount{mpt, 1}, kRate125), STAmount(mpt, 2));
|
||||
EXPECT_EQ(addTransferFee(STAmount{mpt, 1}, kParityRate), STAmount(mpt, 1));
|
||||
}
|
||||
|
||||
TEST(TokenHelpers, subtract_transfer_fee)
|
||||
{
|
||||
Issue const usd{toCurrency("USD"), kIssuer};
|
||||
MPTIssue const mpt{makeMptID(1, kIssuer)};
|
||||
|
||||
EXPECT_EQ(subtractTransferFee(STAmount{usd, 100}, kRate125), STAmount(usd, 80));
|
||||
EXPECT_EQ(subtractTransferFee(STAmount{usd, 100}, kParityRate), STAmount(usd, 100));
|
||||
|
||||
// Integral MPT: the payout rounds down, to zero for a single unit.
|
||||
EXPECT_EQ(subtractTransferFee(STAmount{mpt, 31}, kRate125), STAmount(mpt, 24));
|
||||
EXPECT_EQ(subtractTransferFee(STAmount{mpt, 1}, kRate125), STAmount(mpt));
|
||||
EXPECT_EQ(subtractTransferFee(STAmount{mpt, 1}, kParityRate), STAmount(mpt, 1));
|
||||
}
|
||||
@@ -0,0 +1,386 @@
|
||||
#include <xrpl/basics/Number.h>
|
||||
#include <xrpl/basics/base_uint.h>
|
||||
#include <xrpl/protocol/Asset.h>
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
#include <xrpl/protocol/Issue.h>
|
||||
#include <xrpl/protocol/MPTIssue.h>
|
||||
#include <xrpl/protocol/Protocol.h>
|
||||
#include <xrpl/protocol/SField.h>
|
||||
#include <xrpl/protocol/STAmount.h>
|
||||
#include <xrpl/protocol/STLedgerEntry.h>
|
||||
#include <xrpl/protocol/STNumber.h> // IWYU pragma: keep
|
||||
#include <xrpl/protocol/STTakesAsset.h>
|
||||
#include <xrpl/protocol/UintTypes.h>
|
||||
#include <xrpl/protocol/Units.h>
|
||||
#include <xrpl/tx/transactors/lending/LoanManage.h>
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
#include <helpers/Account.h>
|
||||
#include <protocol/VaultTestHelpers.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <random>
|
||||
#include <sstream>
|
||||
|
||||
namespace xrpl {
|
||||
namespace {
|
||||
|
||||
// Minimal bare LoanBroker SLE carrying the fields calculateDefaultCover reads:
|
||||
// CoverAvailable, DebtTotal, and the two cover rates. Not a valid ledger entry
|
||||
// on its own -- just enough for the function's own field reads.
|
||||
std::shared_ptr<SLE>
|
||||
makeBroker(
|
||||
Asset const& asset,
|
||||
Number const& coverAvailable,
|
||||
Number const& debtTotal,
|
||||
TenthBips32 coverRateMinimum,
|
||||
TenthBips32 coverRateLiquidation)
|
||||
{
|
||||
auto broker = std::make_shared<SLE>(keylet::loanBroker(uint256(2)));
|
||||
broker->at(sfCoverAvailable) = coverAvailable;
|
||||
broker->at(sfDebtTotal) = debtTotal;
|
||||
broker->at(sfCoverRateMinimum) = coverRateMinimum.value();
|
||||
broker->at(sfCoverRateLiquidation) = coverRateLiquidation.value();
|
||||
associateAsset(*broker, asset);
|
||||
return broker;
|
||||
}
|
||||
|
||||
// Minimal bare Loan SLE carrying the only field calculateDefaultCover reads:
|
||||
// PrincipalOutstanding.
|
||||
std::shared_ptr<SLE>
|
||||
makeLoan(Asset const& asset, Number const& principalOutstanding)
|
||||
{
|
||||
auto loan = std::make_shared<SLE>(keylet::loan(uint256(3)));
|
||||
loan->at(sfPrincipalOutstanding) = principalOutstanding;
|
||||
associateAsset(*loan, asset);
|
||||
return loan;
|
||||
}
|
||||
|
||||
// Replicates calculateDefaultCover's uncapped XLS-66 raw cover computation
|
||||
// (same Upward rounding), so tests can independently verify the
|
||||
// CoverAvailable/PrincipalOutstanding cap without duplicating
|
||||
// calculateDefaultCover itself.
|
||||
Number
|
||||
rawCoverFor(
|
||||
Number const& debtTotal,
|
||||
TenthBips32 coverRateMinimum,
|
||||
TenthBips32 coverRateLiquidation,
|
||||
Number const& principalOutstanding)
|
||||
{
|
||||
NumberRoundModeGuard const mg(Number::RoundingMode::Upward);
|
||||
Number const minimumCover = tenthBipsOfValue(debtTotal, coverRateMinimum);
|
||||
return std::min(tenthBipsOfValue(minimumCover, coverRateLiquidation), principalOutstanding);
|
||||
}
|
||||
|
||||
struct CoverCase
|
||||
{
|
||||
Number assetsAvailable;
|
||||
Number coverAvailable;
|
||||
Number debtTotal;
|
||||
TenthBips32 coverRateMinimum;
|
||||
TenthBips32 coverRateLiquidation;
|
||||
Number principalOutstanding;
|
||||
};
|
||||
|
||||
// Builds the ledger entries for one case, calls calculateDefaultCover, and
|
||||
// checks every invariant defaultLoanFixedPrecision itself asserts right after
|
||||
// the call: cover is non-negative, never exceeds the uncapped XLS-66 amount
|
||||
// or the broker's CoverAvailable, leaves AssetsAvailable + cover exactly
|
||||
// representable (STAmount round-trips to the same Number) whenever the cover
|
||||
// can carry AssetsAvailable's digits, and rounds CoverAvailable - cover by at
|
||||
// most half an ulp.
|
||||
void
|
||||
checkCoverCase(Asset const& asset, std::uint8_t scale, CoverCase const& in)
|
||||
{
|
||||
auto const vault = makeVault(
|
||||
asset,
|
||||
in.assetsAvailable,
|
||||
VaultVersion::FixedPrecision,
|
||||
scale,
|
||||
Number{0},
|
||||
in.assetsAvailable);
|
||||
auto const broker = makeBroker(
|
||||
asset, in.coverAvailable, in.debtTotal, in.coverRateMinimum, in.coverRateLiquidation);
|
||||
auto const loan = makeLoan(asset, in.principalOutstanding);
|
||||
|
||||
STAmount const cover = LoanManage::calculateDefaultCover(loan, broker, vault);
|
||||
|
||||
Number const rawCover = rawCoverFor(
|
||||
in.debtTotal, in.coverRateMinimum, in.coverRateLiquidation, in.principalOutstanding);
|
||||
|
||||
std::ostringstream context;
|
||||
context << "scale=" << int(scale) << " assetsAvailable=" << in.assetsAvailable
|
||||
<< " coverAvailable=" << in.coverAvailable << " debtTotal=" << in.debtTotal
|
||||
<< " coverRateMinimum=" << in.coverRateMinimum.value()
|
||||
<< " coverRateLiquidation=" << in.coverRateLiquidation.value()
|
||||
<< " principalOutstanding=" << in.principalOutstanding << " rawCover=" << rawCover
|
||||
<< " cover=" << Number(cover);
|
||||
|
||||
EXPECT_GE(Number(cover), Number{0}) << "cover < 0: " << context.str();
|
||||
EXPECT_LE(Number(cover), rawCover) << "cover exceeds uncapped XLS-66 amount: " << context.str();
|
||||
EXPECT_LE(Number(cover), in.coverAvailable)
|
||||
<< "cover exceeds CoverAvailable: " << context.str();
|
||||
|
||||
// AssetsAvailable + cover is exact unless AssetsAvailable has digits finer
|
||||
// than the 16-digit cover can carry; then no cover cancels them, and the
|
||||
// sum rounds by at most half an ulp of its posterior grid.
|
||||
Number const vaultSum = in.assetsAvailable + Number(cover);
|
||||
STAmount const vaultRounded{asset, vaultSum};
|
||||
bool const coverCanCarryAssetsAvailable =
|
||||
roundToAsset(
|
||||
asset,
|
||||
in.assetsAvailable,
|
||||
xrpl::scale(Number(cover), asset),
|
||||
Number::RoundingMode::TowardsZero) == in.assetsAvailable;
|
||||
if (coverCanCarryAssetsAvailable)
|
||||
{
|
||||
EXPECT_EQ(Number(vaultRounded), vaultSum)
|
||||
<< "AssetsAvailable + cover not exactly 16-digit representable: " << context.str();
|
||||
}
|
||||
else
|
||||
{
|
||||
Number const vaultRoundingError = abs(Number(vaultRounded) - vaultSum);
|
||||
EXPECT_LE(vaultRoundingError, Number(5, xrpl::scale(Number(vaultRounded), asset) - 1))
|
||||
<< "AssetsAvailable + cover rounding error exceeds half a posterior ulp: "
|
||||
<< context.str();
|
||||
}
|
||||
|
||||
// The LoanBroker absorbs the rounding: CoverAvailable - cover rounds to
|
||||
// nearest, so the error stays within half an ulp of its posterior grid.
|
||||
Number const brokerSum = in.coverAvailable - Number(cover);
|
||||
STAmount const brokerRounded{asset, brokerSum};
|
||||
Number const brokerRoundingError = abs(Number(brokerRounded) - brokerSum);
|
||||
EXPECT_LE(brokerRoundingError, Number(5, xrpl::scale(Number(brokerRounded), asset) - 1))
|
||||
<< "CoverAvailable - cover rounding error exceeds half a posterior ulp: " << context.str();
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST(LoanDefaultCover, hand_picked_cases)
|
||||
{
|
||||
test::Account const issuer{"issuer"};
|
||||
Issue const iou{toCurrency("USD"), issuer.id()};
|
||||
std::uint8_t const scale = 6;
|
||||
|
||||
TenthBips32 const halfRate = percentageToTenthBips(50);
|
||||
TenthBips32 const fullRate = lending::kMaxCoverRate;
|
||||
|
||||
// Open zone: AssetsAvailable and CoverAvailable both well under the Open
|
||||
// limit (9 * 10^9 at Scale 6); every grid involved stays at the fine
|
||||
// (scale-6) exponent.
|
||||
checkCoverCase(
|
||||
iou,
|
||||
scale,
|
||||
{
|
||||
.assetsAvailable = Number{1000},
|
||||
.coverAvailable = Number{5000},
|
||||
.debtTotal = Number{10000},
|
||||
.coverRateMinimum = halfRate,
|
||||
.coverRateLiquidation = halfRate,
|
||||
.principalOutstanding = Number{2000},
|
||||
});
|
||||
|
||||
// CoverAvailable coarsened above the Open limit (20,000,000,000.00000,
|
||||
// 16 significant digits); AssetsAvailable stays small and fine.
|
||||
Number const coarseCoverAvailable{2'000'000'000'000'000, -5};
|
||||
checkCoverCase(
|
||||
iou,
|
||||
scale,
|
||||
{
|
||||
.assetsAvailable = Number{100},
|
||||
.coverAvailable = coarseCoverAvailable,
|
||||
.debtTotal = Number{1000},
|
||||
.coverRateMinimum = fullRate,
|
||||
.coverRateLiquidation = fullRate,
|
||||
.principalOutstanding = Number{500},
|
||||
});
|
||||
|
||||
// AssetsAvailable one unit below a power of ten (9,999,999,999.999999,
|
||||
// 16 significant digits already); CoverAvailable stays small and fine.
|
||||
Number const coarseAssetsAvailable{9'999'999'999'999'999, -6};
|
||||
checkCoverCase(
|
||||
iou,
|
||||
scale,
|
||||
{
|
||||
.assetsAvailable = coarseAssetsAvailable,
|
||||
.coverAvailable = Number{50},
|
||||
.debtTotal = Number{1000},
|
||||
.coverRateMinimum = fullRate,
|
||||
.coverRateLiquidation = fullRate,
|
||||
.principalOutstanding = Number{30},
|
||||
});
|
||||
|
||||
// Both AssetsAvailable and CoverAvailable coarsened.
|
||||
checkCoverCase(
|
||||
iou,
|
||||
scale,
|
||||
{
|
||||
.assetsAvailable = coarseAssetsAvailable,
|
||||
.coverAvailable = coarseCoverAvailable,
|
||||
.debtTotal = Number{1000},
|
||||
.coverRateMinimum = fullRate,
|
||||
.coverRateLiquidation = fullRate,
|
||||
.principalOutstanding = Number{900},
|
||||
});
|
||||
|
||||
// Cover capped by CoverAvailable: the uncapped XLS-66 amount vastly
|
||||
// exceeds the broker's available cover.
|
||||
checkCoverCase(
|
||||
iou,
|
||||
scale,
|
||||
{
|
||||
.assetsAvailable = Number{1000},
|
||||
.coverAvailable = Number{1},
|
||||
.debtTotal = Number{1'000'000},
|
||||
.coverRateMinimum = fullRate,
|
||||
.coverRateLiquidation = fullRate,
|
||||
.principalOutstanding = Number{1'000'000},
|
||||
});
|
||||
|
||||
// Cover capped by PrincipalOutstanding: DebtTotal times both rates would
|
||||
// exceed the Loan's own outstanding principal.
|
||||
checkCoverCase(
|
||||
iou,
|
||||
scale,
|
||||
{
|
||||
.assetsAvailable = Number{1000},
|
||||
.coverAvailable = Number{1'000'000},
|
||||
.debtTotal = Number{1'000'000},
|
||||
.coverRateMinimum = fullRate,
|
||||
.coverRateLiquidation = fullRate,
|
||||
.principalOutstanding = Number{5},
|
||||
});
|
||||
}
|
||||
|
||||
// The raw cover carries more than 16 digits and is large enough that its last
|
||||
// 16-digit place is coarser than AssetsAvailable's grid. The sum must be floored
|
||||
// from the exact raw cover: truncating the cover to 16 digits first could cost
|
||||
// the Vault one posterior unit.
|
||||
TEST(LoanDefaultCover, uses_exact_raw_cover)
|
||||
{
|
||||
test::Account const issuer{"issuer"};
|
||||
Issue const iou{toCurrency("USD"), issuer.id()};
|
||||
std::uint8_t const scale = 6;
|
||||
|
||||
Number const assetsAvailable{5, -6};
|
||||
Number const coverAvailable{2, 11};
|
||||
Number const debtTotal{1'234'567'890'123'457LL, -4};
|
||||
TenthBips32 const rate = percentageToTenthBips(33);
|
||||
Number const principalOutstanding{2, 11};
|
||||
|
||||
auto const vault = makeVault(
|
||||
iou, assetsAvailable, VaultVersion::FixedPrecision, scale, Number{0}, assetsAvailable);
|
||||
auto const broker = makeBroker(iou, coverAvailable, debtTotal, rate, rate);
|
||||
auto const loan = makeLoan(iou, principalOutstanding);
|
||||
|
||||
Number const rawCover = rawCoverFor(debtTotal, rate, rate, principalOutstanding);
|
||||
// The case only means something if truncating the raw cover to 16 digits
|
||||
// loses digits that its sum with AssetsAvailable would keep.
|
||||
ASSERT_NE(Number(STAmount{iou, rawCover}), rawCover);
|
||||
|
||||
STAmount const cover = LoanManage::calculateDefaultCover(loan, broker, vault);
|
||||
|
||||
// Floor the exact sum, then take the 16-digit credit, also rounded down.
|
||||
STAmount const expected = [&] {
|
||||
NumberRoundModeGuard const mg(Number::RoundingMode::Downward);
|
||||
Number const sum = assetsAvailable + rawCover;
|
||||
Number const flooredSum =
|
||||
roundToAsset(iou, sum, xrpl::scale(sum, iou), Number::RoundingMode::Downward);
|
||||
return STAmount{iou, flooredSum - assetsAvailable};
|
||||
}();
|
||||
EXPECT_EQ(cover, expected) << "rawCover=" << rawCover << " cover=" << Number(cover);
|
||||
}
|
||||
|
||||
TEST(LoanDefaultCover, integral_asset_cases)
|
||||
{
|
||||
test::Account const issuer{"issuer"};
|
||||
TenthBips32 const fullRate = lending::kMaxCoverRate;
|
||||
|
||||
for (Asset const asset : {Asset{xrpIssue()}, Asset{MPTIssue{makeMptID(1, issuer.id())}}})
|
||||
{
|
||||
// Integral assets have no fractional grid (Scale is unused), so the
|
||||
// same cover arithmetic must still land exactly for whole-unit
|
||||
// amounts.
|
||||
checkCoverCase(
|
||||
asset,
|
||||
0,
|
||||
{
|
||||
.assetsAvailable = Number{1000},
|
||||
.coverAvailable = Number{200},
|
||||
.debtTotal = Number{900},
|
||||
.coverRateMinimum = fullRate,
|
||||
.coverRateLiquidation = fullRate,
|
||||
.principalOutstanding = Number{150},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
TEST(LoanDefaultCover, randomized_sweep)
|
||||
{
|
||||
test::Account const issuer{"issuer"};
|
||||
Issue const iou{toCurrency("USD"), issuer.id()};
|
||||
|
||||
// NOLINTNEXTLINE(bugprone-random-generator-seed): fixed seed for reproducible test
|
||||
std::mt19937 rng{20261002u};
|
||||
constexpr int kIterations = 5000;
|
||||
|
||||
auto const pickScale = [&rng] -> std::uint8_t {
|
||||
std::uniform_int_distribution<int> dist(0, kFixedPrecisionVaultMaximumIouScale);
|
||||
return static_cast<std::uint8_t>(dist(rng));
|
||||
};
|
||||
auto const pickRate = [&rng] -> TenthBips32 {
|
||||
std::uniform_int_distribution<std::uint32_t> dist(0, lending::kMaxCoverRate.value());
|
||||
return TenthBips32{dist(rng)};
|
||||
};
|
||||
// Mantissa with 1 to 16 digits, biased toward landing exactly on a power
|
||||
// of ten or one unit below it (all nines) -- the cusps where coarsening
|
||||
// and carries are most likely to surface a bug. Always positive, since
|
||||
// these stand in for ledger balances.
|
||||
auto const pickMantissa = [&rng] -> std::int64_t {
|
||||
std::uniform_int_distribution<int> digitsDist(1, 16);
|
||||
int const digits = digitsDist(rng);
|
||||
std::int64_t powerOfTen = 1;
|
||||
for (int i = 0; i < digits - 1; ++i)
|
||||
powerOfTen *= 10;
|
||||
|
||||
std::uniform_int_distribution<int> shapeDist(0, 9);
|
||||
int const shape = shapeDist(rng);
|
||||
if (shape == 0)
|
||||
return powerOfTen; // bare power of ten
|
||||
if (shape == 1)
|
||||
return (powerOfTen * 10) - 1; // all nines, one digit up
|
||||
|
||||
std::uniform_int_distribution<std::int64_t> mantissaDist(powerOfTen, (powerOfTen * 10) - 1);
|
||||
return mantissaDist(rng);
|
||||
};
|
||||
// A FixedPrecision Vault never holds a balance finer than its own Scale
|
||||
// (exponent must be >= -scale); minExponent enforces that so every
|
||||
// generated case is a state the real system could actually reach.
|
||||
auto const pickExponent = [&rng](int minExponent) -> int {
|
||||
std::uniform_int_distribution<int> dist(minExponent, minExponent + 25);
|
||||
return dist(rng);
|
||||
};
|
||||
auto const pickNumber = [&](int minExponent) -> Number {
|
||||
return Number{pickMantissa(), pickExponent(minExponent)};
|
||||
};
|
||||
|
||||
for (int i = 0; i < kIterations; ++i)
|
||||
{
|
||||
std::uint8_t const scale = pickScale();
|
||||
int const minExponent = -static_cast<int>(scale);
|
||||
CoverCase const in{
|
||||
.assetsAvailable = pickNumber(minExponent),
|
||||
.coverAvailable = pickNumber(minExponent),
|
||||
.debtTotal = pickNumber(minExponent),
|
||||
.coverRateMinimum = pickRate(),
|
||||
.coverRateLiquidation = pickRate(),
|
||||
.principalOutstanding = pickNumber(minExponent),
|
||||
};
|
||||
|
||||
checkCoverCase(iou, scale, in);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace xrpl
|
||||
@@ -0,0 +1,87 @@
|
||||
#include <xrpl/protocol/STAmount.h>
|
||||
|
||||
#include <xrpl/basics/Number.h>
|
||||
#include <xrpl/protocol/AccountID.h>
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
#include <xrpl/protocol/Issue.h>
|
||||
#include <xrpl/protocol/MPTIssue.h>
|
||||
#include <xrpl/protocol/Protocol.h>
|
||||
#include <xrpl/protocol/Quality.h>
|
||||
#include <xrpl/protocol/UintTypes.h>
|
||||
#include <xrpl/protocol/XRPAmount.h>
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <stdexcept>
|
||||
|
||||
using namespace xrpl;
|
||||
|
||||
namespace {
|
||||
|
||||
// A 25% transfer fee.
|
||||
constexpr std::uint32_t kTransferRate125 = 1'250'000'000;
|
||||
|
||||
AccountID const kUsdIssuer{1};
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST(STAmount, mul_ratio_iou)
|
||||
{
|
||||
Issue const usd{toCurrency("USD"), kUsdIssuer};
|
||||
|
||||
EXPECT_EQ(
|
||||
mulRatio(STAmount{usd, 100}, QUALITY_ONE, kTransferRate125, Number::RoundingMode::Downward),
|
||||
STAmount(usd, 80));
|
||||
EXPECT_EQ(
|
||||
mulRatio(STAmount{usd, 80}, kTransferRate125, QUALITY_ONE, Number::RoundingMode::Upward),
|
||||
STAmount(usd, 100));
|
||||
|
||||
// 1 / 1.7 does not fit in a 16-digit mantissa, so the rounding direction
|
||||
// decides the last digit.
|
||||
STAmount const one{usd, 1};
|
||||
EXPECT_LT(
|
||||
mulRatio(one, QUALITY_ONE, 1'700'000'000, Number::RoundingMode::Downward),
|
||||
mulRatio(one, QUALITY_ONE, 1'700'000'000, Number::RoundingMode::Upward));
|
||||
|
||||
STAmount const full{usd, std::uint64_t{9'999'999'999'999'999}, -5};
|
||||
EXPECT_EQ(mulRatio(full, QUALITY_ONE, QUALITY_ONE, Number::RoundingMode::Downward), full);
|
||||
}
|
||||
|
||||
TEST(STAmount, mul_ratio_mpt)
|
||||
{
|
||||
MPTIssue const mpt{makeMptID(1, kUsdIssuer)};
|
||||
|
||||
EXPECT_EQ(
|
||||
mulRatio(STAmount{mpt, 31}, QUALITY_ONE, kTransferRate125, Number::RoundingMode::Downward),
|
||||
STAmount(mpt, 24));
|
||||
EXPECT_EQ(
|
||||
mulRatio(STAmount{mpt, 31}, QUALITY_ONE, kTransferRate125, Number::RoundingMode::Upward),
|
||||
STAmount(mpt, 25));
|
||||
EXPECT_EQ(
|
||||
mulRatio(STAmount{mpt, 1}, QUALITY_ONE, kTransferRate125, Number::RoundingMode::Downward),
|
||||
STAmount(mpt));
|
||||
EXPECT_EQ(
|
||||
mulRatio(STAmount{mpt, 1}, QUALITY_ONE, kTransferRate125, Number::RoundingMode::Upward),
|
||||
STAmount(mpt, 1));
|
||||
|
||||
STAmount const max{mpt, kMaxMpTokenAmount};
|
||||
EXPECT_EQ(mulRatio(max, QUALITY_ONE, QUALITY_ONE, Number::RoundingMode::Downward), max);
|
||||
EXPECT_NO_THROW(
|
||||
(void)mulRatio(max, QUALITY_ONE, kTransferRate125, Number::RoundingMode::Downward));
|
||||
EXPECT_THROW(
|
||||
(void)mulRatio(max, kTransferRate125, QUALITY_ONE, Number::RoundingMode::Upward),
|
||||
std::overflow_error);
|
||||
}
|
||||
|
||||
TEST(STAmount, mul_ratio_xrp)
|
||||
{
|
||||
EXPECT_EQ(
|
||||
mulRatio(
|
||||
STAmount{XRPAmount{31}}, QUALITY_ONE, kTransferRate125, Number::RoundingMode::Downward),
|
||||
STAmount{XRPAmount{24}});
|
||||
EXPECT_EQ(
|
||||
mulRatio(
|
||||
STAmount{XRPAmount{31}}, QUALITY_ONE, kTransferRate125, Number::RoundingMode::Upward),
|
||||
STAmount{XRPAmount{25}});
|
||||
}
|
||||
@@ -546,6 +546,22 @@ TEST_F(VaultBalance, credit_to_posterior_scale_ignores_ambient_rounding_mode)
|
||||
EXPECT_EQ(Number(credit), exactDownwardCredit);
|
||||
}
|
||||
|
||||
TEST_F(VaultBalance, credit_to_posterior_scale_just_under_power_of_ten_is_not_negative)
|
||||
{
|
||||
// reference is all nines on the scale -10 grid and raw is just under one
|
||||
// unit of it. The exact sum is just under 10^6, but rounds up to it to
|
||||
// nearest. The grid must come from the sum that is floored, so the credit
|
||||
// is zero, not negative.
|
||||
Number const reference{9'999'999'999'999'999LL, -10};
|
||||
Number const raw{9'970'000'001LL, -20};
|
||||
int const baseScale = -10;
|
||||
|
||||
NumberRoundModeGuard const ambient(Number::RoundingMode::ToNearest);
|
||||
STAmount const credit = detail::creditToPosteriorScale(
|
||||
iou_, reference, baseScale, raw, Number::RoundingMode::Downward);
|
||||
EXPECT_EQ(credit, STAmount(iou_, 0));
|
||||
}
|
||||
|
||||
// creditToPosteriorAvailableScale returns
|
||||
// floor16(AssetsAvailable + raw) - AssetsAvailable, never finer than -Scale.
|
||||
|
||||
|
||||
@@ -38,6 +38,7 @@ TEST(MPTokenIssuanceTests, BuilderSettersRoundTrip)
|
||||
auto const auditorEncryptionKeyValue = canonical_VL();
|
||||
auto const issuerKeyEpochValue = canonical_UINT32();
|
||||
auto const auditorKeyEpochValue = canonical_UINT32();
|
||||
auto const initialIssuerEncryptionKeyValue = canonical_VL();
|
||||
auto const confidentialOutstandingAmountValue = canonical_UINT64();
|
||||
|
||||
MPTokenIssuanceBuilder builder{
|
||||
@@ -61,6 +62,7 @@ TEST(MPTokenIssuanceTests, BuilderSettersRoundTrip)
|
||||
builder.setAuditorEncryptionKey(auditorEncryptionKeyValue);
|
||||
builder.setIssuerKeyEpoch(issuerKeyEpochValue);
|
||||
builder.setAuditorKeyEpoch(auditorKeyEpochValue);
|
||||
builder.setInitialIssuerEncryptionKey(initialIssuerEncryptionKeyValue);
|
||||
builder.setConfidentialOutstandingAmount(confidentialOutstandingAmountValue);
|
||||
|
||||
builder.setLedgerIndex(index);
|
||||
@@ -204,6 +206,14 @@ TEST(MPTokenIssuanceTests, BuilderSettersRoundTrip)
|
||||
EXPECT_TRUE(entry.hasAuditorKeyEpoch());
|
||||
}
|
||||
|
||||
{
|
||||
auto const& expected = initialIssuerEncryptionKeyValue;
|
||||
auto const actualOpt = entry.getInitialIssuerEncryptionKey();
|
||||
ASSERT_TRUE(actualOpt.has_value());
|
||||
expectEqualField(expected, *actualOpt, "sfInitialIssuerEncryptionKey");
|
||||
EXPECT_TRUE(entry.hasInitialIssuerEncryptionKey());
|
||||
}
|
||||
|
||||
{
|
||||
auto const& expected = confidentialOutstandingAmountValue;
|
||||
auto const actualOpt = entry.getConfidentialOutstandingAmount();
|
||||
@@ -243,6 +253,7 @@ TEST(MPTokenIssuanceTests, BuilderFromSleRoundTrip)
|
||||
auto const auditorEncryptionKeyValue = canonical_VL();
|
||||
auto const issuerKeyEpochValue = canonical_UINT32();
|
||||
auto const auditorKeyEpochValue = canonical_UINT32();
|
||||
auto const initialIssuerEncryptionKeyValue = canonical_VL();
|
||||
auto const confidentialOutstandingAmountValue = canonical_UINT64();
|
||||
|
||||
auto sle = std::make_shared<SLE>(MPTokenIssuance::entryType, index);
|
||||
@@ -265,6 +276,7 @@ TEST(MPTokenIssuanceTests, BuilderFromSleRoundTrip)
|
||||
sle->at(sfAuditorEncryptionKey) = auditorEncryptionKeyValue;
|
||||
sle->at(sfIssuerKeyEpoch) = issuerKeyEpochValue;
|
||||
sle->at(sfAuditorKeyEpoch) = auditorKeyEpochValue;
|
||||
sle->at(sfInitialIssuerEncryptionKey) = initialIssuerEncryptionKeyValue;
|
||||
sle->at(sfConfidentialOutstandingAmount) = confidentialOutstandingAmountValue;
|
||||
|
||||
MPTokenIssuanceBuilder builderFromSle{sle};
|
||||
@@ -492,6 +504,19 @@ TEST(MPTokenIssuanceTests, BuilderFromSleRoundTrip)
|
||||
expectEqualField(expected, *fromBuilderOpt, "sfAuditorKeyEpoch");
|
||||
}
|
||||
|
||||
{
|
||||
auto const& expected = initialIssuerEncryptionKeyValue;
|
||||
|
||||
auto const fromSleOpt = entryFromSle.getInitialIssuerEncryptionKey();
|
||||
auto const fromBuilderOpt = entryFromBuilder.getInitialIssuerEncryptionKey();
|
||||
|
||||
ASSERT_TRUE(fromSleOpt.has_value());
|
||||
ASSERT_TRUE(fromBuilderOpt.has_value());
|
||||
|
||||
expectEqualField(expected, *fromSleOpt, "sfInitialIssuerEncryptionKey");
|
||||
expectEqualField(expected, *fromBuilderOpt, "sfInitialIssuerEncryptionKey");
|
||||
}
|
||||
|
||||
{
|
||||
auto const& expected = confidentialOutstandingAmountValue;
|
||||
|
||||
@@ -593,6 +618,8 @@ TEST(MPTokenIssuanceTests, OptionalFieldsReturnNullopt)
|
||||
EXPECT_FALSE(entry.getIssuerKeyEpoch().has_value());
|
||||
EXPECT_FALSE(entry.hasAuditorKeyEpoch());
|
||||
EXPECT_FALSE(entry.getAuditorKeyEpoch().has_value());
|
||||
EXPECT_FALSE(entry.hasInitialIssuerEncryptionKey());
|
||||
EXPECT_FALSE(entry.getInitialIssuerEncryptionKey().has_value());
|
||||
EXPECT_FALSE(entry.hasConfidentialOutstandingAmount());
|
||||
EXPECT_FALSE(entry.getConfidentialOutstandingAmount().has_value());
|
||||
}
|
||||
|
||||
@@ -34,6 +34,7 @@ TEST(MPTokenTests, BuilderSettersRoundTrip)
|
||||
auto const auditorEncryptedBalanceValue = canonical_VL();
|
||||
auto const issuerKeyMirrorEpochValue = canonical_UINT32();
|
||||
auto const auditorKeyMirrorEpochValue = canonical_UINT32();
|
||||
auto const issuerMirrorEncryptionKeyValue = canonical_VL();
|
||||
auto const holderEncryptionKeyValue = canonical_VL();
|
||||
auto const recoveryKeyValue = canonical_VL();
|
||||
|
||||
@@ -54,6 +55,7 @@ TEST(MPTokenTests, BuilderSettersRoundTrip)
|
||||
builder.setAuditorEncryptedBalance(auditorEncryptedBalanceValue);
|
||||
builder.setIssuerKeyMirrorEpoch(issuerKeyMirrorEpochValue);
|
||||
builder.setAuditorKeyMirrorEpoch(auditorKeyMirrorEpochValue);
|
||||
builder.setIssuerMirrorEncryptionKey(issuerMirrorEncryptionKeyValue);
|
||||
builder.setHolderEncryptionKey(holderEncryptionKeyValue);
|
||||
builder.setRecoveryKey(recoveryKeyValue);
|
||||
|
||||
@@ -168,6 +170,14 @@ TEST(MPTokenTests, BuilderSettersRoundTrip)
|
||||
EXPECT_TRUE(entry.hasAuditorKeyMirrorEpoch());
|
||||
}
|
||||
|
||||
{
|
||||
auto const& expected = issuerMirrorEncryptionKeyValue;
|
||||
auto const actualOpt = entry.getIssuerMirrorEncryptionKey();
|
||||
ASSERT_TRUE(actualOpt.has_value());
|
||||
expectEqualField(expected, *actualOpt, "sfIssuerMirrorEncryptionKey");
|
||||
EXPECT_TRUE(entry.hasIssuerMirrorEncryptionKey());
|
||||
}
|
||||
|
||||
{
|
||||
auto const& expected = holderEncryptionKeyValue;
|
||||
auto const actualOpt = entry.getHolderEncryptionKey();
|
||||
@@ -211,6 +221,7 @@ TEST(MPTokenTests, BuilderFromSleRoundTrip)
|
||||
auto const auditorEncryptedBalanceValue = canonical_VL();
|
||||
auto const issuerKeyMirrorEpochValue = canonical_UINT32();
|
||||
auto const auditorKeyMirrorEpochValue = canonical_UINT32();
|
||||
auto const issuerMirrorEncryptionKeyValue = canonical_VL();
|
||||
auto const holderEncryptionKeyValue = canonical_VL();
|
||||
auto const recoveryKeyValue = canonical_VL();
|
||||
|
||||
@@ -230,6 +241,7 @@ TEST(MPTokenTests, BuilderFromSleRoundTrip)
|
||||
sle->at(sfAuditorEncryptedBalance) = auditorEncryptedBalanceValue;
|
||||
sle->at(sfIssuerKeyMirrorEpoch) = issuerKeyMirrorEpochValue;
|
||||
sle->at(sfAuditorKeyMirrorEpoch) = auditorKeyMirrorEpochValue;
|
||||
sle->at(sfIssuerMirrorEncryptionKey) = issuerMirrorEncryptionKeyValue;
|
||||
sle->at(sfHolderEncryptionKey) = holderEncryptionKeyValue;
|
||||
sle->at(sfRecoveryKey) = recoveryKeyValue;
|
||||
|
||||
@@ -409,6 +421,19 @@ TEST(MPTokenTests, BuilderFromSleRoundTrip)
|
||||
expectEqualField(expected, *fromBuilderOpt, "sfAuditorKeyMirrorEpoch");
|
||||
}
|
||||
|
||||
{
|
||||
auto const& expected = issuerMirrorEncryptionKeyValue;
|
||||
|
||||
auto const fromSleOpt = entryFromSle.getIssuerMirrorEncryptionKey();
|
||||
auto const fromBuilderOpt = entryFromBuilder.getIssuerMirrorEncryptionKey();
|
||||
|
||||
ASSERT_TRUE(fromSleOpt.has_value());
|
||||
ASSERT_TRUE(fromBuilderOpt.has_value());
|
||||
|
||||
expectEqualField(expected, *fromSleOpt, "sfIssuerMirrorEncryptionKey");
|
||||
expectEqualField(expected, *fromBuilderOpt, "sfIssuerMirrorEncryptionKey");
|
||||
}
|
||||
|
||||
{
|
||||
auto const& expected = holderEncryptionKeyValue;
|
||||
|
||||
@@ -515,6 +540,8 @@ TEST(MPTokenTests, OptionalFieldsReturnNullopt)
|
||||
EXPECT_FALSE(entry.getIssuerKeyMirrorEpoch().has_value());
|
||||
EXPECT_FALSE(entry.hasAuditorKeyMirrorEpoch());
|
||||
EXPECT_FALSE(entry.getAuditorKeyMirrorEpoch().has_value());
|
||||
EXPECT_FALSE(entry.hasIssuerMirrorEncryptionKey());
|
||||
EXPECT_FALSE(entry.getIssuerMirrorEncryptionKey().has_value());
|
||||
EXPECT_FALSE(entry.hasHolderEncryptionKey());
|
||||
EXPECT_FALSE(entry.getHolderEncryptionKey().has_value());
|
||||
EXPECT_FALSE(entry.hasRecoveryKey());
|
||||
|
||||
Loaded 100 of 122 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user