mirror of
https://github.com/XRPLF/rippled.git
synced 2026-10-22 03:27:08 +00:00
Compare commits
52
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b9c1133093 | ||
|
|
9c4410b62e | ||
|
|
8460135936 | ||
|
|
4bf649e9ef | ||
|
|
4d7788ff4a | ||
|
|
6f93cbc011 | ||
|
|
fbed72cedb | ||
|
|
f1f4418d59 | ||
|
|
b64da840c8 | ||
|
|
9869d6e75d | ||
|
|
e079186a2f | ||
|
|
29d8f1d18b | ||
|
|
92225d2838 | ||
|
|
4f7188dc98 | ||
|
|
b12dd38d5c | ||
|
|
70cad8850d | ||
|
|
4a1e0cc7fe | ||
|
|
404be5b054 | ||
|
|
922885a4f8 | ||
|
|
012926ead9 | ||
|
|
9a0b70b45d | ||
|
|
6311292c2b | ||
|
|
4f98a09b65 | ||
|
|
eabfa77c77 | ||
|
|
d689047950 | ||
|
|
4d4ac414ae | ||
|
|
6e751091b1 | ||
|
|
d6459c8ac7 | ||
|
|
40cb63f423 | ||
|
|
98206e6514 | ||
|
|
a047d1bc9b | ||
|
|
2ca91e701e | ||
|
|
3db7e84e06 | ||
|
|
75d0960e1d | ||
|
|
091709e7d7 | ||
|
|
b5756c44bc | ||
|
|
613a94645d | ||
|
|
c0323540f9 | ||
|
|
e0fd480ae7 | ||
|
|
fe80f0e895 | ||
|
|
9988e596e9 | ||
|
|
3523c437a8 | ||
|
|
0f38b4b541 | ||
|
|
f84350c61c | ||
|
|
7a118245f7 | ||
|
|
47ddc34fda | ||
|
|
1f579efc2f | ||
|
|
e464e101be | ||
|
|
4dfa6db32a | ||
|
|
766124ed6d | ||
|
|
5c34a7b8fb | ||
|
|
f6f3542b7e |
No files matched your search
@@ -103,7 +103,6 @@ words:
|
||||
- dearmor
|
||||
- decryptor
|
||||
- dedented
|
||||
- dedup
|
||||
- deleteme
|
||||
- demultiplexer
|
||||
- deserializaton
|
||||
@@ -345,7 +344,6 @@ words:
|
||||
- unambiguity
|
||||
- unauthorizes
|
||||
- unauthorizing
|
||||
- undeserializable
|
||||
- unergonomic
|
||||
- unfetched
|
||||
- unfindable
|
||||
|
||||
@@ -62,7 +62,6 @@ libxrpl.tx > xrpl.protocol
|
||||
libxrpl.tx > xrpl.server
|
||||
libxrpl.tx > xrpl.tx
|
||||
test.app > test.jtx
|
||||
test.app > tests.libxrpl
|
||||
test.app > test.unit_test
|
||||
test.app > xrpl.basics
|
||||
test.app > xrpl.config
|
||||
|
||||
@@ -8,7 +8,6 @@ on:
|
||||
branches:
|
||||
- "develop"
|
||||
- "release/*"
|
||||
- "staging/*"
|
||||
paths:
|
||||
- "crates/**/Cargo.toml"
|
||||
- "crates/Cargo.lock"
|
||||
|
||||
@@ -243,7 +243,7 @@ jobs:
|
||||
- should-run
|
||||
- build-test
|
||||
# Only run when committing to a PR that targets a release branch.
|
||||
if: ${{ github.repository == 'XRPLF/rippled' && needs.should-run.outputs.go == 'true' && github.event_name == 'pull_request' && startsWith(github.event.pull_request.base.ref, 'release/') }}
|
||||
if: ${{ github.repository == 'XRPLF/rippled' && needs.should-run.outputs.go == 'true' && github.event_name == 'pull_request' && startsWith(github.event.pull_request.base.ref, 'release') }}
|
||||
uses: ./.github/workflows/reusable-upload-recipe.yml
|
||||
secrets:
|
||||
remote_username: ${{ secrets.NEXUS_REMOTE_USERNAME }}
|
||||
|
||||
@@ -1,16 +1,14 @@
|
||||
# This workflow runs all workflows to build and test the code on various Linux
|
||||
# flavors, as well as on MacOS and Windows, on a scheduled basis, on merge into
|
||||
# the 'develop', 'release/*' or 'staging/*' branches, or when requested
|
||||
# manually. Upon pushes to the develop branch, it also uploads the libxrpl
|
||||
# recipe to the Conan remote.
|
||||
# the 'develop' or 'release*' branches, or when requested manually. Upon pushes
|
||||
# to the develop branch it also uploads the libxrpl recipe to the Conan remote.
|
||||
name: Trigger
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- "develop"
|
||||
- "release/*"
|
||||
- "staging/*"
|
||||
- "release*"
|
||||
paths:
|
||||
# These paths are unique to `on-trigger.yml`.
|
||||
- ".github/workflows/on-trigger.yml"
|
||||
@@ -64,9 +62,9 @@ on:
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
# When a PR is merged into the develop branch, it will be assigned a unique
|
||||
# When a PR is merged into the develop branch it will be assigned a unique
|
||||
# group identifier, so execution will continue even if another PR is merged
|
||||
# while it is still running. In all other cases, the group identifier is shared
|
||||
# while it is still running. In all other cases the group identifier is shared
|
||||
# per branch, so that any in-progress runs are cancelled when a new commit is
|
||||
# pushed.
|
||||
group: ${{ github.workflow }}-${{ github.event_name == 'push' && github.ref == 'refs/heads/develop' && github.sha || github.ref }}
|
||||
@@ -100,7 +98,7 @@ jobs:
|
||||
# However, we do not enable ccache for events targeting a release branch,
|
||||
# to protect against the rare case that the output produced by ccache is
|
||||
# not identical to a regular compilation.
|
||||
ccache_enabled: ${{ github.repository_owner == 'XRPLF' && !startsWith(github.ref, 'refs/heads/release/') }}
|
||||
ccache_enabled: ${{ github.repository_owner == 'XRPLF' && !startsWith(github.ref, 'refs/heads/release') }}
|
||||
os: ${{ matrix.os }}
|
||||
secrets:
|
||||
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
||||
|
||||
@@ -8,8 +8,7 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- "develop"
|
||||
- "release/*"
|
||||
- "staging/*"
|
||||
- "release*"
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
|
||||
@@ -123,19 +123,13 @@ jobs:
|
||||
|
||||
- name: Fix style
|
||||
if: ${{ steps.files_changed.outcome != 'success' }}
|
||||
# pre-commit installs every hook's environment, even for hooks that match
|
||||
# no files, and node-based hooks need libatomic, which this image lacks.
|
||||
# Exit code 1 just means hooks modified files or found issues.
|
||||
env:
|
||||
SKIP: prettier,cspell
|
||||
run: |
|
||||
git add --update
|
||||
pre-commit run || [ $? -eq 1 ]
|
||||
pre-commit run --all-files || true
|
||||
|
||||
- name: Generate git diff
|
||||
if: ${{ steps.files_changed.outcome != 'success' }}
|
||||
run: |
|
||||
git diff HEAD | tee "${DIFF_FILE}"
|
||||
git diff | tee "${DIFF_FILE}"
|
||||
|
||||
- name: Upload clang-tidy diff output
|
||||
if: ${{ github.event.repository.visibility == 'public' && steps.files_changed.outcome != 'success' }}
|
||||
|
||||
@@ -36,8 +36,6 @@ Version 3.5.0 is not yet released.
|
||||
- `channel_authorize`: The `channel_id` field now returns an `invalidParams` error if the value is not a string. [#7582](https://github.com/XRPLF/rippled/pull/7582)
|
||||
- `channel_verify`: The `channel_id` and `signature` fields now return an `invalidParams` error if the value is not a string. [#7582](https://github.com/XRPLF/rippled/pull/7582)
|
||||
- `account_currencies`, `account_nfts`, `account_offers`: A request with more than one problem may now report a different one of them. An invalid `ledger_hash` or `ledger_index` is reported before an invalid or missing `account`. An invalid `limit` or `marker` is reported before `lgrNotFound` or `actNotFound`, and for `account_offers` also before `actMalformed`. A request with a single problem returns the same error as before. [#8561](https://github.com/XRPLF/rippled/pull/8561)
|
||||
- `deposit_authorized`: A `credentials` entry that is all zeros (for example `"0"` or 64 `0` characters) now returns a `badCredentials` error instead of looking up a credential that can never exist.
|
||||
- `account_info`, `account_lines`, `account_channels`: 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 a missing or invalid `account`, or `ident` for `account_info`. For `account_lines`, a `peer` that is not a string, or an invalid `limit` or `marker`, is reported before `lgrNotFound`, `actNotFound` or a malformed `account`, and an invalid `limit` or `marker` also before a malformed `peer`. For `account_channels`, a malformed `account` is reported before `lgrNotFound`, and an invalid `destination_account`, `limit` or `marker` before `lgrNotFound` or `actNotFound`. A request with a single problem returns the same error as before. [#8573](https://github.com/XRPLF/rippled/pull/8573)
|
||||
|
||||
### Bugfixes in 3.5.0
|
||||
|
||||
|
||||
@@ -2,12 +2,33 @@
|
||||
| ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| These instructions assume you have a C++ development environment ready with Git, Python, Conan, CMake, and a C++ compiler. For help setting one up on Linux, macOS, or Windows, [see this guide](./docs/build/environment.md).<br><br>These instructions also assume a basic familiarity with Conan and CMake. If you are unfamiliar with Conan, you can read our [crash course](./docs/build/conan.md) or the official [Getting Started][conan-getting-started] walkthrough. |
|
||||
|
||||
## Supported platforms
|
||||
## Minimum Requirements
|
||||
|
||||
Building and testing are supported on Linux, macOS, and (best effort) Windows;
|
||||
see [Supported development platforms](./docs/build/environment.md#supported-development-platforms).
|
||||
Running `xrpld` is officially supported on Linux only;
|
||||
see [Requirements](./docs/install.md#requirements).
|
||||
For the hardware needed to run a node, see
|
||||
[System Requirements](https://xrpl.org/system-requirements.html).
|
||||
|
||||
For the software needed to build xrpld, see the
|
||||
[environment setup guide](./docs/build/environment.md).
|
||||
|
||||
## Operating Systems
|
||||
|
||||
### Linux
|
||||
|
||||
The Ubuntu Linux distribution has received the highest level of quality
|
||||
assurance, testing, and support. We also support Red Hat and use Debian
|
||||
internally.
|
||||
Our Linux CI tooling is distro-independent and uses a Nix-based environment, so it should be possible to build on other Linux distributions as well, although we have not tested them.
|
||||
|
||||
### macOS
|
||||
|
||||
Many `xrpld` engineers use macOS for development.
|
||||
The minimum supported version is macOS 15 (Sequoia).
|
||||
CI testing is done in macOS 26 (Tahoe), but the build defaults `CMAKE_OSX_DEPLOYMENT_TARGET` to 15.
|
||||
|
||||
### Windows
|
||||
|
||||
Windows is used by some engineers for development only, and is not recommended
|
||||
for production use.
|
||||
|
||||
## Steps
|
||||
|
||||
|
||||
+1
-1
@@ -332,7 +332,7 @@ This ensures that configuration changes don't introduce new warnings across the
|
||||
|
||||
### Installing clang-tidy
|
||||
|
||||
The [Nix development shell](./docs/build/nix.md) provides `clang-tidy` at the [tested version](./docs/build/environment.md#tested-compiler-versions).
|
||||
See the [environment setup guide](./docs/build/environment.md#clang-tidy) for how to get clang-tidy.
|
||||
|
||||
### Running clang-tidy locally
|
||||
|
||||
|
||||
@@ -1360,39 +1360,6 @@
|
||||
# Example:
|
||||
# owner_reserve = 200000 # 0.2 XRP
|
||||
#
|
||||
# gas_limit = <gas>
|
||||
#
|
||||
# The gas limit is the maximum amount of gas that can be
|
||||
# consumed by a single transaction. The gas limit is used to prevent
|
||||
# transactions from consuming too many resources.
|
||||
#
|
||||
# If this parameter is unspecified, xrpld will use an internal
|
||||
# default. Don't change this without understanding the consequences.
|
||||
#
|
||||
# Example:
|
||||
# gas_limit = 1000000 # 1 million gas
|
||||
#
|
||||
# bytecode_size_limit = <bytes>
|
||||
#
|
||||
# The bytecode size limit is the maximum size of a WASM extension in
|
||||
# bytes. The size limit is used to prevent extensions from consuming
|
||||
# too many resources.
|
||||
#
|
||||
# If this parameter is unspecified, xrpld will use an internal
|
||||
# default. Don't change this without understanding the consequences.
|
||||
#
|
||||
# Example:
|
||||
# bytecode_size_limit = 100000 # 100 kb
|
||||
#
|
||||
# gas_price = <micro-drops>
|
||||
#
|
||||
# The gas price is the conversion between WASM gas and its price in drops.
|
||||
#
|
||||
# If this parameter is unspecified, xrpld will use an internal
|
||||
# default. Don't change this without understanding the consequences.
|
||||
#
|
||||
# Example:
|
||||
# gas_price = 1000000 # 1 drop per gas
|
||||
#-------------------------------------------------------------------------------
|
||||
#
|
||||
# 9. Misc Settings
|
||||
|
||||
@@ -297,10 +297,7 @@ if(xrpld)
|
||||
VALUE_TYPE "::json::Value"
|
||||
VIEW_HEADER "xrpld/rpc/detail/JsonObjectView.hpp"
|
||||
HANDLERS
|
||||
account_channels
|
||||
account_currencies
|
||||
account_info
|
||||
account_lines
|
||||
account_nfts
|
||||
account_offers
|
||||
book_changes
|
||||
|
||||
+1
-1
@@ -3,7 +3,7 @@
|
||||
"requires": [
|
||||
"zlib/1.3.2#1cb806da49011867778ffb6ac7190fcb%1782392402.122708",
|
||||
"xxhash/0.8.3#681d36a0a6111fc56e5e45ea182c19cc%1782392402.420688",
|
||||
"xrpl-rpc-spec/0.1.25#2daa763e2d86ccc4ad0a0fa4d1983e9f%1791907455.579992",
|
||||
"xrpl-rpc-spec/0.1.23#56e8946f5b3fb949a3732e1a51135ca2%1790977482.197086",
|
||||
"sqlite3/3.53.0#324ada52333108388a9a6108bfa96734%1782392403.185447",
|
||||
"soci/4.0.3#e726491a03468795453f7c83fc924a96%1782392402.679521",
|
||||
"snappy/1.1.10#968fef506ff261592ec30c574d4a7809%1782307151.633168",
|
||||
|
||||
+1
-1
@@ -40,7 +40,7 @@ class Xrpl(ConanFile):
|
||||
"nudb/2.0.9",
|
||||
"openssl/3.6.3",
|
||||
"soci/4.0.3",
|
||||
"xrpl-rpc-spec/0.1.25",
|
||||
"xrpl-rpc-spec/0.1.23",
|
||||
"zlib/1.3.2",
|
||||
]
|
||||
|
||||
|
||||
Vendored
+77
-32
@@ -4,18 +4,6 @@ This document explains how to set one up.
|
||||
|
||||
[BUILD.md]: ../../BUILD.md
|
||||
|
||||
## Supported development platforms
|
||||
|
||||
- **Linux**: the [Nix development shell](./nix.md),
|
||||
which provides the same toolchain CI uses on any distribution.
|
||||
- **macOS** 15 (Sequoia) or newer: the [Nix development shell](./nix.md),
|
||||
or the system [Apple Clang](#macos-apple-clang).
|
||||
CI runs on macOS 26 (Tahoe), but the build defaults `CMAKE_OSX_DEPLOYMENT_TARGET` to 15.
|
||||
- **Windows**: best effort, see [Windows](#windows).
|
||||
|
||||
For the platforms supported for running `xrpld`,
|
||||
see [Requirements](../install.md#requirements).
|
||||
|
||||
## Tested compiler versions
|
||||
|
||||
`xrpld` is built in the **C++23** dialect by default, so your toolchain has to
|
||||
@@ -47,16 +35,20 @@ compiler building. Treat support for anything outside the table as best-effort.
|
||||
|
||||
## Required tools
|
||||
|
||||
Besides a compiler, building `xrpld` requires
|
||||
[Git](https://git-scm.com/downloads),
|
||||
[Python](https://www.python.org/downloads/),
|
||||
[Conan](https://conan.io/downloads.html),
|
||||
and [CMake](https://cmake.org/download/).
|
||||
Building with `-Drust=ON` additionally requires a Rust toolchain,
|
||||
see [Rust](#rust).
|
||||
Besides a compiler, building `xrpld` requires:
|
||||
|
||||
| Tool | Minimum version |
|
||||
| ------------------------------------------- | --------------- |
|
||||
| [Git](https://git-scm.com/downloads) | any recent |
|
||||
| [Python](https://www.python.org/downloads/) | 3.11 |
|
||||
| [Conan](https://conan.io/downloads.html) | 2.17 |
|
||||
| [CMake](https://cmake.org/download/) | 3.16 |
|
||||
|
||||
On Linux and macOS, the [Nix development shell](./nix.md) provides all of them
|
||||
at the versions CI uses, recorded in [`nix/check-tools/`](../../nix/check-tools).
|
||||
(see below). On Windows they have to be installed manually.
|
||||
|
||||
Building with `-Drust=ON` additionally requires a Rust toolchain, see
|
||||
[Rust](#rust). A default build does not, so it is not in the table above.
|
||||
|
||||
Once they are in place, verify that everything is installed and runnable with:
|
||||
|
||||
@@ -66,19 +58,60 @@ Once they are in place, verify that everything is installed and runnable with:
|
||||
|
||||
## Linux and macOS
|
||||
|
||||
Use the [Nix development shell](./nix.md):
|
||||
it provides the compiler and every tool CI uses,
|
||||
without installing anything system-wide.
|
||||
The **recommended way** to get a development environment on Linux and macOS is
|
||||
the Nix development shell. It provides the exact tooling used in CI — `git`,
|
||||
`python`, `conan`, `cmake`, `clang-tidy`, `clang-format`, and everything else —
|
||||
with a single command and without installing anything system-wide:
|
||||
|
||||
### macOS: Apple Clang
|
||||
```bash
|
||||
nix --experimental-features 'nix-command flakes' develop
|
||||
```
|
||||
|
||||
You can also build with the system Apple Clang,
|
||||
installing the other [required tools](#required-tools) any way you like
|
||||
(or getting them from `nix develop .#apple-clang`).
|
||||
Its version is whatever your installed Xcode (or Command Line Tools) provides,
|
||||
so check that `clang --version` is at least the [tested one](#tested-compiler-versions).
|
||||
If several Xcode versions are installed,
|
||||
select one with `sudo xcode-select -s <path>` or the `DEVELOPER_DIR` environment variable.
|
||||
On **Linux**, Nix also provides the compiler (GCC); on **macOS**, it provides
|
||||
Clang. If you instead opt to use your system-wide Apple Clang (via
|
||||
`nix develop .#apple-clang`), you need to manage its version yourself (see
|
||||
below).
|
||||
|
||||
See [Using the Nix development shell](./nix.md) for installation and usage
|
||||
details, including how to select a different compiler and why we recommend Nix
|
||||
over a hand-maintained environment.
|
||||
|
||||
### macOS: managing the Apple Clang version
|
||||
|
||||
If you use your system-wide Apple Clang on macOS (via `nix develop .#apple-clang`),
|
||||
the compiler version is whatever your installed Xcode (or Command Line Tools)
|
||||
provides. The following command should return a version greater than or equal to
|
||||
the [tested one](#tested-compiler-versions):
|
||||
|
||||
```bash
|
||||
clang --version
|
||||
```
|
||||
|
||||
If you develop other applications using Xcode, you might be consistently
|
||||
updating to the newest version of Apple Clang, which will likely cause issues
|
||||
building xrpld. You may want to install and pin a specific version of Xcode:
|
||||
|
||||
1. **Download Xcode**
|
||||
- Visit [Apple Developer Downloads](https://developer.apple.com/download/more/)
|
||||
- Sign in with your Apple Developer account
|
||||
- Search for an Xcode version that includes the expected Apple Clang version
|
||||
- Download the `.xip` file
|
||||
|
||||
2. **Install and configure Xcode**
|
||||
|
||||
```bash
|
||||
# Extract the .xip file and rename for version management
|
||||
# Example: Xcode_16.2.app
|
||||
|
||||
# Move to Applications directory
|
||||
sudo mv Xcode_16.2.app /Applications/
|
||||
|
||||
# Set as default toolchain (persistent)
|
||||
sudo xcode-select -s /Applications/Xcode_16.2.app/Contents/Developer
|
||||
|
||||
# Set as environment variable (temporary)
|
||||
export DEVELOPER_DIR=/Applications/Xcode_16.2.app/Contents/Developer
|
||||
```
|
||||
|
||||
## Windows
|
||||
|
||||
@@ -90,7 +123,8 @@ manually:
|
||||
"x64 Native Tools Command Prompt". CI configures CMake with the
|
||||
`Visual Studio 18 2026` generator.
|
||||
- [Git for Windows](https://git-scm.com/download/win)
|
||||
- recent releases of Python, Conan, and CMake
|
||||
- Python, Conan, and CMake, at the versions listed in
|
||||
[Required tools](#required-tools).
|
||||
- a [Rust toolchain](https://rustup.rs) — only needed to build with
|
||||
`-Drust=ON`, see [Rust](#rust)
|
||||
|
||||
@@ -111,5 +145,16 @@ version.
|
||||
Everything else the Rust build needs on the CMake side comes from Conan along
|
||||
with the rest of the dependencies, so there is nothing further to install.
|
||||
|
||||
## Clang-tidy
|
||||
|
||||
`clang-tidy` is required to run static analysis checks locally (see
|
||||
[CONTRIBUTING.md](../../CONTRIBUTING.md)). It is not required to build the
|
||||
project. The version this project uses is listed in
|
||||
[Tested compiler versions](#tested-compiler-versions).
|
||||
|
||||
On Linux and macOS, the [Nix development shell](./nix.md) provides that exact
|
||||
version out of the box — run it via `run-clang-tidy`. No separate installation
|
||||
is needed.
|
||||
|
||||
[cpp23-support]: https://en.cppreference.com/w/cpp/compiler_support/23
|
||||
[packages.nix]: ../../nix/packages.nix
|
||||
Vendored
+9
-2
@@ -9,7 +9,9 @@ This guide explains how to use Nix to set up a reproducible development environm
|
||||
- **Reproducible environment**: Everyone gets the same versions of tools and compilers
|
||||
- **Matches CI**: The Linux CI runs in Docker images built from this exact Nix environment, and CI builds some macOS configurations in it as well
|
||||
- **No system pollution**: Dependencies are isolated and don't affect your system packages
|
||||
- **Consistent compilers**: The GCC and Clang shells use the same versions as CI
|
||||
- **Quick setup**: Get started with a single command
|
||||
- **Works on Linux and macOS**: Consistent experience across platforms
|
||||
|
||||
## Install Nix
|
||||
|
||||
@@ -25,8 +27,13 @@ From the root of the xrpld repository, enter the default development shell:
|
||||
nix --experimental-features 'nix-command flakes' develop
|
||||
```
|
||||
|
||||
This downloads and sets up all required development tools (CMake, Ninja, Conan, etc.)
|
||||
and the compiler for your platform (see [Platform notes](#platform-notes)).
|
||||
This will:
|
||||
|
||||
- Download and set up all required development tools (CMake, Ninja, Conan, etc.)
|
||||
- Configure the appropriate compiler for your platform:
|
||||
- **Linux**: GCC (provided by Nix)
|
||||
- **macOS**: Clang (provided by Nix)
|
||||
|
||||
The first time you run this command, it will take a few minutes to download and build the environment. Subsequent runs will be much faster.
|
||||
|
||||
### Platform notes
|
||||
|
||||
@@ -10,19 +10,6 @@ and DNF on Red Hat-based distributions such as RHEL, AlmaLinux, and Rocky Linux,
|
||||
where `yum` is a symlink to `dnf`.
|
||||
To build from source instead, see [BUILD.md](../BUILD.md).
|
||||
|
||||
## Requirements
|
||||
|
||||
Linux is the only operating system officially supported for running `xrpld`;
|
||||
macOS and Windows builds are for development only.
|
||||
|
||||
Any x86-64 Linux distribution with APT or DNF and glibc 2.31 or newer should work
|
||||
(see [How libc is handled](../nix/docker/README.md#how-libc-is-handled)).
|
||||
CI tests the packages on Debian 11–13, Ubuntu 20.04–26.04,
|
||||
RHEL (UBI) 9–10, AlmaLinux 9–10, and Rocky Linux 9–10.
|
||||
|
||||
For the hardware needed to run a node,
|
||||
see [System Requirements](https://xrpl.org/system-requirements.html).
|
||||
|
||||
## Release channels
|
||||
|
||||
Packages are published to four channels:
|
||||
|
||||
@@ -352,9 +352,9 @@ class Number final
|
||||
using rep = std::int64_t;
|
||||
using InternalRep = MantissaRange::rep;
|
||||
|
||||
bool negative_{false};
|
||||
InternalRep mantissa_{0};
|
||||
int exponent_{std::numeric_limits<int>::lowest()};
|
||||
bool negative_{false};
|
||||
|
||||
public:
|
||||
// The range for the exponent when normalized
|
||||
@@ -650,12 +650,8 @@ private:
|
||||
shiftExponent(int exponentDelta) const;
|
||||
};
|
||||
|
||||
// The number class currently has 3 tail padding bytes. If you are adding new
|
||||
// members, try to fit them there before expanding the size of the class.
|
||||
static_assert(sizeof(Number) == 16);
|
||||
|
||||
constexpr Number::Number(bool negative, InternalRep mantissa, int exponent, Unchecked) noexcept
|
||||
: mantissa_{mantissa}, exponent_{exponent}, negative_(negative)
|
||||
: negative_(negative), mantissa_{mantissa}, exponent_{exponent}
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
@@ -94,7 +94,6 @@ struct Keys
|
||||
static constexpr auto kBbtOptions = "bbt_options";
|
||||
static constexpr auto kBgThreads = "bg_threads";
|
||||
static constexpr auto kBlockSize = "block_size";
|
||||
static constexpr auto kBytecodeSizeLimit = "bytecode_size_limit";
|
||||
static constexpr auto kCacheAge = "cache_age";
|
||||
static constexpr auto kCacheMb = "cache_mb";
|
||||
static constexpr auto kCacheSize = "cache_size";
|
||||
@@ -109,8 +108,6 @@ struct Keys
|
||||
static constexpr auto kFileSizeMult = "file_size_mult";
|
||||
static constexpr auto kFilterBits = "filter_bits";
|
||||
static constexpr auto kFilterFull = "filter_full";
|
||||
static constexpr auto kGasLimit = "gas_limit";
|
||||
static constexpr auto kGasPrice = "gas_price";
|
||||
static constexpr auto kHardSet = "hard_set";
|
||||
static constexpr auto kHighThreads = "high_threads";
|
||||
static constexpr auto kHoldTime = "hold_time";
|
||||
|
||||
@@ -257,42 +257,13 @@ public:
|
||||
header_.validated = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark this ledger as accepted and attempt to make it immutable.
|
||||
*
|
||||
* The close-time fields are recorded first, since the ledger hash covers
|
||||
* them.
|
||||
*
|
||||
* @param closeTime The consensus-agreed close time.
|
||||
* @param closeResolution The close time resolution.
|
||||
* @param correctCloseTime Whether consensus agreed on the close time. If
|
||||
* false, kSLcfNoConsensusTime is recorded in closeFlags instead.
|
||||
* @return What setImmutable() returned.
|
||||
*/
|
||||
[[nodiscard]] bool
|
||||
void
|
||||
setAccepted(
|
||||
NetClock::time_point closeTime,
|
||||
NetClock::duration closeResolution,
|
||||
bool correctCloseTime);
|
||||
|
||||
/**
|
||||
* Mark this ledger as immutable, so it can no longer be modified.
|
||||
*
|
||||
* A ledger built or loaded locally cannot have an invalid map, since only
|
||||
* a map syncing against hashes from outside can be proven impossible (see
|
||||
* SHAMap::addKnownNode), so a caller on such a path may treat a false
|
||||
* return as a broken internal invariant. A caller assembling a ledger from
|
||||
* peer data may not: for it, false is an outcome a peer can produce.
|
||||
*
|
||||
* @param rehash Whether to recompute the ledger hash from the header
|
||||
* fields. The transaction and account hashes are recomputed from the
|
||||
* maps too, but only the first time.
|
||||
* @return false if either map is Invalid, leaving the immutable flag as it
|
||||
* was and the header untouched. A map invalidated partway through can
|
||||
* still leave the other one immutable, so a false return means the
|
||||
* ledger must be discarded rather than retried.
|
||||
*/
|
||||
[[nodiscard]] bool
|
||||
void
|
||||
setImmutable(bool rehash = true);
|
||||
|
||||
bool
|
||||
@@ -301,28 +272,23 @@ public:
|
||||
return immutable_;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Whether both maps can still be the maps the header names. See
|
||||
* SHAMap::isValid().
|
||||
*/
|
||||
[[nodiscard]] bool
|
||||
mapsValid() const
|
||||
{
|
||||
return txMap_.isValid() && stateMap_.isValid();
|
||||
}
|
||||
/* Mark this ledger as "should be full".
|
||||
|
||||
/**
|
||||
* Mark this ledger as "should be full", indicating that the local server
|
||||
* wants all the corresponding nodes in durable storage.
|
||||
*
|
||||
* Const because it reflects metadata, not data this ledger shares with
|
||||
* other nodes on the network.
|
||||
*/
|
||||
"Full" is metadata property of the ledger, it indicates
|
||||
that the local server wants all the corresponding nodes
|
||||
in durable storage.
|
||||
|
||||
This is marked `const` because it reflects metadata
|
||||
and not data that is in common with other nodes on the
|
||||
network.
|
||||
*/
|
||||
void
|
||||
setFull() const
|
||||
{
|
||||
txMap_.setFull(header_.seq);
|
||||
stateMap_.setFull(header_.seq);
|
||||
txMap_.setFull();
|
||||
txMap_.setLedgerSeq(header_.seq);
|
||||
stateMap_.setFull();
|
||||
stateMap_.setLedgerSeq(header_.seq);
|
||||
}
|
||||
|
||||
void
|
||||
@@ -452,22 +418,6 @@ private:
|
||||
static std::pair<std::shared_ptr<STTx const>, std::shared_ptr<STObject const>>
|
||||
deserializeTxPlusMeta(SHAMapItem const& item);
|
||||
|
||||
/**
|
||||
* Make both maps immutable.
|
||||
*
|
||||
* Each map is settled on its own, so neither is left mid-sync because the
|
||||
* other refused.
|
||||
*
|
||||
* @return Whether both maps are immutable.
|
||||
*/
|
||||
[[nodiscard]] bool
|
||||
setMapsImmutable()
|
||||
{
|
||||
bool const txImmutable = txMap_.setImmutable();
|
||||
bool const stateImmutable = stateMap_.setImmutable();
|
||||
return txImmutable && stateImmutable;
|
||||
}
|
||||
|
||||
bool immutable_;
|
||||
|
||||
// A SHAMap containing the transactions associated with this ledger.
|
||||
|
||||
@@ -14,7 +14,6 @@
|
||||
#include <xrpl/protocol/AccountID.h>
|
||||
#include <xrpl/protocol/Concepts.h>
|
||||
#include <xrpl/protocol/Feature.h>
|
||||
#include <xrpl/protocol/Fees.h>
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
#include <xrpl/protocol/Issue.h>
|
||||
#include <xrpl/protocol/Keylet.h>
|
||||
@@ -28,8 +27,6 @@
|
||||
#include <xrpl/protocol/TER.h>
|
||||
#include <xrpl/protocol/UintTypes.h>
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
/**
|
||||
@@ -563,54 +560,4 @@ escrowUnlockApplyHelper<MPTIssue>(
|
||||
journal);
|
||||
}
|
||||
|
||||
/**
|
||||
* Smart escrow kill switches, driven by the voted FeeSettings.
|
||||
*
|
||||
* Zeroing `bytecodeSizeLimit` stops new uploads while leaving existing escrows
|
||||
* finishable without forcing holders to wait for `CancelAfter`.
|
||||
* Zeroing `gasLimit` stops both.
|
||||
*/
|
||||
/** @{ */
|
||||
inline bool
|
||||
isBytecodeUploadDisabled(Fees const& fees)
|
||||
{
|
||||
return fees.bytecodeSizeLimit == 0 || fees.gasLimit == 0;
|
||||
}
|
||||
|
||||
inline bool
|
||||
isBytecodeExecutionDisabled(Fees const& fees)
|
||||
{
|
||||
return fees.gasLimit == 0;
|
||||
}
|
||||
/** @} */
|
||||
|
||||
/**
|
||||
* Owner count an escrow costs.
|
||||
*
|
||||
* An escrow costs one increment per started 500 bytes of bytecode, and one
|
||||
* increment if it carries no bytecode.
|
||||
*/
|
||||
template <class T>
|
||||
int32_t
|
||||
calculateAdditionalReserve(T const& finishFunction)
|
||||
{
|
||||
static auto constexpr kBytecodeReserveIncrement = 500;
|
||||
|
||||
if (!finishFunction)
|
||||
return 1;
|
||||
|
||||
auto const size = finishFunction->size();
|
||||
if (size == 0)
|
||||
{
|
||||
// LCOV_EXCL_START
|
||||
// Empty Bytecode is refused at create, so no escrow carries it. Ceiling
|
||||
// division would answer 0, which would subtract less than the create added.
|
||||
UNREACHABLE("xrpl::calculateAdditionalReserve : empty Bytecode");
|
||||
return 1;
|
||||
// LCOV_EXCL_STOP
|
||||
}
|
||||
|
||||
return static_cast<int32_t>((size + kBytecodeReserveIncrement - 1) / kBytecodeReserveIncrement);
|
||||
}
|
||||
|
||||
} // namespace xrpl
|
||||
@@ -330,6 +330,14 @@ 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
|
||||
|
||||
@@ -164,20 +164,18 @@ posteriorAssetScale(
|
||||
|
||||
/**
|
||||
* Shared core of creditToPosteriorAvailableScale and
|
||||
* creditToPosteriorBrokerCoverScale: floors reference + raw at the sum's
|
||||
* own live scale (never finer than baseScale) and returns the difference
|
||||
* from reference, so the delta applied is exactly what moves reference onto
|
||||
* the floored sum. See creditToPosteriorAvailableScale's doc for why the
|
||||
* sum, not raw alone, must be floored. raw is the exact amount; it is not
|
||||
* rounded on its own before being added to reference. The sum and its scale
|
||||
* are computed once, in roundingMode, so a Downward credit is always
|
||||
* between zero and raw.
|
||||
* 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.
|
||||
*/
|
||||
[[nodiscard]] STAmount
|
||||
creditToPosteriorScale(
|
||||
Asset const& asset,
|
||||
Number const& reference,
|
||||
int baseScale,
|
||||
int atScale,
|
||||
Number const& raw,
|
||||
Number::RoundingMode roundingMode);
|
||||
|
||||
|
||||
@@ -348,19 +348,16 @@ isAuditorMirrorCurrent(SLE const& issuance, SLE const& mptoken);
|
||||
areMirrorsCurrent(SLE const& issuance, SLE const& mptoken);
|
||||
|
||||
/**
|
||||
* @brief Set the holder's issuer mirror epoch, and the issuer mirror key, to match the issuance's
|
||||
* current key.
|
||||
* @brief Set the holder's issuer mirror epoch to match the issuance's current issuer key epoch.
|
||||
*
|
||||
* Call this after writing the issuer mirror ciphertext under the issuance's
|
||||
* currently registered issuer key, so that the mirror reads as current afterwards.
|
||||
* Also records sfIssuerMirrorEncryptionKey as the issuance's current issuer key
|
||||
* (both fields stay absent at epoch 0, where the original issuer key applies).
|
||||
*
|
||||
* @param issuance The MPTokenIssuance ledger object.
|
||||
* @param mptoken The holder's MPToken ledger entry to update.
|
||||
*/
|
||||
void
|
||||
setIssuerMirrorCurrent(SLE const& issuance, SLE& mptoken);
|
||||
setIssuerMirrorEpoch(SLE const& issuance, SLE& mptoken);
|
||||
|
||||
/**
|
||||
* @brief Set the holder's auditor mirror epoch to match the issuance's current auditor key epoch.
|
||||
@@ -375,18 +372,16 @@ void
|
||||
setAuditorMirrorEpoch(SLE const& issuance, SLE& mptoken);
|
||||
|
||||
/**
|
||||
* @brief Set the holder's MPToken mirror epochs, and the issuer mirror key, to match the issuance's
|
||||
* current keys.
|
||||
* @brief Set the holder's MPToken mirror epochs to match the issuance's current key epochs.
|
||||
*
|
||||
* Call this after writing mirror ciphertexts under the issuance's currently
|
||||
* registered keys, so that the mirrors read as current afterwards. Also records
|
||||
* sfIssuerMirrorEncryptionKey via setIssuerMirrorCurrent.
|
||||
* registered keys, so that the mirrors read as current afterwards.
|
||||
*
|
||||
* @param issuance The MPTokenIssuance ledger object.
|
||||
* @param mptoken The holder's MPToken ledger entry to update.
|
||||
*/
|
||||
void
|
||||
setMirrorsCurrent(SLE const& issuance, SLE& mptoken);
|
||||
setMirrorEpochs(SLE const& issuance, SLE& mptoken);
|
||||
|
||||
/**
|
||||
* @brief Verifies revealed amount encryptions for all recipients.
|
||||
|
||||
@@ -10,25 +10,6 @@ namespace xrpl {
|
||||
// This was the reference fee units used in the old fee calculation.
|
||||
inline constexpr std::uint32_t kFeeUnitsDeprecated = 10;
|
||||
|
||||
// Number of micro-drops in one drop.
|
||||
inline constexpr std::uint32_t microDropsPerDrop{1'000'000};
|
||||
|
||||
/**
|
||||
* Hard protocol bounds on the Feature Extension fee settings. A voted value
|
||||
* can never fall outside these, so `preflight`, which has no view, may bound
|
||||
* against them. Gas and bytecode limits are capped; gas price has a floor
|
||||
* (zero would make WASM execution effectively free).
|
||||
*/
|
||||
inline constexpr std::uint32_t kMaxGasLimit{2'000'000};
|
||||
inline constexpr std::uint32_t kMaxBytecodeSizeLimit{200'000};
|
||||
inline constexpr std::uint32_t kMinGasPrice{1};
|
||||
|
||||
// The following default values of fee settings will seed into FeeSettings
|
||||
// and write to the ledger on featureSmartEscrow activation.
|
||||
inline constexpr std::uint32_t kDefaultGasLimit{1'000'000};
|
||||
inline constexpr std::uint32_t kDefaultBytecodeSizeLimit{100'000};
|
||||
inline constexpr std::uint32_t kDefaultGasPrice{1'000'000};
|
||||
|
||||
/**
|
||||
* Reflects the fee settings for a particular ledger.
|
||||
*
|
||||
@@ -52,47 +33,13 @@ struct Fees
|
||||
*/
|
||||
XRPAmount increment{0};
|
||||
|
||||
/**
|
||||
* @brief Gas limit for Feature Extensions (instructions).
|
||||
*
|
||||
* This and the one below default to the protocol values rather than 0,
|
||||
* because an explicit 0 is the voted kill switch.
|
||||
*/
|
||||
std::uint32_t gasLimit{kDefaultGasLimit};
|
||||
|
||||
/**
|
||||
* @brief Bytecode size limit for Feature Extensions (bytes).
|
||||
*/
|
||||
std::uint32_t bytecodeSizeLimit{kDefaultBytecodeSizeLimit};
|
||||
|
||||
/**
|
||||
* @brief Price of WASM gas (micro-drops).
|
||||
*/
|
||||
std::uint32_t gasPrice{kDefaultGasPrice};
|
||||
|
||||
explicit Fees() = default;
|
||||
Fees(Fees const&) = default;
|
||||
Fees&
|
||||
operator=(Fees const&) = default;
|
||||
|
||||
Fees(XRPAmount base, XRPAmount reserve, XRPAmount increment)
|
||||
: base{base}, reserve{reserve}, increment{increment}
|
||||
{
|
||||
}
|
||||
|
||||
Fees(
|
||||
XRPAmount base,
|
||||
XRPAmount reserve,
|
||||
XRPAmount increment,
|
||||
std::uint32_t gasLimit,
|
||||
std::uint32_t bytecodeSizeLimit,
|
||||
std::uint32_t gasPrice)
|
||||
: base{base}
|
||||
, reserve{reserve}
|
||||
, increment{increment}
|
||||
, gasLimit{gasLimit}
|
||||
, bytecodeSizeLimit{bytecodeSizeLimit}
|
||||
, gasPrice{gasPrice}
|
||||
: base(base), reserve(reserve), increment(increment)
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
@@ -398,11 +398,6 @@ constexpr std::uint32_t kMaxInvestmentPeriod = std::chrono::seconds{std::chrono:
|
||||
*/
|
||||
constexpr std::uint8_t kMaxAssetCheckDepth = 5;
|
||||
|
||||
/**
|
||||
* Maximum length of a Data field in Escrow object that can be updated by WASM code.
|
||||
*/
|
||||
constexpr std::size_t kMaxWasmDataLength = 1 * 1024; // 1KB
|
||||
|
||||
/**
|
||||
* A ledger index.
|
||||
*/
|
||||
|
||||
@@ -133,6 +133,7 @@ enum TEMcodes : TERUnderlyingType {
|
||||
temBAD_MPT,
|
||||
temBAD_CIPHERTEXT,
|
||||
temINVALID_BYTECODE,
|
||||
temTEMP_DISABLED,
|
||||
};
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
@@ -183,8 +184,6 @@ enum TEFcodes : TERUnderlyingType {
|
||||
tefBAD_PATH_COUNT,
|
||||
tefNO_BYTECODE,
|
||||
tefBYTECODE_NOT_INCLUDED,
|
||||
tefTEMP_DISABLED,
|
||||
tefEXCEEDS_LIMIT,
|
||||
};
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
@@ -417,7 +417,6 @@ LEDGER_ENTRY(ltMPTOKEN_ISSUANCE, 0x007e, MPTokenIssuance, mpt_issuance, ({
|
||||
{sfAuditorEncryptionKey, SoeOptional},
|
||||
{sfIssuerKeyEpoch, SoeOptional},
|
||||
{sfAuditorKeyEpoch, SoeOptional},
|
||||
{sfInitialIssuerEncryptionKey, SoeOptional},
|
||||
{sfConfidentialOutstandingAmount, SoeDefault},
|
||||
}))
|
||||
|
||||
@@ -439,7 +438,6 @@ LEDGER_ENTRY(ltMPTOKEN, 0x007f, MPToken, mptoken, ({
|
||||
{sfAuditorEncryptedBalance, SoeOptional},
|
||||
{sfIssuerKeyMirrorEpoch, SoeOptional},
|
||||
{sfAuditorKeyMirrorEpoch, SoeOptional},
|
||||
{sfIssuerMirrorEncryptionKey, SoeOptional},
|
||||
{sfHolderEncryptionKey, SoeOptional},
|
||||
{sfRecoveryKey, SoeOptional},
|
||||
}))
|
||||
|
||||
@@ -333,8 +333,6 @@ TYPED_SFIELD(sfAmountCommitment, VL, 45)
|
||||
TYPED_SFIELD(sfBalanceCommitment, VL, 46)
|
||||
TYPED_SFIELD(sfBytecode, VL, 47)
|
||||
TYPED_SFIELD(sfRecoveryKey, VL, 48)
|
||||
TYPED_SFIELD(sfInitialIssuerEncryptionKey, VL, 49)
|
||||
TYPED_SFIELD(sfIssuerMirrorEncryptionKey, VL, 50)
|
||||
|
||||
// account (common)
|
||||
TYPED_SFIELD(sfAccount, ACCOUNT, 1)
|
||||
|
||||
@@ -252,9 +252,6 @@ 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,30 +316,6 @@ public:
|
||||
return this->sle_->isFieldPresent(sfAuditorKeyMirrorEpoch);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get sfIssuerMirrorEncryptionKey (SoeOptional)
|
||||
* @return The field value, or std::nullopt if not present.
|
||||
*/
|
||||
[[nodiscard]]
|
||||
protocol_autogen::Optional<SF_VL::type::value_type>
|
||||
getIssuerMirrorEncryptionKey() const
|
||||
{
|
||||
if (hasIssuerMirrorEncryptionKey())
|
||||
return this->sle_->at(sfIssuerMirrorEncryptionKey);
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Check if sfIssuerMirrorEncryptionKey is present.
|
||||
* @return True if the field is present, false otherwise.
|
||||
*/
|
||||
[[nodiscard]]
|
||||
bool
|
||||
hasIssuerMirrorEncryptionKey() const
|
||||
{
|
||||
return this->sle_->isFieldPresent(sfIssuerMirrorEncryptionKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get sfHolderEncryptionKey (SoeOptional)
|
||||
* @return The field value, or std::nullopt if not present.
|
||||
@@ -589,17 +565,6 @@ public:
|
||||
return *this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Set sfIssuerMirrorEncryptionKey (SoeOptional)
|
||||
* @return Reference to this builder for method chaining.
|
||||
*/
|
||||
MPTokenBuilder&
|
||||
setIssuerMirrorEncryptionKey(std::decay_t<typename SF_VL::type::value_type> const& value)
|
||||
{
|
||||
object_[sfIssuerMirrorEncryptionKey] = value;
|
||||
return *this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Set sfHolderEncryptionKey (SoeOptional)
|
||||
* @return Reference to this builder for method chaining.
|
||||
|
||||
@@ -399,30 +399,6 @@ public:
|
||||
return this->sle_->isFieldPresent(sfAuditorKeyEpoch);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get sfInitialIssuerEncryptionKey (SoeOptional)
|
||||
* @return The field value, or std::nullopt if not present.
|
||||
*/
|
||||
[[nodiscard]]
|
||||
protocol_autogen::Optional<SF_VL::type::value_type>
|
||||
getInitialIssuerEncryptionKey() const
|
||||
{
|
||||
if (hasInitialIssuerEncryptionKey())
|
||||
return this->sle_->at(sfInitialIssuerEncryptionKey);
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Check if sfInitialIssuerEncryptionKey is present.
|
||||
* @return True if the field is present, false otherwise.
|
||||
*/
|
||||
[[nodiscard]]
|
||||
bool
|
||||
hasInitialIssuerEncryptionKey() const
|
||||
{
|
||||
return this->sle_->isFieldPresent(sfInitialIssuerEncryptionKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get sfConfidentialOutstandingAmount (SoeDefault)
|
||||
* @return The field value, or std::nullopt if not present.
|
||||
@@ -694,17 +670,6 @@ public:
|
||||
return *this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Set sfInitialIssuerEncryptionKey (SoeOptional)
|
||||
* @return Reference to this builder for method chaining.
|
||||
*/
|
||||
MPTokenIssuanceBuilder&
|
||||
setInitialIssuerEncryptionKey(std::decay_t<typename SF_VL::type::value_type> const& value)
|
||||
{
|
||||
object_[sfInitialIssuerEncryptionKey] = value;
|
||||
return *this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Set sfConfidentialOutstandingAmount (SoeDefault)
|
||||
* @return Reference to this builder for method chaining.
|
||||
|
||||
+28
-250
@@ -2,7 +2,6 @@
|
||||
|
||||
#include <xrpl/basics/Blob.h>
|
||||
#include <xrpl/basics/IntrusivePointer.h>
|
||||
#include <xrpl/basics/Log.h>
|
||||
#include <xrpl/basics/SHAMapHash.h>
|
||||
#include <xrpl/basics/base_uint.h>
|
||||
#include <xrpl/beast/utility/Journal.h>
|
||||
@@ -18,7 +17,6 @@
|
||||
#include <xrpl/shamap/SHAMapNodeID.h>
|
||||
#include <xrpl/shamap/SHAMapTreeNode.h>
|
||||
|
||||
#include <atomic>
|
||||
#include <condition_variable>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
@@ -42,7 +40,7 @@ class SHAMapSyncFilter;
|
||||
/**
|
||||
* Describes the current state of a given SHAMap
|
||||
*/
|
||||
enum class SHAMapState : std::uint8_t {
|
||||
enum class SHAMapState {
|
||||
/**
|
||||
* The map is in flux and objects can be added and removed.
|
||||
*
|
||||
@@ -122,42 +120,16 @@ private:
|
||||
*/
|
||||
std::uint32_t cowid_ = 1;
|
||||
|
||||
// ledgerSeq_, state_ and full_ are touched on the nodestore fetch path and on a
|
||||
// getMissingNodes() walk, neither of which may block, so pin them lock-free.
|
||||
static_assert(std::atomic<std::uint32_t>::is_always_lock_free);
|
||||
static_assert(std::atomic<SHAMapState>::is_always_lock_free);
|
||||
static_assert(std::atomic<bool>::is_always_lock_free);
|
||||
|
||||
/**
|
||||
* The sequence of the ledger that this map references, if any.
|
||||
*
|
||||
* Written when a map's ledger sequence is established (Ledger::setFull(),
|
||||
* InboundLedger) while a nodestore reader thread reads it. Relaxed either
|
||||
* way: it serves as a lookup hint for a nodestore keyed by hash.
|
||||
*/
|
||||
std::atomic<std::uint32_t> ledgerSeq_ = 0;
|
||||
std::uint32_t ledgerSeq_ = 0;
|
||||
|
||||
SHAMapTreeNodePtr root_;
|
||||
|
||||
/**
|
||||
* The map's state.
|
||||
*
|
||||
* A getMissingNodes() walk writes it, through setInvalid() and
|
||||
* clearSynching(), while whatever drives the acquisition reads it.
|
||||
* The walk runs with the acquisition's lock released, so this is atomic
|
||||
* rather than guarded.
|
||||
*/
|
||||
mutable std::atomic<SHAMapState> state_;
|
||||
mutable SHAMapState state_;
|
||||
SHAMapType const type_;
|
||||
bool backed_ = true; // Map is backed by the database
|
||||
|
||||
/**
|
||||
* Map is believed complete in database.
|
||||
*
|
||||
* Atomic because finishFetch() clears it on any nodestore reader thread,
|
||||
* several of which can run at once.
|
||||
*/
|
||||
mutable std::atomic<bool> full_ = false;
|
||||
bool backed_ = true; // Map is backed by the database
|
||||
mutable bool full_ = false; // Map is believed complete in database
|
||||
|
||||
public:
|
||||
/**
|
||||
@@ -180,14 +152,7 @@ public:
|
||||
SHAMap&
|
||||
operator=(SHAMap const&) = delete;
|
||||
|
||||
/**
|
||||
* Take a snapshot of the given map.
|
||||
*
|
||||
* @param other The map to snapshot. An Invalid source yields an Invalid
|
||||
* snapshot.
|
||||
* @param isMutable Whether the snapshot may be modified. Ignored when other
|
||||
* is Invalid.
|
||||
*/
|
||||
// Take a snapshot of the given map:
|
||||
SHAMap(SHAMap const& other, bool isMutable);
|
||||
|
||||
// build new map
|
||||
@@ -225,33 +190,17 @@ public:
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Return a new map that is a snapshot of this one.
|
||||
*
|
||||
* Handles copy on write for mutable snapshots. An invalid map yields an
|
||||
* invalid snapshot.
|
||||
*
|
||||
* @param isMutable Whether the snapshot may be modified.
|
||||
* @return The snapshot.
|
||||
*/
|
||||
// Returns a new map that's a snapshot of this one.
|
||||
// Handles copy on write for mutable snapshots.
|
||||
std::shared_ptr<SHAMap>
|
||||
snapShot(bool isMutable) const;
|
||||
|
||||
/**
|
||||
* Mark this map as "should be full": the local server wants every node
|
||||
* it references in durable storage, so a nodestore miss is a gap to report.
|
||||
*
|
||||
* Publishes the ledger sequence before the flag. The sequence store is
|
||||
* relaxed and the flag store is release, and finishFetch() reads the
|
||||
* sequence only after acquiring the flag, so the one miss that withdraws
|
||||
* the flag always reports the sequence stored here. Keeping both stores in
|
||||
* one place is what makes that order impossible to get wrong.
|
||||
*
|
||||
* @param ledgerSeq Sequence of the ledger this map belongs to, which the
|
||||
* gap report names.
|
||||
*/
|
||||
/* Mark this SHAMap as "should be full", indicating
|
||||
that the local server wants all the corresponding nodes
|
||||
in durable storage.
|
||||
*/
|
||||
void
|
||||
setFull(std::uint32_t ledgerSeq);
|
||||
setFull();
|
||||
|
||||
void
|
||||
setLedgerSeq(std::uint32_t lseq);
|
||||
@@ -347,13 +296,9 @@ public:
|
||||
* concurrency, to discover nodes referenced in the
|
||||
* SHAMap but not available locally.
|
||||
*
|
||||
* Only a leaf may occupy a position at or beyond kLeafDepth. A map that
|
||||
* breaks that is marked Invalid and the traversal is abandoned, so callers
|
||||
* ask isValid() to tell an empty result from a satisfied map.
|
||||
*
|
||||
* @param maxNodes The maximum number of found nodes to return
|
||||
* @param filter The filter to use when retrieving nodes
|
||||
* @return The nodes known to be missing, or empty if the map is Invalid
|
||||
* @param return The nodes known to be missing
|
||||
*/
|
||||
std::vector<std::pair<SHAMapNodeID, UInt256>>
|
||||
getMissingNodes(int maxNodes, SHAMapSyncFilter const* filter);
|
||||
@@ -396,9 +341,6 @@ public:
|
||||
* This function is used when receiving the root node of a SHAMap from a peer during ledger
|
||||
* synchronization. The node must already have been deserialized.
|
||||
*
|
||||
* A root offered under a hash the map does not hold names another tree and
|
||||
* is reported as invalid data. A root matching that hash is a duplicate.
|
||||
*
|
||||
* @param hash The expected hash of the root node.
|
||||
* @param rootNode A deserialized root node to add.
|
||||
* @param filter Optional sync filter to track received nodes.
|
||||
@@ -418,10 +360,6 @@ public:
|
||||
* is inserted at the position specified by nodeID. The node must already have been
|
||||
* deserialized.
|
||||
*
|
||||
* A node that no valid tree can hold makes the map Invalid, which is
|
||||
* terminal: the root hash committed to an impossible shape, so no peer
|
||||
* can satisfy it.
|
||||
*
|
||||
* @param nodeID The position in the tree where this node belongs.
|
||||
* @param treeNode A deserialized tree node to add.
|
||||
* @param filter Optional sync filter to track received nodes.
|
||||
@@ -437,57 +375,16 @@ public:
|
||||
SHAMapTreeNodePtr treeNode,
|
||||
SHAMapSyncFilter const* filter);
|
||||
|
||||
/**
|
||||
* Mark this map as immutable, so it can no longer be modified.
|
||||
*
|
||||
* @return false if the map is Invalid and was left unchanged, true
|
||||
* otherwise.
|
||||
*/
|
||||
[[nodiscard]] bool
|
||||
// status functions
|
||||
void
|
||||
setImmutable();
|
||||
|
||||
/**
|
||||
* @return Whether the map is being synced against a hash it was given, so
|
||||
* its hash is fixed while nodes may still be added.
|
||||
*/
|
||||
[[nodiscard]] bool
|
||||
bool
|
||||
isSynching() const;
|
||||
|
||||
/**
|
||||
* @return Whether the map is settled, with its hash and nodes fixed.
|
||||
*/
|
||||
[[nodiscard]] bool
|
||||
isImmutable() const;
|
||||
|
||||
/**
|
||||
* Mark this map as syncing, fixing its hash while still allowing missing
|
||||
* nodes to be added.
|
||||
*
|
||||
* The map must be freshly constructed. The body asserts that the map is not
|
||||
* Invalid, so a caller that ignores the precondition stops a build with
|
||||
* assertions enabled.
|
||||
*/
|
||||
void
|
||||
setSynching();
|
||||
|
||||
/**
|
||||
* Mark this map as no longer syncing, so it can be modified again.
|
||||
*
|
||||
* Moves only a Synching map. An Immutable map stays settled, a Modifying
|
||||
* map has nothing to clear, and Invalid is terminal.
|
||||
*/
|
||||
void
|
||||
clearSynching();
|
||||
|
||||
/**
|
||||
* Whether the map can still be the map it claims to be.
|
||||
*
|
||||
* A map that is merely missing nodes is valid, and stays valid until
|
||||
* something proves the tree it is syncing against cannot exist.
|
||||
*
|
||||
* @return Whether the map has not been proven impossible.
|
||||
*/
|
||||
[[nodiscard]] bool
|
||||
bool
|
||||
isValid() const;
|
||||
|
||||
// caution: otherMap must be accessed only by this function
|
||||
@@ -627,51 +524,6 @@ private:
|
||||
using DeltaRef =
|
||||
std::pair<boost::intrusive_ptr<SHAMapItem const>, boost::intrusive_ptr<SHAMapItem const>>;
|
||||
|
||||
/**
|
||||
* The sequence of the ledger this map references, read atomically.
|
||||
*
|
||||
* @return The sequence, or zero if the map references no ledger.
|
||||
*/
|
||||
[[nodiscard]] std::uint32_t
|
||||
ledgerSeq() const;
|
||||
|
||||
/**
|
||||
* The current state, read atomically.
|
||||
*
|
||||
* Orders state_ alone. The tree's nodes carry no ordering guarantees.
|
||||
*
|
||||
* @return The state as of the call, which a concurrent walk may already
|
||||
* have moved past.
|
||||
*/
|
||||
[[nodiscard]] SHAMapState
|
||||
state() const;
|
||||
|
||||
/**
|
||||
* Record that the map is provably not the one it claims to be.
|
||||
*
|
||||
* Private because only the map itself can prove that, from a node that
|
||||
* contradicts the hashes it is syncing against. Cannot fail, since
|
||||
* Invalid outranks every other state; see trySetState().
|
||||
*/
|
||||
void
|
||||
setInvalid();
|
||||
|
||||
/**
|
||||
* Move the map to a new state, atomically.
|
||||
*
|
||||
* With clearSynching(), the only writer of state_ past construction, so
|
||||
* the order between the states lives in one place: Invalid outranks all
|
||||
* of them and is always stored, while every other transition is refused
|
||||
* once the map is Invalid, which is what makes that verdict terminal.
|
||||
* clearSynching() is narrower and moves only Synching.
|
||||
*
|
||||
* @param desired The state to move to.
|
||||
* @return false if the map is Invalid and the requested state is not,
|
||||
* leaving it unchanged; true otherwise.
|
||||
*/
|
||||
bool
|
||||
trySetState(SHAMapState desired);
|
||||
|
||||
// tree node cache operations
|
||||
SHAMapTreeNodePtr
|
||||
cacheLookup(SHAMapHash const& hash) const;
|
||||
@@ -887,120 +739,46 @@ private:
|
||||
};
|
||||
|
||||
inline void
|
||||
SHAMap::setFull(std::uint32_t ledgerSeq)
|
||||
SHAMap::setFull()
|
||||
{
|
||||
ledgerSeq_.store(ledgerSeq, std::memory_order_relaxed);
|
||||
full_.store(true, std::memory_order_release);
|
||||
full_ = true;
|
||||
}
|
||||
|
||||
inline void
|
||||
SHAMap::setLedgerSeq(std::uint32_t lseq)
|
||||
{
|
||||
ledgerSeq_.store(lseq, std::memory_order_relaxed);
|
||||
ledgerSeq_ = lseq;
|
||||
}
|
||||
|
||||
inline std::uint32_t
|
||||
SHAMap::ledgerSeq() const
|
||||
{
|
||||
return ledgerSeq_.load(std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
inline SHAMapState
|
||||
SHAMap::state() const
|
||||
{
|
||||
return state_.load(std::memory_order_acquire);
|
||||
}
|
||||
|
||||
inline bool
|
||||
SHAMap::trySetState(SHAMapState desired)
|
||||
{
|
||||
// Invalid is stored outright: a walk reaching that verdict has to win against a thread
|
||||
// settling the map.
|
||||
if (desired == SHAMapState::Invalid)
|
||||
{
|
||||
state_.store(SHAMapState::Invalid, std::memory_order_release);
|
||||
return true;
|
||||
}
|
||||
|
||||
// A failed exchange both reports the state and refreshes expected, so no load is needed
|
||||
// ahead of the loop.
|
||||
auto expected = SHAMapState::Modifying;
|
||||
while (expected != SHAMapState::Invalid)
|
||||
{
|
||||
if (state_.compare_exchange_weak(
|
||||
expected, desired, std::memory_order_acq_rel, std::memory_order_acquire))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
inline bool
|
||||
inline void
|
||||
SHAMap::setImmutable()
|
||||
{
|
||||
SOMETIMES(!isValid(), "xrpl::SHAMap::setImmutable : map is invalid");
|
||||
return trySetState(SHAMapState::Immutable);
|
||||
XRPL_ASSERT(state_ != SHAMapState::Invalid, "xrpl::SHAMap::setImmutable : state is valid");
|
||||
state_ = SHAMapState::Immutable;
|
||||
}
|
||||
|
||||
inline bool
|
||||
SHAMap::isSynching() const
|
||||
{
|
||||
return state() == SHAMapState::Synching;
|
||||
}
|
||||
|
||||
inline bool
|
||||
SHAMap::isImmutable() const
|
||||
{
|
||||
return state() == SHAMapState::Immutable;
|
||||
return state_ == SHAMapState::Synching;
|
||||
}
|
||||
|
||||
inline void
|
||||
SHAMap::setSynching()
|
||||
{
|
||||
// Guarded, so this is not a way out of Invalid.
|
||||
if (!trySetState(SHAMapState::Synching))
|
||||
{
|
||||
// Only ever called on a freshly constructed map.
|
||||
// LCOV_EXCL_START
|
||||
UNREACHABLE("xrpl::SHAMap::setSynching : map is invalid");
|
||||
// LCOV_EXCL_STOP
|
||||
}
|
||||
state_ = SHAMapState::Synching;
|
||||
}
|
||||
|
||||
inline void
|
||||
SHAMap::clearSynching()
|
||||
{
|
||||
// Only Synching moves. A walk can end after the ledger settled, and then the map stays
|
||||
// Immutable; a Modifying map has nothing to clear; an invalid map stays invalid. Only that
|
||||
// last refusal is logged, since a concurrent walk is what produces it.
|
||||
auto expected = SHAMapState::Synching;
|
||||
if (state_.compare_exchange_strong(
|
||||
expected, SHAMapState::Modifying, std::memory_order_acq_rel, std::memory_order_acquire))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
bool const invalid = expected == SHAMapState::Invalid;
|
||||
SOMETIMES(invalid, "xrpl::SHAMap::clearSynching : map is invalid");
|
||||
if (invalid)
|
||||
{
|
||||
JLOG(journal_.warn()) << "Refused to clear synching on an invalid map, root hash "
|
||||
<< root_->getHash();
|
||||
}
|
||||
state_ = SHAMapState::Modifying;
|
||||
}
|
||||
|
||||
inline bool
|
||||
SHAMap::isValid() const
|
||||
{
|
||||
return state() != SHAMapState::Invalid;
|
||||
}
|
||||
|
||||
inline void
|
||||
SHAMap::setInvalid()
|
||||
{
|
||||
// Through trySetState() like every other transition, so state_ has one writer funnel.
|
||||
trySetState(SHAMapState::Invalid);
|
||||
return state_ != SHAMapState::Invalid;
|
||||
}
|
||||
|
||||
inline void
|
||||
|
||||
@@ -31,19 +31,7 @@ private:
|
||||
*/
|
||||
TaggedPointer hashesAndChildren_;
|
||||
|
||||
// Pin that wrapping fullBelowGen_ in an atomic keeps the packed layout, and that the load
|
||||
// isFullBelow() does once per node of every walk stays lock-free.
|
||||
static_assert(std::atomic<std::uint32_t>::is_always_lock_free);
|
||||
static_assert(sizeof(std::atomic<std::uint32_t>) == sizeof(std::uint32_t));
|
||||
static_assert(alignof(std::atomic<std::uint32_t>) == alignof(std::uint32_t));
|
||||
|
||||
/**
|
||||
* Written from more than one thread, since canonicalization shares a node
|
||||
* between maps and a walk can run with the acquisition lock released (see
|
||||
* SHAMap::state_). Relaxed both ways, since a generation is only compared
|
||||
* for equality and the children it vouches for are published through lock_.
|
||||
*/
|
||||
std::atomic<std::uint32_t> fullBelowGen_ = 0;
|
||||
std::uint32_t fullBelowGen_ = 0;
|
||||
std::uint16_t isBranch_ = 0;
|
||||
|
||||
/**
|
||||
@@ -216,13 +204,13 @@ SHAMapInnerNode::getBranchCount() const
|
||||
inline bool
|
||||
SHAMapInnerNode::isFullBelow(std::uint32_t generation) const
|
||||
{
|
||||
return fullBelowGen_.load(std::memory_order_relaxed) == generation;
|
||||
return fullBelowGen_ == generation;
|
||||
}
|
||||
|
||||
inline void
|
||||
SHAMapInnerNode::setFullBelowGen(std::uint32_t gen)
|
||||
{
|
||||
fullBelowGen_.store(gen, std::memory_order_relaxed);
|
||||
fullBelowGen_ = gen;
|
||||
}
|
||||
|
||||
} // namespace xrpl
|
||||
@@ -1,52 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <xrpl/basics/base_uint.h>
|
||||
#include <xrpl/beast/utility/Journal.h>
|
||||
#include <xrpl/protocol/Keylet.h>
|
||||
#include <xrpl/protocol/STLedgerEntry.h>
|
||||
#include <xrpl/protocol/STTx.h>
|
||||
#include <xrpl/protocol/TER.h>
|
||||
#include <xrpl/protocol/XRPAmount.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <set>
|
||||
#include <vector>
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
class ReadView;
|
||||
|
||||
/**
|
||||
* @brief Invariants for TransactionProposal ledger entries.
|
||||
*
|
||||
* - Only transaction types implementing proposal lifecycle operations may
|
||||
* create or modify proposals, and a failed one leaves no proposal effect.
|
||||
* - A proposal is deleted only by TransactionProposalCancel or together with
|
||||
* the Ticket it is keyed to, and a deleted Ticket takes the proposal keyed
|
||||
* to it along.
|
||||
* - Collected signer arrays remain sorted, unique, and bounded.
|
||||
*/
|
||||
class ValidTransactionProposal
|
||||
{
|
||||
// The Ticket each deleted proposal was keyed to; a proposal may go with
|
||||
// its Ticket, or by Cancel, and no other way.
|
||||
std::vector<uint256> ticketsOfDeletedProposals_;
|
||||
// The proposal each deleted Ticket was keyed to, whether or not one
|
||||
// existed; none may survive its Ticket.
|
||||
std::vector<Keylet> proposalsOfDeletedTickets_;
|
||||
std::set<uint256> deletedTickets_;
|
||||
std::uint32_t created_ = 0;
|
||||
std::uint32_t modified_ = 0;
|
||||
std::uint32_t deleted_ = 0;
|
||||
bool invalidSignerArrays_ = false;
|
||||
|
||||
public:
|
||||
void
|
||||
visitEntry(bool isDelete, SLE::ConstRef before, SLE::ConstRef after);
|
||||
|
||||
[[nodiscard]] bool
|
||||
finalize(STTx const& tx, TER result, XRPAmount, ReadView const& view, beast::Journal const& j)
|
||||
const;
|
||||
};
|
||||
|
||||
} // namespace xrpl
|
||||
@@ -9,7 +9,6 @@
|
||||
#include <xrpl/protocol/TER.h>
|
||||
#include <xrpl/protocol/XRPAmount.h>
|
||||
#include <xrpl/tx/invariants/AMMInvariant.h>
|
||||
#include <xrpl/tx/invariants/CosignerInvariant.h>
|
||||
#include <xrpl/tx/invariants/DirectoryInvariant.h>
|
||||
#include <xrpl/tx/invariants/FreezeInvariant.h>
|
||||
#include <xrpl/tx/invariants/LoanBrokerInvariant.h>
|
||||
@@ -484,7 +483,6 @@ using InvariantChecks = std::tuple<
|
||||
ValidAmounts,
|
||||
ValidMPTTransfer,
|
||||
ObjectHasPseudoAccount,
|
||||
ValidTransactionProposal,
|
||||
SponsorshipOwnerCountsMatch,
|
||||
SponsorshipAccountCountMatchesField>;
|
||||
|
||||
|
||||
@@ -20,14 +20,11 @@ public:
|
||||
{
|
||||
}
|
||||
|
||||
static bool
|
||||
checkExtraFeatures(PreflightContext const& ctx);
|
||||
|
||||
static TxConsequences
|
||||
makeTxConsequences(PreflightContext const& ctx);
|
||||
|
||||
static XRPAmount
|
||||
calculateBaseFee(ReadView const& view, STTx const& tx);
|
||||
static bool
|
||||
checkExtraFeatures(PreflightContext const& ctx);
|
||||
|
||||
static NotTEC
|
||||
preflight(PreflightContext const& ctx);
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
#include <xrpl/ledger/ApplyView.h>
|
||||
#include <xrpl/ledger/ReadView.h>
|
||||
#include <xrpl/protocol/Asset.h>
|
||||
#include <xrpl/protocol/STAmount.h>
|
||||
#include <xrpl/protocol/STTx.h>
|
||||
#include <xrpl/protocol/TER.h>
|
||||
#include <xrpl/protocol/XRPAmount.h>
|
||||
@@ -46,29 +45,9 @@ public:
|
||||
SLE::Ref loanSle,
|
||||
SLE::Ref brokerSle,
|
||||
SLE::Ref vaultSle,
|
||||
Asset const& vaultAsset,
|
||||
beast::Journal j);
|
||||
|
||||
/**
|
||||
* @brief Compute the First-Loss Capital cover amount for a defaulted Loan on a FixedPrecision
|
||||
* Vault, capped at the LoanBroker's CoverAvailable and floored so that AssetsAvailable + cover
|
||||
* is exactly on its posterior grid.
|
||||
*
|
||||
* The LoanBroker absorbs the rounding. When CoverAvailable - cover needs finer digits than the
|
||||
* broker's posterior grid, it rounds to nearest, as the broker's trust line does, so
|
||||
* CoverAvailable can move by up to half a posterior ulp more or less than the cover. When the
|
||||
* two grids differ, no cover is exact on both sides; the Vault side is kept exact.
|
||||
*
|
||||
* The one exception: when AssetsAvailable has digits finer than the 16-digit cover can carry,
|
||||
* no cover cancels them, and the balance writer rounds AssetsAvailable + cover to nearest,
|
||||
* by at most half a posterior ulp. VaultDeposit and LoanPay credits have the same limit.
|
||||
* @param loanSle The Loan being defaulted.
|
||||
* @param brokerSle The Loan's LoanBroker.
|
||||
* @param vaultSle The LoanBroker's Vault.
|
||||
* @return The cover amount, in the Vault's Asset.
|
||||
*/
|
||||
static STAmount
|
||||
calculateDefaultCover(SLE::Ref loanSle, SLE::Ref brokerSle, SLE::Ref vaultSle);
|
||||
|
||||
/**
|
||||
* Helper function that might be needed by other transactors
|
||||
*/
|
||||
|
||||
@@ -52,13 +52,6 @@ private:
|
||||
TER
|
||||
applyFee();
|
||||
|
||||
/**
|
||||
* Write the default Feature Extension fee settings into FeeSettings on
|
||||
* `featureSmartEscrow` activation.
|
||||
*/
|
||||
void
|
||||
initializeVMFees();
|
||||
|
||||
TER
|
||||
applyUNLModify();
|
||||
};
|
||||
|
||||
@@ -200,23 +200,11 @@ Ledger::Ledger(
|
||||
sle->at(sfReserveIncrement) = *f;
|
||||
sle->at(sfReferenceFeeUnits) = kFeeUnitsDeprecated;
|
||||
}
|
||||
if (std::ranges::find(amendments, featureSmartEscrow) != amendments.end())
|
||||
{
|
||||
sle->at(sfGasLimit) = fees.gasLimit;
|
||||
sle->at(sfBytecodeSizeLimit) = fees.bytecodeSizeLimit;
|
||||
sle->at(sfGasPrice) = fees.gasPrice;
|
||||
}
|
||||
rawInsert(sle);
|
||||
}
|
||||
|
||||
stateMap_.flushDirty(NodeObjectType::AccountNode);
|
||||
// Built locally. See Ledger::setImmutable().
|
||||
if (!setImmutable())
|
||||
{
|
||||
// LCOV_EXCL_START
|
||||
logicError("Ledger::Ledger(CreateGenesisT, ...): genesis ledger map is invalid");
|
||||
// LCOV_EXCL_STOP
|
||||
}
|
||||
setImmutable();
|
||||
}
|
||||
|
||||
Ledger::Ledger(
|
||||
@@ -227,7 +215,7 @@ Ledger::Ledger(
|
||||
Fees const& fees,
|
||||
Family& family,
|
||||
beast::Journal j)
|
||||
: immutable_(false)
|
||||
: immutable_(true)
|
||||
, txMap_(SHAMapType::TRANSACTION, info.txHash, family)
|
||||
, stateMap_(SHAMapType::STATE, info.accountHash, family)
|
||||
, fees_(fees)
|
||||
@@ -250,20 +238,8 @@ Ledger::Ledger(
|
||||
JLOG(j.warn()) << "Don't have state data root for ledger" << header_.seq;
|
||||
}
|
||||
|
||||
// Loaded locally. See Ledger::setImmutable().
|
||||
if (setMapsImmutable())
|
||||
{
|
||||
immutable_ = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
// LCOV_EXCL_START
|
||||
JLOG(j.error()) << "Invalid map for ledger " << header_.seq;
|
||||
UNREACHABLE("xrpl::Ledger::Ledger(LedgerHeader const&, ...) : map is invalid");
|
||||
// Treat it as a damaged ledger: the code below recomputes the hash and re-acquires.
|
||||
loaded = false;
|
||||
// LCOV_EXCL_STOP
|
||||
}
|
||||
txMap_.setImmutable();
|
||||
stateMap_.setImmutable();
|
||||
|
||||
if (!setup())
|
||||
loaded = false;
|
||||
@@ -334,50 +310,27 @@ Ledger::Ledger(
|
||||
setup();
|
||||
}
|
||||
|
||||
bool
|
||||
void
|
||||
Ledger::setImmutable(bool rehash)
|
||||
{
|
||||
// A map found structurally invalid during sync must never be made immutable: isValid() tests
|
||||
// only for Invalid, and an immutable ledger is treated as persistable. Asked before anything is
|
||||
// written, so a refusal leaves the header exactly as it was rather than half relabeled.
|
||||
if (!mapsValid())
|
||||
return false;
|
||||
|
||||
// Read here but written to the header below, once the maps are settled: getHash() can unshare a
|
||||
// dirty tree, so it has to run while the map is still mutable, while a write to the header must
|
||||
// wait until both maps have made it. Skipped once the ledger is immutable, since its maps can
|
||||
// no longer change.
|
||||
bool const deriveMapHashes = !immutable_ && rehash;
|
||||
UInt256 const txHash = deriveMapHashes ? txMap_.getHash().asUInt256() : UInt256{};
|
||||
UInt256 const accountHash = deriveMapHashes ? stateMap_.getHash().asUInt256() : UInt256{};
|
||||
|
||||
// Both were valid at the check above, but a concurrent walk can invalidate one in between (see
|
||||
// SHAMap::state_), so the result is checked. setInvalid() outranks Immutable and can land
|
||||
// after both maps have been settled, so this narrows the window it guards.
|
||||
bool const bothImmutable = setMapsImmutable();
|
||||
SOMETIMES(!bothImmutable, "xrpl::Ledger::setImmutable : map invalidated while going immutable");
|
||||
if (!bothImmutable)
|
||||
return false;
|
||||
|
||||
// Written only now, so losing the race above leaves the header describing what the ledger was
|
||||
// built from rather than a map that has since been abandoned. Forced rather than conditional,
|
||||
// since this is the only place the hash transitions to valid.
|
||||
if (deriveMapHashes)
|
||||
// Force update, since this is the only
|
||||
// place the hash transitions to valid
|
||||
if (!immutable_ && rehash)
|
||||
{
|
||||
header_.txHash = txHash;
|
||||
header_.accountHash = accountHash;
|
||||
header_.txHash = txMap_.getHash().asUInt256();
|
||||
header_.accountHash = stateMap_.getHash().asUInt256();
|
||||
}
|
||||
|
||||
if (rehash)
|
||||
header_.hash = calculateLedgerHash(header_);
|
||||
|
||||
// Set last, so isImmutable() reports only a ledger whose maps are both immutable.
|
||||
immutable_ = true;
|
||||
txMap_.setImmutable();
|
||||
stateMap_.setImmutable();
|
||||
setup();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool
|
||||
void
|
||||
Ledger::setAccepted(
|
||||
NetClock::time_point closeTime,
|
||||
NetClock::duration closeResolution,
|
||||
@@ -389,17 +342,7 @@ Ledger::setAccepted(
|
||||
header_.closeTime = closeTime;
|
||||
header_.closeTimeResolution = closeResolution;
|
||||
header_.closeFlags = correctCloseTime ? 0 : kSLcfNoConsensusTime;
|
||||
// Built locally. See Ledger::setImmutable().
|
||||
if (!setImmutable())
|
||||
{
|
||||
// LCOV_EXCL_START
|
||||
JLOG(j_.error()) << "Invalid map for accepted ledger " << header_.seq;
|
||||
UNREACHABLE("xrpl::Ledger::setAccepted : map is invalid");
|
||||
return false;
|
||||
// LCOV_EXCL_STOP
|
||||
}
|
||||
|
||||
return true;
|
||||
setImmutable();
|
||||
}
|
||||
|
||||
bool
|
||||
@@ -623,7 +566,6 @@ Ledger::setup()
|
||||
{
|
||||
bool oldFees = false;
|
||||
bool newFees = false;
|
||||
bool extensionFees = false;
|
||||
{
|
||||
auto const baseFee = sle->at(~sfBaseFee);
|
||||
auto const reserveBase = sle->at(~sfReserveBase);
|
||||
@@ -658,20 +600,6 @@ Ledger::setup()
|
||||
assign(fees_.increment, reserveIncrementXRP);
|
||||
newFees = baseFeeXRP || reserveBaseXRP || reserveIncrementXRP;
|
||||
}
|
||||
{
|
||||
auto const gasLimit = sle->at(~sfGasLimit);
|
||||
auto const bytecodeSizeLimit = sle->at(~sfBytecodeSizeLimit);
|
||||
auto const gasPrice = sle->at(~sfGasPrice);
|
||||
|
||||
// An absent field resolves to the protocol constant.
|
||||
if (rules_.enabled(featureSmartEscrow))
|
||||
{
|
||||
fees_.gasLimit = gasLimit.value_or(kDefaultGasLimit);
|
||||
fees_.bytecodeSizeLimit = bytecodeSizeLimit.value_or(kDefaultBytecodeSizeLimit);
|
||||
fees_.gasPrice = gasPrice.value_or(kDefaultGasPrice);
|
||||
}
|
||||
extensionFees = gasLimit || bytecodeSizeLimit || gasPrice;
|
||||
}
|
||||
if (oldFees && newFees)
|
||||
{
|
||||
// Should be all of one or the other, but not both
|
||||
@@ -682,12 +610,6 @@ Ledger::setup()
|
||||
// Can't populate the new fees before the amendment is enabled
|
||||
ret = false;
|
||||
}
|
||||
if (!rules_.enabled(featureSmartEscrow) && extensionFees)
|
||||
{
|
||||
// Can't populate the extension fees before the amendment is
|
||||
// enabled
|
||||
ret = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (SHAMapMissingNode const&)
|
||||
|
||||
@@ -587,6 +587,489 @@ doWithdraw(
|
||||
ctx.view, sourceAcct, dstAcct, destinationAmount, j, *sponsorSle, WaiveTransferFee::Yes);
|
||||
}
|
||||
|
||||
// Direct send w/o fees:
|
||||
// - Redeeming IOUs and/or sending sender's own IOUs.
|
||||
// - Create trust line if needed.
|
||||
// --> bCheckIssuer : normally require issuer to be involved.
|
||||
static TER
|
||||
rippleCreditIOU(
|
||||
ApplyView& view,
|
||||
AccountID const& uSenderID,
|
||||
AccountID const& uReceiverID,
|
||||
STAmount const& saAmount,
|
||||
bool bCheckIssuer,
|
||||
beast::Journal j)
|
||||
{
|
||||
AccountID const& issuer = saAmount.getIssuer();
|
||||
Currency const& currency = saAmount.getCurrency();
|
||||
|
||||
// Make sure issuer is involved.
|
||||
XRPL_ASSERT(
|
||||
!bCheckIssuer || uSenderID == issuer || uReceiverID == issuer,
|
||||
"xrpl::rippleCreditIOU : matching issuer or don't care");
|
||||
(void)issuer;
|
||||
|
||||
// Disallow sending to self.
|
||||
XRPL_ASSERT(uSenderID != uReceiverID, "xrpl::rippleCreditIOU : sender is not receiver");
|
||||
|
||||
bool const bSenderHigh = uSenderID > uReceiverID;
|
||||
auto const index = keylet::line(uSenderID, uReceiverID, currency);
|
||||
|
||||
XRPL_ASSERT(
|
||||
!isXRP(uSenderID) && uSenderID != noAccount(), "xrpl::rippleCreditIOU : sender is not XRP");
|
||||
XRPL_ASSERT(
|
||||
!isXRP(uReceiverID) && uReceiverID != noAccount(),
|
||||
"xrpl::rippleCreditIOU : receiver is not XRP");
|
||||
|
||||
// If the line exists, modify it accordingly.
|
||||
if (auto const sleRippleState = view.peek(index))
|
||||
{
|
||||
STAmount saBalance = sleRippleState->getFieldAmount(sfBalance);
|
||||
|
||||
if (bSenderHigh)
|
||||
saBalance.negate(); // Put balance in sender terms.
|
||||
|
||||
view.creditHook(uSenderID, uReceiverID, saAmount, saBalance);
|
||||
|
||||
STAmount const saBefore = saBalance;
|
||||
|
||||
saBalance -= saAmount;
|
||||
|
||||
JLOG(j.trace()) << "rippleCreditIOU: " << to_string(uSenderID) << " -> "
|
||||
<< to_string(uReceiverID) << " : before=" << saBefore.getFullText()
|
||||
<< " amount=" << saAmount.getFullText()
|
||||
<< " after=" << saBalance.getFullText();
|
||||
|
||||
std::uint32_t const uFlags(sleRippleState->getFieldU32(sfFlags));
|
||||
bool bDelete = false;
|
||||
|
||||
// FIXME This NEEDS to be cleaned up and simplified. It's impossible
|
||||
// for anyone to understand.
|
||||
if (saBefore > beast::zero
|
||||
// Sender balance was positive.
|
||||
&& saBalance <= beast::zero
|
||||
// Sender is zero or negative.
|
||||
&& (uFlags & (!bSenderHigh ? lsfLowReserve : lsfHighReserve))
|
||||
// Sender reserve is set.
|
||||
&& static_cast<bool>(uFlags & (!bSenderHigh ? lsfLowNoRipple : lsfHighNoRipple)) !=
|
||||
static_cast<bool>(
|
||||
view.read(keylet::account(uSenderID))->getFlags() & lsfDefaultRipple) &&
|
||||
!(uFlags & (!bSenderHigh ? lsfLowFreeze : lsfHighFreeze)) &&
|
||||
!sleRippleState->getFieldAmount(!bSenderHigh ? sfLowLimit : sfHighLimit)
|
||||
// Sender trust limit is 0.
|
||||
&& !sleRippleState->getFieldU32(!bSenderHigh ? sfLowQualityIn : sfHighQualityIn)
|
||||
// Sender quality in is 0.
|
||||
&& !sleRippleState->getFieldU32(!bSenderHigh ? sfLowQualityOut : sfHighQualityOut))
|
||||
// Sender quality out is 0.
|
||||
{
|
||||
// Clear the reserve of the sender, possibly delete the line!
|
||||
adjustOwnerCount(view, view.peek(keylet::account(uSenderID)), -1, j);
|
||||
|
||||
// Clear reserve flag.
|
||||
sleRippleState->setFieldU32(
|
||||
sfFlags, uFlags & (!bSenderHigh ? ~lsfLowReserve : ~lsfHighReserve));
|
||||
|
||||
// Balance is zero, receiver reserve is clear.
|
||||
bDelete = !saBalance // Balance is zero.
|
||||
&& !(uFlags & (bSenderHigh ? lsfLowReserve : lsfHighReserve));
|
||||
// Receiver reserve is clear.
|
||||
}
|
||||
|
||||
if (bSenderHigh)
|
||||
saBalance.negate();
|
||||
|
||||
// Want to reflect balance to zero even if we are deleting line.
|
||||
sleRippleState->setFieldAmount(sfBalance, saBalance);
|
||||
// ONLY: Adjust ripple balance.
|
||||
|
||||
if (bDelete)
|
||||
{
|
||||
return trustDelete(
|
||||
view,
|
||||
sleRippleState,
|
||||
bSenderHigh ? uReceiverID : uSenderID,
|
||||
!bSenderHigh ? uReceiverID : uSenderID,
|
||||
j);
|
||||
}
|
||||
|
||||
view.update(sleRippleState);
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
STAmount const saReceiverLimit(Issue{currency, uReceiverID});
|
||||
STAmount saBalance{saAmount};
|
||||
|
||||
saBalance.setIssuer(noAccount());
|
||||
|
||||
JLOG(j.debug()) << "rippleCreditIOU: "
|
||||
"create line: "
|
||||
<< to_string(uSenderID) << " -> " << to_string(uReceiverID) << " : "
|
||||
<< saAmount.getFullText();
|
||||
|
||||
auto const sleAccount = view.peek(keylet::account(uReceiverID));
|
||||
if (!sleAccount)
|
||||
return tefINTERNAL; // LCOV_EXCL_LINE
|
||||
|
||||
bool const noRipple = (sleAccount->getFlags() & lsfDefaultRipple) == 0;
|
||||
|
||||
return trustCreate(
|
||||
view,
|
||||
bSenderHigh,
|
||||
uSenderID,
|
||||
uReceiverID,
|
||||
index.key,
|
||||
sleAccount,
|
||||
false,
|
||||
noRipple,
|
||||
false,
|
||||
false,
|
||||
saBalance,
|
||||
saReceiverLimit,
|
||||
0,
|
||||
0,
|
||||
j);
|
||||
}
|
||||
|
||||
// Send regardless of limits.
|
||||
// --> saAmount: Amount/currency/issuer to deliver to receiver.
|
||||
// <-- saActual: Amount actually cost. Sender pays fees.
|
||||
static TER
|
||||
rippleSendIOU(
|
||||
ApplyView& view,
|
||||
AccountID const& uSenderID,
|
||||
AccountID const& uReceiverID,
|
||||
STAmount const& saAmount,
|
||||
STAmount& saActual,
|
||||
beast::Journal j,
|
||||
WaiveTransferFee waiveFee)
|
||||
{
|
||||
auto const& issuer = saAmount.getIssuer();
|
||||
|
||||
XRPL_ASSERT(
|
||||
!isXRP(uSenderID) && !isXRP(uReceiverID),
|
||||
"xrpl::rippleSendIOU : neither sender nor receiver is XRP");
|
||||
XRPL_ASSERT(uSenderID != uReceiverID, "xrpl::rippleSendIOU : sender is not receiver");
|
||||
|
||||
if (uSenderID == issuer || uReceiverID == issuer || issuer == noAccount())
|
||||
{
|
||||
// Direct send: redeeming IOUs and/or sending own IOUs.
|
||||
auto const ter = rippleCreditIOU(view, uSenderID, uReceiverID, saAmount, false, j);
|
||||
if (ter != tesSUCCESS)
|
||||
return ter;
|
||||
saActual = saAmount;
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
// Sending 3rd party IOUs: transit.
|
||||
|
||||
// Calculate the amount to transfer accounting
|
||||
// for any transfer fees if the fee is not waived:
|
||||
saActual = (waiveFee == WaiveTransferFee::Yes) ? saAmount
|
||||
: multiply(saAmount, transferRate(view, issuer));
|
||||
|
||||
JLOG(j.debug()) << "rippleSendIOU> " << to_string(uSenderID) << " - > "
|
||||
<< to_string(uReceiverID) << " : deliver=" << saAmount.getFullText()
|
||||
<< " cost=" << saActual.getFullText();
|
||||
|
||||
TER terResult = rippleCreditIOU(view, issuer, uReceiverID, saAmount, true, j);
|
||||
|
||||
if (tesSUCCESS == terResult)
|
||||
terResult = rippleCreditIOU(view, uSenderID, issuer, saActual, true, j);
|
||||
|
||||
return terResult;
|
||||
}
|
||||
|
||||
template <class TAsset>
|
||||
static TER
|
||||
doSendMulti(
|
||||
std::string const& name,
|
||||
ApplyView& view,
|
||||
AccountID const& senderID,
|
||||
TAsset const& issue,
|
||||
MultiplePaymentDestinations const& receivers,
|
||||
STAmount& actual,
|
||||
beast::Journal j,
|
||||
WaiveTransferFee waiveFee,
|
||||
// Don't pass back parameters that the caller already has
|
||||
std::function<
|
||||
TER(AccountID const& senderID,
|
||||
AccountID const& receiverID,
|
||||
STAmount const& amount,
|
||||
bool checkIssuer)> doCredit,
|
||||
std::function<
|
||||
TER(AccountID const& issuer, STAmount const& takeFromSender, STAmount const& amount)>
|
||||
preMint = {})
|
||||
{
|
||||
// Use the same pattern for all the SendMulti functions to help avoid
|
||||
// divergence and copy/paste errors.
|
||||
auto const& issuer = issue.getIssuer();
|
||||
|
||||
// These values may not stay in sync
|
||||
STAmount takeFromSender{issue};
|
||||
actual = takeFromSender;
|
||||
|
||||
// Failures return immediately.
|
||||
for (auto const& r : receivers)
|
||||
{
|
||||
auto const& receiverID = r.first;
|
||||
STAmount amount{issue, r.second};
|
||||
|
||||
if (amount < beast::zero)
|
||||
{
|
||||
return tecINTERNAL; // LCOV_EXCL_LINE
|
||||
}
|
||||
|
||||
/* If we aren't sending anything or if the sender is the same as the
|
||||
* receiver then we don't need to do anything.
|
||||
*/
|
||||
if (!amount || (senderID == receiverID))
|
||||
continue;
|
||||
|
||||
using namespace std::string_literals;
|
||||
XRPL_ASSERT(!isXRP(receiverID), ("xrpl::"s + name + " : receiver is not XRP").c_str());
|
||||
|
||||
if (senderID == issuer || receiverID == issuer || issuer == noAccount())
|
||||
{
|
||||
if (preMint)
|
||||
{
|
||||
if (auto const ter = preMint(issuer, takeFromSender, amount))
|
||||
return ter;
|
||||
}
|
||||
// Direct send: redeeming IOUs and/or sending own IOUs.
|
||||
if (auto const ter = doCredit(senderID, receiverID, amount, false))
|
||||
return ter;
|
||||
actual += amount;
|
||||
// Do not add amount to takeFromSender, because doCredit took
|
||||
// it.
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
// Sending 3rd party: transit.
|
||||
|
||||
// Calculate the amount to transfer accounting
|
||||
// for any transfer fees if the fee is not waived:
|
||||
STAmount actualSend = (waiveFee == WaiveTransferFee::Yes || issue.native())
|
||||
? amount
|
||||
: multiply(amount, transferRate(view, amount));
|
||||
actual += actualSend;
|
||||
takeFromSender += actualSend;
|
||||
|
||||
JLOG(j.debug()) << name << "> " << to_string(senderID) << " - > " << to_string(receiverID)
|
||||
<< " : deliver=" << amount.getFullText()
|
||||
<< " cost=" << actualSend.getFullText();
|
||||
|
||||
if (TER const terResult = doCredit(issuer, receiverID, amount, true))
|
||||
return terResult;
|
||||
}
|
||||
|
||||
if (senderID != issuer && takeFromSender)
|
||||
{
|
||||
if (TER const terResult = doCredit(senderID, issuer, takeFromSender, true))
|
||||
return terResult;
|
||||
}
|
||||
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
// Send regardless of limits.
|
||||
// --> receivers: Amount/currency/issuer to deliver to receivers.
|
||||
// <-- saActual: Amount actually cost to sender. Sender pays fees.
|
||||
static TER
|
||||
rippleSendMultiIOU(
|
||||
ApplyView& view,
|
||||
AccountID const& senderID,
|
||||
Issue const& issue,
|
||||
MultiplePaymentDestinations const& receivers,
|
||||
STAmount& actual,
|
||||
beast::Journal j,
|
||||
WaiveTransferFee waiveFee)
|
||||
{
|
||||
XRPL_ASSERT(!isXRP(senderID), "xrpl::rippleSendMultiIOU : sender is not XRP");
|
||||
|
||||
auto doCredit = [&view, j](
|
||||
AccountID const& senderID,
|
||||
AccountID const& receiverID,
|
||||
STAmount const& amount,
|
||||
bool checkIssuer) {
|
||||
return rippleCreditIOU(view, senderID, receiverID, amount, checkIssuer, j);
|
||||
};
|
||||
|
||||
return doSendMulti(
|
||||
"rippleSendMultiIOU", view, senderID, issue, receivers, actual, j, waiveFee, doCredit);
|
||||
}
|
||||
|
||||
static TER
|
||||
rippleCreditMPT(
|
||||
ApplyView& view,
|
||||
AccountID const& uSenderID,
|
||||
AccountID const& uReceiverID,
|
||||
STAmount const& saAmount,
|
||||
beast::Journal j)
|
||||
{
|
||||
// Do not check MPT authorization here - it must have been checked earlier
|
||||
auto const mptID = keylet::mptIssuance(saAmount.get<MPTIssue>().getMptID());
|
||||
auto const& issuer = saAmount.getIssuer();
|
||||
auto sleIssuance = view.peek(mptID);
|
||||
if (!sleIssuance)
|
||||
return tecOBJECT_NOT_FOUND;
|
||||
if (uSenderID == issuer)
|
||||
{
|
||||
(*sleIssuance)[sfOutstandingAmount] += saAmount.mpt().value();
|
||||
view.update(sleIssuance);
|
||||
}
|
||||
else
|
||||
{
|
||||
auto const mptokenID = keylet::mptoken(mptID.key, uSenderID);
|
||||
if (auto sle = view.peek(mptokenID))
|
||||
{
|
||||
auto const amt = sle->getFieldU64(sfMPTAmount);
|
||||
auto const pay = saAmount.mpt().value();
|
||||
if (amt < pay)
|
||||
return tecINSUFFICIENT_FUNDS;
|
||||
(*sle)[sfMPTAmount] = amt - pay;
|
||||
view.update(sle);
|
||||
}
|
||||
else
|
||||
return tecNO_AUTH;
|
||||
}
|
||||
|
||||
if (uReceiverID == issuer)
|
||||
{
|
||||
auto const outstanding = sleIssuance->getFieldU64(sfOutstandingAmount);
|
||||
auto const redeem = saAmount.mpt().value();
|
||||
if (outstanding >= redeem)
|
||||
{
|
||||
sleIssuance->setFieldU64(sfOutstandingAmount, outstanding - redeem);
|
||||
view.update(sleIssuance);
|
||||
}
|
||||
else
|
||||
return tecINTERNAL; // LCOV_EXCL_LINE
|
||||
}
|
||||
else
|
||||
{
|
||||
auto const mptokenID = keylet::mptoken(mptID.key, uReceiverID);
|
||||
if (auto sle = view.peek(mptokenID))
|
||||
{
|
||||
(*sle)[sfMPTAmount] += saAmount.mpt().value();
|
||||
view.update(sle);
|
||||
}
|
||||
else
|
||||
return tecNO_AUTH;
|
||||
}
|
||||
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
static TER
|
||||
rippleSendMPT(
|
||||
ApplyView& view,
|
||||
AccountID const& uSenderID,
|
||||
AccountID const& uReceiverID,
|
||||
STAmount const& saAmount,
|
||||
STAmount& saActual,
|
||||
beast::Journal j,
|
||||
WaiveTransferFee waiveFee)
|
||||
{
|
||||
XRPL_ASSERT(uSenderID != uReceiverID, "xrpl::rippleSendMPT : sender is not receiver");
|
||||
|
||||
// Safe to get MPT since rippleSendMPT is only called by accountSendMPT
|
||||
auto const& issuer = saAmount.getIssuer();
|
||||
|
||||
auto const sle = view.read(keylet::mptIssuance(saAmount.get<MPTIssue>().getMptID()));
|
||||
if (!sle)
|
||||
return tecOBJECT_NOT_FOUND;
|
||||
|
||||
if (uSenderID == issuer || uReceiverID == issuer)
|
||||
{
|
||||
// if sender is issuer, check that the new OutstandingAmount will not
|
||||
// exceed MaximumAmount
|
||||
if (uSenderID == issuer)
|
||||
{
|
||||
auto const sendAmount = saAmount.mpt().value();
|
||||
auto const maximumAmount = sle->at(~sfMaximumAmount).value_or(maxMPTokenAmount);
|
||||
if (sendAmount > maximumAmount ||
|
||||
sle->getFieldU64(sfOutstandingAmount) > maximumAmount - sendAmount)
|
||||
return tecPATH_DRY;
|
||||
}
|
||||
|
||||
// Direct send: redeeming MPTs and/or sending own MPTs.
|
||||
auto const ter = rippleCreditMPT(view, uSenderID, uReceiverID, saAmount, j);
|
||||
if (ter != tesSUCCESS)
|
||||
return ter;
|
||||
saActual = saAmount;
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
// Sending 3rd party MPTs: transit.
|
||||
saActual = (waiveFee == WaiveTransferFee::Yes)
|
||||
? saAmount
|
||||
: multiply(saAmount, transferRate(view, saAmount.get<MPTIssue>().getMptID()));
|
||||
|
||||
JLOG(j.debug()) << "rippleSendMPT> " << to_string(uSenderID) << " - > "
|
||||
<< to_string(uReceiverID) << " : deliver=" << saAmount.getFullText()
|
||||
<< " cost=" << saActual.getFullText();
|
||||
|
||||
if (auto const terResult = rippleCreditMPT(view, issuer, uReceiverID, saAmount, j);
|
||||
terResult != tesSUCCESS)
|
||||
return terResult;
|
||||
|
||||
return rippleCreditMPT(view, uSenderID, issuer, saActual, j);
|
||||
}
|
||||
|
||||
static TER
|
||||
rippleSendMultiMPT(
|
||||
ApplyView& view,
|
||||
AccountID const& senderID,
|
||||
MPTIssue const& mptIssue,
|
||||
MultiplePaymentDestinations const& receivers,
|
||||
STAmount& actual,
|
||||
beast::Journal j,
|
||||
WaiveTransferFee waiveFee)
|
||||
{
|
||||
auto const sle = view.read(keylet::mptIssuance(mptIssue.getMptID()));
|
||||
if (!sle)
|
||||
return tecOBJECT_NOT_FOUND;
|
||||
|
||||
auto preMint = [&](AccountID const& issuer,
|
||||
STAmount const& takeFromSender,
|
||||
STAmount const& amount) -> TER {
|
||||
// if sender is issuer, check that the new OutstandingAmount will
|
||||
// not exceed MaximumAmount
|
||||
if (senderID == issuer)
|
||||
{
|
||||
XRPL_ASSERT_PARTS(
|
||||
takeFromSender == beast::zero,
|
||||
"rippler::rippleSendMultiMPT",
|
||||
"sender == issuer, takeFromSender == zero");
|
||||
auto const sendAmount = amount.mpt().value();
|
||||
auto const maximumAmount = sle->at(~sfMaximumAmount).value_or(maxMPTokenAmount);
|
||||
if (sendAmount > maximumAmount ||
|
||||
sle->getFieldU64(sfOutstandingAmount) > maximumAmount - sendAmount)
|
||||
return tecPATH_DRY;
|
||||
}
|
||||
|
||||
return tesSUCCESS;
|
||||
};
|
||||
auto doCredit =
|
||||
[&view, j](
|
||||
AccountID const& senderID, AccountID const& receiverID, STAmount const& amount, bool) {
|
||||
return rippleCreditMPT(view, senderID, receiverID, amount, j);
|
||||
};
|
||||
|
||||
return doSendMulti(
|
||||
"rippleSendMultiMPT",
|
||||
view,
|
||||
senderID,
|
||||
mptIssue,
|
||||
receivers,
|
||||
actual,
|
||||
j,
|
||||
waiveFee,
|
||||
doCredit,
|
||||
preMint);
|
||||
}
|
||||
|
||||
TER
|
||||
cleanupOnAccountDelete(
|
||||
ApplyView& view,
|
||||
|
||||
@@ -130,8 +130,8 @@ creditToPosteriorBrokerCoverScale(
|
||||
}
|
||||
|
||||
Number const reference = broker->at(sfCoverAvailable);
|
||||
return detail::creditToPosteriorScale(
|
||||
asset, reference, getVaultBaseScale(vault), raw, roundingMode);
|
||||
int const scale = detail::getPosteriorBrokerCoverScale(vault, broker, raw);
|
||||
return detail::creditToPosteriorScale(asset, reference, scale, raw, roundingMode);
|
||||
}
|
||||
|
||||
[[nodiscard]] STAmount
|
||||
|
||||
@@ -43,9 +43,6 @@ constexpr auto kProposalTxTypes = std::to_array<TxType>({
|
||||
bool
|
||||
isValidProposalTxnType(STObject const& proposedTx)
|
||||
{
|
||||
if (!proposedTx.isFieldPresent(sfTransactionType))
|
||||
return false;
|
||||
|
||||
auto const type = proposedTx.getFieldU16(sfTransactionType);
|
||||
|
||||
if (std::ranges::find(kProposalTxTypes, type) != kProposalTxTypes.end())
|
||||
|
||||
@@ -600,7 +600,7 @@ requireAuth(ReadView const& view, Issue const& issue, AccountID const& account,
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
TER
|
||||
[[nodiscard]] TER
|
||||
canTransfer(ReadView const& view, Issue const& issue, AccountID const& from, AccountID const& to)
|
||||
{
|
||||
if (issue.native())
|
||||
@@ -638,7 +638,7 @@ canTransfer(ReadView const& view, Issue const& issue, AccountID const& from, Acc
|
||||
//
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
TER
|
||||
[[nodiscard]] TER
|
||||
addEmptyHolding(
|
||||
ApplyViewContext ctx,
|
||||
AccountID const& accountID,
|
||||
@@ -719,7 +719,7 @@ addEmptyHolding(
|
||||
journal);
|
||||
}
|
||||
|
||||
TER
|
||||
[[nodiscard]] TER
|
||||
removeEmptyHolding(
|
||||
ApplyViewContext ctx,
|
||||
AccountID const& accountID,
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
#include <xrpl/ledger/ApplyView.h>
|
||||
#include <xrpl/ledger/ReadView.h>
|
||||
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
|
||||
#include <xrpl/ledger/helpers/EscrowHelpers.h>
|
||||
#include <xrpl/ledger/helpers/OracleHelpers.h>
|
||||
#include <xrpl/ledger/helpers/ProposalHelpers.h>
|
||||
#include <xrpl/protocol/AccountID.h>
|
||||
@@ -320,8 +319,6 @@ getLedgerEntryOwnerCount(SLE const& sle)
|
||||
UNREACHABLE("AccountRoots are not supported by object sponsorship.");
|
||||
return 0;
|
||||
// LCOV_EXCL_STOP
|
||||
case ltESCROW:
|
||||
return calculateAdditionalReserve(sle[~sfBytecode]);
|
||||
default:
|
||||
return 1;
|
||||
}
|
||||
|
||||
@@ -1132,6 +1132,85 @@ accountSendMultiIOU(
|
||||
<< receivers.size() << " receivers.";
|
||||
}
|
||||
|
||||
auto doCredit = [&view, &sender, &receivers, j](
|
||||
AccountID const& senderID,
|
||||
AccountID const& receiverID,
|
||||
STAmount const& amount,
|
||||
bool /*checkIssuer*/) -> TER {
|
||||
if (!senderID)
|
||||
{
|
||||
SLE::pointer receiver =
|
||||
receiverID != beast::zero ? view.peek(keylet::account(receiverID)) : SLE::pointer();
|
||||
|
||||
if (auto stream = j.trace())
|
||||
{
|
||||
std::string receiver_bal("-");
|
||||
|
||||
if (receiver)
|
||||
receiver_bal = receiver->getFieldAmount(sfBalance).getFullText();
|
||||
|
||||
stream << "accountSendMultiIOU> " << to_string(senderID) << " -> "
|
||||
<< to_string(receiverID) << " (" << receiver_bal
|
||||
<< ") : " << amount.getFullText();
|
||||
}
|
||||
|
||||
if (receiver)
|
||||
{
|
||||
// Increment XRP balance.
|
||||
auto const rcvBal = receiver->getFieldAmount(sfBalance);
|
||||
receiver->setFieldAmount(sfBalance, rcvBal + amount);
|
||||
view.creditHook(xrpAccount(), receiverID, amount, -rcvBal);
|
||||
|
||||
view.update(receiver);
|
||||
}
|
||||
|
||||
if (auto stream = j.trace())
|
||||
{
|
||||
std::string receiver_bal("-");
|
||||
|
||||
if (receiver)
|
||||
receiver_bal = receiver->getFieldAmount(sfBalance).getFullText();
|
||||
|
||||
stream << "accountSendMultiIOU< " << to_string(senderID) << " -> "
|
||||
<< to_string(receiverID) << " (" << receiver_bal
|
||||
<< ") : " << amount.getFullText();
|
||||
}
|
||||
return tesSUCCESS;
|
||||
}
|
||||
// Sender
|
||||
if (sender)
|
||||
{
|
||||
if (sender->getFieldAmount(sfBalance) < amount)
|
||||
{
|
||||
return TER{tecFAILED_PROCESSING};
|
||||
}
|
||||
else
|
||||
{
|
||||
auto const sndBal = sender->getFieldAmount(sfBalance);
|
||||
view.creditHook(senderID, xrpAccount(), amount, sndBal);
|
||||
|
||||
// Decrement XRP balance.
|
||||
sender->setFieldAmount(sfBalance, sndBal - amount);
|
||||
view.update(sender);
|
||||
}
|
||||
}
|
||||
|
||||
if (auto stream = j.trace())
|
||||
{
|
||||
std::string sender_bal("-");
|
||||
|
||||
if (sender)
|
||||
sender_bal = sender->getFieldAmount(sfBalance).getFullText();
|
||||
|
||||
stream << "accountSendMultiIOU< " << to_string(senderID) << " (" << sender_bal
|
||||
<< ") -> " << receivers.size() << " receivers.";
|
||||
}
|
||||
|
||||
return tesSUCCESS;
|
||||
};
|
||||
return doSendMulti(
|
||||
"accountSendMultiIOU", view, senderID, issue, receivers, actual, j, waiveFee, doCredit);
|
||||
|
||||
// Failures return immediately.
|
||||
STAmount takeFromSender{issue};
|
||||
for (auto const& r : receivers)
|
||||
|
||||
@@ -48,7 +48,7 @@ fixedBaseScale(SLE::ConstRef vault)
|
||||
}
|
||||
|
||||
// Thin, vault-specific wrapper over posteriorAssetScale: used by
|
||||
// getPosteriorVaultScale and the cash outflow rounding below.
|
||||
// getPosteriorVaultScale and creditToPosteriorAvailableScale.
|
||||
[[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 baseScale,
|
||||
int atScale,
|
||||
Number const& raw,
|
||||
Number::RoundingMode roundingMode)
|
||||
{
|
||||
// Floor the SUM (reference + raw), not just raw. See
|
||||
// Floor the SUM (reference + raw), not just raw, at atScale. 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,21 +208,9 @@ 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 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;
|
||||
Number const flooredSum = roundToAsset(asset, reference + raw, atScale, roundingMode);
|
||||
return STAmount{asset, flooredSum - reference};
|
||||
}
|
||||
|
||||
[[nodiscard]] int
|
||||
@@ -411,8 +399,8 @@ creditToPosteriorAvailableScale(
|
||||
}
|
||||
|
||||
Number const reference = vault->at(sfAssetsAvailable);
|
||||
return detail::creditToPosteriorScale(
|
||||
asset, reference, fixedBaseScale(vault), raw, roundingMode);
|
||||
int const scale = posteriorScale(vault, reference, raw);
|
||||
return detail::creditToPosteriorScale(asset, reference, scale, raw, roundingMode);
|
||||
}
|
||||
|
||||
[[nodiscard]] Number
|
||||
|
||||
@@ -435,25 +435,18 @@ areMirrorsCurrent(SLE const& issuance, SLE const& mptoken)
|
||||
}
|
||||
|
||||
void
|
||||
setIssuerMirrorCurrent(SLE const& issuance, SLE& mptoken)
|
||||
setIssuerMirrorEpoch(SLE const& issuance, SLE& mptoken)
|
||||
{
|
||||
XRPL_ASSERT(
|
||||
issuance.getType() == ltMPTOKEN_ISSUANCE,
|
||||
"xrpl::setIssuerMirrorCurrent : issuance MPTokenIssuance object");
|
||||
"xrpl::setIssuerMirrorEpoch : issuance MPTokenIssuance object");
|
||||
XRPL_ASSERT(
|
||||
mptoken.getType() == ltMPTOKEN, "xrpl::setIssuerMirrorCurrent : mptoken MPToken object");
|
||||
mptoken.getType() == ltMPTOKEN, "xrpl::setIssuerMirrorEpoch : mptoken MPToken object");
|
||||
|
||||
// Unlike the auditor mirror, the issuer mirror is not optional: every
|
||||
// confidential MPToken carries one, so there is no existence check here.
|
||||
//
|
||||
// Record the key and epoch under which the mirror is now encrypted. Both fields remain absent
|
||||
// at epoch 0: such mirrors use the original issuer key. The original key is stored in
|
||||
// sfInitialIssuerEncryptionKey after the first rotation.
|
||||
if (auto const epoch = issuance[~sfIssuerKeyEpoch].value_or(0); epoch != 0)
|
||||
{
|
||||
mptoken[sfIssuerKeyMirrorEpoch] = epoch;
|
||||
mptoken[sfIssuerMirrorEncryptionKey] = issuance[sfIssuerEncryptionKey];
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
@@ -473,9 +466,9 @@ setAuditorMirrorEpoch(SLE const& issuance, SLE& mptoken)
|
||||
}
|
||||
|
||||
void
|
||||
setMirrorsCurrent(SLE const& issuance, SLE& mptoken)
|
||||
setMirrorEpochs(SLE const& issuance, SLE& mptoken)
|
||||
{
|
||||
setIssuerMirrorCurrent(issuance, mptoken);
|
||||
setIssuerMirrorEpoch(issuance, mptoken);
|
||||
setAuditorMirrorEpoch(issuance, mptoken);
|
||||
}
|
||||
|
||||
|
||||
@@ -137,8 +137,6 @@ transResults()
|
||||
MAKE_ERROR(tefBAD_PATH_COUNT, "Malformed: Too many paths."),
|
||||
MAKE_ERROR(tefNO_BYTECODE, "There is no WASM code to run, but a WASM-specific field was included."),
|
||||
MAKE_ERROR(tefBYTECODE_NOT_INCLUDED, "WASM code requires a field that was not included."),
|
||||
MAKE_ERROR(tefTEMP_DISABLED, "The transaction requires logic that is currently temporarily disabled."),
|
||||
MAKE_ERROR(tefEXCEEDS_LIMIT, "The transaction exceeds a limit set by fee voting."),
|
||||
|
||||
MAKE_ERROR(telLOCAL_ERROR, "Local failure."),
|
||||
MAKE_ERROR(telBAD_DOMAIN, "Domain too long."),
|
||||
@@ -211,6 +209,7 @@ transResults()
|
||||
MAKE_ERROR(temINVALID_INNER_BATCH, "Malformed: Invalid inner batch transaction."),
|
||||
MAKE_ERROR(temBAD_CIPHERTEXT, "Malformed: Invalid ciphertext."),
|
||||
MAKE_ERROR(temINVALID_BYTECODE, "Malformed: Provided byte code is invalid."),
|
||||
MAKE_ERROR(temTEMP_DISABLED, "The transaction requires logic that is currently temporarily disabled."),
|
||||
|
||||
MAKE_ERROR(terRETRY, "Retry transaction."),
|
||||
MAKE_ERROR(terFUNDS_SPENT, "DEPRECATED."),
|
||||
|
||||
@@ -26,7 +26,6 @@
|
||||
|
||||
#include <boost/smart_ptr/intrusive_ptr.hpp>
|
||||
|
||||
#include <atomic>
|
||||
#include <cstdint>
|
||||
#include <exception>
|
||||
#include <functional>
|
||||
@@ -78,23 +77,14 @@ SHAMap::SHAMap(SHAMap const& other, bool isMutable)
|
||||
: f_(other.f_)
|
||||
, journal_(other.f_.journal())
|
||||
, cowid_(other.cowid_ + 1)
|
||||
, ledgerSeq_(other.ledgerSeq())
|
||||
, ledgerSeq_(other.ledgerSeq_)
|
||||
, root_(other.root_)
|
||||
, state_(isMutable ? SHAMapState::Modifying : SHAMapState::Immutable)
|
||||
, type_(other.type_)
|
||||
, backed_(other.backed_)
|
||||
{
|
||||
// A snapshot shares the source's root, so Invalid carries over. Read once into a local, since
|
||||
// the source's state can change while this runs.
|
||||
auto const otherState = other.state();
|
||||
auto const ownState = [&] {
|
||||
if (otherState == SHAMapState::Invalid)
|
||||
return SHAMapState::Invalid;
|
||||
return isMutable ? SHAMapState::Modifying : SHAMapState::Immutable;
|
||||
}();
|
||||
state_.store(ownState, std::memory_order_release);
|
||||
|
||||
// If either map may change, they cannot share nodes
|
||||
if ((ownState != SHAMapState::Immutable) || (otherState != SHAMapState::Immutable))
|
||||
if ((state_ != SHAMapState::Immutable) || (other.state_ != SHAMapState::Immutable))
|
||||
{
|
||||
unshare();
|
||||
}
|
||||
@@ -115,7 +105,7 @@ SHAMap::dirtyUp(NodePathStack& stack, UInt256 const& target, SHAMapTreeNodePtr c
|
||||
// child can be an inner node or a leaf
|
||||
|
||||
XRPL_ASSERT(
|
||||
(state() != SHAMapState::Synching) && (state() != SHAMapState::Immutable),
|
||||
(state_ != SHAMapState::Synching) && (state_ != SHAMapState::Immutable),
|
||||
"xrpl::SHAMap::dirtyUp : valid state");
|
||||
XRPL_ASSERT(child && (child->cowid() == cowid_), "xrpl::SHAMap::dirtyUp : valid child input");
|
||||
|
||||
@@ -180,7 +170,7 @@ SHAMapTreeNodePtr
|
||||
SHAMap::fetchNodeFromDB(SHAMapHash const& hash) const
|
||||
{
|
||||
XRPL_ASSERT(backed_, "xrpl::SHAMap::fetchNodeFromDB : is backed");
|
||||
auto const obj = f_.db().fetchNodeObject(hash.asUInt256(), ledgerSeq());
|
||||
auto const obj = f_.db().fetchNodeObject(hash.asUInt256(), ledgerSeq_);
|
||||
return finishFetch(hash, obj);
|
||||
}
|
||||
|
||||
@@ -193,17 +183,10 @@ SHAMap::finishFetch(SHAMapHash const& hash, std::shared_ptr<NodeObject> const& o
|
||||
{
|
||||
if (!object)
|
||||
{
|
||||
// A missing node disproves full_, so withdraw it and report the gap. The exchange
|
||||
// makes exactly one reader report it, and its acquire pairs with the release store in
|
||||
// setFull(). It needs no release: the only other reader of full_ is this same path,
|
||||
// and a reader that loses the exchange does nothing with what the winner did. The load
|
||||
// ahead of it is relaxed, since all it decides is whether to try the exchange: a stale
|
||||
// true costs one exchange that loses, and a stale false means another reader won, or
|
||||
// setFull() is not visible yet and the gap goes unreported.
|
||||
if (full_.load(std::memory_order_relaxed) &&
|
||||
full_.exchange(false, std::memory_order_acquire))
|
||||
if (full_)
|
||||
{
|
||||
f_.missingNodeAcquireBySeq(ledgerSeq(), hash.asUInt256());
|
||||
full_ = false;
|
||||
f_.missingNodeAcquireBySeq(ledgerSeq_, hash.asUInt256());
|
||||
}
|
||||
return {};
|
||||
}
|
||||
@@ -236,7 +219,7 @@ SHAMap::checkFilter(SHAMapHash const& hash, SHAMapSyncFilter const* filter) cons
|
||||
auto node = SHAMapTreeNode::makeFromPrefix(makeSlice(*nodeData), hash);
|
||||
if (node)
|
||||
{
|
||||
filter->gotNode(true, hash, ledgerSeq(), std::move(*nodeData), node->getType());
|
||||
filter->gotNode(true, hash, ledgerSeq_, std::move(*nodeData), node->getType());
|
||||
if (backed_)
|
||||
canonicalize(hash, node);
|
||||
}
|
||||
@@ -416,7 +399,7 @@ SHAMap::descendAsync(
|
||||
{
|
||||
f_.db().asyncFetch(
|
||||
hash.asUInt256(),
|
||||
ledgerSeq(),
|
||||
ledgerSeq_,
|
||||
[this, hash, cb{std::move(callback)}](std::shared_ptr<NodeObject> const& object) {
|
||||
auto const node = finishFetch(hash, object);
|
||||
cb(node, hash);
|
||||
@@ -441,7 +424,7 @@ SHAMap::unshareNode(intr_ptr::SharedPtr<Node> node, SHAMapNodeID const& nodeID)
|
||||
if (node->cowid() != cowid_)
|
||||
{
|
||||
// have a CoW
|
||||
XRPL_ASSERT(state() != SHAMapState::Immutable, "xrpl::SHAMap::unshareNode : not immutable");
|
||||
XRPL_ASSERT(state_ != SHAMapState::Immutable, "xrpl::SHAMap::unshareNode : not immutable");
|
||||
node = intr_ptr::staticPointerCast<Node>(node->clone(cowid_));
|
||||
if (nodeID.isRoot())
|
||||
root_ = node;
|
||||
@@ -654,7 +637,7 @@ bool
|
||||
SHAMap::delItem(UInt256 const& id)
|
||||
{
|
||||
// delete the item with this ID
|
||||
XRPL_ASSERT(state() != SHAMapState::Immutable, "xrpl::SHAMap::delItem : not immutable");
|
||||
XRPL_ASSERT(state_ != SHAMapState::Immutable, "xrpl::SHAMap::delItem : not immutable");
|
||||
|
||||
NodePathStack stack;
|
||||
walkTowardsKey(id, &stack);
|
||||
@@ -736,7 +719,7 @@ SHAMap::delItem(UInt256 const& id)
|
||||
bool
|
||||
SHAMap::addGiveItem(SHAMapNodeType type, boost::intrusive_ptr<SHAMapItem const> item)
|
||||
{
|
||||
XRPL_ASSERT(state() != SHAMapState::Immutable, "xrpl::SHAMap::addGiveItem : not immutable");
|
||||
XRPL_ASSERT(state_ != SHAMapState::Immutable, "xrpl::SHAMap::addGiveItem : not immutable");
|
||||
XRPL_ASSERT(type != SHAMapNodeType::TnInner, "xrpl::SHAMap::addGiveItem : valid type input");
|
||||
|
||||
// add the specified item, does not update
|
||||
@@ -827,7 +810,7 @@ SHAMap::updateGiveItem(SHAMapNodeType type, boost::intrusive_ptr<SHAMapItem cons
|
||||
// can't change the tag but can change the hash
|
||||
UInt256 const tag = item->key();
|
||||
|
||||
XRPL_ASSERT(state() != SHAMapState::Immutable, "xrpl::SHAMap::updateGiveItem : not immutable");
|
||||
XRPL_ASSERT(state_ != SHAMapState::Immutable, "xrpl::SHAMap::updateGiveItem : not immutable");
|
||||
|
||||
NodePathStack stack;
|
||||
walkTowardsKey(tag, &stack);
|
||||
@@ -918,7 +901,7 @@ SHAMap::writeNode(NodeObjectType t, SHAMapTreeNodePtr node) const
|
||||
|
||||
Serializer s;
|
||||
node->serializeWithPrefix(s);
|
||||
f_.db().store(t, std::move(s.modData()), node->getHash().asUInt256(), ledgerSeq());
|
||||
f_.db().store(t, std::move(s.modData()), node->getHash().asUInt256(), ledgerSeq_);
|
||||
return node;
|
||||
}
|
||||
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
#include <xrpl/shamap/detail/TaggedPointer.h>
|
||||
#include <xrpl/shamap/detail/TaggedPointer.ipp>
|
||||
|
||||
#include <atomic>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <mutex>
|
||||
@@ -78,9 +77,7 @@ SHAMapInnerNode::clone(std::uint32_t cowid) const
|
||||
auto p = intr_ptr::makeShared<SHAMapInnerNode>(cowid, branchCount);
|
||||
p->hash_ = hash_;
|
||||
p->isBranch_ = isBranch_;
|
||||
// Relaxed, as everywhere. p is not reachable by another thread until this returns.
|
||||
p->fullBelowGen_.store(
|
||||
fullBelowGen_.load(std::memory_order_relaxed), std::memory_order_relaxed);
|
||||
p->fullBelowGen_ = fullBelowGen_;
|
||||
SHAMapHash* cloneHashes = nullptr;
|
||||
SHAMapHash* thisHashes = nullptr;
|
||||
SHAMapTreeNodePtr* cloneChildren = nullptr;
|
||||
|
||||
@@ -31,25 +31,6 @@
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
namespace {
|
||||
|
||||
/**
|
||||
* Whether a depth is at or past the deepest an inner node may occupy.
|
||||
*
|
||||
* Nibbles run out at SHAMap::kLeafDepth, so only a leaf may sit there. True for
|
||||
* every deeper position too, which lies past the end of a key.
|
||||
*
|
||||
* @param depth The depth to judge.
|
||||
* @return Whether an inner node at that depth makes the map impossible.
|
||||
*/
|
||||
[[nodiscard]] bool
|
||||
isLeafDepth(unsigned int depth)
|
||||
{
|
||||
return depth >= SHAMap::kLeafDepth;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void
|
||||
SHAMap::visitLeaves(
|
||||
std::function<void(boost::intrusive_ptr<SHAMapItem const> const& item)> const& leafFunction)
|
||||
@@ -162,10 +143,13 @@ SHAMap::visitDifferences(
|
||||
if (!function(*node))
|
||||
return;
|
||||
|
||||
// Only a leaf belongs at kLeafDepth. The node is still reported, since the wire form
|
||||
// carries no depth and the recipient hooks blobs by hash. Its children are skipped, since
|
||||
// getChildNodeID has no answer past kLeafDepth.
|
||||
if (isLeafDepth(nodeID.getDepth()))
|
||||
// Nibbles run out at kLeafDepth, so only a leaf belongs there. A well-formed map never
|
||||
// holds an inner node at that depth: addKnownNode marks the map invalid rather than hooking
|
||||
// one in, and fetch-pack data is hash-verified against a validated root, so reaching this
|
||||
// means a defect or a corrupt store, not something a peer can provoke. Report the node
|
||||
// anyway - the wire form carries no depth, and the recipient hooks blobs in by hash - but
|
||||
// skip the children rather than letting getChildNodeID throw on them.
|
||||
if (nodeID.getDepth() >= kLeafDepth)
|
||||
{
|
||||
// LCOV_EXCL_START
|
||||
UNREACHABLE("xrpl::SHAMap::visitDifferences : inner node at leaf depth");
|
||||
@@ -223,12 +207,7 @@ SHAMap::gmnProcessNodes(MissingNodes& mn, MissingNodes::StackEntry& se)
|
||||
// we already know this child node is missing
|
||||
fullBelow = false;
|
||||
}
|
||||
// The depth test precedes the cache lookup for the same reason it does in addKnownNode():
|
||||
// the cache is keyed by node hash and shared across maps, and a hash covers a node's
|
||||
// children but not its depth. Skipping the shortcut forgoes an optimization only.
|
||||
else if (
|
||||
!backed_ || isLeafDepth(nodeID.getDepth() + 1) ||
|
||||
!f_.getFullBelowCache()->touchIfExists(childHash.asUInt256()))
|
||||
else if (!backed_ || !f_.getFullBelowCache()->touchIfExists(childHash.asUInt256()))
|
||||
{
|
||||
bool pending = false;
|
||||
auto d = descendAsync(
|
||||
@@ -259,17 +238,6 @@ SHAMap::gmnProcessNodes(MissingNodes& mn, MissingNodes::StackEntry& se)
|
||||
if (--mn.max <= 0)
|
||||
return;
|
||||
}
|
||||
else if (d->isInner() && isLeafDepth(nodeID.getDepth() + 1))
|
||||
{
|
||||
// Only a leaf belongs that deep (see isLeafDepth and SHAMap::addKnownNode). A node
|
||||
// resolved locally reaches the walk without passing through addKnownNode(), so the
|
||||
// walk reaches this verdict itself. Ordered ahead of the full-below test below,
|
||||
// which canonicalization shares across maps.
|
||||
JLOG(journal_.warn()) << "Inner node at branch " << branch << " below " << nodeID
|
||||
<< " makes the map invalid";
|
||||
setInvalid();
|
||||
return;
|
||||
}
|
||||
else if (d->isInner() && !safeDowncast<SHAMapInnerNode*>(d)->isFullBelow(mn.generation))
|
||||
{
|
||||
mn.stack.push(se);
|
||||
@@ -333,8 +301,6 @@ SHAMap::gmnProcessDeferredReads(MissingNodes& mn)
|
||||
}
|
||||
else if ((mn.max > 0) && mn.missingHashes.insert(nodeHash).second)
|
||||
{
|
||||
// getChildNodeID() is safe here: gmnProcessNodes refuses to descend into an inner node
|
||||
// at kLeafDepth, so a deferred parent sits at most one level above it.
|
||||
mn.missingNodes.emplace_back(parentID.getChildNodeID(branch), nodeHash.asUInt256());
|
||||
--mn.max;
|
||||
}
|
||||
@@ -345,20 +311,17 @@ SHAMap::gmnProcessDeferredReads(MissingNodes& mn)
|
||||
mn.deferred = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a list of node IDs and hashes for nodes that are part of this SHAMap
|
||||
* but not available locally. The filter can hold alternate sources of
|
||||
* nodes that are not permanently stored locally
|
||||
*/
|
||||
std::vector<std::pair<SHAMapNodeID, UInt256>>
|
||||
SHAMap::getMissingNodes(int max, SHAMapSyncFilter const* filter)
|
||||
{
|
||||
XRPL_ASSERT(root_->getHash().isNonZero(), "xrpl::SHAMap::getMissingNodes : nonzero root hash");
|
||||
XRPL_ASSERT(max > 0, "xrpl::SHAMap::getMissingNodes : valid max input");
|
||||
|
||||
if (!isValid())
|
||||
{
|
||||
// The root node's own hash, since getHash() unshares the tree on a zero hash.
|
||||
JLOG(journal_.warn()) << "getMissingNodes called on an invalid map, root hash "
|
||||
<< root_->getHash() << " seq " << ledgerSeq();
|
||||
return {};
|
||||
}
|
||||
|
||||
MissingNodes mn(
|
||||
max,
|
||||
filter,
|
||||
@@ -391,11 +354,6 @@ SHAMap::getMissingNodes(int max, SHAMapSyncFilter const* filter)
|
||||
{
|
||||
gmnProcessNodes(mn, pos);
|
||||
|
||||
// The walk just invalidated the map. The loop stops descending here but falls through
|
||||
// to the drain below, since every posted read must be drained while `mn` is alive.
|
||||
if (!isValid())
|
||||
break;
|
||||
|
||||
if (mn.max <= 0)
|
||||
break;
|
||||
|
||||
@@ -425,11 +383,6 @@ SHAMap::getMissingNodes(int max, SHAMapSyncFilter const* filter)
|
||||
if (mn.deferred != 0)
|
||||
gmnProcessDeferredReads(mn);
|
||||
|
||||
// Reads are drained, so the map can be abandoned. What was collected belongs to a tree
|
||||
// that cannot exist.
|
||||
if (!isValid())
|
||||
return {};
|
||||
|
||||
if (mn.max <= 0)
|
||||
return std::move(mn.missingNodes);
|
||||
|
||||
@@ -463,11 +416,6 @@ SHAMap::getMissingNodes(int max, SHAMapSyncFilter const* filter)
|
||||
|
||||
} while (node != nullptr);
|
||||
|
||||
// addKnownNode() on another thread can write the verdict after the loop's own test, so the map
|
||||
// is judged once more before the result is returned.
|
||||
if (!isValid())
|
||||
return {}; // LCOV_EXCL_LINE: only that other thread reaches this, so no test does
|
||||
|
||||
if (mn.missingNodes.empty())
|
||||
clearSynching();
|
||||
|
||||
@@ -579,19 +527,11 @@ SHAMap::addRootNode(
|
||||
XRPL_ASSERT(cowid_ >= 1, "xrpl::SHAMap::addRootNode : valid cowid");
|
||||
XRPL_ASSERT(rootNode, "xrpl::SHAMap::addRootNode : non-null root node");
|
||||
|
||||
// A map syncs against one hash and installs a root once, so a root already held is a duplicate
|
||||
// only once it hashes to the hash asked for.
|
||||
// we already have a root_ node
|
||||
if (root_->getHash().isNonZero())
|
||||
{
|
||||
JLOG(journal_.trace()) << "Got root node, already have one";
|
||||
|
||||
if (root_->getHash() != hash)
|
||||
{
|
||||
JLOG(journal_.warn()) << "Root node offered under hash " << hash
|
||||
<< ", but the map holds " << root_->getHash();
|
||||
return SHAMapAddNode::invalid();
|
||||
}
|
||||
|
||||
XRPL_ASSERT(root_->getHash() == hash, "xrpl::SHAMap::addRootNode : valid hash");
|
||||
return SHAMapAddNode::duplicate();
|
||||
}
|
||||
|
||||
@@ -615,7 +555,7 @@ SHAMap::addRootNode(
|
||||
Serializer s;
|
||||
root_->serializeWithPrefix(s);
|
||||
filter->gotNode(
|
||||
false, root_->getHash(), ledgerSeq(), std::move(s.modData()), root_->getType());
|
||||
false, root_->getHash(), ledgerSeq_, std::move(s.modData()), root_->getType());
|
||||
}
|
||||
|
||||
return SHAMapAddNode::useful();
|
||||
@@ -658,13 +598,7 @@ SHAMap::addKnownNode(
|
||||
}
|
||||
|
||||
auto childHash = inner->getChildHash(branch);
|
||||
|
||||
// Depth before the cache: the cache is keyed by node hash and shared across the family, and
|
||||
// a hash covers a node's children but not its depth, so the same subtree can be cached as
|
||||
// complete at one depth and reached at another. Skipping the shortcut only forgoes an
|
||||
// optimization.
|
||||
if (!isLeafDepth(currNodeID.getDepth() + 1) &&
|
||||
f_.getFullBelowCache()->touchIfExists(childHash.asUInt256()))
|
||||
if (f_.getFullBelowCache()->touchIfExists(childHash.asUInt256()))
|
||||
{
|
||||
return SHAMapAddNode::duplicate();
|
||||
}
|
||||
@@ -682,32 +616,25 @@ SHAMap::addKnownNode(
|
||||
return SHAMapAddNode::invalid();
|
||||
}
|
||||
|
||||
// Only leaves may sit at kLeafDepth (see isLeafDepth), so an inner node there makes the map
|
||||
// impossible. The node is reported as bad data.
|
||||
//
|
||||
// Every node from the root down hash-verified to get here, so it is the requested root hash
|
||||
// itself that commits to a shape no valid tree can have. The verdict belongs to that hash
|
||||
// rather than to our copy of the tree.
|
||||
bool const badDepth = treeNode->isInner() && isLeafDepth(currNodeID.getDepth());
|
||||
SOMETIMES(badDepth, "xrpl::SHAMap::addKnownNode : map is invalid");
|
||||
if (badDepth)
|
||||
// Inner nodes must be at a level strictly less than 64
|
||||
// but leaf nodes (while notionally at level 64) can be
|
||||
// at any depth up to and including 64:
|
||||
if ((currNodeID.getDepth() > kLeafDepth) ||
|
||||
(treeNode->isInner() && currNodeID.getDepth() == kLeafDepth))
|
||||
{
|
||||
JLOG(journal_.warn()) << "Node " << nodeID << " makes the map invalid at "
|
||||
<< currNodeID;
|
||||
setInvalid();
|
||||
return SHAMapAddNode::invalid();
|
||||
// Map is provably invalid
|
||||
state_ = SHAMapState::Invalid;
|
||||
return SHAMapAddNode::useful();
|
||||
}
|
||||
|
||||
// The data hashes to the child at currNodeID but claims to belong at nodeID, so it is not
|
||||
// the node that was asked for. Only the label is wrong, so the map stays sound and the node
|
||||
// is still obtainable from another sender.
|
||||
bool const badPosition = (currNodeID != nodeID);
|
||||
SOMETIMES(badPosition, "xrpl::SHAMap::addKnownNode : node ID does not match its position");
|
||||
if (badPosition)
|
||||
if (currNodeID != nodeID)
|
||||
{
|
||||
JLOG(journal_.warn()) << "Unable to hook node " << nodeID << ", stuck at "
|
||||
<< currNodeID;
|
||||
return SHAMapAddNode::invalid();
|
||||
// Either this node is broken or we didn't request it (yet)
|
||||
JLOG(journal_.warn()) << "unable to hook node " << nodeID;
|
||||
JLOG(journal_.info()) << " stuck at " << currNodeID;
|
||||
JLOG(journal_.info()) << "got depth=" << nodeID.getDepth()
|
||||
<< ", walked to= " << currNodeID.getDepth();
|
||||
return SHAMapAddNode::useful();
|
||||
}
|
||||
|
||||
if (backed_)
|
||||
@@ -720,7 +647,7 @@ SHAMap::addKnownNode(
|
||||
Serializer s;
|
||||
treeNode->serializeWithPrefix(s);
|
||||
filter->gotNode(
|
||||
false, childHash, ledgerSeq(), std::move(s.modData()), treeNode->getType());
|
||||
false, childHash, ledgerSeq_, std::move(s.modData()), treeNode->getType());
|
||||
}
|
||||
|
||||
return SHAMapAddNode::useful();
|
||||
@@ -839,7 +766,7 @@ SHAMap::hasLeafNode(UInt256 const& tag, SHAMapHash const& targetNodeHash) const
|
||||
// Same kLeafDepth hazard as in visitDifferences above. That guard bounds the caller's own
|
||||
// traversal, not the map queried here, and the loop below descends from this map's root
|
||||
// independently, so this check is what keeps a malformed map from reaching getChildNodeID.
|
||||
if (isLeafDepth(nodeID.getDepth()))
|
||||
if (nodeID.getDepth() >= kLeafDepth)
|
||||
{
|
||||
// LCOV_EXCL_START
|
||||
UNREACHABLE("xrpl::SHAMap::hasLeafNode : inner node at leaf depth");
|
||||
@@ -922,8 +849,8 @@ SHAMap::verifyProofPath(UInt256 const& rootHash, UInt256 const& key, std::vector
|
||||
// there. These nodes come off the wire, so a peer can still claim an inner one;
|
||||
// reject it rather than passing this depth to selectBranch.
|
||||
SOMETIMES(
|
||||
isLeafDepth(depth), "xrpl::SHAMap::verifyProofPath : inner at leaf depth");
|
||||
if (isLeafDepth(depth))
|
||||
depth >= kLeafDepth, "xrpl::SHAMap::verifyProofPath : inner at leaf depth");
|
||||
if (depth >= kLeafDepth)
|
||||
return false;
|
||||
|
||||
auto const nodeId = SHAMapNodeID::createID(depth, key);
|
||||
@@ -932,9 +859,10 @@ SHAMap::verifyProofPath(UInt256 const& rootHash, UInt256 const& key, std::vector
|
||||
}
|
||||
else
|
||||
{
|
||||
// The hash chain up to rootHash proves this leaf sits where the path claims. Any
|
||||
// leaf whose subtree hashes the same at every level above satisfies that chain, so
|
||||
// the terminal leaf's own key is what ties the proof to `key`.
|
||||
// The hash chain up to rootHash only proves this leaf sits where the path claims,
|
||||
// not that it is the leaf for `key`: a peer could substitute any other leaf whose
|
||||
// subtree hashes to the same value at every level above it. Checking the terminal
|
||||
// leaf's own key is what ties the proof to `key` specifically.
|
||||
if (leafKey(*node) != key)
|
||||
return false;
|
||||
|
||||
|
||||
@@ -31,8 +31,6 @@
|
||||
#include <xrpl/protocol/detail/transactions.macro>
|
||||
|
||||
#undef TRANSACTION
|
||||
#undef TRANSACTION_INCLUDE
|
||||
|
||||
#pragma pop_macro("TRANSACTION")
|
||||
|
||||
// DO NOT INCLUDE TRANSACTOR HEADER FILES HERE.
|
||||
|
||||
@@ -1,225 +0,0 @@
|
||||
#include <xrpl/tx/invariants/CosignerInvariant.h>
|
||||
|
||||
#include <xrpl/basics/Log.h>
|
||||
#include <xrpl/basics/base_uint.h>
|
||||
#include <xrpl/beast/utility/Journal.h>
|
||||
#include <xrpl/ledger/ReadView.h>
|
||||
#include <xrpl/protocol/AccountID.h>
|
||||
#include <xrpl/protocol/Feature.h>
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
#include <xrpl/protocol/Keylet.h>
|
||||
#include <xrpl/protocol/LedgerFormats.h>
|
||||
#include <xrpl/protocol/Protocol.h>
|
||||
#include <xrpl/protocol/SField.h>
|
||||
#include <xrpl/protocol/STArray.h>
|
||||
#include <xrpl/protocol/STLedgerEntry.h>
|
||||
#include <xrpl/protocol/STObject.h>
|
||||
#include <xrpl/protocol/STTx.h>
|
||||
#include <xrpl/protocol/SeqProxy.h>
|
||||
#include <xrpl/protocol/TER.h>
|
||||
#include <xrpl/protocol/TxFormats.h>
|
||||
#include <xrpl/protocol/XRPAmount.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <optional>
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
namespace {
|
||||
|
||||
bool
|
||||
validSignerArray(STArray const& signers, std::size_t limit, SField const& elementName)
|
||||
{
|
||||
if (signers.size() > limit)
|
||||
return false;
|
||||
|
||||
std::optional<AccountID> previous;
|
||||
for (auto const& signer : signers)
|
||||
{
|
||||
if (signer.getFName() != elementName || !signer.isFieldPresent(sfAccount))
|
||||
return false;
|
||||
|
||||
auto const account = signer.getAccountID(sfAccount);
|
||||
if (previous && account <= *previous)
|
||||
return false;
|
||||
previous = account;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool
|
||||
validNestedSigners(STObject const& signature)
|
||||
{
|
||||
return !signature.isFieldPresent(sfSigners) ||
|
||||
validSignerArray(signature.getFieldArray(sfSigners), STTx::kMaxMultiSigners, sfSigner);
|
||||
}
|
||||
|
||||
bool
|
||||
validSignerArrays(STObject const& proposedTx)
|
||||
{
|
||||
if (!validNestedSigners(proposedTx))
|
||||
return false;
|
||||
|
||||
for (auto const* field :
|
||||
std::array<SField const*, 2>{&sfCounterpartySignature, &sfSponsorSignature})
|
||||
{
|
||||
if (proposedTx.isFieldPresent(*field) &&
|
||||
!validNestedSigners(proposedTx.getFieldObject(*field)))
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!proposedTx.isFieldPresent(sfBatchSigners))
|
||||
return true;
|
||||
|
||||
auto const& batchSigners = proposedTx.getFieldArray(sfBatchSigners);
|
||||
if (!validSignerArray(batchSigners, kMaxBatchSigners, sfBatchSigner))
|
||||
return false;
|
||||
|
||||
return std::ranges::all_of(
|
||||
batchSigners, [](auto const& batchSigner) { return validNestedSigners(batchSigner); });
|
||||
}
|
||||
|
||||
// The Ticket a proposal is keyed to: the one its proposed transaction spends.
|
||||
uint256
|
||||
proposalTicketKey(SLE const& proposal)
|
||||
{
|
||||
auto const& proposedTx = proposal.getFieldObject(sfProposedTransaction);
|
||||
return keylet::ticket(
|
||||
proposedTx.getAccountID(sfAccount),
|
||||
SeqProxy::rawTicket(proposedTx.getFieldU32(sfTicketSequence)))
|
||||
.key;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void
|
||||
ValidTransactionProposal::visitEntry(bool isDelete, SLE::ConstRef before, SLE::ConstRef after)
|
||||
{
|
||||
// `after` is always present (on deletion it holds the final state), so it
|
||||
// alone determines the entry type. LedgerEntryTypesMatch owns malformed
|
||||
// type transitions; skip them rather than read one side with the other's
|
||||
// schema.
|
||||
if (!after || (before && before->getType() != after->getType()))
|
||||
return;
|
||||
|
||||
auto const type = after->getType();
|
||||
|
||||
// Consuming a Ticket deletes the proposal keyed to it (XLS-0103 §4.5).
|
||||
// Remember each Ticket that went, so finalize can tell a proposal deleted
|
||||
// with its Ticket from one that simply vanished, and the proposal each
|
||||
// Ticket was keyed to, so finalize can check that none stayed behind.
|
||||
if (type == ltTICKET)
|
||||
{
|
||||
if (isDelete)
|
||||
{
|
||||
deletedTickets_.insert(after->key());
|
||||
proposalsOfDeletedTickets_.push_back(
|
||||
keylet::txProposal(
|
||||
after->getAccountID(sfAccount), after->getFieldU32(sfTicketSequence)));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (type != ltTRANSACTION_PROPOSAL)
|
||||
return;
|
||||
|
||||
if (!before)
|
||||
{
|
||||
++created_;
|
||||
}
|
||||
else if (isDelete)
|
||||
{
|
||||
++deleted_;
|
||||
ticketsOfDeletedProposals_.push_back(proposalTicketKey(*before));
|
||||
}
|
||||
else
|
||||
{
|
||||
++modified_;
|
||||
}
|
||||
|
||||
// A live proposal's signer lists must stay submittable: within size
|
||||
// limits, sorted by account, and free of duplicates.
|
||||
if (!isDelete && !validSignerArrays(after->getFieldObject(sfProposedTransaction)))
|
||||
invalidSignerArrays_ = true;
|
||||
}
|
||||
|
||||
bool
|
||||
ValidTransactionProposal::finalize(
|
||||
STTx const& tx,
|
||||
TER result,
|
||||
XRPAmount,
|
||||
ReadView const& view,
|
||||
beast::Journal const& j) const
|
||||
{
|
||||
if (!view.rules().enabled(featureCosign))
|
||||
return true;
|
||||
|
||||
if (invalidSignerArrays_)
|
||||
{
|
||||
JLOG(j.fatal()) << "Invariant failed: TransactionProposal signer arrays are not canonical.";
|
||||
return false;
|
||||
}
|
||||
|
||||
// A proposal leaves the ledger in two ways (XLS-0103 §4.5): a
|
||||
// TransactionProposalCancel deletes the proposal it names, and consuming
|
||||
// the Ticket a proposal is keyed to deletes the proposal with it, whether
|
||||
// the proposed transaction executed (or failed with a claimed-fee tec) or
|
||||
// AccountDelete swept the target's Tickets.
|
||||
std::uint32_t deletedWithTicket = 0;
|
||||
for (auto const& ticketKey : ticketsOfDeletedProposals_)
|
||||
{
|
||||
if (deletedTickets_.contains(ticketKey))
|
||||
++deletedWithTicket;
|
||||
}
|
||||
|
||||
bool const succeeded = isTesSuccess(result);
|
||||
bool effectsMatch = false;
|
||||
if (tx.getTxnType() == ttTRANSACTION_PROPOSAL_CREATE)
|
||||
{
|
||||
effectsMatch = created_ == (succeeded ? 1u : 0u) && modified_ == 0 && deleted_ == 0;
|
||||
}
|
||||
else if (tx.getTxnType() == ttTRANSACTION_PROPOSAL_CANCEL)
|
||||
{
|
||||
// A Cancel deletes one proposal and nothing else. It cannot
|
||||
// spend a reserved Ticket, so no proposal goes with its Ticket here.
|
||||
effectsMatch = created_ == 0 && modified_ == 0 && deletedWithTicket == 0 &&
|
||||
deleted_ == (succeeded ? 1u : 0u);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Any other transaction deletes a proposal only together with its
|
||||
// Ticket, and modifies at most one, only as a successful
|
||||
// SponsorshipTransfer.
|
||||
// TODO: extend this when TransactionProposalSign is implemented.
|
||||
std::uint32_t const modifiable =
|
||||
tx.getTxnType() == ttSPONSORSHIP_TRANSFER && succeeded ? 1 : 0;
|
||||
effectsMatch = created_ == 0 && modified_ <= modifiable && deleted_ == deletedWithTicket;
|
||||
}
|
||||
|
||||
if (!effectsMatch)
|
||||
{
|
||||
JLOG(j.fatal())
|
||||
<< "Invariant failed: TransactionProposal changes do not match transaction result.";
|
||||
return false;
|
||||
}
|
||||
|
||||
// The other direction of the deletion rule: ticketDelete deletes the
|
||||
// proposal keyed to a Ticket along with it (XLS-0103 §4.5), so no
|
||||
// proposal may outlive its Ticket, orphaned on a key that can no longer
|
||||
// be spent.
|
||||
bool const orphaned = std::ranges::any_of(
|
||||
proposalsOfDeletedTickets_, [&view](Keylet const& keylet) { return view.exists(keylet); });
|
||||
if (orphaned)
|
||||
{
|
||||
JLOG(j.fatal())
|
||||
<< "Invariant failed: TransactionProposal outlived the Ticket it is keyed to.";
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace xrpl
|
||||
@@ -935,12 +935,8 @@ ValidClawback::finalize(
|
||||
return !mptV2Enabled;
|
||||
}
|
||||
|
||||
// IOU balances keep 16 significant digits, so a clawback
|
||||
// much smaller than the balance changes it by a rounded
|
||||
// amount, possibly zero. Compare with the same rounded
|
||||
// subtraction the balance update performs.
|
||||
if (*afterBalance > *beforeBalance ||
|
||||
*afterBalance != *beforeBalance - std::min(*beforeBalance, clawAmount))
|
||||
(*beforeBalance - *afterBalance) != std::min(*beforeBalance, clawAmount))
|
||||
{
|
||||
JLOG(j.fatal())
|
||||
<< "Invariant failed: trustline clawback balance change is invalid";
|
||||
|
||||
@@ -254,15 +254,6 @@ private:
|
||||
TAmounts<TIn, TOut> const& stepAmt,
|
||||
TOut const& ownerGives) const;
|
||||
|
||||
template <template <typename, typename> typename Offer>
|
||||
void
|
||||
tryConsumeOffer(
|
||||
PaymentSandbox& sb,
|
||||
Offer<TIn, TOut>& offer,
|
||||
TAmounts<TIn, TOut> const& ofrAmt,
|
||||
TAmounts<TIn, TOut> const& stepAmt,
|
||||
TOut const& ownerGives) const;
|
||||
|
||||
// If clobQuality is available and has a better quality then return nullopt,
|
||||
// otherwise if amm liquidity is available return AMM offer adjusted based
|
||||
// on clobQuality.
|
||||
@@ -819,11 +810,10 @@ BookStep<TIn, TOut, TDerived>::forEachOffer(
|
||||
auto const amtIn = [&](TIn const& in) { return capInMPT && *maxIn_ < in ? *maxIn_ : in; };
|
||||
try
|
||||
{
|
||||
// All arithmetic in this block runs before the offer is consumed.
|
||||
// A crafted MPTokensV2 offer can overflow while transfer rates or
|
||||
// crossing limits are applied, here or in the callback's
|
||||
// limitStepIn()/limitStepOut(); remove that unusable offer instead
|
||||
// of letting it persist as a tecINTERNAL source. Overflows while
|
||||
// consuming the offer are handled by tryConsumeOffer().
|
||||
// crossing limits are applied; remove that unusable offer instead
|
||||
// of letting it persist as a tecINTERNAL source.
|
||||
stpAmt.in = mulRatio(amtIn(ofrAmt.in), ofrInRate, QUALITY_ONE, /*roundUp*/ true);
|
||||
|
||||
// owner pays the transfer fee. An MPT gross-up that doesn't fit in
|
||||
@@ -951,9 +941,16 @@ BookStep<TIn, TOut, TDerived>::forEachOffer(
|
||||
removeOffer("Removing offer with overflowing amount calculation");
|
||||
return true;
|
||||
}
|
||||
// Legacy behavior: the exception escapes flow() and the
|
||||
// transaction fails with tecINTERNAL.
|
||||
// An overflow can only be produced by a crafted MPT offer, and MPT
|
||||
// offers require featureMPTokensV2 (enforced at OfferCreate
|
||||
// preflight). So the amendment is always enabled when we get here
|
||||
// and this legacy re-throw is unreachable in practice.
|
||||
// LCOV_EXCL_START
|
||||
XRPL_ASSERT(
|
||||
sb.rules().enabled(featureMPTokensV2),
|
||||
"xrpl::BookStep::forEachOffer : overflow implies MPTokensV2");
|
||||
throw;
|
||||
// LCOV_EXCL_STOP
|
||||
}
|
||||
};
|
||||
|
||||
@@ -994,32 +991,6 @@ BookStep<TIn, TOut, TDerived>::forEachOffer(
|
||||
return {offers.permToRemove(), counter.count()};
|
||||
}
|
||||
|
||||
template <class TIn, class TOut, class TDerived>
|
||||
template <template <typename, typename> typename Offer>
|
||||
void
|
||||
BookStep<TIn, TOut, TDerived>::tryConsumeOffer(
|
||||
PaymentSandbox& sb,
|
||||
Offer<TIn, TOut>& offer,
|
||||
TAmounts<TIn, TOut> const& ofrAmt,
|
||||
TAmounts<TIn, TOut> const& stepAmt,
|
||||
TOut const& ownerGives) const
|
||||
{
|
||||
try
|
||||
{
|
||||
// Under MPTokensV2, an overflow while consuming the offer fails the
|
||||
// strand instead of being handled by forEachOffer().
|
||||
consumeOffer(sb, offer, ofrAmt, stepAmt, ownerGives);
|
||||
}
|
||||
catch (std::overflow_error const&)
|
||||
{
|
||||
if (sb.rules().enabled(featureMPTokensV2))
|
||||
{
|
||||
Throw<FlowException>(tecINTERNAL, "Overflow while consuming offer");
|
||||
}
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
template <class TIn, class TOut, class TDerived>
|
||||
template <template <typename, typename> typename Offer>
|
||||
void
|
||||
@@ -1221,7 +1192,7 @@ BookStep<TIn, TOut, TDerived>::revImp(
|
||||
savedOuts.insert(stpAmt.out);
|
||||
result = TAmounts<TIn, TOut>(sum(savedIns), sum(savedOuts));
|
||||
remainingOut = out - result.out;
|
||||
this->tryConsumeOffer(sb, offer, ofrAmt, stpAmt, ownerGives);
|
||||
this->consumeOffer(sb, offer, ofrAmt, stpAmt, ownerGives);
|
||||
// return true b/c even if the payment is satisfied,
|
||||
// we need to consume the offer. An offer limited by what the
|
||||
// issuer can issue is not consumed, though: its remainder is still
|
||||
@@ -1253,7 +1224,7 @@ BookStep<TIn, TOut, TDerived>::revImp(
|
||||
savedOuts.insert(remainingOut);
|
||||
result.in = sum(savedIns);
|
||||
result.out = out;
|
||||
this->tryConsumeOffer(sb, offer, ofrAdjAmt, stpAdjAmt, ownerGivesAdj);
|
||||
this->consumeOffer(sb, offer, ofrAdjAmt, stpAdjAmt, ownerGivesAdj);
|
||||
|
||||
// Explicitly check whether the offer is funded. Given that we have
|
||||
// (stpAmt.out > remainingOut), it's natural to assume the offer
|
||||
@@ -1437,7 +1408,7 @@ BookStep<TIn, TOut, TDerived>::fwdImp(
|
||||
savedOuts = std::move(savedOutsAdj);
|
||||
result = resultAdj;
|
||||
remainingIn = in - result.in;
|
||||
this->tryConsumeOffer(sb, offer, ofrAdjAmt, stpAdjAmt, ownerGivesAdj);
|
||||
this->consumeOffer(sb, offer, ofrAdjAmt, stpAdjAmt, ownerGivesAdj);
|
||||
|
||||
// When the mantissas of two iou amounts differ by less than ten, then
|
||||
// subtracting them leaves a result of zero. This can cause the check
|
||||
@@ -1541,18 +1512,6 @@ BookStep<TIn, TOut, TDerived>::check(StrandContext const& ctx) const
|
||||
return temBAD_PATH;
|
||||
}
|
||||
|
||||
// An issuer paying with its own MPT issues it into the first book. A
|
||||
// holder-sourced strand records the MPT in the first MPTEndpointStep and
|
||||
// so rejects a later book that outputs it again; do the same here. The
|
||||
// step after such a book issues the MPT a second time and the payment
|
||||
// engine cannot keep the two within MaximumAmount once StrandFlow
|
||||
// rebuilds the sandbox on a limiting step.
|
||||
if (ctx.isFirst && book_.in.holds<MPTIssue>() && book_.in.getIssuer() == ctx.strandSrc &&
|
||||
ctx.view.rules().enabled(featureMPTokensV2))
|
||||
{
|
||||
ctx.seenDirectAssets[0].insert(book_.in);
|
||||
}
|
||||
|
||||
// Do not allow two books to output the same issue. This may cause offers on
|
||||
// one step to unfund offers in another step.
|
||||
if (!ctx.seenBookOuts.insert(book_.out).second ||
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
#include <xrpl/beast/utility/instrumentation.h>
|
||||
#include <xrpl/core/ServiceRegistry.h>
|
||||
#include <xrpl/ledger/PaymentSandbox.h>
|
||||
#include <xrpl/ledger/View.h>
|
||||
#include <xrpl/ledger/helpers/MPTokenHelpers.h>
|
||||
#include <xrpl/ledger/helpers/TokenHelpers.h>
|
||||
#include <xrpl/protocol/AccountID.h>
|
||||
@@ -875,18 +874,15 @@ MPTEndpointStep<TDerived>::check(StrandContext const& ctx) const
|
||||
|
||||
// pure issue/redeem can't be frozen (issuer/holder)
|
||||
// For the first step: check global freeze of the step's own asset.
|
||||
// For the last step: check only the per-holder lock.
|
||||
// For the last step: check only the per-holder MPToken lock.
|
||||
// Global freeze of the deliver asset is not checked here
|
||||
// because MPT semantics allow issuer<->holder transfers even when globally
|
||||
// locked — only holder-to-holder DEX paths are restricted.
|
||||
// For both steps: a vault share inherits the freeze of the vault's
|
||||
// underlying asset.
|
||||
if (!(ctx.isLast && ctx.isFirst))
|
||||
{
|
||||
auto const& account = ctx.isFirst ? src_ : dst_;
|
||||
bool const frozen = (ctx.isFirst && isGlobalFrozen(ctx.view, mptIssue_)) ||
|
||||
isIndividualFrozen(ctx.view, account, mptIssue_) ||
|
||||
isVaultPseudoAccountFrozen(ctx.view, account, mptIssue_, 0);
|
||||
isIndividualFrozen(ctx.view, account, mptIssue_);
|
||||
if (frozen)
|
||||
return terLOCKED;
|
||||
}
|
||||
|
||||
@@ -110,16 +110,11 @@ EscrowCancel::doApply()
|
||||
auto const sle = ctx_.view().peek(keylet::account(account));
|
||||
STAmount const amount = escrow->getFieldAmount(sfAmount);
|
||||
|
||||
auto const reserveToSubtract = calculateAdditionalReserve((*escrow)[~sfBytecode]);
|
||||
|
||||
// The return can re-create a holding the owner deleted while the escrow
|
||||
// was pending; the removed escrow must not be counted against its reserve.
|
||||
bool const recycleReserve = ctx_.view().rules().enabled(fixCleanup3_4_0);
|
||||
if (recycleReserve)
|
||||
{
|
||||
decreaseOwnerCountForObject(
|
||||
ctx_.view(), sle, escrow.mutableRawSle(), reserveToSubtract, ctx_.journal);
|
||||
}
|
||||
decreaseOwnerCountForObject(ctx_.view(), sle, escrow.mutableRawSle(), 1, ctx_.journal);
|
||||
|
||||
// Transfer amount back to the owner
|
||||
if (isXRP(amount))
|
||||
@@ -165,10 +160,7 @@ EscrowCancel::doApply()
|
||||
}
|
||||
|
||||
if (!recycleReserve)
|
||||
{
|
||||
decreaseOwnerCountForObject(
|
||||
ctx_.view(), sle, escrow.mutableRawSle(), reserveToSubtract, ctx_.journal);
|
||||
}
|
||||
decreaseOwnerCountForObject(ctx_.view(), sle, escrow.mutableRawSle(), 1, ctx_.journal);
|
||||
|
||||
// Remove escrow from ledger
|
||||
escrow.erase();
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
#include <xrpl/basics/chrono.h>
|
||||
#include <xrpl/beast/utility/Zero.h>
|
||||
#include <xrpl/conditions/Condition.h>
|
||||
#include <xrpl/core/ServiceRegistry.h>
|
||||
#include <xrpl/ledger/ApplyView.h>
|
||||
#include <xrpl/ledger/ReadView.h>
|
||||
#include <xrpl/ledger/View.h>
|
||||
#include <xrpl/ledger/entries/EscrowEntry.h>
|
||||
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
|
||||
@@ -16,7 +16,6 @@
|
||||
#include <xrpl/protocol/AccountID.h>
|
||||
#include <xrpl/protocol/Concepts.h>
|
||||
#include <xrpl/protocol/Feature.h>
|
||||
#include <xrpl/protocol/Fees.h>
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
#include <xrpl/protocol/Issue.h>
|
||||
#include <xrpl/protocol/Keylet.h>
|
||||
@@ -131,19 +130,6 @@ escrowCreatePreflightHelper<MPTIssue>(PreflightContext const& ctx)
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
XRPAmount
|
||||
EscrowCreate::calculateBaseFee(ReadView const& view, STTx const& tx)
|
||||
{
|
||||
XRPAmount txnFees{Transactor::calculateBaseFee(view, tx)};
|
||||
if (tx.isFieldPresent(sfBytecode))
|
||||
{
|
||||
// Ten base fees in all: `Transactor::calculateBaseFee` charges one,
|
||||
// this adds nine. Plus 5 drops per byte.
|
||||
txnFees += 9 * view.fees().base + 5 * tx[sfBytecode].size();
|
||||
}
|
||||
return txnFees;
|
||||
}
|
||||
|
||||
NotTEC
|
||||
EscrowCreate::preflight(PreflightContext const& ctx)
|
||||
{
|
||||
@@ -175,19 +161,12 @@ EscrowCreate::preflight(PreflightContext const& ctx)
|
||||
ctx.tx[sfCancelAfter] <= ctx.tx[sfFinishAfter])
|
||||
return temBAD_EXPIRATION;
|
||||
|
||||
if (ctx.tx.isFieldPresent(sfBytecode) && !ctx.tx.isFieldPresent(sfCancelAfter))
|
||||
return temBAD_EXPIRATION;
|
||||
|
||||
// In the absence of a FinishAfter, the escrow can be finished
|
||||
// immediately, which can be confusing. When creating an escrow,
|
||||
// we want to ensure that either a FinishAfter time is explicitly
|
||||
// specified or a completion condition is attached.
|
||||
if (!ctx.tx[~sfFinishAfter] && !ctx.tx[~sfCondition] && !ctx.tx[~sfBytecode])
|
||||
{
|
||||
JLOG(ctx.j.debug()) << "Must have at least one of FinishAfter, "
|
||||
"Condition, or Bytecode.";
|
||||
if (!ctx.tx[~sfFinishAfter] && !ctx.tx[~sfCondition])
|
||||
return temMALFORMED;
|
||||
}
|
||||
|
||||
if (auto const cb = ctx.tx[~sfCondition])
|
||||
{
|
||||
@@ -203,33 +182,6 @@ EscrowCreate::preflight(PreflightContext const& ctx)
|
||||
}
|
||||
}
|
||||
|
||||
if (ctx.tx.isFieldPresent(sfData))
|
||||
{
|
||||
if (!ctx.tx.isFieldPresent(sfBytecode))
|
||||
{
|
||||
JLOG(ctx.j.debug()) << "EscrowCreate with Data requires Bytecode";
|
||||
return temMALFORMED;
|
||||
}
|
||||
auto const data = ctx.tx.getFieldVL(sfData);
|
||||
if (data.size() > kMaxWasmDataLength)
|
||||
{
|
||||
JLOG(ctx.j.debug()) << "EscrowCreate.Data bad size " << data.size();
|
||||
return temMALFORMED;
|
||||
}
|
||||
}
|
||||
|
||||
// amendment was checked by checkExtraFeatures
|
||||
if (ctx.tx.isFieldPresent(sfBytecode))
|
||||
{
|
||||
auto const code = ctx.tx.getFieldVL(sfBytecode);
|
||||
// Protocol ceiling only; `preflight` has no view.
|
||||
if (code.empty() || code.size() > kMaxBytecodeSizeLimit)
|
||||
{
|
||||
JLOG(ctx.j.debug()) << "EscrowCreate.Bytecode bad size " << code.size();
|
||||
return temMALFORMED;
|
||||
}
|
||||
}
|
||||
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
@@ -264,30 +216,6 @@ EscrowCreate::preclaim(PreclaimContext const& ctx)
|
||||
!isTesSuccess(ret))
|
||||
return ret;
|
||||
}
|
||||
|
||||
// The limits are voted, so they can change between ledgers. A `tef`, unlike
|
||||
// a `tem`, does not mark the transaction bad for the hold time.
|
||||
if (ctx.tx.isFieldPresent(sfBytecode))
|
||||
{
|
||||
auto const& fees = ctx.view.fees();
|
||||
if (isBytecodeUploadDisabled(fees))
|
||||
{
|
||||
JLOG(ctx.j.debug()) << "WASM runtime deactivated by fee voting";
|
||||
return tefTEMP_DISABLED;
|
||||
}
|
||||
|
||||
auto const code = ctx.tx.getFieldVL(sfBytecode);
|
||||
if (code.size() > fees.bytecodeSizeLimit)
|
||||
{
|
||||
JLOG(ctx.j.debug()) << "EscrowCreate.Bytecode over voted limit " << code.size();
|
||||
return tefEXCEEDS_LIMIT;
|
||||
}
|
||||
|
||||
// TODO(SmartEscrow): screen the module itself here - that it compiles,
|
||||
// and that it imports and exports what the engine can serve - once the
|
||||
// WASM engine lands. Until then any blob of the right size is taken.
|
||||
}
|
||||
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
@@ -308,7 +236,6 @@ EscrowCreate::doApply()
|
||||
|
||||
// Check reserve and funds availability
|
||||
STAmount const amount{ctx_.tx[sfAmount]};
|
||||
auto const reserveToAdd = calculateAdditionalReserve(ctx_.tx[~sfBytecode]);
|
||||
|
||||
auto const balance = sle->getFieldAmount(sfBalance).xrp();
|
||||
// First check: whoever is on the hook for the new owner increment
|
||||
@@ -316,8 +243,8 @@ EscrowCreate::doApply()
|
||||
// validates the sponsor's reserve + remaining credit. When
|
||||
// unsponsored this hits the source branch and validates the
|
||||
// source's pre-lock balance against base + (currentOC+1)*increment.
|
||||
if (auto const ret = checkReserve(
|
||||
ctx_.getApplyViewContext(), sle, balance, {.ownerCountDelta = reserveToAdd}, j_);
|
||||
if (auto const ret =
|
||||
checkReserve(ctx_.getApplyViewContext(), sle, balance, {.ownerCountDelta = 1}, j_);
|
||||
!isTesSuccess(ret))
|
||||
return ret;
|
||||
|
||||
@@ -331,15 +258,11 @@ EscrowCreate::doApply()
|
||||
// checkReserve) because that helper diverts to the sponsor's balance
|
||||
// when a sponsor is present and would ignore the source's post-lock
|
||||
// balance entirely. ownerCountDelta differs by case:
|
||||
// - sponsored: 0 — sponsor covers the new owner increment, so
|
||||
// the source only owes reserve for its current owners.
|
||||
// - unsponsored: reserveToAdd — source owes reserve including the new
|
||||
// increment, which Bytecode can make larger than one unit.
|
||||
// - sponsored: 0 — sponsor covers the new owner increment, so the
|
||||
// source only owes reserve for its current owners.
|
||||
// - unsponsored: 1 — source owes reserve including the new increment.
|
||||
auto const sourceReserve = accountReserve(
|
||||
ctx_.view(),
|
||||
sle,
|
||||
j_,
|
||||
{.ownerCountDelta = getTxReserveSponsorID(ctx_.tx) ? 0 : reserveToAdd});
|
||||
ctx_.view(), sle, j_, {.ownerCountDelta = getTxReserveSponsorID(ctx_.tx) ? 0 : 1});
|
||||
if (balance - STAmount(amount).xrp() < sourceReserve)
|
||||
return tecUNFUNDED;
|
||||
}
|
||||
@@ -366,8 +289,6 @@ EscrowCreate::doApply()
|
||||
(*escrow)[~sfCancelAfter] = ctx_.tx[~sfCancelAfter];
|
||||
(*escrow)[~sfFinishAfter] = ctx_.tx[~sfFinishAfter];
|
||||
(*escrow)[~sfDestinationTag] = ctx_.tx[~sfDestinationTag];
|
||||
(*escrow)[~sfBytecode] = ctx_.tx[~sfBytecode];
|
||||
(*escrow)[~sfData] = ctx_.tx[~sfData];
|
||||
|
||||
if (ctx_.view().rules().enabled(fixIncludeKeyletFields))
|
||||
{
|
||||
@@ -435,7 +356,7 @@ EscrowCreate::doApply()
|
||||
}
|
||||
|
||||
// increment owner count
|
||||
increaseOwnerCount(ctx_.getApplyViewContext(), sle, reserveToAdd, ctx_.journal);
|
||||
increaseOwnerCount(ctx_.getApplyViewContext(), sle, 1, ctx_.journal);
|
||||
addSponsorToLedgerEntry(ctx_.getApplyViewContext(), escrow.mutableRawSle());
|
||||
ctx_.view().update(sle);
|
||||
return tesSUCCESS;
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
#include <xrpl/protocol/AccountID.h>
|
||||
#include <xrpl/protocol/Concepts.h>
|
||||
#include <xrpl/protocol/Feature.h>
|
||||
#include <xrpl/protocol/Fees.h>
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
#include <xrpl/protocol/Rate.h>
|
||||
#include <xrpl/protocol/SField.h>
|
||||
@@ -28,8 +27,6 @@
|
||||
#include <xrpl/protocol/XRPAmount.h>
|
||||
#include <xrpl/tx/Transactor.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <optional>
|
||||
#include <system_error>
|
||||
#include <variant>
|
||||
|
||||
@@ -82,24 +79,7 @@ EscrowFinish::preflight(PreflightContext const& ctx)
|
||||
// If you specify a condition, then you must also specify
|
||||
// a fulfillment.
|
||||
if (static_cast<bool>(cb) != static_cast<bool>(fb))
|
||||
{
|
||||
JLOG(ctx.j.debug()) << "Condition != Fulfillment";
|
||||
return temMALFORMED;
|
||||
}
|
||||
|
||||
if (auto const allowance = ctx.tx[~sfGas]; allowance)
|
||||
{
|
||||
// Protocol ceiling only; `preflight` has no view.
|
||||
if (*allowance == 0)
|
||||
{
|
||||
return temBAD_LIMIT;
|
||||
}
|
||||
if (*allowance > kMaxGasLimit)
|
||||
{
|
||||
JLOG(ctx.j.debug()) << "Gas too large: " << *allowance;
|
||||
return temBAD_LIMIT;
|
||||
}
|
||||
}
|
||||
|
||||
return tesSUCCESS;
|
||||
}
|
||||
@@ -168,15 +148,7 @@ EscrowFinish::calculateBaseFee(ReadView const& view, STTx const& tx)
|
||||
{
|
||||
extraFee += view.fees().base * (32 + (fb->size() / 16));
|
||||
}
|
||||
if (auto const allowance = tx[~sfGas]; allowance)
|
||||
{
|
||||
// The extra fee is the allowance in drops, rounded up to the nearest
|
||||
// whole drop. Widen before multiplying: both factors are 32-bit.
|
||||
std::uint64_t const allowanceFee =
|
||||
((std::uint64_t{*allowance} * view.fees().gasPrice) + microDropsPerDrop - 1) /
|
||||
microDropsPerDrop;
|
||||
extraFee += allowanceFee;
|
||||
}
|
||||
|
||||
return Transactor::calculateBaseFee(view, tx) + extraFee;
|
||||
}
|
||||
|
||||
@@ -190,66 +162,25 @@ EscrowFinish::preclaim(PreclaimContext const& ctx)
|
||||
return err;
|
||||
}
|
||||
|
||||
if (ctx.view.rules().enabled(featureTokenEscrow) ||
|
||||
ctx.view.rules().enabled(featureSmartEscrow))
|
||||
if (ctx.view.rules().enabled(featureTokenEscrow))
|
||||
{
|
||||
// this check is done in doApply before this amendment is enabled
|
||||
auto const seqProxy = SeqProxy::rawSequence(ctx.tx[sfOfferSequence]);
|
||||
EscrowEntryR const escrow(ctx.tx[sfOwner], seqProxy, ctx.view);
|
||||
if (!escrow)
|
||||
return tecNO_TARGET;
|
||||
|
||||
if (ctx.view.rules().enabled(featureSmartEscrow))
|
||||
AccountID const dest = (*escrow)[sfDestination];
|
||||
STAmount const amount = (*escrow)[sfAmount];
|
||||
|
||||
if (!isXRP(amount))
|
||||
{
|
||||
if (escrow->isFieldPresent(sfBytecode))
|
||||
{
|
||||
if (!ctx.tx.isFieldPresent(sfGas))
|
||||
{
|
||||
JLOG(ctx.j.debug()) << "Bytecode requires Gas";
|
||||
return tefBYTECODE_NOT_INCLUDED;
|
||||
}
|
||||
|
||||
// The limits are voted, so they can change between ledgers.
|
||||
// A `tef`, unlike a `tem`, does not mark the transaction bad
|
||||
// for the hold time.
|
||||
auto const& fees = ctx.view.fees();
|
||||
if (isBytecodeExecutionDisabled(fees))
|
||||
{
|
||||
JLOG(ctx.j.debug()) << "WASM runtime deactivated by fee voting";
|
||||
return tefTEMP_DISABLED;
|
||||
}
|
||||
|
||||
if (ctx.tx[sfGas] > fees.gasLimit)
|
||||
{
|
||||
JLOG(ctx.j.debug()) << "Gas over voted limit: " << ctx.tx[sfGas];
|
||||
return tefEXCEEDS_LIMIT;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (ctx.tx.isFieldPresent(sfGas))
|
||||
{
|
||||
JLOG(ctx.j.debug()) << "Bytecode not present, "
|
||||
"Gas present";
|
||||
return tefNO_BYTECODE;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (ctx.view.rules().enabled(featureTokenEscrow))
|
||||
{
|
||||
AccountID const dest = (*escrow)[sfDestination];
|
||||
STAmount const amount = (*escrow)[sfAmount];
|
||||
|
||||
if (!isXRP(amount))
|
||||
{
|
||||
if (auto const ret = std::visit(
|
||||
[&]<typename T>(T const&) {
|
||||
return escrowUnlockPreclaimHelper<T>(ctx.view, dest, amount);
|
||||
},
|
||||
amount.asset().value());
|
||||
!isTesSuccess(ret))
|
||||
return ret;
|
||||
}
|
||||
if (auto const ret = std::visit(
|
||||
[&]<typename T>(T const&) {
|
||||
return escrowUnlockPreclaimHelper<T>(ctx.view, dest, amount);
|
||||
},
|
||||
amount.asset().value());
|
||||
!isTesSuccess(ret))
|
||||
return ret;
|
||||
}
|
||||
}
|
||||
return tesSUCCESS;
|
||||
@@ -262,8 +193,7 @@ EscrowFinish::doApply()
|
||||
EscrowEntryW escrow(ctx_.tx[sfOwner], seqProxy, ctx_.view(), j_);
|
||||
if (!escrow)
|
||||
{
|
||||
if (ctx_.view().rules().enabled(featureTokenEscrow) ||
|
||||
ctx_.view().rules().enabled(featureSmartEscrow))
|
||||
if (ctx_.view().rules().enabled(featureTokenEscrow))
|
||||
return tecINTERNAL; // LCOV_EXCL_LINE
|
||||
|
||||
return tecNO_TARGET;
|
||||
@@ -281,20 +211,6 @@ EscrowFinish::doApply()
|
||||
if ((*escrow)[~sfCancelAfter] && after(now, (*escrow)[sfCancelAfter]))
|
||||
return tecNO_PERMISSION;
|
||||
|
||||
AccountID const destID = (*escrow)[sfDestination];
|
||||
auto const sled = ctx_.view().peek(keylet::account(destID));
|
||||
if (ctx_.view().rules().enabled(featureSmartEscrow))
|
||||
{
|
||||
// NOTE: Escrow payments cannot be used to fund accounts.
|
||||
if (!sled)
|
||||
return tecNO_DST;
|
||||
|
||||
if (auto err =
|
||||
verifyDepositPreauth(ctx_.tx, ctx_.view(), accountID_, destID, sled, ctx_.journal);
|
||||
!isTesSuccess(err))
|
||||
return err;
|
||||
}
|
||||
|
||||
// Check cryptocondition fulfillment
|
||||
{
|
||||
auto const id = ctx_.tx.getTransactionID();
|
||||
@@ -348,33 +264,16 @@ EscrowFinish::doApply()
|
||||
return tecCRYPTOCONDITION_ERROR;
|
||||
}
|
||||
|
||||
if (!ctx_.view().rules().enabled(featureSmartEscrow))
|
||||
{
|
||||
// NOTE: Escrow payments cannot be used to fund accounts.
|
||||
if (!sled)
|
||||
return tecNO_DST;
|
||||
// NOTE: Escrow payments cannot be used to fund accounts.
|
||||
AccountID const destID = (*escrow)[sfDestination];
|
||||
auto const sled = ctx_.view().peek(keylet::account(destID));
|
||||
if (!sled)
|
||||
return tecNO_DST;
|
||||
|
||||
if (auto err =
|
||||
verifyDepositPreauth(ctx_.tx, ctx_.view(), accountID_, destID, sled, ctx_.journal);
|
||||
!isTesSuccess(err))
|
||||
return err;
|
||||
}
|
||||
|
||||
// Execute custom release function
|
||||
if ((*escrow)[~sfBytecode])
|
||||
{
|
||||
// TODO(SmartEscrow): run the escrow's contract here once the WASM
|
||||
// engine lands. That change also brings with it the reporting the run
|
||||
// produces - the gas it consumed and the code it returned, in the
|
||||
// transaction metadata - and the tecBYTECODE_REJECTED result that lets
|
||||
// a rejected escrow survive with the data the contract wrote.
|
||||
//
|
||||
// Until then an escrow carrying bytecode can be created, funded and
|
||||
// cancelled, but not finished. Reachable only under featureSmartEscrow,
|
||||
// which is Supported::No.
|
||||
JLOG(j_.debug()) << "EscrowFinish: no WASM engine to run the escrow's bytecode";
|
||||
return tecFAILED_PROCESSING;
|
||||
}
|
||||
if (auto err =
|
||||
verifyDepositPreauth(ctx_.tx, ctx_.view(), accountID_, destID, sled, ctx_.journal);
|
||||
!isTesSuccess(err))
|
||||
return err;
|
||||
|
||||
AccountID const account = (*escrow)[sfAccount];
|
||||
|
||||
@@ -402,18 +301,13 @@ EscrowFinish::doApply()
|
||||
}
|
||||
}
|
||||
|
||||
auto const reserveToSubtract = calculateAdditionalReserve((*escrow)[~sfBytecode]);
|
||||
|
||||
// Delivery can auto-create the destination's holding; the removed escrow
|
||||
// must not be counted against its reserve. The two share a reserve payer
|
||||
// for a self-escrow, or when one sponsor covers both.
|
||||
bool const recycleReserve =
|
||||
ctx_.view().rules().enabled(featureSponsor) || ctx_.view().rules().enabled(fixCleanup3_4_0);
|
||||
if (recycleReserve)
|
||||
{
|
||||
decreaseOwnerCountForObject(
|
||||
ctx_.view(), account, escrow.mutableRawSle(), reserveToSubtract, ctx_.journal);
|
||||
}
|
||||
decreaseOwnerCountForObject(ctx_.view(), account, escrow.mutableRawSle(), 1, ctx_.journal);
|
||||
|
||||
STAmount const amount = escrow->getFieldAmount(sfAmount);
|
||||
// Transfer amount to destination
|
||||
@@ -465,10 +359,7 @@ EscrowFinish::doApply()
|
||||
ctx_.view().update(sled);
|
||||
|
||||
if (!recycleReserve)
|
||||
{
|
||||
decreaseOwnerCountForObject(
|
||||
ctx_.view(), account, escrow.mutableRawSle(), reserveToSubtract, ctx_.journal);
|
||||
}
|
||||
decreaseOwnerCountForObject(ctx_.view(), account, escrow.mutableRawSle(), 1, ctx_.journal);
|
||||
|
||||
// Remove escrow from ledger
|
||||
escrow.erase();
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
#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>
|
||||
@@ -130,133 +129,25 @@ LoanManage::preclaim(PreclaimContext const& ctx)
|
||||
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())
|
||||
if (auto const vault = ctx.view.read(keylet::vault(loanBrokerSle->at(sfVaultID)));
|
||||
vault && getVaultVersion(vault) == VaultVersion::FixedPrecision)
|
||||
{
|
||||
// Fix the mode so the STAmount conversion truncates the fractional part.
|
||||
NumberRoundModeGuard const mg(Number::RoundingMode::TowardsZero);
|
||||
return STAmount{vaultAsset, cappedCover};
|
||||
// FixedPrecision Vaults are not yet supported by LoanManage.
|
||||
return tecNO_PERMISSION;
|
||||
}
|
||||
|
||||
// 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);
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
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);
|
||||
@@ -293,7 +184,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 = getVaultScale(vaultSle);
|
||||
auto const vaultScale = getAssetsTotalScale(vaultSle);
|
||||
|
||||
{
|
||||
// Decrease the Total Value of the Vault:
|
||||
@@ -416,39 +307,22 @@ LoanManage::impairLoan(
|
||||
return tecTOO_SOON;
|
||||
}
|
||||
|
||||
bool const fixedPrecision = getVaultVersion(vaultSle) == VaultVersion::FixedPrecision;
|
||||
Number const lossUnrealized = loanVaultExposure(vaultSle, loanSle);
|
||||
|
||||
// Update the Vault object (set "paper loss")
|
||||
if (fixedPrecision)
|
||||
// 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))
|
||||
{
|
||||
// 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
|
||||
}
|
||||
// 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;
|
||||
}
|
||||
view.update(vaultSle);
|
||||
|
||||
@@ -477,32 +351,23 @@ LoanManage::unimpairLoan(
|
||||
Asset const& vaultAsset,
|
||||
beast::Journal j)
|
||||
{
|
||||
// Update the Vault object (clear "paper loss")
|
||||
bool const fixedPrecision = getVaultVersion(vaultSle) == VaultVersion::FixedPrecision;
|
||||
// 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);
|
||||
Number const lossReversed = loanVaultExposure(vaultSle, loanSle);
|
||||
if (fixedPrecision)
|
||||
if (vaultLossUnrealizedProxy < lossReversed)
|
||||
{
|
||||
// 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);
|
||||
// LCOV_EXCL_START
|
||||
JLOG(j.warn()) << "Vault unrealized loss is less than the amount to be cleared";
|
||||
return tefBAD_LEDGER;
|
||||
// LCOV_EXCL_STOP
|
||||
}
|
||||
// Reverse the "paper loss"
|
||||
adjustImpreciseNumber(vaultLossUnrealizedProxy, -lossReversed, vaultAsset, vaultScale);
|
||||
|
||||
view.update(vaultSle);
|
||||
|
||||
@@ -557,7 +422,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, j_);
|
||||
return defaultLoan(view, loanSle, brokerSle, vaultSle, vaultAsset, j_);
|
||||
if (tx.isFlag(tfLoanImpair))
|
||||
return impairLoan(view, loanSle, vaultSle, vaultAsset, j_);
|
||||
if (tx.isFlag(tfLoanUnimpair))
|
||||
@@ -570,27 +435,9 @@ 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,12 +46,14 @@ isPaymentAmountInvalid(STAmount const& amount)
|
||||
return amount <= beast::kZero;
|
||||
}
|
||||
|
||||
// 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.
|
||||
// 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.
|
||||
STAmount
|
||||
storedBalance(ReadView const& view, AccountID const& id, Asset const& asset, beast::Journal j)
|
||||
conservationBalance(ReadView const& view, AccountID const& id, Asset const& asset, beast::Journal j)
|
||||
{
|
||||
if (isXRP(asset))
|
||||
{
|
||||
@@ -60,25 +62,6 @@ storedBalance(ReadView const& view, AccountID const& id, Asset const& asset, bea
|
||||
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,
|
||||
@@ -300,6 +283,11 @@ 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);
|
||||
|
||||
@@ -374,11 +362,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);
|
||||
|
||||
bool const fixedPrecision = getVaultVersion(vaultSle) == VaultVersion::FixedPrecision;
|
||||
auto const vaultScale = getVaultScale(vaultSle);
|
||||
auto const vaultScale = getAssetsTotalScale(vaultSle);
|
||||
|
||||
// Send the broker fee to the owner if they have sufficient cover available,
|
||||
// _and_ if the owner can receive funds
|
||||
@@ -404,8 +392,7 @@ LoanPay::doApply()
|
||||
return roundToAsset(
|
||||
asset, tenthBipsOfValue(debtTotalProxy.value(), coverRateMinimum), loanScale);
|
||||
}();
|
||||
return brokerSle->at(sfCoverAvailable) >= minCover &&
|
||||
!isDeepFrozen(view, brokerOwner, asset) &&
|
||||
return coverAvailableProxy >= minCover && !isDeepFrozen(view, brokerOwner, asset) &&
|
||||
!requireAuth(view, asset, brokerOwner, AuthType::StrongAuth);
|
||||
}();
|
||||
|
||||
@@ -438,12 +425,6 @@ 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))
|
||||
@@ -457,6 +438,7 @@ LoanPay::doApply()
|
||||
|
||||
std::expected<LoanPaymentParts, TER> const paymentParts =
|
||||
loanMakePayment(asset, view, loanSle, brokerSle, amount, paymentType, j_);
|
||||
|
||||
if (!paymentParts)
|
||||
{
|
||||
XRPL_ASSERT_PARTS(
|
||||
@@ -464,12 +446,10 @@ 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,
|
||||
@@ -496,49 +476,35 @@ LoanPay::doApply()
|
||||
// LCOV_EXCL_STOP
|
||||
}
|
||||
|
||||
//------------------------------------------------------
|
||||
// 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;
|
||||
auto const [assetsTotalDelta, debtTotalDelta] = loanPaymentDeltas(vaultSle, *paymentParts);
|
||||
|
||||
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
|
||||
<< ", scheduled interest delta: " << scheduledInterestDelta;
|
||||
<< ", 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");
|
||||
|
||||
// Decrease LoanBroker Debt by the amount paid, add the Loan value change
|
||||
// (which might be negative). debtTotalDelta may be negative, increasing the
|
||||
@@ -548,35 +514,45 @@ 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 on Legacy/CashBasis Vaults. This is because
|
||||
// the broker may have more than one loan with significantly different scales.
|
||||
adjustBrokerDebtTotal(brokerSle, vaultSle, -debtTotalDelta, vaultScale);
|
||||
// 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);
|
||||
|
||||
//------------------------------------------------------
|
||||
// Vault object state changes
|
||||
view.update(vaultSle);
|
||||
|
||||
if (fixedPrecision)
|
||||
Number const assetsAvailableBefore = *assetsAvailableProxy;
|
||||
Number const assetsTotalBefore = *assetsTotalProxy;
|
||||
#if !NDEBUG
|
||||
{
|
||||
// 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;
|
||||
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");
|
||||
}
|
||||
#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(
|
||||
@@ -587,11 +563,10 @@ LoanPay::doApply()
|
||||
if (!sendBrokerFeeToOwner)
|
||||
{
|
||||
// If there is not enough first-loss capital, add the fee to First Loss
|
||||
// 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;
|
||||
// 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;
|
||||
}
|
||||
|
||||
associateAsset(*loanSle, asset);
|
||||
@@ -599,48 +574,28 @@ LoanPay::doApply()
|
||||
associateAsset(*vaultSle, asset);
|
||||
|
||||
// Duplicate some checks after rounding
|
||||
Number const assetsAvailableAfter = vaultSle->at(sfAssetsAvailable);
|
||||
Number const assetsTotalAfter = vaultSle->at(sfAssetsTotal);
|
||||
Number const assetsAvailableAfter = *assetsAvailableProxy;
|
||||
Number const assetsTotalAfter = *assetsTotalProxy;
|
||||
|
||||
XRPL_ASSERT_PARTS(
|
||||
assetsAvailableAfter <= assetsTotalAfter,
|
||||
"xrpl::LoanPay::doApply",
|
||||
"assets available must not be greater than assets outstanding");
|
||||
if (assetsAvailableAfter == assetsAvailableBefore)
|
||||
{
|
||||
// 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;
|
||||
}
|
||||
// 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
|
||||
}
|
||||
if (!fixedPrecision && assetsTotalDelta != beast::kZero &&
|
||||
assetsTotalAfter == assetsTotalBefore)
|
||||
if (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
|
||||
@@ -656,8 +611,7 @@ LoanPay::doApply()
|
||||
return tecPRECISION_LOSS;
|
||||
// LCOV_EXCL_STOP
|
||||
}
|
||||
if (!fixedPrecision && assetsTotalDelta == beast::kZero &&
|
||||
assetsTotalAfter != assetsTotalBefore)
|
||||
if (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
|
||||
@@ -736,6 +690,22 @@ 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
|
||||
@@ -784,6 +754,10 @@ 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);
|
||||
@@ -834,26 +808,8 @@ 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 ||
|
||||
abs(Number(totalBalanceChange)) <= accountUnit + vaultUnit + brokerUnit;
|
||||
totalBalanceChangeRounded == beast::kZero;
|
||||
if (totalBalanceBeforeRounded != totalBalanceAfterRounded)
|
||||
{
|
||||
JLOG((goodRounding ? j_.debug() : j_.warn()))
|
||||
@@ -874,20 +830,11 @@ LoanPay::doApply()
|
||||
goodRounding, "xrpl::LoanPay::doApply", "funds are conserved (with rounding)");
|
||||
|
||||
XRPL_ASSERT_PARTS(
|
||||
// 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),
|
||||
accountBalanceAfter < accountBalanceBefore || accountID_ == asset.getIssuer(),
|
||||
"xrpl::LoanPay::doApply",
|
||||
"account balance decreased");
|
||||
XRPL_ASSERT_PARTS(
|
||||
// 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())),
|
||||
vaultBalanceAfter >= beast::kZero && brokerBalanceAfter >= beast::kZero,
|
||||
"xrpl::LoanPay::doApply",
|
||||
"non-negative vault and broker balances");
|
||||
XRPL_ASSERT_PARTS(
|
||||
@@ -899,11 +846,7 @@ LoanPay::doApply()
|
||||
"xrpl::LoanPay::doApply",
|
||||
"broker balance did not decrease");
|
||||
XRPL_ASSERT_PARTS(
|
||||
// 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),
|
||||
vaultBalanceAfter > vaultBalanceBefore || brokerBalanceAfter > brokerBalanceBefore,
|
||||
"xrpl::LoanPay::doApply",
|
||||
"vault and/or broker balance increased");
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@ TransactionProposalCancel::preclaim(PreclaimContext const& ctx)
|
||||
|
||||
// Never read the zero keylet. Preflight already rejected a zero
|
||||
// ProposalID, and no real proposal can have one (every ProposalID is a
|
||||
// hash), so reaching here with zero is an internal error (and a mathematical rarity).
|
||||
// hash), so reaching here with zero is an internal error. (and a mathematical rarity. )
|
||||
if (proposalID == beast::kZero)
|
||||
return tefINTERNAL; // LCOV_EXCL_LINE
|
||||
|
||||
|
||||
@@ -12,7 +12,6 @@
|
||||
#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>
|
||||
@@ -125,23 +124,12 @@ Change::preclaim(PreclaimContext const& ctx)
|
||||
ctx.tx.isFieldPresent(sfReserveIncrementDrops))
|
||||
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;
|
||||
}
|
||||
// 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;
|
||||
return tesSUCCESS;
|
||||
case ttAMENDMENT:
|
||||
case ttUNL_MODIFY:
|
||||
@@ -176,28 +164,6 @@ 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()
|
||||
{
|
||||
@@ -274,9 +240,6 @@ Change::applyAmendment()
|
||||
<< " activated: server blocked.";
|
||||
ctx_.registry.get().getOPs().setAmendmentBlocked();
|
||||
}
|
||||
|
||||
if (amendment == featureSmartEscrow)
|
||||
initializeVMFees();
|
||||
}
|
||||
|
||||
if (newMajorities.empty())
|
||||
@@ -303,12 +266,14 @@ Change::applyFee()
|
||||
feeObject.newSLE();
|
||||
feeObject.insert();
|
||||
}
|
||||
auto const set = [](FeeSettingsEntryW& feeObject, STTx const& tx, auto const&... fields) {
|
||||
((feeObject->at(fields) = tx[fields]), ...);
|
||||
auto const set = [](FeeSettingsEntryW& feeObject, STTx const& tx, auto const& field) {
|
||||
feeObject->at(field) = tx[field];
|
||||
};
|
||||
if (view().rules().enabled(featureXRPFees))
|
||||
{
|
||||
set(feeObject, ctx_.tx, sfBaseFeeDrops, sfReserveBaseDrops, sfReserveIncrementDrops);
|
||||
set(feeObject, ctx_.tx, sfBaseFeeDrops);
|
||||
set(feeObject, ctx_.tx, sfReserveBaseDrops);
|
||||
set(feeObject, ctx_.tx, sfReserveIncrementDrops);
|
||||
// Ensure the old fields are removed
|
||||
feeObject->makeFieldAbsent(sfBaseFee);
|
||||
feeObject->makeFieldAbsent(sfReferenceFeeUnits);
|
||||
@@ -317,11 +282,10 @@ Change::applyFee()
|
||||
}
|
||||
else
|
||||
{
|
||||
set(feeObject, ctx_.tx, sfBaseFee, sfReferenceFeeUnits, sfReserveBase, sfReserveIncrement);
|
||||
}
|
||||
if (view().rules().enabled(featureSmartEscrow))
|
||||
{
|
||||
set(feeObject, ctx_.tx, sfGasLimit, sfBytecodeSizeLimit, sfGasPrice);
|
||||
set(feeObject, ctx_.tx, sfBaseFee);
|
||||
set(feeObject, ctx_.tx, sfReferenceFeeUnits);
|
||||
set(feeObject, ctx_.tx, sfReserveBase);
|
||||
set(feeObject, ctx_.tx, sfReserveIncrement);
|
||||
}
|
||||
|
||||
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))
|
||||
setMirrorsCurrent(*sleIssuance, *sleHolderMPToken);
|
||||
setMirrorEpochs(*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))
|
||||
setMirrorsCurrent(*sleIssuance, *sleMptoken);
|
||||
setMirrorEpochs(*sleIssuance, *sleMptoken);
|
||||
|
||||
// Spending balance starts at zero. Must use canonical zero encryption
|
||||
// (deterministic ciphertext) so the ledger state is reproducible.
|
||||
|
||||
@@ -85,9 +85,6 @@ 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;
|
||||
|
||||
@@ -146,9 +143,6 @@ 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,9 +201,6 @@ 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;
|
||||
}
|
||||
|
||||
@@ -244,7 +241,7 @@ ConfidentialMPTMirrorUpdate::doApply()
|
||||
if (ctx_.tx.isFieldPresent(sfIssuerEncryptedAmount))
|
||||
{
|
||||
(*sleMptoken)[sfIssuerEncryptedBalance] = ctx_.tx[sfIssuerEncryptedAmount];
|
||||
setIssuerMirrorCurrent(*sleIssuance, *sleMptoken);
|
||||
setIssuerMirrorEpoch(*sleIssuance, *sleMptoken);
|
||||
}
|
||||
|
||||
if (ctx_.tx.isFieldPresent(sfAuditorEncryptedAmount))
|
||||
|
||||
@@ -453,7 +453,10 @@ MPTokenIssuanceSet::doApply()
|
||||
return tecINTERNAL; // LCOV_EXCL_LINE
|
||||
|
||||
// NOTE: presence must be checked before the key is overwritten below.
|
||||
if (sle->isFieldPresent(keyField))
|
||||
bool const isRotation = sle->isFieldPresent(keyField);
|
||||
sle->setFieldVL(keyField, *pubKey);
|
||||
|
||||
if (isRotation)
|
||||
{
|
||||
// Preclaim rejects overwriting an existing key unless the amendment is
|
||||
// enabled.
|
||||
@@ -476,20 +479,9 @@ 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;
|
||||
};
|
||||
|
||||
|
||||
@@ -32,7 +32,6 @@
|
||||
#include <xrpl/json/json_value.h>
|
||||
#include <xrpl/json/to_string.h>
|
||||
#include <xrpl/ledger/ReadView.h>
|
||||
#include <xrpl/protocol/ErrorCodes.h>
|
||||
#include <xrpl/protocol/Feature.h>
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
#include <xrpl/protocol/Issue.h>
|
||||
@@ -895,9 +894,8 @@ public:
|
||||
auto const jNoCred = credentials::ledgerEntry(env, becky, carol, credType);
|
||||
BEAST_EXPECT(
|
||||
jNoCred.isObject() && jNoCred.isMember(jss::result) &&
|
||||
jNoCred[jss::result][jss::error] == "entryNotFound" &&
|
||||
jNoCred[jss::result][jss::error_code] == RpcEntryNotFound &&
|
||||
jNoCred[jss::result][jss::error_message] == "Entry not found.");
|
||||
jNoCred[jss::result].isMember(jss::error) &&
|
||||
jNoCred[jss::result][jss::error] == "entryNotFound");
|
||||
}
|
||||
|
||||
testcase("Credentials that aren't required");
|
||||
@@ -920,9 +918,8 @@ public:
|
||||
|
||||
BEAST_EXPECT(
|
||||
jNoCred.isObject() && jNoCred.isMember(jss::result) &&
|
||||
jNoCred[jss::result][jss::error] == "entryNotFound" &&
|
||||
jNoCred[jss::result][jss::error_code] == RpcEntryNotFound &&
|
||||
jNoCred[jss::result][jss::error_message] == "Entry not found.");
|
||||
jNoCred[jss::result].isMember(jss::error) &&
|
||||
jNoCred[jss::result][jss::error] == "entryNotFound");
|
||||
}
|
||||
|
||||
{
|
||||
@@ -960,9 +957,8 @@ public:
|
||||
|
||||
BEAST_EXPECT(
|
||||
jNoCred.isObject() && jNoCred.isMember(jss::result) &&
|
||||
jNoCred[jss::result][jss::error] == "entryNotFound" &&
|
||||
jNoCred[jss::result][jss::error_code] == RpcEntryNotFound &&
|
||||
jNoCred[jss::result][jss::error_message] == "Entry not found.");
|
||||
jNoCred[jss::result].isMember(jss::error) &&
|
||||
jNoCred[jss::result][jss::error] == "entryNotFound");
|
||||
}
|
||||
|
||||
testcase("Expired credentials");
|
||||
@@ -998,9 +994,8 @@ public:
|
||||
auto jv = credentials::ledgerEntry(env, john, carol, credType);
|
||||
BEAST_EXPECT(
|
||||
jv.isObject() && jv.isMember(jss::result) &&
|
||||
jv[jss::result][jss::error] == "entryNotFound" &&
|
||||
jv[jss::result][jss::error_code] == RpcEntryNotFound &&
|
||||
jv[jss::result][jss::error_message] == "Entry not found.");
|
||||
jv[jss::result].isMember(jss::error) &&
|
||||
jv[jss::result][jss::error] == "entryNotFound");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1084,9 +1079,8 @@ public:
|
||||
auto const jv = credentials::ledgerEntry(env, becky, carol, credType);
|
||||
BEAST_EXPECT(
|
||||
jv.isObject() && jv.isMember(jss::result) &&
|
||||
jv[jss::result][jss::error] == "entryNotFound" &&
|
||||
jv[jss::result][jss::error_code] == RpcEntryNotFound &&
|
||||
jv[jss::result][jss::error_message] == "Entry not found.");
|
||||
jv[jss::result].isMember(jss::error) &&
|
||||
jv[jss::result][jss::error] == "entryNotFound");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1127,9 +1121,8 @@ public:
|
||||
auto const jv = credentials::ledgerEntry(env, becky, carol, credType);
|
||||
BEAST_EXPECT(
|
||||
jv.isObject() && jv.isMember(jss::result) &&
|
||||
jv[jss::result][jss::error] == "entryNotFound" &&
|
||||
jv[jss::result][jss::error_code] == RpcEntryNotFound &&
|
||||
jv[jss::result][jss::error_message] == "Entry not found.");
|
||||
jv[jss::result].isMember(jss::error) &&
|
||||
jv[jss::result][jss::error] == "entryNotFound");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,365 +0,0 @@
|
||||
#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
|
||||
@@ -43,7 +43,6 @@
|
||||
#include <xrpl/ledger/OpenView.h>
|
||||
#include <xrpl/protocol/AccountID.h>
|
||||
#include <xrpl/protocol/Batch.h>
|
||||
#include <xrpl/protocol/ErrorCodes.h>
|
||||
#include <xrpl/protocol/Feature.h>
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
#include <xrpl/protocol/Issue.h>
|
||||
@@ -2801,7 +2800,6 @@ class Batch_test : public beast::unit_test::Suite
|
||||
auto const jrr = env.rpc("submit", strHex(slice))[jss::result];
|
||||
expect(
|
||||
jrr[jss::status] == "error" && jrr[jss::error] == "invalidTransaction" &&
|
||||
!jrr.isMember(jss::error_code) && jrr[jss::error_message].isNull() &&
|
||||
jrr[jss::error_exception] ==
|
||||
"fails local checks: Batch inner transactions are never "
|
||||
"considered validly signed.",
|
||||
@@ -3311,6 +3309,8 @@ 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;
|
||||
@@ -5409,10 +5409,7 @@ class Batch_test : public beast::unit_test::Suite
|
||||
|
||||
// The delegated payment never applied.
|
||||
BEAST_EXPECT(env.balance(mallory) == preMallory);
|
||||
auto const txResult = env.rpc("tx", txIDs[1])[jss::result];
|
||||
BEAST_EXPECT(txResult[jss::error] == "txnNotFound");
|
||||
BEAST_EXPECT(txResult[jss::error_code] == RpcTxnNotFound);
|
||||
BEAST_EXPECT(txResult[jss::error_message] == "Transaction not found.");
|
||||
BEAST_EXPECT(env.rpc("tx", txIDs[1])[jss::result][jss::error] == "txnNotFound");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5823,10 +5820,7 @@ class Batch_test : public beast::unit_test::Suite
|
||||
validateClosedLedger(env, testCases);
|
||||
|
||||
// verify jv2 is not present in the closed ledger.
|
||||
auto const txResult = env.rpc("tx", txIDs[1])[jss::result];
|
||||
BEAST_EXPECT(txResult[jss::error] == "txnNotFound");
|
||||
BEAST_EXPECT(txResult[jss::error_code] == RpcTxnNotFound);
|
||||
BEAST_EXPECT(txResult[jss::error_message] == "Transaction not found.");
|
||||
BEAST_EXPECT(env.rpc("tx", txIDs[1])[jss::result][jss::error] == "txnNotFound");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
#include <test/jtx/Account.h>
|
||||
#include <test/jtx/CaptureLogs.h>
|
||||
#include <test/jtx/Env.h>
|
||||
#include <test/jtx/TestHelpers.h>
|
||||
#include <test/jtx/amount.h>
|
||||
@@ -15,17 +14,13 @@
|
||||
#include <xrpl/beast/unit_test/suite.h>
|
||||
#include <xrpl/protocol/Feature.h>
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
#include <xrpl/protocol/Issue.h>
|
||||
#include <xrpl/protocol/LedgerFormats.h>
|
||||
#include <xrpl/protocol/SField.h>
|
||||
#include <xrpl/protocol/STAmount.h>
|
||||
#include <xrpl/protocol/TER.h>
|
||||
#include <xrpl/protocol/TxFlags.h>
|
||||
#include <xrpl/protocol/UintTypes.h>
|
||||
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
|
||||
@@ -808,108 +803,6 @@ class Clawback_test : public beast::unit_test::Suite
|
||||
BEAST_EXPECT(ownerCount(env, bob) == 0);
|
||||
}
|
||||
|
||||
void
|
||||
testSubResolutionAmount(FeatureBitset features)
|
||||
{
|
||||
testcase("Sub-resolution amount");
|
||||
using namespace test::jtx;
|
||||
|
||||
// IOU balances keep 16 significant digits. When alice claws back an
|
||||
// amount much smaller than bob's balance, the balance changes by the
|
||||
// rounded amount, which can be zero, less, or more than requested. The
|
||||
// clawback invariant must accept that instead of failing the
|
||||
// transaction under MPTokensV2 or logging a failure without it.
|
||||
Account const alice{"alice"};
|
||||
Account const bob{"bob"};
|
||||
|
||||
struct Case
|
||||
{
|
||||
std::uint64_t balanceMantissa;
|
||||
int balanceExponent;
|
||||
std::uint64_t clawMantissa;
|
||||
int clawExponent;
|
||||
std::uint64_t afterMantissa;
|
||||
int afterExponent;
|
||||
};
|
||||
std::array const cases{
|
||||
// 1e7 - 1e-10 rounds back to 1e7
|
||||
Case{
|
||||
.balanceMantissa = 10'000'000,
|
||||
.balanceExponent = 0,
|
||||
.clawMantissa = 1,
|
||||
.clawExponent = -10,
|
||||
.afterMantissa = 10'000'000,
|
||||
.afterExponent = 0,
|
||||
},
|
||||
// 1234567890.123456 - 0.0000005 rounds back to the balance
|
||||
Case{
|
||||
.balanceMantissa = 1'234'567'890'123'456,
|
||||
.balanceExponent = -6,
|
||||
.clawMantissa = 5,
|
||||
.clawExponent = -7,
|
||||
.afterMantissa = 1'234'567'890'123'456,
|
||||
.afterExponent = -6,
|
||||
},
|
||||
// 0.0000014 removes 0.000001
|
||||
Case{
|
||||
.balanceMantissa = 1'234'567'890'123'456,
|
||||
.balanceExponent = -6,
|
||||
.clawMantissa = 14,
|
||||
.clawExponent = -7,
|
||||
.afterMantissa = 1'234'567'890'123'455,
|
||||
.afterExponent = -6,
|
||||
},
|
||||
// 0.0000016 removes 0.000002
|
||||
Case{
|
||||
.balanceMantissa = 1'234'567'890'123'456,
|
||||
.balanceExponent = -6,
|
||||
.clawMantissa = 16,
|
||||
.clawExponent = -7,
|
||||
.afterMantissa = 1'234'567'890'123'454,
|
||||
.afterExponent = -6,
|
||||
},
|
||||
};
|
||||
|
||||
for (auto const feat : {features, features - featureMPTokensV2})
|
||||
{
|
||||
for (auto const& c : cases)
|
||||
{
|
||||
std::string logs;
|
||||
{
|
||||
Env env(*this, feat, std::make_unique<test::CaptureLogs>(&logs));
|
||||
|
||||
env.fund(XRP(1000), alice, bob);
|
||||
env.close();
|
||||
|
||||
auto const usd = alice["USD"];
|
||||
|
||||
env(fset(alice, asfAllowTrustLineClawback));
|
||||
env.close();
|
||||
|
||||
env.trust(usd(10'000'000'000), bob);
|
||||
env(pay(
|
||||
alice, bob, STAmount{usd.issue(), c.balanceMantissa, c.balanceExponent}));
|
||||
env.close();
|
||||
|
||||
env(claw(
|
||||
alice,
|
||||
STAmount{
|
||||
Issue{usd.currency, bob.id()}, c.clawMantissa, c.clawExponent}),
|
||||
Ter(tesSUCCESS));
|
||||
env.close();
|
||||
|
||||
BEAST_EXPECT(
|
||||
env.balance(bob, usd).value() ==
|
||||
STAmount(usd.issue(), c.afterMantissa, c.afterExponent));
|
||||
}
|
||||
// Without MPTokensV2 a failed invariant only logs and the
|
||||
// transaction still succeeds, so check the logs instead.
|
||||
if (!feat[featureMPTokensV2])
|
||||
BEAST_EXPECT(!logs.contains("Invariant failed"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testTickets(FeatureBitset features)
|
||||
{
|
||||
@@ -979,7 +872,6 @@ class Clawback_test : public beast::unit_test::Suite
|
||||
testDeleteDefaultLine(features);
|
||||
testFrozenLine(features);
|
||||
testAmountExceedsAvailable(features);
|
||||
testSubResolutionAmount(features);
|
||||
testTickets(features);
|
||||
}
|
||||
|
||||
|
||||
@@ -3156,174 +3156,6 @@ 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)
|
||||
@@ -3347,9 +3179,6 @@ public:
|
||||
testMPTokenIssuanceSetWithFeats(all);
|
||||
testMPTokenIssuanceSetWithFeats(all - featureConfidentialMPTKeyRotation);
|
||||
|
||||
testInitialIssuerEncryptionKey(all);
|
||||
testIssuerMirrorEncryptionKey(all);
|
||||
|
||||
testConfidentialMPTConvertEpoch(all);
|
||||
testConfidentialMPTConvertEpoch(all - featureConfidentialMPTKeyRotation);
|
||||
testConfidentialMPTSendEpoch(all);
|
||||
|
||||
@@ -20,7 +20,6 @@
|
||||
#include <xrpl/beast/unit_test/suite.h>
|
||||
#include <xrpl/json/json_value.h>
|
||||
#include <xrpl/protocol/ConfidentialTransfer.h>
|
||||
#include <xrpl/protocol/ErrorCodes.h>
|
||||
#include <xrpl/protocol/Feature.h>
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
#include <xrpl/protocol/Protocol.h>
|
||||
@@ -260,9 +259,8 @@ class ConfidentialTransferExtended_test : public ConfidentialTransferTestBase
|
||||
|
||||
auto const credentialDeleted = [&](Env& env, Account const& subject) -> bool {
|
||||
auto const credentials = credentials::ledgerEntry(env, subject, dpIssuer, credType);
|
||||
return credentials[jss::result][jss::error] == "entryNotFound" &&
|
||||
credentials[jss::result][jss::error_code] == RpcEntryNotFound &&
|
||||
credentials[jss::result][jss::error_message] == "Entry not found.";
|
||||
return credentials[jss::result].isMember(jss::error) &&
|
||||
credentials[jss::result][jss::error] == "entryNotFound";
|
||||
};
|
||||
|
||||
// TEST 4: Expired credential with matching depositPreauth entry.
|
||||
|
||||
@@ -7944,8 +7944,6 @@ class ConfidentialTransfer_test : public ConfidentialTransferTestBase
|
||||
// Signature verification fails — rejected before ZKP check
|
||||
auto const jr = env.rpc("submit", strHex(tampered.slice()));
|
||||
BEAST_EXPECT(jr[jss::result][jss::error] == "invalidTransaction");
|
||||
BEAST_EXPECT(!jr[jss::result].isMember(jss::error_code));
|
||||
BEAST_EXPECT(jr[jss::result][jss::error_message].isNull());
|
||||
}
|
||||
|
||||
// Variant B: Re-signed with inflated ciphertext.
|
||||
@@ -8053,8 +8051,6 @@ class ConfidentialTransfer_test : public ConfidentialTransferTestBase
|
||||
|
||||
auto const jr = env.rpc("submit", strHex(tampered.slice()));
|
||||
BEAST_EXPECT(jr[jss::result][jss::error] == "invalidTransaction");
|
||||
BEAST_EXPECT(!jr[jss::result].isMember(jss::error_code));
|
||||
BEAST_EXPECT(jr[jss::result][jss::error_message].isNull());
|
||||
}
|
||||
|
||||
// Variant B: Re-signed with all negated ciphertexts.
|
||||
@@ -8158,8 +8154,6 @@ class ConfidentialTransfer_test : public ConfidentialTransferTestBase
|
||||
|
||||
auto const jr = env.rpc("submit", strHex(tampered.slice()));
|
||||
BEAST_EXPECT(jr[jss::result][jss::error] == "invalidTransaction");
|
||||
BEAST_EXPECT(!jr[jss::result].isMember(jss::error_code));
|
||||
BEAST_EXPECT(jr[jss::result][jss::error_message].isNull());
|
||||
}
|
||||
|
||||
// Variant B: Re-signed with combined ciphertext.
|
||||
@@ -8284,8 +8278,6 @@ class ConfidentialTransfer_test : public ConfidentialTransferTestBase
|
||||
// Signature verification fails
|
||||
auto const jr = env.rpc("submit", strHex(tampered.slice()));
|
||||
BEAST_EXPECT(jr[jss::result][jss::error] == "invalidTransaction");
|
||||
BEAST_EXPECT(!jr[jss::result].isMember(jss::error_code));
|
||||
BEAST_EXPECT(jr[jss::result][jss::error_message].isNull());
|
||||
}
|
||||
|
||||
// Variant B: Re-signed C1 substitution.
|
||||
@@ -8376,8 +8368,6 @@ class ConfidentialTransfer_test : public ConfidentialTransferTestBase
|
||||
// signed — transaction rejected before ZKP verification.
|
||||
auto const jr = env.rpc("submit", strHex(tampered.slice()));
|
||||
BEAST_EXPECT(jr[jss::result][jss::error] == "invalidTransaction");
|
||||
BEAST_EXPECT(!jr[jss::result].isMember(jss::error_code));
|
||||
BEAST_EXPECT(jr[jss::result][jss::error_message].isNull());
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------
|
||||
|
||||
@@ -23,7 +23,6 @@
|
||||
#include <xrpl/ledger/ApplyViewImpl.h>
|
||||
#include <xrpl/ledger/helpers/CredentialHelpers.h>
|
||||
#include <xrpl/protocol/AccountID.h>
|
||||
#include <xrpl/protocol/ErrorCodes.h>
|
||||
#include <xrpl/protocol/Feature.h>
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
#include <xrpl/protocol/Issue.h>
|
||||
@@ -125,9 +124,8 @@ struct Credentials_test : public beast::unit_test::Suite
|
||||
auto const jle = credentials::ledgerEntry(env, subject, issuer, credType);
|
||||
BEAST_EXPECT(
|
||||
jle.isObject() && jle.isMember(jss::result) &&
|
||||
jle[jss::result][jss::error] == "entryNotFound" &&
|
||||
jle[jss::result][jss::error_code] == RpcEntryNotFound &&
|
||||
jle[jss::result][jss::error_message] == "Entry not found.");
|
||||
jle[jss::result].isMember(jss::error) &&
|
||||
jle[jss::result][jss::error] == "entryNotFound");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -175,9 +173,8 @@ struct Credentials_test : public beast::unit_test::Suite
|
||||
auto const jle = credentials::ledgerEntry(env, issuer, issuer, credType);
|
||||
BEAST_EXPECT(
|
||||
jle.isObject() && jle.isMember(jss::result) &&
|
||||
jle[jss::result][jss::error] == "entryNotFound" &&
|
||||
jle[jss::result][jss::error_code] == RpcEntryNotFound &&
|
||||
jle[jss::result][jss::error_message] == "Entry not found.");
|
||||
jle[jss::result].isMember(jss::error) &&
|
||||
jle[jss::result][jss::error] == "entryNotFound");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -225,9 +222,8 @@ struct Credentials_test : public beast::unit_test::Suite
|
||||
auto const jle = credentials::ledgerEntry(env, subject, issuer, credType);
|
||||
BEAST_EXPECT(
|
||||
jle.isObject() && jle.isMember(jss::result) &&
|
||||
jle[jss::result][jss::error] == "entryNotFound" &&
|
||||
jle[jss::result][jss::error_code] == RpcEntryNotFound &&
|
||||
jle[jss::result][jss::error_message] == "Entry not found.");
|
||||
jle[jss::result].isMember(jss::error) &&
|
||||
jle[jss::result][jss::error] == "entryNotFound");
|
||||
}
|
||||
|
||||
// resurrection
|
||||
@@ -263,9 +259,8 @@ struct Credentials_test : public beast::unit_test::Suite
|
||||
auto const jle = credentials::ledgerEntry(env, subject, issuer, credType);
|
||||
BEAST_EXPECT(
|
||||
jle.isObject() && jle.isMember(jss::result) &&
|
||||
jle[jss::result][jss::error] == "entryNotFound" &&
|
||||
jle[jss::result][jss::error_code] == RpcEntryNotFound &&
|
||||
jle[jss::result][jss::error_message] == "Entry not found.");
|
||||
jle[jss::result].isMember(jss::error) &&
|
||||
jle[jss::result][jss::error] == "entryNotFound");
|
||||
}
|
||||
|
||||
// resurrection
|
||||
@@ -299,9 +294,8 @@ struct Credentials_test : public beast::unit_test::Suite
|
||||
auto const jle = credentials::ledgerEntry(env, subject, issuer, credType);
|
||||
BEAST_EXPECT(
|
||||
jle.isObject() && jle.isMember(jss::result) &&
|
||||
jle[jss::result][jss::error] == "entryNotFound" &&
|
||||
jle[jss::result][jss::error_code] == RpcEntryNotFound &&
|
||||
jle[jss::result][jss::error_message] == "Entry not found.");
|
||||
jle[jss::result].isMember(jss::error) &&
|
||||
jle[jss::result][jss::error] == "entryNotFound");
|
||||
}
|
||||
|
||||
// resurrection
|
||||
@@ -337,9 +331,8 @@ struct Credentials_test : public beast::unit_test::Suite
|
||||
auto const jle = credentials::ledgerEntry(env, subject, issuer, credType);
|
||||
BEAST_EXPECT(
|
||||
jle.isObject() && jle.isMember(jss::result) &&
|
||||
jle[jss::result][jss::error] == "entryNotFound" &&
|
||||
jle[jss::result][jss::error_code] == RpcEntryNotFound &&
|
||||
jle[jss::result][jss::error_message] == "Entry not found.");
|
||||
jle[jss::result].isMember(jss::error) &&
|
||||
jle[jss::result][jss::error] == "entryNotFound");
|
||||
}
|
||||
|
||||
// resurrection
|
||||
@@ -375,9 +368,8 @@ struct Credentials_test : public beast::unit_test::Suite
|
||||
auto const jle = credentials::ledgerEntry(env, subject, issuer, credType);
|
||||
BEAST_EXPECT(
|
||||
jle.isObject() && jle.isMember(jss::result) &&
|
||||
jle[jss::result][jss::error] == "entryNotFound" &&
|
||||
jle[jss::result][jss::error_code] == RpcEntryNotFound &&
|
||||
jle[jss::result][jss::error_message] == "Entry not found.");
|
||||
jle[jss::result].isMember(jss::error) &&
|
||||
jle[jss::result][jss::error] == "entryNotFound");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -398,9 +390,8 @@ struct Credentials_test : public beast::unit_test::Suite
|
||||
auto const jle = credentials::ledgerEntry(env, subject, issuer, credType);
|
||||
BEAST_EXPECT(
|
||||
jle.isObject() && jle.isMember(jss::result) &&
|
||||
jle[jss::result][jss::error] == "entryNotFound" &&
|
||||
jle[jss::result][jss::error_code] == RpcEntryNotFound &&
|
||||
jle[jss::result][jss::error_message] == "Entry not found.");
|
||||
jle[jss::result].isMember(jss::error) &&
|
||||
jle[jss::result][jss::error] == "entryNotFound");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -419,9 +410,8 @@ struct Credentials_test : public beast::unit_test::Suite
|
||||
auto const jle = credentials::ledgerEntry(env, subject, issuer, credType);
|
||||
BEAST_EXPECT(
|
||||
jle.isObject() && jle.isMember(jss::result) &&
|
||||
jle[jss::result][jss::error] == "entryNotFound" &&
|
||||
jle[jss::result][jss::error_code] == RpcEntryNotFound &&
|
||||
jle[jss::result][jss::error_message] == "Entry not found.");
|
||||
jle[jss::result].isMember(jss::error) &&
|
||||
jle[jss::result][jss::error] == "entryNotFound");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -754,9 +744,8 @@ struct Credentials_test : public beast::unit_test::Suite
|
||||
auto const jDelCred = credentials::ledgerEntry(env, subject, issuer, credType2);
|
||||
BEAST_EXPECT(
|
||||
jDelCred.isObject() && jDelCred.isMember(jss::result) &&
|
||||
jDelCred[jss::result][jss::error] == "entryNotFound" &&
|
||||
jDelCred[jss::result][jss::error_code] == RpcEntryNotFound &&
|
||||
jDelCred[jss::result][jss::error_message] == "Entry not found.");
|
||||
jDelCred[jss::result].isMember(jss::error) &&
|
||||
jDelCred[jss::result][jss::error] == "entryNotFound");
|
||||
|
||||
BEAST_EXPECT(ownerCount(env, issuer) == 0);
|
||||
BEAST_EXPECT(ownerCount(env, subject) == 1);
|
||||
@@ -792,9 +781,8 @@ struct Credentials_test : public beast::unit_test::Suite
|
||||
auto const jDelCred = credentials::ledgerEntry(env, subject, issuer, credType);
|
||||
BEAST_EXPECT(
|
||||
jDelCred.isObject() && jDelCred.isMember(jss::result) &&
|
||||
jDelCred[jss::result][jss::error] == "entryNotFound" &&
|
||||
jDelCred[jss::result][jss::error_code] == RpcEntryNotFound &&
|
||||
jDelCred[jss::result][jss::error_message] == "Entry not found.");
|
||||
jDelCred[jss::result].isMember(jss::error) &&
|
||||
jDelCred[jss::result][jss::error] == "entryNotFound");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,7 +38,6 @@
|
||||
#include <xrpl/json/to_string.h>
|
||||
#include <xrpl/ledger/Dir.h>
|
||||
#include <xrpl/ledger/helpers/DelegateHelpers.h>
|
||||
#include <xrpl/protocol/ErrorCodes.h>
|
||||
#include <xrpl/protocol/Feature.h>
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
#include <xrpl/protocol/Issue.h>
|
||||
@@ -160,8 +159,6 @@ class Delegate_test : public beast::unit_test::Suite
|
||||
env.close();
|
||||
auto const jle = delegate::entry(env, gw, alice);
|
||||
BEAST_EXPECT(jle[jss::result][jss::error] == "entryNotFound");
|
||||
BEAST_EXPECT(jle[jss::result][jss::error_code] == RpcEntryNotFound);
|
||||
BEAST_EXPECT(jle[jss::result][jss::error_message] == "Entry not found.");
|
||||
|
||||
// alice can delegate permissions to gw as well
|
||||
env(delegate::set(alice, gw, permissions));
|
||||
@@ -170,8 +167,6 @@ class Delegate_test : public beast::unit_test::Suite
|
||||
auto const response = delegate::entry(env, gw, alice);
|
||||
// alice has not been granted any permissions by gw
|
||||
BEAST_EXPECT(response[jss::result][jss::error] == "entryNotFound");
|
||||
BEAST_EXPECT(response[jss::result][jss::error_code] == RpcEntryNotFound);
|
||||
BEAST_EXPECT(response[jss::result][jss::error_message] == "Entry not found.");
|
||||
}
|
||||
|
||||
void
|
||||
@@ -2785,11 +2780,9 @@ class Delegate_test : public beast::unit_test::Suite
|
||||
jv[jss::secret] = bob.name();
|
||||
auto jrr = env.rpc("json", "sign_for", to_string(jv))[jss::result];
|
||||
BEAST_EXPECT(jrr[jss::status] == "error");
|
||||
BEAST_EXPECT(jrr[jss::error] == "invalidParams");
|
||||
BEAST_EXPECT(jrr[jss::error_code] == RpcInvalidParams);
|
||||
BEAST_EXPECT(
|
||||
jrr[jss::error_message] ==
|
||||
"A Signer may not be the transaction's Account (" + bob.human() + ").");
|
||||
jrr[jss::error_message].asString().contains(
|
||||
"A Signer may not be the transaction's Account"));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -28,7 +28,6 @@
|
||||
#include <xrpl/json/json_value.h>
|
||||
#include <xrpl/json/to_string.h>
|
||||
#include <xrpl/protocol/AccountID.h>
|
||||
#include <xrpl/protocol/ErrorCodes.h>
|
||||
#include <xrpl/protocol/Feature.h>
|
||||
#include <xrpl/protocol/Issue.h>
|
||||
#include <xrpl/protocol/LedgerFormats.h>
|
||||
@@ -1144,9 +1143,8 @@ struct DepositPreauth_test : public beast::unit_test::Suite
|
||||
ledgerEntryDepositPreauth(env, bob, {{.issuer = issuer, .credType = credType}});
|
||||
BEAST_EXPECT(
|
||||
jDP.isObject() && jDP.isMember(jss::result) &&
|
||||
jDP[jss::result][jss::error] == "entryNotFound" &&
|
||||
jDP[jss::result][jss::error_code] == RpcEntryNotFound &&
|
||||
jDP[jss::result][jss::error_message] == "Entry not found.");
|
||||
jDP[jss::result].isMember(jss::error) &&
|
||||
jDP[jss::result][jss::error] == "entryNotFound");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1234,9 +1232,8 @@ struct DepositPreauth_test : public beast::unit_test::Suite
|
||||
auto const jDelCred = credentials::ledgerEntry(env, alice, issuer, credType);
|
||||
BEAST_EXPECT(
|
||||
jDelCred.isObject() && jDelCred.isMember(jss::result) &&
|
||||
jDelCred[jss::result][jss::error] == "entryNotFound" &&
|
||||
jDelCred[jss::result][jss::error_code] == RpcEntryNotFound &&
|
||||
jDelCred[jss::result][jss::error_message] == "Entry not found.");
|
||||
jDelCred[jss::result].isMember(jss::error) &&
|
||||
jDelCred[jss::result][jss::error] == "entryNotFound");
|
||||
}
|
||||
|
||||
{
|
||||
@@ -1286,9 +1283,8 @@ struct DepositPreauth_test : public beast::unit_test::Suite
|
||||
auto const jDelCred = credentials::ledgerEntry(env, gw, issuer, credType);
|
||||
BEAST_EXPECT(
|
||||
jDelCred.isObject() && jDelCred.isMember(jss::result) &&
|
||||
jDelCred[jss::result][jss::error] == "entryNotFound" &&
|
||||
jDelCred[jss::result][jss::error_code] == RpcEntryNotFound &&
|
||||
jDelCred[jss::result][jss::error_message] == "Entry not found.");
|
||||
jDelCred[jss::result].isMember(jss::error) &&
|
||||
jDelCred[jss::result][jss::error] == "entryNotFound");
|
||||
|
||||
BEAST_EXPECT(ownerCount(env, issuer) == 0);
|
||||
BEAST_EXPECT(ownerCount(env, gw) == 0);
|
||||
@@ -1362,9 +1358,8 @@ struct DepositPreauth_test : public beast::unit_test::Suite
|
||||
auto const jDelCred = credentials::ledgerEntry(env, zelda, issuer, credType);
|
||||
BEAST_EXPECT(
|
||||
jDelCred.isObject() && jDelCred.isMember(jss::result) &&
|
||||
jDelCred[jss::result][jss::error] == "entryNotFound" &&
|
||||
jDelCred[jss::result][jss::error_code] == RpcEntryNotFound &&
|
||||
jDelCred[jss::result][jss::error_message] == "Entry not found.");
|
||||
jDelCred[jss::result].isMember(jss::error) &&
|
||||
jDelCred[jss::result][jss::error] == "entryNotFound");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,787 +0,0 @@
|
||||
#include <test/jtx/Account.h>
|
||||
#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/ter.h>
|
||||
|
||||
#include <xrpld/core/Config.h>
|
||||
|
||||
#include <xrpl/basics/strHex.h>
|
||||
#include <xrpl/beast/unit_test/suite.h>
|
||||
#include <xrpl/config/Constants.h>
|
||||
#include <xrpl/core/StartUpType.h>
|
||||
#include <xrpl/protocol/Feature.h>
|
||||
#include <xrpl/protocol/Fees.h>
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
#include <xrpl/protocol/Protocol.h>
|
||||
#include <xrpl/protocol/SField.h>
|
||||
#include <xrpl/protocol/SeqProxy.h>
|
||||
#include <xrpl/protocol/TER.h>
|
||||
#include <xrpl/protocol/XRPAmount.h>
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
namespace xrpl::test {
|
||||
|
||||
/**
|
||||
* Smart Escrow's `Bytecode`, `Data` and `Gas` fields.
|
||||
*
|
||||
* The WASM engine that runs an escrow's bytecode is not in this build, so the
|
||||
* `Bytecode` field is opaque here: `EscrowCreate` weighs its size and nothing
|
||||
* reads it, and `EscrowFinish` has nothing to run it with. Everything around
|
||||
* the run is testable, and is tested below - the preflight and preclaim rules,
|
||||
* the fee the bytecode costs, the owner reserve it occupies, and the refund of
|
||||
* that reserve.
|
||||
*
|
||||
* What waits on the engine: a successful finish, tecBYTECODE_REJECTED, the
|
||||
* gas and return code the run reports in the transaction metadata, and the
|
||||
* `Data` a rejected contract leaves behind.
|
||||
*/
|
||||
struct EscrowSmart_test : public beast::unit_test::Suite
|
||||
{
|
||||
// A blob of `bytes` bytes, as hex. Opens with the WASM preamble - "\0asm"
|
||||
// and version 1 - so a reader can see what the field is meant to carry,
|
||||
// but nothing in this build parses it.
|
||||
static std::string
|
||||
bytecodeOfSize(std::size_t bytes)
|
||||
{
|
||||
static std::string const kPreamble = "0061736D01000000";
|
||||
std::string hex = kPreamble;
|
||||
hex.append((bytes * 2) - kPreamble.size(), 'A');
|
||||
return hex;
|
||||
}
|
||||
|
||||
// What EscrowCreate charges for `bytecodeHex`: ten base fees, plus five
|
||||
// drops per byte.
|
||||
static XRPAmount
|
||||
createFeeFor(jtx::Env const& env, std::string const& bytecodeHex)
|
||||
{
|
||||
return env.current()->fees().base * 10 + bytecodeHex.size() / 2 * 5;
|
||||
}
|
||||
|
||||
// A validator's config that votes for `value` on the fee setting `key`.
|
||||
static std::unique_ptr<Config>
|
||||
votingFor(std::string const& key, std::uint32_t value)
|
||||
{
|
||||
auto cfg = jtx::envconfig(jtx::validator, "");
|
||||
cfg->section(Sections::kVoting).set(key, std::to_string(value));
|
||||
return cfg;
|
||||
}
|
||||
|
||||
// Close past the flag ledger, so the vote lands in FeeSettings.
|
||||
static void
|
||||
closePastFlagLedger(jtx::Env& env)
|
||||
{
|
||||
for (auto i = env.current()->seq(); i <= 257; ++i)
|
||||
env.close();
|
||||
}
|
||||
|
||||
// What EscrowFinish charges for `gas`: the gas cost rounded up to the drop.
|
||||
static XRPAmount
|
||||
finishFeeFor(jtx::Env const& env, std::uint64_t gas)
|
||||
{
|
||||
return env.current()->fees().base +
|
||||
((gas * env.current()->fees().gasPrice) + microDropsPerDrop - 1) / microDropsPerDrop;
|
||||
}
|
||||
|
||||
void
|
||||
testCreatePreflight(FeatureBitset features)
|
||||
{
|
||||
testcase("EscrowCreate preflight");
|
||||
|
||||
using namespace jtx;
|
||||
using namespace std::chrono;
|
||||
|
||||
Account const alice{"alice"};
|
||||
Account const carol{"carol"};
|
||||
|
||||
auto const bytecode = bytecodeOfSize(64);
|
||||
|
||||
{
|
||||
// featureSmartEscrow disabled
|
||||
Env env(*this, features - featureSmartEscrow);
|
||||
env.fund(XRP(5000), alice, carol);
|
||||
XRPAmount const txnFees = env.current()->fees().base + 1000;
|
||||
auto const escrowCreate = escrow::create(alice, carol, XRP(1000));
|
||||
env(escrowCreate,
|
||||
escrow::Bytecode(bytecode),
|
||||
escrow::kCancelTime(env.now() + 100s),
|
||||
Fee(txnFees),
|
||||
Ter(temDISABLED));
|
||||
env.close();
|
||||
|
||||
env(escrowCreate,
|
||||
escrow::Bytecode(bytecode),
|
||||
escrow::kCancelTime(env.now() + 100s),
|
||||
escrow::Data("00112233"),
|
||||
Fee(txnFees),
|
||||
Ter(temDISABLED));
|
||||
env.close();
|
||||
|
||||
env(escrowCreate,
|
||||
escrow::Data("00112233"),
|
||||
escrow::kFinishTime(env.now() + 100s),
|
||||
Fee(txnFees),
|
||||
Ter(temDISABLED));
|
||||
env.close();
|
||||
}
|
||||
|
||||
{
|
||||
// Bytecode > max length
|
||||
Env env(*this, votingFor(Keys::kBytecodeSizeLimit, 10), features); // 10 bytes
|
||||
closePastFlagLedger(env);
|
||||
XRPAmount const txnFees = env.current()->fees().base + 1000;
|
||||
env.fund(XRP(5000), alice, carol);
|
||||
|
||||
auto const escrowCreate = escrow::create(alice, carol, XRP(500));
|
||||
|
||||
// 11-byte string
|
||||
std::string const longBytecode = "00112233445566778899AA";
|
||||
env(escrowCreate,
|
||||
escrow::Bytecode(longBytecode),
|
||||
escrow::kCancelTime(env.now() + 100s),
|
||||
Fee(txnFees),
|
||||
Ter(tefEXCEEDS_LIMIT));
|
||||
env.close();
|
||||
}
|
||||
|
||||
{
|
||||
// gas limit set to 0
|
||||
Env env(*this, votingFor(Keys::kGasLimit, 0), features); // WASM runtime disabled
|
||||
closePastFlagLedger(env);
|
||||
XRPAmount const txnFees = env.current()->fees().base + 1000;
|
||||
env.fund(XRP(5000), alice, carol);
|
||||
|
||||
auto const escrowCreate = escrow::create(alice, carol, XRP(500));
|
||||
|
||||
env(escrowCreate,
|
||||
escrow::Bytecode(bytecode),
|
||||
escrow::kCancelTime(env.now() + 100s),
|
||||
Fee(txnFees),
|
||||
Ter(tefTEMP_DISABLED));
|
||||
env.close();
|
||||
}
|
||||
|
||||
{
|
||||
// size limit set to 0
|
||||
Env env(
|
||||
*this, votingFor(Keys::kBytecodeSizeLimit, 0), features); // WASM upload disabled
|
||||
closePastFlagLedger(env);
|
||||
XRPAmount const txnFees = env.current()->fees().base + 1000;
|
||||
env.fund(XRP(5000), alice, carol);
|
||||
|
||||
auto const escrowCreate = escrow::create(alice, carol, XRP(500));
|
||||
|
||||
// 1-byte string
|
||||
env(escrowCreate,
|
||||
escrow::Bytecode("AA"),
|
||||
escrow::kCancelTime(env.now() + 100s),
|
||||
Fee(txnFees),
|
||||
Ter(tefTEMP_DISABLED));
|
||||
env.close();
|
||||
|
||||
env(escrowCreate,
|
||||
escrow::Bytecode(bytecode),
|
||||
escrow::kCancelTime(env.now() + 100s),
|
||||
Fee(txnFees),
|
||||
Ter(tefTEMP_DISABLED));
|
||||
env.close();
|
||||
}
|
||||
|
||||
{
|
||||
// Data without Bytecode
|
||||
Env env(*this, features);
|
||||
XRPAmount const txnFees = env.current()->fees().base + 100000;
|
||||
env.fund(XRP(5000), alice, carol);
|
||||
|
||||
auto const escrowCreate = escrow::create(alice, carol, XRP(500));
|
||||
|
||||
std::string const data(4, 'A');
|
||||
env(escrowCreate,
|
||||
escrow::Data(data),
|
||||
escrow::kFinishTime(env.now() + 100s),
|
||||
Fee(txnFees),
|
||||
Ter(temMALFORMED));
|
||||
env.close();
|
||||
}
|
||||
|
||||
{
|
||||
// Data > max length
|
||||
Env env(*this, features);
|
||||
XRPAmount const txnFees = env.current()->fees().base + 100000;
|
||||
env.fund(XRP(5000), alice, carol);
|
||||
|
||||
auto const escrowCreate = escrow::create(alice, carol, XRP(500));
|
||||
|
||||
// string of length (kMaxWasmDataLength + 1) * 2
|
||||
std::string const longData((kMaxWasmDataLength + 1) * 2, 'B');
|
||||
env(escrowCreate,
|
||||
escrow::Data(longData),
|
||||
escrow::Bytecode(bytecode),
|
||||
escrow::kCancelTime(env.now() + 100s),
|
||||
Fee(txnFees),
|
||||
Ter(temMALFORMED));
|
||||
env.close();
|
||||
}
|
||||
|
||||
// Bytecode joins FinishAfter and Condition as a way to say how the
|
||||
// escrow completes, but it always needs a CancelAfter: nothing else
|
||||
// can free the funds if the contract never accepts.
|
||||
Env env(
|
||||
*this,
|
||||
envconfig([](std::unique_ptr<Config> cfg) {
|
||||
cfg->startUp = StartUpType::Fresh;
|
||||
return cfg;
|
||||
}),
|
||||
features);
|
||||
env.fund(XRP(5000), alice, carol);
|
||||
|
||||
auto const escrowCreate = escrow::create(alice, carol, XRP(500));
|
||||
XRPAmount const txnFees = createFeeFor(env, bytecode);
|
||||
|
||||
// Success situations
|
||||
{
|
||||
// Bytecode + CancelAfter
|
||||
env(escrowCreate,
|
||||
escrow::Bytecode(bytecode),
|
||||
escrow::kCancelTime(env.now() + 20s),
|
||||
Fee(txnFees));
|
||||
env.close();
|
||||
}
|
||||
{
|
||||
// Bytecode + Condition + CancelAfter
|
||||
env(escrowCreate,
|
||||
escrow::Bytecode(bytecode),
|
||||
escrow::kCancelTime(env.now() + 30s),
|
||||
escrow::kCondition(escrow::kCb1),
|
||||
Fee(txnFees));
|
||||
env.close();
|
||||
}
|
||||
{
|
||||
// Bytecode + FinishAfter + CancelAfter
|
||||
env(escrowCreate,
|
||||
escrow::Bytecode(bytecode),
|
||||
escrow::kCancelTime(env.now() + 40s),
|
||||
escrow::kFinishTime(env.now() + 2s),
|
||||
Fee(txnFees));
|
||||
env.close();
|
||||
}
|
||||
{
|
||||
// Bytecode + Data + CancelAfter
|
||||
env(escrowCreate,
|
||||
escrow::Bytecode(bytecode),
|
||||
escrow::Data("00112233"),
|
||||
escrow::kCancelTime(env.now() + 50s),
|
||||
Fee(txnFees));
|
||||
env.close();
|
||||
}
|
||||
|
||||
// Failure situations (i.e. all other combinations)
|
||||
{
|
||||
// only Bytecode
|
||||
env(escrowCreate, escrow::Bytecode(bytecode), Fee(txnFees), Ter(temBAD_EXPIRATION));
|
||||
env.close();
|
||||
}
|
||||
{
|
||||
// Bytecode + FinishAfter
|
||||
env(escrowCreate,
|
||||
escrow::Bytecode(bytecode),
|
||||
escrow::kFinishTime(env.now() + 2s),
|
||||
Fee(txnFees),
|
||||
Ter(temBAD_EXPIRATION));
|
||||
env.close();
|
||||
}
|
||||
{
|
||||
// Bytecode + Condition
|
||||
env(escrowCreate,
|
||||
escrow::Bytecode(bytecode),
|
||||
escrow::kCondition(escrow::kCb1),
|
||||
Fee(txnFees),
|
||||
Ter(temBAD_EXPIRATION));
|
||||
env.close();
|
||||
}
|
||||
{
|
||||
// Bytecode 0 length
|
||||
env(escrowCreate,
|
||||
escrow::Bytecode(""),
|
||||
escrow::kCancelTime(env.now() + 60s),
|
||||
Fee(txnFees),
|
||||
Ter(temMALFORMED));
|
||||
env.close();
|
||||
}
|
||||
{
|
||||
// Not enough fees
|
||||
env(escrowCreate,
|
||||
escrow::Bytecode(bytecode),
|
||||
escrow::kCancelTime(env.now() + 70s),
|
||||
Fee(txnFees - 1),
|
||||
Ter(telINSUF_FEE_P));
|
||||
env.close();
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testCreateFeeAndReserve(FeatureBitset features)
|
||||
{
|
||||
testcase("EscrowCreate fee and reserve");
|
||||
|
||||
using namespace jtx;
|
||||
using namespace std::chrono;
|
||||
|
||||
Account const alice{"alice"};
|
||||
Account const carol{"carol"};
|
||||
|
||||
{
|
||||
// The fee scales with the bytecode: ten base fees plus five drops
|
||||
// per byte, and a drop short of it is refused.
|
||||
Env env(*this, features);
|
||||
env.fund(XRP(5000), alice, carol);
|
||||
env.close();
|
||||
|
||||
auto const bytecode = bytecodeOfSize(1'000);
|
||||
auto const escrowCreate = escrow::create(alice, carol, XRP(500));
|
||||
auto const fee = createFeeFor(env, bytecode);
|
||||
BEAST_EXPECT(fee == env.current()->fees().base * 10 + 5'000);
|
||||
|
||||
env(escrowCreate,
|
||||
escrow::Bytecode(bytecode),
|
||||
escrow::kCancelTime(env.now() + 100s),
|
||||
Fee(fee - 1),
|
||||
Ter(telINSUF_FEE_P));
|
||||
env.close();
|
||||
|
||||
auto const seq = env.seq(alice);
|
||||
env(escrowCreate,
|
||||
escrow::Bytecode(bytecode),
|
||||
escrow::Data("00112233"),
|
||||
escrow::kCancelTime(env.now() + 100s),
|
||||
Fee(fee));
|
||||
env.close();
|
||||
|
||||
// The ledger entry carries both fields.
|
||||
auto const sle = env.le(keylet::escrow(alice, SeqProxy::rawSequence(seq)));
|
||||
if (BEAST_EXPECT(sle))
|
||||
{
|
||||
BEAST_EXPECT(sle->isFieldPresent(sfBytecode));
|
||||
BEAST_EXPECT(strHex(sle->getFieldVL(sfBytecode)) == bytecode);
|
||||
BEAST_EXPECT(strHex(sle->getFieldVL(sfData)) == "00112233");
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
// The reserve is one owner increment per started 500 bytes of
|
||||
// bytecode.
|
||||
Env env(*this, features);
|
||||
env.fund(XRP(5000), alice, carol);
|
||||
env.close();
|
||||
|
||||
std::uint32_t expectedOwnerCount = 0;
|
||||
for (auto const size : {
|
||||
std::size_t{64},
|
||||
std::size_t{500},
|
||||
std::size_t{501},
|
||||
std::size_t{1'000},
|
||||
std::size_t{1'001},
|
||||
})
|
||||
{
|
||||
auto const bytecode = bytecodeOfSize(size);
|
||||
env(escrow::create(alice, carol, XRP(100)),
|
||||
escrow::Bytecode(bytecode),
|
||||
escrow::kCancelTime(env.now() + 1000s),
|
||||
Fee(createFeeFor(env, bytecode)));
|
||||
env.close();
|
||||
|
||||
expectedOwnerCount += (size + 499) / 500;
|
||||
BEAST_EXPECTS(
|
||||
env.ownerCount(alice) == expectedOwnerCount,
|
||||
std::to_string(size) + " bytes: " + std::to_string(env.ownerCount(alice)));
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
// A bytecode escrow the owner cannot reserve is refused, where the
|
||||
// same escrow without bytecode would have been accepted.
|
||||
Env env(*this, features);
|
||||
// Base 200 XRP + 50 XRP per owner object, from the unit-test
|
||||
// config. Enough for one owner object, not for two.
|
||||
env.fund(XRP(300), alice, carol);
|
||||
env.close();
|
||||
|
||||
auto const bytecode = bytecodeOfSize(1'000);
|
||||
env(escrow::create(alice, carol, XRP(20)),
|
||||
escrow::Bytecode(bytecode),
|
||||
escrow::kCancelTime(env.now() + 100s),
|
||||
Fee(createFeeFor(env, bytecode)),
|
||||
Ter(tecINSUFFICIENT_RESERVE));
|
||||
env.close();
|
||||
BEAST_EXPECT(env.ownerCount(alice) == 0);
|
||||
|
||||
// The same escrow without bytecode costs one increment and fits.
|
||||
env(escrow::create(alice, carol, XRP(20)),
|
||||
escrow::kFinishTime(env.now() + 10s),
|
||||
escrow::kCancelTime(env.now() + 100s));
|
||||
env.close();
|
||||
BEAST_EXPECT(env.ownerCount(alice) == 1);
|
||||
}
|
||||
|
||||
{
|
||||
// After the lock, an XRP escrow's owner must still hold the
|
||||
// reserve the bytecode costs, not the single increment a plain
|
||||
// escrow costs.
|
||||
Env env(*this, features);
|
||||
Account const dave{"dave"};
|
||||
Account const erin{"erin"};
|
||||
env.fund(XRP(5000), carol);
|
||||
env.close();
|
||||
|
||||
auto const bytecode = bytecodeOfSize(600); // two increments
|
||||
auto const fee = createFeeFor(env, bytecode);
|
||||
auto const amount = XRP(1'000);
|
||||
auto const fees = env.current()->fees();
|
||||
|
||||
// One increment short after the lock.
|
||||
env.fund(
|
||||
STAmount{fees.accountReserve(1, 1) + amount.value().xrp() + fee}, noripple(dave));
|
||||
// Exactly enough after the lock.
|
||||
env.fund(
|
||||
STAmount{fees.accountReserve(2, 1) + amount.value().xrp() + fee}, noripple(erin));
|
||||
env.close();
|
||||
|
||||
env(escrow::create(dave, carol, amount),
|
||||
escrow::Bytecode(bytecode),
|
||||
escrow::kCancelTime(env.now() + 100s),
|
||||
Fee(fee),
|
||||
Ter(tecUNFUNDED));
|
||||
env(escrow::create(erin, carol, amount),
|
||||
escrow::Bytecode(bytecode),
|
||||
escrow::kCancelTime(env.now() + 100s),
|
||||
Fee(fee));
|
||||
env.close();
|
||||
BEAST_EXPECT(env.ownerCount(dave) == 0);
|
||||
BEAST_EXPECT(env.ownerCount(erin) == 2);
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testFinishPreflight(FeatureBitset features)
|
||||
{
|
||||
testcase("EscrowFinish preflight");
|
||||
|
||||
using namespace jtx;
|
||||
using namespace std::chrono;
|
||||
|
||||
Account const alice{"alice"};
|
||||
Account const carol{"carol"};
|
||||
|
||||
{
|
||||
// featureSmartEscrow disabled
|
||||
Env env(*this, features - featureSmartEscrow);
|
||||
env.fund(XRP(5000), alice, carol);
|
||||
env(escrow::finish(carol, alice, 1),
|
||||
Fee(env.current()->fees().base + 1000),
|
||||
escrow::Gas(4),
|
||||
Ter(temDISABLED));
|
||||
env.close();
|
||||
}
|
||||
|
||||
{
|
||||
// Gas > gas limit
|
||||
Env env(*this, votingFor(Keys::kGasLimit, 1'000), features); // in gas
|
||||
env.fund(XRP(5000), alice, carol);
|
||||
env.close();
|
||||
|
||||
// Gas above the voted limit is checked against the escrow it
|
||||
// would run, so there has to be one.
|
||||
auto const bytecode = bytecodeOfSize(64);
|
||||
auto const seq = env.seq(alice);
|
||||
env(escrow::create(alice, carol, XRP(500)),
|
||||
escrow::Bytecode(bytecode),
|
||||
escrow::kCancelTime(env.now() + 100'000s),
|
||||
Fee(createFeeFor(env, bytecode)));
|
||||
closePastFlagLedger(env);
|
||||
|
||||
auto const gas = 1'001;
|
||||
env(escrow::finish(carol, alice, seq),
|
||||
Fee(finishFeeFor(env, gas)),
|
||||
escrow::Gas(gas),
|
||||
Ter(tefEXCEEDS_LIMIT));
|
||||
}
|
||||
|
||||
{
|
||||
// Gas of 0
|
||||
Env env(*this, features);
|
||||
env.fund(XRP(5000), alice, carol);
|
||||
env.close();
|
||||
env(escrow::finish(carol, alice, 1),
|
||||
Fee(env.current()->fees().base + 1000),
|
||||
escrow::Gas(0),
|
||||
Ter(temBAD_LIMIT));
|
||||
}
|
||||
|
||||
{
|
||||
// Gas above the protocol ceiling, refused before any view is read
|
||||
Env env(*this, features);
|
||||
env.fund(XRP(5000), alice, carol);
|
||||
env.close();
|
||||
env(escrow::finish(carol, alice, 1),
|
||||
Fee(env.current()->fees().base + 1000),
|
||||
escrow::Gas(kMaxGasLimit + 1),
|
||||
Ter(temBAD_LIMIT));
|
||||
}
|
||||
|
||||
{
|
||||
// WASM compute disabled after the escrow was created.
|
||||
Env env(*this, votingFor(Keys::kGasLimit, 0), features);
|
||||
env.fund(XRP(5000), alice, carol);
|
||||
env.close();
|
||||
|
||||
auto const bytecode = bytecodeOfSize(64);
|
||||
auto const seq = env.seq(alice);
|
||||
env(escrow::create(alice, carol, XRP(500)),
|
||||
escrow::Bytecode(bytecode),
|
||||
escrow::kCancelTime(env.now() + 100'000s),
|
||||
Fee(createFeeFor(env, bytecode)));
|
||||
closePastFlagLedger(env);
|
||||
|
||||
auto const gas = 1'000;
|
||||
env(escrow::finish(carol, alice, seq),
|
||||
escrow::Gas(gas),
|
||||
Fee(finishFeeFor(env, gas)),
|
||||
Ter(tefTEMP_DISABLED));
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testFinishBytecodePairing(FeatureBitset features)
|
||||
{
|
||||
testcase("EscrowFinish Bytecode and Gas must agree");
|
||||
|
||||
using namespace jtx;
|
||||
using namespace std::chrono;
|
||||
|
||||
Account const alice{"alice"};
|
||||
Account const carol{"carol"};
|
||||
|
||||
Env env(*this, features);
|
||||
|
||||
// Run past the flag ledger so that a Fee change vote occurs and
|
||||
// updates FeeSettings. (It also activates all supported amendments.)
|
||||
for (auto i = env.current()->seq(); i <= 257; ++i)
|
||||
env.close();
|
||||
|
||||
env.fund(XRP(5000), alice, carol);
|
||||
|
||||
auto const bytecode = bytecodeOfSize(64);
|
||||
auto const seq = env.seq(alice);
|
||||
env(escrow::create(alice, carol, XRP(500)),
|
||||
escrow::Bytecode(bytecode),
|
||||
escrow::kCancelTime(env.now() + 100s),
|
||||
Fee(createFeeFor(env, bytecode)));
|
||||
env.close();
|
||||
|
||||
{
|
||||
// Bytecode on the escrow, no Gas on the finish
|
||||
env(escrow::finish(carol, alice, seq), Ter(tefBYTECODE_NOT_INCLUDED));
|
||||
}
|
||||
|
||||
{
|
||||
// Gas on the finish, no Bytecode on the escrow
|
||||
auto const plainSeq = env.seq(alice);
|
||||
env(escrow::create(alice, carol, XRP(500)),
|
||||
escrow::kFinishTime(env.now() + 10s),
|
||||
escrow::kCancelTime(env.now() + 100s));
|
||||
env.close();
|
||||
|
||||
auto const gas = 100;
|
||||
env(escrow::finish(carol, alice, plainSeq),
|
||||
Fee(finishFeeFor(env, gas)),
|
||||
escrow::Gas(gas),
|
||||
Ter(tefNO_BYTECODE));
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testFinishFee(FeatureBitset features)
|
||||
{
|
||||
testcase("EscrowFinish fee");
|
||||
|
||||
using namespace jtx;
|
||||
using namespace std::chrono;
|
||||
|
||||
Account const alice{"alice"};
|
||||
Account const carol{"carol"};
|
||||
|
||||
Env env(
|
||||
*this,
|
||||
envconfig([](std::unique_ptr<Config> cfg) {
|
||||
cfg->fees.gasPrice = 1'000'000; // 1 drop per gas
|
||||
return cfg;
|
||||
}),
|
||||
features);
|
||||
// Run past the flag ledger so that a Fee change vote occurs and
|
||||
// updates FeeSettings. (It also activates all supported amendments.)
|
||||
for (auto i = env.current()->seq(); i <= 257; ++i)
|
||||
env.close();
|
||||
|
||||
env.fund(XRP(5000), alice, carol);
|
||||
|
||||
auto const bytecode = bytecodeOfSize(64);
|
||||
auto const seq = env.seq(alice);
|
||||
env(escrow::create(alice, carol, XRP(1000)),
|
||||
escrow::Bytecode(bytecode),
|
||||
escrow::kCancelTime(env.now() + 100s),
|
||||
Fee(createFeeFor(env, bytecode)));
|
||||
env.close();
|
||||
|
||||
// A large gas allowance costs more than the allowance itself, and does
|
||||
// not wrap.
|
||||
auto const gas = 996'433;
|
||||
auto const fee = finishFeeFor(env, gas);
|
||||
BEAST_EXPECT(fee.drops() > gas);
|
||||
// At one drop per gas the cost divides evenly, so no drop is added.
|
||||
BEAST_EXPECT(fee == env.current()->fees().base + gas);
|
||||
|
||||
// Intentional low value to test overflow handling
|
||||
env(escrow::finish(carol, alice, seq),
|
||||
Fee(drops(30)),
|
||||
escrow::Gas(gas),
|
||||
Ter(telINSUF_FEE_P));
|
||||
|
||||
env(escrow::finish(carol, alice, seq), Fee(fee - 1), escrow::Gas(gas), Ter(telINSUF_FEE_P));
|
||||
|
||||
// TODO(SmartEscrow): tesSUCCESS once the WASM engine lands. Until then
|
||||
// the fee is charged and the run reports that it could not happen.
|
||||
env(escrow::finish(carol, alice, seq),
|
||||
Fee(fee),
|
||||
escrow::Gas(gas),
|
||||
Ter(tecFAILED_PROCESSING));
|
||||
|
||||
{
|
||||
// A gas cost of a fraction of a drop rounds up to a whole drop:
|
||||
// two gas at one micro-drop each costs one drop, not zero, and not
|
||||
// one drop per gas.
|
||||
Env env(*this, votingFor(Keys::kGasPrice, 1), features);
|
||||
env.fund(XRP(5000), alice, carol);
|
||||
env.close();
|
||||
|
||||
auto const seq = env.seq(alice);
|
||||
env(escrow::create(alice, carol, XRP(1000)),
|
||||
escrow::Bytecode(bytecode),
|
||||
escrow::kCancelTime(env.now() + 100'000s),
|
||||
Fee(createFeeFor(env, bytecode)));
|
||||
closePastFlagLedger(env);
|
||||
BEAST_EXPECT(env.current()->fees().gasPrice == 1);
|
||||
|
||||
auto const base = env.current()->fees().base;
|
||||
env(escrow::finish(carol, alice, seq), Fee(base), escrow::Gas(2), Ter(telINSUF_FEE_P));
|
||||
env(escrow::finish(carol, alice, seq),
|
||||
Fee(base + 1),
|
||||
escrow::Gas(2),
|
||||
Ter(tecFAILED_PROCESSING));
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testFinishWithoutEngine(FeatureBitset features)
|
||||
{
|
||||
testcase("EscrowFinish has no WASM engine");
|
||||
|
||||
using namespace jtx;
|
||||
using namespace std::chrono;
|
||||
|
||||
Account const alice{"alice"};
|
||||
Account const carol{"carol"};
|
||||
|
||||
Env env(*this, features);
|
||||
env.fund(XRP(5000), alice, carol);
|
||||
env.close();
|
||||
|
||||
auto const bytecode = bytecodeOfSize(1'000);
|
||||
auto const seq = env.seq(alice);
|
||||
env(escrow::create(alice, carol, XRP(500)),
|
||||
escrow::Bytecode(bytecode),
|
||||
escrow::kCancelTime(env.now() + 20s),
|
||||
Fee(createFeeFor(env, bytecode)));
|
||||
env.close();
|
||||
BEAST_EXPECT(env.ownerCount(alice) == 2);
|
||||
|
||||
// TODO(SmartEscrow): the contract decides this once the WASM engine
|
||||
// lands - tesSUCCESS on accept, tecBYTECODE_REJECTED on reject.
|
||||
auto const gas = 1'000;
|
||||
env(escrow::finish(carol, alice, seq),
|
||||
Fee(finishFeeFor(env, gas)),
|
||||
escrow::Gas(gas),
|
||||
Ter(tecFAILED_PROCESSING));
|
||||
env.close();
|
||||
|
||||
// The escrow and its reserve survive.
|
||||
BEAST_EXPECT(env.le(keylet::escrow(alice, SeqProxy::rawSequence(seq))));
|
||||
BEAST_EXPECT(env.ownerCount(alice) == 2);
|
||||
|
||||
// Cancelling returns the whole reserve, not just one increment.
|
||||
env.close(30s);
|
||||
env(escrow::cancel(carol, alice, seq));
|
||||
env.close();
|
||||
BEAST_EXPECT(!env.le(keylet::escrow(alice, SeqProxy::rawSequence(seq))));
|
||||
BEAST_EXPECT(env.ownerCount(alice) == 0);
|
||||
}
|
||||
|
||||
void
|
||||
testFinishReleasesReserve(FeatureBitset features)
|
||||
{
|
||||
testcase("EscrowFinish releases the reserve");
|
||||
|
||||
using namespace jtx;
|
||||
using namespace std::chrono;
|
||||
|
||||
Account const alice{"alice"};
|
||||
Account const carol{"carol"};
|
||||
|
||||
// Without featureSponsor and fixCleanup3_4_0, the reserve is released
|
||||
// after delivery rather than before.
|
||||
Env env(*this, features - featureSponsor - fixCleanup3_4_0);
|
||||
env.fund(XRP(5000), alice, carol);
|
||||
env.close();
|
||||
|
||||
auto const seq = env.seq(alice);
|
||||
env(escrow::create(alice, carol, XRP(500)), escrow::kFinishTime(env.now() + 1s));
|
||||
env.close();
|
||||
BEAST_EXPECT(env.ownerCount(alice) == 1);
|
||||
|
||||
env(escrow::finish(carol, alice, seq));
|
||||
env.close();
|
||||
BEAST_EXPECT(!env.le(keylet::escrow(alice, SeqProxy::rawSequence(seq))));
|
||||
BEAST_EXPECT(env.ownerCount(alice) == 0);
|
||||
}
|
||||
|
||||
void
|
||||
testWithFeats(FeatureBitset features)
|
||||
{
|
||||
testCreatePreflight(features);
|
||||
testCreateFeeAndReserve(features);
|
||||
testFinishPreflight(features);
|
||||
testFinishBytecodePairing(features);
|
||||
testFinishFee(features);
|
||||
testFinishWithoutEngine(features);
|
||||
testFinishReleasesReserve(features);
|
||||
}
|
||||
|
||||
public:
|
||||
void
|
||||
run() override
|
||||
{
|
||||
using namespace test::jtx;
|
||||
FeatureBitset const all{testableAmendments()};
|
||||
testWithFeats(all);
|
||||
}
|
||||
};
|
||||
|
||||
BEAST_DEFINE_TESTSUITE(EscrowSmart, app, xrpl);
|
||||
|
||||
} // namespace xrpl::test
|
||||
@@ -1524,7 +1524,7 @@ struct Escrow_test : public beast::unit_test::Suite
|
||||
Account const alice{"alice"};
|
||||
Account const bob{"bob"};
|
||||
Account const carol{"carol"};
|
||||
Account const dillon{"dillon"};
|
||||
Account const dillon{"dillon "};
|
||||
Account const zelda{"zelda"};
|
||||
|
||||
char const credType[] = "abcde";
|
||||
@@ -1682,8 +1682,6 @@ public:
|
||||
testWithFeats(all);
|
||||
testWithFeats(all - featureTokenEscrow);
|
||||
testWithFeats(all - fixCleanup3_5_0);
|
||||
testWithFeats(all - featureSmartEscrow);
|
||||
testWithFeats(all - featureTokenEscrow - featureSmartEscrow);
|
||||
testTags(all - fixIncludeKeyletFields);
|
||||
}
|
||||
};
|
||||
|
||||
+35
-804
File diff suppressed because it is too large.
Load diff
@@ -1215,61 +1215,6 @@ struct FlowMPT_test : public beast::unit_test::Suite
|
||||
BEAST_EXPECT(env.balance(bob, y) == y(0));
|
||||
}
|
||||
|
||||
void
|
||||
testConsumeOfferOverflow(FeatureBitset features)
|
||||
{
|
||||
// An overflow while consuming an offer fails the strand under
|
||||
// MPTokensV2, and the transaction with tecINTERNAL otherwise. Either
|
||||
// way no balances change and the offer stays on the book.
|
||||
testcase("BookStep consumeOffer overflow");
|
||||
|
||||
using namespace jtx;
|
||||
|
||||
Account const gw("gateway");
|
||||
Account const eurGW("eur_gateway");
|
||||
Account const maker("maker");
|
||||
Account const alice("alice");
|
||||
Account const bob("bob");
|
||||
|
||||
for (auto const& testFeatures : {features, features - featureMPTokensV2})
|
||||
{
|
||||
Env env(*this, testFeatures);
|
||||
|
||||
auto const withV2 = testFeatures[featureMPTokensV2];
|
||||
|
||||
env.fund(XRP(10'000), gw, eurGW, maker, alice, bob);
|
||||
env.close();
|
||||
|
||||
auto const usd = gw["USD"];
|
||||
auto const eur = eurGW["EUR"];
|
||||
STAmount const usdMax{usd.issue(), STAmount::kMaxValue, STAmount::kMaxOffset};
|
||||
STAmount const usdIn{usd.issue(), 1, 90};
|
||||
|
||||
env.trust(usdMax, maker, alice);
|
||||
env.trust(eur(1'000), maker, bob);
|
||||
env.close();
|
||||
|
||||
env(pay(gw, maker, usdMax));
|
||||
env(pay(gw, alice, usdIn));
|
||||
env(pay(eurGW, maker, eur(100)));
|
||||
env(offer(maker, usdIn, eur(100)));
|
||||
env.close();
|
||||
|
||||
env(pay(alice, bob, eur(100)),
|
||||
Path(~eur),
|
||||
Sendmax(usdIn),
|
||||
Ter(withV2 ? TER{tecPATH_PARTIAL} : TER{tecINTERNAL}));
|
||||
env.close();
|
||||
|
||||
env.require(
|
||||
Balance(bob, eur(0)),
|
||||
Balance(maker, eur(100)),
|
||||
Balance(maker, usdMax),
|
||||
Balance(alice, usdIn));
|
||||
BEAST_EXPECT(isOffer(env, maker, usdIn, eur(100)));
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testFalseDry(FeatureBitset features)
|
||||
{
|
||||
@@ -3063,7 +3008,6 @@ struct FlowMPT_test : public beast::unit_test::Suite
|
||||
testBookStepInputAboveMaximumAmount(features);
|
||||
testBookStepInputAboveMaximumAmountWithFee(features);
|
||||
testBookStepMissingIssuance(features);
|
||||
testConsumeOfferOverflow(features);
|
||||
testSelfPayment1(features);
|
||||
testSelfPayment2(features);
|
||||
testSelfFundedXRPEndpoint(false, features);
|
||||
@@ -3072,85 +3016,6 @@ struct FlowMPT_test : public beast::unit_test::Suite
|
||||
testReExecuteDirectStep(features);
|
||||
testSelfPayLowQualityOffer(features);
|
||||
testLockedMidPathHolder(features);
|
||||
testIssuerSourceMPTLoop(features);
|
||||
}
|
||||
|
||||
void
|
||||
testIssuerSourceMPTLoop(FeatureBitset features)
|
||||
{
|
||||
// An issuer paying with its own MPT may not route it back through a
|
||||
// book that outputs the same MPT. It would issue at the first book
|
||||
// and again after the second, and with the issuer's own offer in
|
||||
// the second book the engine could not keep the sum within
|
||||
// MaximumAmount. Holders already get temBAD_PATH_LOOP for this.
|
||||
testcase("Issuer-sourced payment looping through its own MPT");
|
||||
|
||||
using namespace jtx;
|
||||
|
||||
Account const gw{"gw"};
|
||||
Account const alice{"alice"};
|
||||
Account const c1{"c1"};
|
||||
Account const e1{"e1"};
|
||||
Account const dan{"dan"};
|
||||
|
||||
Env env(*this, features);
|
||||
env.fund(XRP(1'000), gw, alice, c1, e1, dan);
|
||||
env.close();
|
||||
|
||||
MPT const x =
|
||||
MPTTester({.env = env, .issuer = gw, .holders = {alice, c1, e1, dan}, .maxAmt = 100});
|
||||
auto const eur = gw["EUR"];
|
||||
auto const usd = gw["USD"];
|
||||
env(trust(e1, eur(1'000)));
|
||||
env(trust(e1, usd(1'000)));
|
||||
env(trust(dan, eur(1'000)));
|
||||
env(pay(gw, e1, eur(1'000)));
|
||||
env(pay(gw, e1, usd(1'000)));
|
||||
env(pay(gw, alice, x(10)));
|
||||
env.close();
|
||||
|
||||
// Liquidity for every path below, arranged so nothing crosses at
|
||||
// creation.
|
||||
env(offer(gw, XRP(60), x(30))); // issuer sells x for XRP
|
||||
env(offer(gw, usd(100), x(100))); // issuer sells x for usd
|
||||
env.close();
|
||||
env(offer(c1, x(100), XRP(100))); // buys x for XRP
|
||||
env(offer(e1, XRP(70), usd(70))); // sells usd for XRP
|
||||
env(offer(e1, x(100), eur(100))); // sells eur for x
|
||||
env.close();
|
||||
BEAST_EXPECT(expectOffers(env, gw, 2));
|
||||
BEAST_EXPECT(expectOffers(env, c1, 1));
|
||||
BEAST_EXPECT(expectOffers(env, e1, 2));
|
||||
|
||||
auto const flags = Txflags(tfNoRippleDirect | tfPartialPayment);
|
||||
|
||||
// x -> XRP -> x, the issuer's own offer feeds the endpoint
|
||||
env(pay(gw, dan, x(100)), Sendmax(x(100)), Path(~XRP, ~x), flags, Ter(temBAD_PATH_LOOP));
|
||||
// x -> XRP -> x -> eur, the issuer's own offer feeds a third book
|
||||
env(pay(gw, dan, eur(100)),
|
||||
Sendmax(x(100)),
|
||||
Path(~XRP, ~x, ~eur),
|
||||
flags,
|
||||
Ter(temBAD_PATH_LOOP));
|
||||
// x -> XRP -> usd -> x, a limiting book before the issuer's own offer
|
||||
env(pay(gw, dan, x(100)),
|
||||
Sendmax(x(100)),
|
||||
Path(~XRP, ~usd, ~x),
|
||||
flags,
|
||||
Ter(temBAD_PATH_LOOP));
|
||||
// Same loop from a holder is already rejected
|
||||
env(pay(alice, dan, x(10)), Sendmax(x(10)), Path(~XRP, ~x), flags, Ter(temBAD_PATH_LOOP));
|
||||
env.close();
|
||||
|
||||
BEAST_EXPECT(env.balance(gw, x) == x(-10));
|
||||
BEAST_EXPECT(env.balance(c1, x) == x(0));
|
||||
BEAST_EXPECT(env.balance(dan, x) == x(0));
|
||||
|
||||
// The issuer paying with another asset may still take its own offer
|
||||
env(pay(gw, dan, x(50)), Sendmax(usd(50)), Path(~x), flags);
|
||||
env.close();
|
||||
BEAST_EXPECT(env.balance(dan, x) == x(50));
|
||||
BEAST_EXPECT(env.balance(gw, x) == x(-60));
|
||||
}
|
||||
|
||||
void
|
||||
|
||||
@@ -1,694 +0,0 @@
|
||||
#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,7 +11,6 @@
|
||||
|
||||
#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>
|
||||
@@ -25,7 +24,6 @@
|
||||
|
||||
#include <cassert>
|
||||
#include <memory>
|
||||
#include <stdexcept>
|
||||
#include <vector>
|
||||
|
||||
namespace xrpl::test {
|
||||
@@ -74,15 +72,11 @@ public:
|
||||
}
|
||||
res->unshare();
|
||||
|
||||
// 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");
|
||||
}
|
||||
// Accept ledger
|
||||
res->setAccepted(
|
||||
res->header().closeTime,
|
||||
res->header().closeTimeResolution,
|
||||
true /* close time correct*/);
|
||||
lh.insert(res, false);
|
||||
return res;
|
||||
}
|
||||
|
||||
@@ -39,7 +39,6 @@
|
||||
#include <xrpl/ledger/helpers/MPTokenHelpers.h>
|
||||
#include <xrpl/ledger/helpers/TokenHelpers.h>
|
||||
#include <xrpl/protocol/Asset.h>
|
||||
#include <xrpl/protocol/ErrorCodes.h>
|
||||
#include <xrpl/protocol/Feature.h>
|
||||
#include <xrpl/protocol/IOUAmount.h>
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
@@ -995,9 +994,8 @@ class MPToken_test : public beast::unit_test::Suite
|
||||
test::jtx::MPT const mpT1("MPT", makeMptID(env.seq(alice) + 10, alice));
|
||||
err = !mpTokensV2 ? Ter(temMALFORMED) : Ter(tecOBJECT_NOT_FOUND);
|
||||
env(pay(alice, carol, mpT1(100)), Sendmax(mpt(100)), err);
|
||||
// "paths" is invalid in V1. In V2 any path from the issuer back
|
||||
// to its own MPT is a loop, like it is for a holder.
|
||||
err = !mpTokensV2 ? Ter(temMALFORMED) : Ter(temBAD_PATH_LOOP);
|
||||
// "paths" is invalid in V1
|
||||
err = !mpTokensV2 ? Ter(temMALFORMED) : Ter(tesSUCCESS);
|
||||
env(pay(alice, carol, mpt(100)), Path(~usd), err);
|
||||
}
|
||||
|
||||
@@ -1021,7 +1019,6 @@ class MPToken_test : public beast::unit_test::Suite
|
||||
payment[jss::build_path] = true;
|
||||
auto jrr = env.rpc("json", "submit", to_string(payment));
|
||||
BEAST_EXPECT(jrr[jss::result][jss::error] == "invalidParams");
|
||||
BEAST_EXPECT(jrr[jss::result][jss::error_code] == RpcInvalidParams);
|
||||
BEAST_EXPECT(
|
||||
jrr[jss::result][jss::error_message] ==
|
||||
"Field 'build_path' not allowed in this context.");
|
||||
@@ -1583,8 +1580,6 @@ class MPToken_test : public beast::unit_test::Suite
|
||||
jv[jss::tx_json][jss::Amount][jss::value] = std::to_string(kMaxMpTokenAmount + 1);
|
||||
auto const jrr = env.rpc("json", "submit", to_string(jv));
|
||||
BEAST_EXPECT(jrr[jss::result][jss::error] == "invalidParams");
|
||||
BEAST_EXPECT(jrr[jss::result][jss::error_code] == RpcInvalidParams);
|
||||
BEAST_EXPECT(jrr[jss::result][jss::error_message] == "Invalid field 'tx_json.Amount'.");
|
||||
}
|
||||
|
||||
// Pay maximum amount with the transfer fee, SendMax, and
|
||||
@@ -1949,8 +1944,6 @@ class MPToken_test : public beast::unit_test::Suite
|
||||
jtx.stx->add(s);
|
||||
auto jrr = env.rpc("submit", strHex(s.slice()));
|
||||
BEAST_EXPECT(jrr[jss::result][jss::error] == "invalidTransaction");
|
||||
BEAST_EXPECT(!jrr[jss::result].isMember(jss::error_code));
|
||||
BEAST_EXPECT(jrr[jss::result][jss::error_message].isNull());
|
||||
|
||||
// tx is unsigned
|
||||
json::Value jv1;
|
||||
@@ -1958,13 +1951,9 @@ class MPToken_test : public beast::unit_test::Suite
|
||||
jv1[jss::tx_json] = jv;
|
||||
jrr = env.rpc("json", "submit", to_string(jv1));
|
||||
BEAST_EXPECT(jrr[jss::result][jss::error] == "invalidParams");
|
||||
BEAST_EXPECT(jrr[jss::result][jss::error_code] == RpcInvalidParams);
|
||||
BEAST_EXPECT(jrr[jss::result][jss::error_message] == "Amount can not be MPT.");
|
||||
|
||||
jrr = env.rpc("json", "sign", to_string(jv1));
|
||||
BEAST_EXPECT(jrr[jss::result][jss::error] == "invalidParams");
|
||||
BEAST_EXPECT(jrr[jss::result][jss::error_code] == RpcInvalidParams);
|
||||
BEAST_EXPECT(jrr[jss::result][jss::error_message] == "Amount can not be MPT.");
|
||||
};
|
||||
auto setMPTFields = [&](SField const& field, json::Value& jv, bool withAmount = true) {
|
||||
jv[jss::Asset] = toJson(xrpIssue());
|
||||
@@ -3208,9 +3197,6 @@ class MPToken_test : public beast::unit_test::Suite
|
||||
jv1[jss::tx_json] = jv;
|
||||
auto const jrr = env.rpc("json", "submit", to_string(jv1));
|
||||
BEAST_EXPECT(jrr[jss::result][jss::error] == "invalidParams");
|
||||
BEAST_EXPECT(jrr[jss::result][jss::error_code] == RpcInvalidParams);
|
||||
BEAST_EXPECT(
|
||||
jrr[jss::result][jss::error_message] == "Field 'tx_json.Amount' has invalid data.");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -31,7 +31,6 @@
|
||||
#include <xrpl/config/Constants.h>
|
||||
#include <xrpl/json/json_value.h>
|
||||
#include <xrpl/json/to_string.h>
|
||||
#include <xrpl/protocol/ErrorCodes.h>
|
||||
#include <xrpl/protocol/Feature.h>
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
#include <xrpl/protocol/KeyType.h>
|
||||
@@ -587,7 +586,6 @@ public:
|
||||
auto jrr = env.rpc("json", "sign_for", to_string(jvOne))[jss::result];
|
||||
BEAST_EXPECT(jrr[jss::status] == "error");
|
||||
BEAST_EXPECT(jrr[jss::error] == "invalidParams");
|
||||
BEAST_EXPECT(jrr[jss::error_code] == RpcInvalidParams);
|
||||
BEAST_EXPECT(
|
||||
jrr[jss::error_message] ==
|
||||
"When multi-signing 'tx_json.SigningPubKey' must be empty.");
|
||||
@@ -615,7 +613,6 @@ public:
|
||||
jrr = env.rpc("json", "submit_multisigned", to_string(jvSubmit))[jss::result];
|
||||
BEAST_EXPECT(jrr[jss::status] == "error");
|
||||
BEAST_EXPECT(jrr[jss::error] == "invalidParams");
|
||||
BEAST_EXPECT(jrr[jss::error_code] == RpcInvalidParams);
|
||||
BEAST_EXPECT(
|
||||
jrr[jss::error_message] == "Invalid Fee field. Fees must be greater than zero.");
|
||||
}
|
||||
@@ -642,7 +639,6 @@ public:
|
||||
jrr = env.rpc("json", "submit_multisigned", to_string(jvSubmit))[jss::result];
|
||||
BEAST_EXPECT(jrr[jss::status] == "error");
|
||||
BEAST_EXPECT(jrr[jss::error] == "internal");
|
||||
BEAST_EXPECT(jrr[jss::error_code] == RpcInternal);
|
||||
BEAST_EXPECT(jrr[jss::error_message] == "Internal error.");
|
||||
}
|
||||
|
||||
@@ -655,8 +651,6 @@ public:
|
||||
auto jrr = env.rpc("json", "sign_for", to_string(jv))[jss::result];
|
||||
BEAST_EXPECT(jrr[jss::status] == "error");
|
||||
BEAST_EXPECT(jrr[jss::error] == "masterDisabled");
|
||||
BEAST_EXPECT(jrr[jss::error_code] == RpcMasterDisabled);
|
||||
BEAST_EXPECT(jrr[jss::error_message] == "Master key is disabled.");
|
||||
env.close();
|
||||
BEAST_EXPECT(env.seq(alice) == aliceSeq);
|
||||
}
|
||||
@@ -696,16 +690,12 @@ public:
|
||||
auto jrr = env.rpc("json", "sign_for", to_string(jv))[jss::result];
|
||||
BEAST_EXPECT(jrr[jss::status] == "error");
|
||||
BEAST_EXPECT(jrr[jss::error] == "srcActMalformed");
|
||||
BEAST_EXPECT(jrr[jss::error_code] == RpcSrcActMalformed);
|
||||
BEAST_EXPECT(jrr[jss::error_message] == "Invalid field 'tx_json.Account'.");
|
||||
|
||||
Account const jimmy{"jimmy"};
|
||||
jv[jss::tx_json][jss::Account] = jimmy.human();
|
||||
jrr = env.rpc("json", "sign_for", to_string(jv))[jss::result];
|
||||
BEAST_EXPECT(jrr[jss::status] == "error");
|
||||
BEAST_EXPECT(jrr[jss::error] == "srcActNotFound");
|
||||
BEAST_EXPECT(jrr[jss::error_code] == RpcSrcActNotFound);
|
||||
BEAST_EXPECT(jrr[jss::error_message] == "Source account not found.");
|
||||
}
|
||||
|
||||
{
|
||||
@@ -716,7 +706,6 @@ public:
|
||||
auto jrr = env.rpc("json", "submit_multisigned", to_string(jv))[jss::result];
|
||||
BEAST_EXPECT(jrr[jss::status] == "error");
|
||||
BEAST_EXPECT(jrr[jss::error] == "invalidParams");
|
||||
BEAST_EXPECT(jrr[jss::error_code] == RpcInvalidParams);
|
||||
BEAST_EXPECT(jrr[jss::error_message] == "tx_json.Signers array may not be empty.");
|
||||
env.close();
|
||||
BEAST_EXPECT(env.seq(alice) == aliceSeq);
|
||||
@@ -1079,9 +1068,6 @@ public:
|
||||
STTx local = *tx.stx;
|
||||
local.setFieldVL(sfSigningPubKey, Blob()); // Empty SigningPubKey
|
||||
auto const info = submitSTTx(local);
|
||||
BEAST_EXPECT(info[jss::result][jss::error] == "invalidTransaction");
|
||||
BEAST_EXPECT(!info[jss::result].isMember(jss::error_code));
|
||||
BEAST_EXPECT(info[jss::result][jss::error_message].isNull());
|
||||
BEAST_EXPECT(
|
||||
info[jss::result][jss::error_exception] ==
|
||||
"fails local checks: Empty SigningPubKey.");
|
||||
@@ -1096,9 +1082,6 @@ public:
|
||||
local.setFieldVL(sfTxnSignature, badSig);
|
||||
// Signature should fail.
|
||||
auto const info = submitSTTx(local);
|
||||
BEAST_EXPECT(info[jss::result][jss::error] == "invalidTransaction");
|
||||
BEAST_EXPECT(!info[jss::result].isMember(jss::error_code));
|
||||
BEAST_EXPECT(info[jss::result][jss::error_message].isNull());
|
||||
BEAST_EXPECT(
|
||||
info[jss::result][jss::error_exception] ==
|
||||
"fails local checks: Invalid signature.");
|
||||
@@ -1112,9 +1095,6 @@ public:
|
||||
local.setFieldU32(sfSequence, seq + 1);
|
||||
// Signature should fail.
|
||||
auto const info = submitSTTx(local);
|
||||
BEAST_EXPECT(info[jss::result][jss::error] == "invalidTransaction");
|
||||
BEAST_EXPECT(!info[jss::result].isMember(jss::error_code));
|
||||
BEAST_EXPECT(info[jss::result][jss::error_message].isNull());
|
||||
BEAST_EXPECT(
|
||||
info[jss::result][jss::error_exception] ==
|
||||
"fails local checks: Invalid signature.");
|
||||
@@ -1125,9 +1105,6 @@ public:
|
||||
STTx local = *tx.stx;
|
||||
local[sfSigningPubKey] = alice.pk(); // Insert sfSigningPubKey
|
||||
auto const info = submitSTTx(local);
|
||||
BEAST_EXPECT(info[jss::result][jss::error] == "invalidTransaction");
|
||||
BEAST_EXPECT(!info[jss::result].isMember(jss::error_code));
|
||||
BEAST_EXPECT(info[jss::result][jss::error_message].isNull());
|
||||
BEAST_EXPECT(
|
||||
info[jss::result][jss::error_exception] ==
|
||||
"fails local checks: Cannot both single- and multi-sign.");
|
||||
@@ -1139,9 +1116,6 @@ public:
|
||||
local.sign(alice.pk(), alice.sk());
|
||||
local.setFieldVL(sfSigningPubKey, Blob()); // Empty SigningPubKey
|
||||
auto const info = submitSTTx(local);
|
||||
BEAST_EXPECT(info[jss::result][jss::error] == "invalidTransaction");
|
||||
BEAST_EXPECT(!info[jss::result].isMember(jss::error_code));
|
||||
BEAST_EXPECT(info[jss::result][jss::error_message].isNull());
|
||||
BEAST_EXPECT(
|
||||
info[jss::result][jss::error_exception] ==
|
||||
"fails local checks: Cannot both single- and multi-sign.");
|
||||
@@ -1157,9 +1131,6 @@ public:
|
||||
signer.setFieldVL(sfTxnSignature, badSig);
|
||||
// Signature should fail.
|
||||
auto const info = submitSTTx(local);
|
||||
BEAST_EXPECT(info[jss::result][jss::error] == "invalidTransaction");
|
||||
BEAST_EXPECT(!info[jss::result].isMember(jss::error_code));
|
||||
BEAST_EXPECT(info[jss::result][jss::error_message].isNull());
|
||||
BEAST_EXPECT(
|
||||
info[jss::result][jss::error_exception].asString().contains(
|
||||
"Invalid signature on account r"));
|
||||
@@ -1170,9 +1141,6 @@ public:
|
||||
STTx local = *tx.stx;
|
||||
local.peekFieldArray(sfSigners).clear(); // Empty Signers array.
|
||||
auto const info = submitSTTx(local);
|
||||
BEAST_EXPECT(info[jss::result][jss::error] == "invalidTransaction");
|
||||
BEAST_EXPECT(!info[jss::result].isMember(jss::error_code));
|
||||
BEAST_EXPECT(info[jss::result][jss::error_message].isNull());
|
||||
BEAST_EXPECT(
|
||||
info[jss::result][jss::error_exception] ==
|
||||
"fails local checks: Invalid Signers array size.");
|
||||
@@ -1218,9 +1186,6 @@ public:
|
||||
bogie_));
|
||||
STTx const local = *tx.stx;
|
||||
auto const info = submitSTTx(local);
|
||||
BEAST_EXPECT(info[jss::result][jss::error] == "invalidTransaction");
|
||||
BEAST_EXPECT(!info[jss::result].isMember(jss::error_code));
|
||||
BEAST_EXPECT(info[jss::result][jss::error_message].isNull());
|
||||
BEAST_EXPECT(
|
||||
info[jss::result][jss::error_exception] ==
|
||||
"fails local checks: Invalid Signers array size.");
|
||||
@@ -1230,9 +1195,6 @@ public:
|
||||
JTx const tx = env.jt(noop(alice), Fee(2 * baseFee), Msig(alice));
|
||||
STTx const local = *tx.stx;
|
||||
auto const info = submitSTTx(local);
|
||||
BEAST_EXPECT(info[jss::result][jss::error] == "invalidTransaction");
|
||||
BEAST_EXPECT(!info[jss::result].isMember(jss::error_code));
|
||||
BEAST_EXPECT(info[jss::result][jss::error_message].isNull());
|
||||
BEAST_EXPECT(
|
||||
info[jss::result][jss::error_exception] ==
|
||||
"fails local checks: Invalid multisigner.");
|
||||
@@ -1242,9 +1204,6 @@ public:
|
||||
JTx const tx = env.jt(noop(alice), Fee(2 * baseFee), Msig(bogie_, bogie_));
|
||||
STTx const local = *tx.stx;
|
||||
auto const info = submitSTTx(local);
|
||||
BEAST_EXPECT(info[jss::result][jss::error] == "invalidTransaction");
|
||||
BEAST_EXPECT(!info[jss::result].isMember(jss::error_code));
|
||||
BEAST_EXPECT(info[jss::result][jss::error_message].isNull());
|
||||
BEAST_EXPECT(
|
||||
info[jss::result][jss::error_exception] ==
|
||||
"fails local checks: Duplicate Signers not allowed.");
|
||||
@@ -1258,9 +1217,6 @@ public:
|
||||
std::ranges::reverse(signers);
|
||||
// Signature should fail.
|
||||
auto const info = submitSTTx(local);
|
||||
BEAST_EXPECT(info[jss::result][jss::error] == "invalidTransaction");
|
||||
BEAST_EXPECT(!info[jss::result].isMember(jss::error_code));
|
||||
BEAST_EXPECT(info[jss::result][jss::error_message].isNull());
|
||||
BEAST_EXPECT(
|
||||
info[jss::result][jss::error_exception] ==
|
||||
"fails local checks: Unsorted Signers array.");
|
||||
|
||||
@@ -30,7 +30,6 @@
|
||||
#include <xrpl/json/to_string.h>
|
||||
#include <xrpl/ledger/OpenView.h>
|
||||
#include <xrpl/protocol/ApiVersion.h>
|
||||
#include <xrpl/protocol/ErrorCodes.h>
|
||||
#include <xrpl/protocol/Feature.h>
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
#include <xrpl/protocol/Protocol.h>
|
||||
@@ -4654,22 +4653,18 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite
|
||||
if (expect(
|
||||
nftOffers.isMember(jss::result), "expected \"result\"", __FILE__, line))
|
||||
{
|
||||
json::Value const& result = nftOffers[jss::result];
|
||||
expect(
|
||||
result[jss::error].asString() == "objectNotFound",
|
||||
"expected \"objectNotFound\"",
|
||||
__FILE__,
|
||||
line);
|
||||
expect(
|
||||
result[jss::error_code] == RpcObjectNotFound,
|
||||
"expected RpcObjectNotFound",
|
||||
__FILE__,
|
||||
line);
|
||||
expect(
|
||||
result[jss::error_message] == "The requested object was not found.",
|
||||
"expected \"The requested object was not found.\"",
|
||||
__FILE__,
|
||||
line);
|
||||
if (expect(
|
||||
nftOffers[jss::result].isMember(jss::error),
|
||||
"expected \"error\"",
|
||||
__FILE__,
|
||||
line))
|
||||
{
|
||||
expect(
|
||||
nftOffers[jss::result][jss::error].asString() == "objectNotFound",
|
||||
"expected \"objectNotFound\"",
|
||||
__FILE__,
|
||||
line);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
@@ -4865,15 +4860,11 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite
|
||||
// Sell-side marker passed to nft_buy_offers.
|
||||
json::Value const result = queryWithMarker("nft_buy_offers", sellOfferIndex);
|
||||
BEAST_EXPECT(result[jss::error].asString() == "invalidParams");
|
||||
BEAST_EXPECT(result[jss::error_code] == RpcInvalidParams);
|
||||
BEAST_EXPECT(result[jss::error_message] == "Invalid parameters.");
|
||||
}
|
||||
{
|
||||
// Buy-side marker passed to nft_sell_offers.
|
||||
json::Value const result = queryWithMarker("nft_sell_offers", buyOfferIndex);
|
||||
BEAST_EXPECT(result[jss::error].asString() == "invalidParams");
|
||||
BEAST_EXPECT(result[jss::error_code] == RpcInvalidParams);
|
||||
BEAST_EXPECT(result[jss::error_message] == "Invalid parameters.");
|
||||
}
|
||||
|
||||
// A same-side marker is still accepted. With a single offer on each
|
||||
|
||||
@@ -34,7 +34,6 @@
|
||||
#include <xrpl/ledger/helpers/DirectoryHelpers.h>
|
||||
#include <xrpl/protocol/AccountID.h>
|
||||
#include <xrpl/protocol/Book.h>
|
||||
#include <xrpl/protocol/ErrorCodes.h>
|
||||
#include <xrpl/protocol/Feature.h>
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
#include <xrpl/protocol/Issue.h>
|
||||
@@ -2756,8 +2755,6 @@ public:
|
||||
|
||||
jro = ledgerEntryOffer(env, bob, bobOfferSeq);
|
||||
BEAST_EXPECT(jro[jss::error] == "entryNotFound");
|
||||
BEAST_EXPECT(jro[jss::error_code] == RpcEntryNotFound);
|
||||
BEAST_EXPECT(jro[jss::error_message] == "Entry not found.");
|
||||
|
||||
env.require(Owners(alice, 1), Owners(bob, 1));
|
||||
}
|
||||
@@ -2791,8 +2788,6 @@ public:
|
||||
|
||||
auto jro = ledgerEntryOffer(env, bob, bobOfferSeq);
|
||||
BEAST_EXPECT(jro[jss::error] == "entryNotFound");
|
||||
BEAST_EXPECT(jro[jss::error_code] == RpcEntryNotFound);
|
||||
BEAST_EXPECT(jro[jss::error_message] == "Entry not found.");
|
||||
};
|
||||
testHelper3TokensMix(test);
|
||||
}
|
||||
@@ -2852,8 +2847,6 @@ public:
|
||||
// Verify the offer was consumed
|
||||
jro = ledgerEntryOffer(env, bob, bobOfferSeq);
|
||||
BEAST_EXPECT(jro[jss::error] == "entryNotFound");
|
||||
BEAST_EXPECT(jro[jss::error_code] == RpcEntryNotFound);
|
||||
BEAST_EXPECT(jro[jss::error_message] == "Entry not found.");
|
||||
|
||||
// verify balances look right after the partial payment
|
||||
// only 300 XRP should have been payed since that's all
|
||||
|
||||
@@ -30,7 +30,6 @@
|
||||
#include <xrpl/json/to_string.h>
|
||||
#include <xrpl/ledger/helpers/DirectoryHelpers.h>
|
||||
#include <xrpl/protocol/AccountID.h>
|
||||
#include <xrpl/protocol/ErrorCodes.h>
|
||||
#include <xrpl/protocol/Feature.h>
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
#include <xrpl/protocol/Issue.h>
|
||||
@@ -1708,8 +1707,6 @@ public:
|
||||
|
||||
jro = ledgerEntryOffer(env, bob, bobOfferSeq);
|
||||
BEAST_EXPECT(jro[jss::error] == "entryNotFound");
|
||||
BEAST_EXPECT(jro[jss::error_code] == RpcEntryNotFound);
|
||||
BEAST_EXPECT(jro[jss::error_message] == "Entry not found.");
|
||||
|
||||
env.require(Owners(alice, 1), Owners(bob, 1));
|
||||
}
|
||||
@@ -1741,8 +1738,6 @@ public:
|
||||
|
||||
auto jro = ledgerEntryOffer(env, bob, bobOfferSeq);
|
||||
BEAST_EXPECT(jro[jss::error] == "entryNotFound");
|
||||
BEAST_EXPECT(jro[jss::error_code] == RpcEntryNotFound);
|
||||
BEAST_EXPECT(jro[jss::error_message] == "Entry not found.");
|
||||
}
|
||||
|
||||
void
|
||||
@@ -1803,8 +1798,6 @@ public:
|
||||
// Verify the offer was consumed
|
||||
jro = ledgerEntryOffer(env, bob, bobOfferSeq);
|
||||
BEAST_EXPECT(jro[jss::error] == "entryNotFound");
|
||||
BEAST_EXPECT(jro[jss::error_code] == RpcEntryNotFound);
|
||||
BEAST_EXPECT(jro[jss::error_message] == "Entry not found.");
|
||||
|
||||
// verify balances look right after the partial payment
|
||||
// only 300 XRP should be have been payed since that's all
|
||||
|
||||
@@ -26,7 +26,6 @@
|
||||
#include <xrpl/json/json_value.h>
|
||||
#include <xrpl/protocol/AccountID.h>
|
||||
#include <xrpl/protocol/ApiVersion.h>
|
||||
#include <xrpl/protocol/ErrorCodes.h>
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
#include <xrpl/protocol/PathAsset.h>
|
||||
#include <xrpl/protocol/SField.h>
|
||||
@@ -163,9 +162,7 @@ public:
|
||||
g.signal();
|
||||
});
|
||||
BEAST_EXPECT(g.waitFor(5s));
|
||||
BEAST_EXPECT(result[jss::error] == "srcCurMalformed");
|
||||
BEAST_EXPECT(result[jss::error_code] == RpcSrcCurMalformed);
|
||||
BEAST_EXPECT(result[jss::error_message] == "Source currency is malformed.");
|
||||
BEAST_EXPECT(result.isMember(jss::error));
|
||||
|
||||
// Test rpc::tuning::max_auto_src_cur source currencies.
|
||||
numSrc.clear();
|
||||
@@ -192,9 +189,7 @@ public:
|
||||
g.signal();
|
||||
});
|
||||
BEAST_EXPECT(g.waitFor(5s));
|
||||
BEAST_EXPECT(result[jss::error] == "internal");
|
||||
BEAST_EXPECT(result[jss::error_code] == RpcInternal);
|
||||
BEAST_EXPECT(result[jss::error_message] == "Internal error.");
|
||||
BEAST_EXPECT(result.isMember(jss::error));
|
||||
}
|
||||
|
||||
void
|
||||
|
||||
@@ -33,7 +33,6 @@
|
||||
#include <xrpl/json/json_value.h>
|
||||
#include <xrpl/protocol/AccountID.h>
|
||||
#include <xrpl/protocol/ApiVersion.h>
|
||||
#include <xrpl/protocol/ErrorCodes.h>
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
#include <xrpl/protocol/Issue.h>
|
||||
#include <xrpl/protocol/SField.h>
|
||||
@@ -319,9 +318,7 @@ public:
|
||||
g.signal();
|
||||
});
|
||||
BEAST_EXPECT(g.waitFor(5s));
|
||||
BEAST_EXPECT(result[jss::error] == "srcCurMalformed");
|
||||
BEAST_EXPECT(result[jss::error_code] == RpcSrcCurMalformed);
|
||||
BEAST_EXPECT(result[jss::error_message] == "Source currency is malformed.");
|
||||
BEAST_EXPECT(result.isMember(jss::error));
|
||||
|
||||
// Test rpc::tuning::max_auto_src_cur source currencies.
|
||||
for (auto i = 0; i < (rpc::tuning::kMaxAutoSrcCur - 1); ++i)
|
||||
@@ -344,9 +341,7 @@ public:
|
||||
g.signal();
|
||||
});
|
||||
BEAST_EXPECT(g.waitFor(5s));
|
||||
BEAST_EXPECT(result[jss::error] == "internal");
|
||||
BEAST_EXPECT(result[jss::error_code] == RpcInternal);
|
||||
BEAST_EXPECT(result[jss::error_message] == "Internal error.");
|
||||
BEAST_EXPECT(result.isMember(jss::error));
|
||||
}
|
||||
|
||||
void
|
||||
|
||||
@@ -31,7 +31,6 @@
|
||||
#include <xrpl/ledger/Dir.h>
|
||||
#include <xrpl/ledger/ReadView.h>
|
||||
#include <xrpl/protocol/ApiVersion.h>
|
||||
#include <xrpl/protocol/ErrorCodes.h>
|
||||
#include <xrpl/protocol/Feature.h>
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
#include <xrpl/protocol/KeyType.h>
|
||||
@@ -1010,7 +1009,6 @@ struct PayChan_test : public beast::unit_test::Suite
|
||||
auto const jrr =
|
||||
env.rpc("json", "account_channels", to_string(params))[jss::result];
|
||||
BEAST_EXPECT(jrr[jss::error] == "invalidParams");
|
||||
BEAST_EXPECT(jrr[jss::error_code] == RpcInvalidParams);
|
||||
BEAST_EXPECT(jrr[jss::error_message] == "Invalid field 'account'.");
|
||||
};
|
||||
|
||||
@@ -1030,7 +1028,6 @@ struct PayChan_test : public beast::unit_test::Suite
|
||||
auto const jrr =
|
||||
env.rpc("json", "account_channels", to_string(params))[jss::result];
|
||||
BEAST_EXPECT(jrr[jss::error] == "invalidParams");
|
||||
BEAST_EXPECT(jrr[jss::error_code] == RpcInvalidParams);
|
||||
BEAST_EXPECT(jrr[jss::error_message] == "Invalid field 'destination_account'.");
|
||||
};
|
||||
|
||||
@@ -1041,55 +1038,6 @@ struct PayChan_test : public beast::unit_test::Suite
|
||||
testInvalidDestAccountParam(json::Value(json::ValueType::Object));
|
||||
testInvalidDestAccountParam(json::Value(json::ValueType::Array));
|
||||
}
|
||||
{
|
||||
// An empty destination_account is no destination at all.
|
||||
json::Value params;
|
||||
params[jss::account] = alice.human();
|
||||
params[jss::destination_account] = "";
|
||||
auto const jrr = env.rpc("json", "account_channels", to_string(params))[jss::result];
|
||||
BEAST_EXPECT(jrr[jss::channels].size() == 1);
|
||||
BEAST_EXPECT(jrr[jss::channels][0u][jss::channel_id] == chan1Str);
|
||||
BEAST_EXPECT(jrr[jss::channels][0u][jss::destination_account] == bob.human());
|
||||
}
|
||||
{
|
||||
// A malformed account is reported without the ledger fields.
|
||||
json::Value params;
|
||||
params[jss::account] = "llIIOO";
|
||||
auto const jrr = env.rpc("json", "account_channels", to_string(params))[jss::result];
|
||||
BEAST_EXPECT(jrr[jss::error] == "actMalformed");
|
||||
BEAST_EXPECT(jrr[jss::error_code] == RpcActMalformed);
|
||||
BEAST_EXPECT(jrr[jss::error_message] == "Account malformed.");
|
||||
BEAST_EXPECT(!jrr.isMember(jss::ledger_index));
|
||||
BEAST_EXPECT(!jrr.isMember(jss::ledger_current_index));
|
||||
}
|
||||
{
|
||||
// A bad limit is reported before an account that is not found.
|
||||
json::Value params;
|
||||
params[jss::account] = Account("bogie").human();
|
||||
params[jss::limit] = 0;
|
||||
auto const jrr = env.rpc("json", "account_channels", to_string(params))[jss::result];
|
||||
BEAST_EXPECT(jrr[jss::error] == "invalidParams");
|
||||
BEAST_EXPECT(jrr[jss::error_code] == RpcInvalidParams);
|
||||
BEAST_EXPECT(jrr[jss::error_message] == "Invalid field 'limit'.");
|
||||
}
|
||||
{
|
||||
// A non-string marker names the field; one that does not parse does not.
|
||||
json::Value params;
|
||||
params[jss::account] = alice.human();
|
||||
params[jss::marker] = 1;
|
||||
auto const notString =
|
||||
env.rpc("json", "account_channels", to_string(params))[jss::result];
|
||||
BEAST_EXPECT(notString[jss::error] == "invalidParams");
|
||||
BEAST_EXPECT(notString[jss::error_code] == RpcInvalidParams);
|
||||
BEAST_EXPECT(notString[jss::error_message] == "Invalid field 'marker', not string.");
|
||||
|
||||
params[jss::marker] = "NOT_A_MARKER";
|
||||
auto const malformed =
|
||||
env.rpc("json", "account_channels", to_string(params))[jss::result];
|
||||
BEAST_EXPECT(malformed[jss::error] == "invalidParams");
|
||||
BEAST_EXPECT(malformed[jss::error_code] == RpcInvalidParams);
|
||||
BEAST_EXPECT(malformed[jss::error_message] == "Invalid parameters.");
|
||||
}
|
||||
{
|
||||
auto const r = env.rpc("account_channels", alice.human(), bob.human());
|
||||
BEAST_EXPECT(r[jss::result][jss::channels].size() == 1);
|
||||
@@ -1233,34 +1181,7 @@ struct PayChan_test : public beast::unit_test::Suite
|
||||
{
|
||||
// degenerate case
|
||||
auto const r = testLimit(env, alice, 0);
|
||||
BEAST_EXPECT(r[jss::error] == "invalidParams");
|
||||
BEAST_EXPECT(r[jss::error_code] == RpcInvalidParams);
|
||||
BEAST_EXPECT(r[jss::error_message] == "Invalid field 'limit'.");
|
||||
}
|
||||
|
||||
{
|
||||
// The marker's start hint may have a leading '+', and anything after a second
|
||||
// comma is ignored.
|
||||
auto const first = testLimit(env, alice, 1);
|
||||
auto const marker = first[jss::marker].asString();
|
||||
auto const comma = marker.find(',');
|
||||
BEAST_EXPECT(comma != std::string::npos);
|
||||
|
||||
auto const rest = testLimit(
|
||||
env,
|
||||
alice,
|
||||
std::nullopt,
|
||||
marker.substr(0, comma) + ",+" + marker.substr(comma + 1) + ",ignored");
|
||||
BEAST_EXPECT(rest[jss::channels].size() == bobs.size() - 1);
|
||||
|
||||
std::set<std::string> found{
|
||||
first[jss::channels][0u][jss::destination_account].asString(),
|
||||
};
|
||||
|
||||
for (auto const& c : rest[jss::channels])
|
||||
BEAST_EXPECT(found.insert(c[jss::destination_account].asString()).second);
|
||||
|
||||
BEAST_EXPECT(found == bobsB58);
|
||||
BEAST_EXPECT(r.isMember(jss::error_message));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1329,12 +1250,7 @@ struct PayChan_test : public beast::unit_test::Suite
|
||||
auto const rs =
|
||||
env.rpc("json", "channel_authorize", args.toStyledString())[jss::result];
|
||||
auto const error = apiVersion < 2u ? "invalidParams" : "badKeyType";
|
||||
auto const errorCode = apiVersion < 2u ? RpcInvalidParams : RpcBadKeyType;
|
||||
auto const errorMessage =
|
||||
apiVersion < 2u ? "Invalid field 'key_type'." : "Bad key type.";
|
||||
BEAST_EXPECT(rs[jss::error] == error);
|
||||
BEAST_EXPECT(rs[jss::error_code] == errorCode);
|
||||
BEAST_EXPECT(rs[jss::error_message] == errorMessage);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1423,40 +1339,22 @@ struct PayChan_test : public beast::unit_test::Suite
|
||||
auto const pkAsHex = sliceToHex(pk.slice());
|
||||
auto rv = env.rpc("channel_verify", pkAsHex, chan1Str, "1000x", sig);
|
||||
BEAST_EXPECT(rv[jss::error] == "channelAmtMalformed");
|
||||
BEAST_EXPECT(rv[jss::error_code] == RpcChannelAmtMalformed);
|
||||
BEAST_EXPECT(rv[jss::error_message] == "Payment channel amount is malformed.");
|
||||
rv = env.rpc("channel_verify", pkAsHex, chan1Str, "1000 ", sig);
|
||||
BEAST_EXPECT(rv[jss::error] == "channelAmtMalformed");
|
||||
BEAST_EXPECT(rv[jss::error_code] == RpcChannelAmtMalformed);
|
||||
BEAST_EXPECT(rv[jss::error_message] == "Payment channel amount is malformed.");
|
||||
rv = env.rpc("channel_verify", pkAsHex, chan1Str, "x1000", sig);
|
||||
BEAST_EXPECT(rv[jss::error] == "channelAmtMalformed");
|
||||
BEAST_EXPECT(rv[jss::error_code] == RpcChannelAmtMalformed);
|
||||
BEAST_EXPECT(rv[jss::error_message] == "Payment channel amount is malformed.");
|
||||
rv = env.rpc("channel_verify", pkAsHex, chan1Str, "x", sig);
|
||||
BEAST_EXPECT(rv[jss::error] == "channelAmtMalformed");
|
||||
BEAST_EXPECT(rv[jss::error_code] == RpcChannelAmtMalformed);
|
||||
BEAST_EXPECT(rv[jss::error_message] == "Payment channel amount is malformed.");
|
||||
rv = env.rpc("channel_verify", pkAsHex, chan1Str, " ", sig);
|
||||
BEAST_EXPECT(rv[jss::error] == "channelAmtMalformed");
|
||||
BEAST_EXPECT(rv[jss::error_code] == RpcChannelAmtMalformed);
|
||||
BEAST_EXPECT(rv[jss::error_message] == "Payment channel amount is malformed.");
|
||||
rv = env.rpc("channel_verify", pkAsHex, chan1Str, "1000 1000", sig);
|
||||
BEAST_EXPECT(rv[jss::error] == "channelAmtMalformed");
|
||||
BEAST_EXPECT(rv[jss::error_code] == RpcChannelAmtMalformed);
|
||||
BEAST_EXPECT(rv[jss::error_message] == "Payment channel amount is malformed.");
|
||||
rv = env.rpc("channel_verify", pkAsHex, chan1Str, "1,000", sig);
|
||||
BEAST_EXPECT(rv[jss::error] == "channelAmtMalformed");
|
||||
BEAST_EXPECT(rv[jss::error_code] == RpcChannelAmtMalformed);
|
||||
BEAST_EXPECT(rv[jss::error_message] == "Payment channel amount is malformed.");
|
||||
rv = env.rpc("channel_verify", pkAsHex, chan1Str, " 1000", sig);
|
||||
BEAST_EXPECT(rv[jss::error] == "channelAmtMalformed");
|
||||
BEAST_EXPECT(rv[jss::error_code] == RpcChannelAmtMalformed);
|
||||
BEAST_EXPECT(rv[jss::error_message] == "Payment channel amount is malformed.");
|
||||
rv = env.rpc("channel_verify", pkAsHex, chan1Str, "", sig);
|
||||
BEAST_EXPECT(rv[jss::error] == "channelAmtMalformed");
|
||||
BEAST_EXPECT(rv[jss::error_code] == RpcChannelAmtMalformed);
|
||||
BEAST_EXPECT(rv[jss::error_message] == "Payment channel amount is malformed.");
|
||||
}
|
||||
{
|
||||
// malformed channel
|
||||
@@ -1465,34 +1363,22 @@ struct PayChan_test : public beast::unit_test::Suite
|
||||
chan1StrBad.pop_back();
|
||||
auto rv = env.rpc("channel_verify", pkAsHex, chan1StrBad, "1000", sig);
|
||||
BEAST_EXPECT(rv[jss::error] == "channelMalformed");
|
||||
BEAST_EXPECT(rv[jss::error_code] == RpcChannelMalformed);
|
||||
BEAST_EXPECT(rv[jss::error_message] == "Payment channel is malformed.");
|
||||
rv = env.rpc("channel_authorize", "alice", chan1StrBad, "1000");
|
||||
BEAST_EXPECT(rv[jss::error] == "channelMalformed");
|
||||
BEAST_EXPECT(rv[jss::error_code] == RpcChannelMalformed);
|
||||
BEAST_EXPECT(rv[jss::error_message] == "Payment channel is malformed.");
|
||||
|
||||
chan1StrBad = chan1Str;
|
||||
chan1StrBad.push_back('0');
|
||||
rv = env.rpc("channel_verify", pkAsHex, chan1StrBad, "1000", sig);
|
||||
BEAST_EXPECT(rv[jss::error] == "channelMalformed");
|
||||
BEAST_EXPECT(rv[jss::error_code] == RpcChannelMalformed);
|
||||
BEAST_EXPECT(rv[jss::error_message] == "Payment channel is malformed.");
|
||||
rv = env.rpc("channel_authorize", "alice", chan1StrBad, "1000");
|
||||
BEAST_EXPECT(rv[jss::error] == "channelMalformed");
|
||||
BEAST_EXPECT(rv[jss::error_code] == RpcChannelMalformed);
|
||||
BEAST_EXPECT(rv[jss::error_message] == "Payment channel is malformed.");
|
||||
|
||||
chan1StrBad = chan1Str;
|
||||
chan1StrBad.back() = 'x';
|
||||
rv = env.rpc("channel_verify", pkAsHex, chan1StrBad, "1000", sig);
|
||||
BEAST_EXPECT(rv[jss::error] == "channelMalformed");
|
||||
BEAST_EXPECT(rv[jss::error_code] == RpcChannelMalformed);
|
||||
BEAST_EXPECT(rv[jss::error_message] == "Payment channel is malformed.");
|
||||
rv = env.rpc("channel_authorize", "alice", chan1StrBad, "1000");
|
||||
BEAST_EXPECT(rv[jss::error] == "channelMalformed");
|
||||
BEAST_EXPECT(rv[jss::error_code] == RpcChannelMalformed);
|
||||
BEAST_EXPECT(rv[jss::error_message] == "Payment channel is malformed.");
|
||||
}
|
||||
{
|
||||
// channel_id is not a string.
|
||||
@@ -1505,8 +1391,6 @@ struct PayChan_test : public beast::unit_test::Suite
|
||||
auto const ra =
|
||||
env.rpc("json", "channel_authorize", args.toStyledString())[jss::result];
|
||||
BEAST_EXPECT(ra[jss::error] == "invalidParams");
|
||||
BEAST_EXPECT(ra[jss::error_code] == RpcInvalidParams);
|
||||
BEAST_EXPECT(ra[jss::error_message] == "Invalid parameters.");
|
||||
}
|
||||
{
|
||||
// channel_id is not a string.
|
||||
@@ -1519,8 +1403,6 @@ struct PayChan_test : public beast::unit_test::Suite
|
||||
auto const rv =
|
||||
env.rpc("json", "channel_verify", args.toStyledString())[jss::result];
|
||||
BEAST_EXPECT(rv[jss::error] == "invalidParams");
|
||||
BEAST_EXPECT(rv[jss::error_code] == RpcInvalidParams);
|
||||
BEAST_EXPECT(rv[jss::error_message] == "Invalid parameters.");
|
||||
}
|
||||
{
|
||||
// give an ill formed base 58 public key
|
||||
@@ -1579,8 +1461,6 @@ struct PayChan_test : public beast::unit_test::Suite
|
||||
// Try to authorize using an unknown key type, except an error:
|
||||
auto const rs1 = env.rpc("channel_authorize", "charlie", "nyx", chan, "1000");
|
||||
BEAST_EXPECT(rs1[jss::error] == "badKeyType");
|
||||
BEAST_EXPECT(rs1[jss::error_code] == RpcBadKeyType);
|
||||
BEAST_EXPECT(rs1[jss::error_message] == "Bad key type.");
|
||||
|
||||
// Try to authorize using secp256k1; the authorization _should_
|
||||
// succeed but the verification should fail:
|
||||
@@ -1606,16 +1486,10 @@ struct PayChan_test : public beast::unit_test::Suite
|
||||
// send malformed amounts rpc requests
|
||||
auto rs = env.rpc("channel_authorize", "alice", chan1Str, "1000x");
|
||||
BEAST_EXPECT(rs[jss::error] == "channelAmtMalformed");
|
||||
BEAST_EXPECT(rs[jss::error_code] == RpcChannelAmtMalformed);
|
||||
BEAST_EXPECT(rs[jss::error_message] == "Payment channel amount is malformed.");
|
||||
rs = env.rpc("channel_authorize", "alice", chan1Str, "x1000");
|
||||
BEAST_EXPECT(rs[jss::error] == "channelAmtMalformed");
|
||||
BEAST_EXPECT(rs[jss::error_code] == RpcChannelAmtMalformed);
|
||||
BEAST_EXPECT(rs[jss::error_message] == "Payment channel amount is malformed.");
|
||||
rs = env.rpc("channel_authorize", "alice", chan1Str, "x");
|
||||
BEAST_EXPECT(rs[jss::error] == "channelAmtMalformed");
|
||||
BEAST_EXPECT(rs[jss::error_code] == RpcChannelAmtMalformed);
|
||||
BEAST_EXPECT(rs[jss::error_message] == "Payment channel amount is malformed.");
|
||||
{
|
||||
// Missing channel_id
|
||||
json::Value args{json::ValueType::Object};
|
||||
@@ -1624,8 +1498,6 @@ struct PayChan_test : public beast::unit_test::Suite
|
||||
args[jss::passphrase] = "passphrase_can_be_anything";
|
||||
rs = env.rpc("json", "channel_authorize", args.toStyledString())[jss::result];
|
||||
BEAST_EXPECT(rs[jss::error] == "invalidParams");
|
||||
BEAST_EXPECT(rs[jss::error_code] == RpcInvalidParams);
|
||||
BEAST_EXPECT(rs[jss::error_message] == "Missing field 'channel_id'.");
|
||||
}
|
||||
{
|
||||
// Missing amount
|
||||
@@ -1635,8 +1507,6 @@ struct PayChan_test : public beast::unit_test::Suite
|
||||
args[jss::passphrase] = "passphrase_can_be_anything";
|
||||
rs = env.rpc("json", "channel_authorize", args.toStyledString())[jss::result];
|
||||
BEAST_EXPECT(rs[jss::error] == "invalidParams");
|
||||
BEAST_EXPECT(rs[jss::error_code] == RpcInvalidParams);
|
||||
BEAST_EXPECT(rs[jss::error_message] == "Missing field 'amount'.");
|
||||
}
|
||||
{
|
||||
// Missing key_type and no secret.
|
||||
@@ -1646,8 +1516,6 @@ struct PayChan_test : public beast::unit_test::Suite
|
||||
args[jss::passphrase] = "passphrase_can_be_anything";
|
||||
rs = env.rpc("json", "channel_authorize", args.toStyledString())[jss::result];
|
||||
BEAST_EXPECT(rs[jss::error] == "invalidParams");
|
||||
BEAST_EXPECT(rs[jss::error_code] == RpcInvalidParams);
|
||||
BEAST_EXPECT(rs[jss::error_message] == "Missing field 'secret'.");
|
||||
}
|
||||
{
|
||||
// Both passphrase and seed specified.
|
||||
@@ -1659,11 +1527,6 @@ struct PayChan_test : public beast::unit_test::Suite
|
||||
args[jss::seed] = "seed can be anything";
|
||||
rs = env.rpc("json", "channel_authorize", args.toStyledString())[jss::result];
|
||||
BEAST_EXPECT(rs[jss::error] == "invalidParams");
|
||||
BEAST_EXPECT(rs[jss::error_code] == RpcInvalidParams);
|
||||
BEAST_EXPECT(
|
||||
rs[jss::error_message] ==
|
||||
"Exactly one of the following must be specified: passphrase, secret, seed or "
|
||||
"seed_hex");
|
||||
}
|
||||
{
|
||||
// channel_id is not exact hex.
|
||||
@@ -1674,8 +1537,6 @@ struct PayChan_test : public beast::unit_test::Suite
|
||||
args[jss::passphrase] = "passphrase_can_be_anything";
|
||||
rs = env.rpc("json", "channel_authorize", args.toStyledString())[jss::result];
|
||||
BEAST_EXPECT(rs[jss::error] == "channelMalformed");
|
||||
BEAST_EXPECT(rs[jss::error_code] == RpcChannelMalformed);
|
||||
BEAST_EXPECT(rs[jss::error_message] == "Payment channel is malformed.");
|
||||
}
|
||||
{
|
||||
// amount is not a string
|
||||
@@ -1686,8 +1547,6 @@ struct PayChan_test : public beast::unit_test::Suite
|
||||
args[jss::passphrase] = "passphrase_can_be_anything";
|
||||
rs = env.rpc("json", "channel_authorize", args.toStyledString())[jss::result];
|
||||
BEAST_EXPECT(rs[jss::error] == "channelAmtMalformed");
|
||||
BEAST_EXPECT(rs[jss::error_code] == RpcChannelAmtMalformed);
|
||||
BEAST_EXPECT(rs[jss::error_message] == "Payment channel amount is malformed.");
|
||||
}
|
||||
{
|
||||
// Amount is not a decimal string.
|
||||
@@ -1698,8 +1557,6 @@ struct PayChan_test : public beast::unit_test::Suite
|
||||
args[jss::passphrase] = "passphrase_can_be_anything";
|
||||
rs = env.rpc("json", "channel_authorize", args.toStyledString())[jss::result];
|
||||
BEAST_EXPECT(rs[jss::error] == "channelAmtMalformed");
|
||||
BEAST_EXPECT(rs[jss::error_code] == RpcChannelAmtMalformed);
|
||||
BEAST_EXPECT(rs[jss::error_message] == "Payment channel amount is malformed.");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1950,8 +1807,6 @@ struct PayChan_test : public beast::unit_test::Suite
|
||||
)*";
|
||||
auto const r = env.rpc("json", "channel_verify", txn);
|
||||
BEAST_EXPECT(r["result"]["error"] == "publicMalformed");
|
||||
BEAST_EXPECT(r["result"]["error_code"] == RpcPublicMalformed);
|
||||
BEAST_EXPECT(r["result"]["error_message"] == "Public key is malformed.");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -43,12 +43,6 @@ 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) {
|
||||
@@ -113,9 +107,7 @@ 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)
|
||||
{
|
||||
BEAST_EXPECT(next->setImmutable());
|
||||
next->setImmutable();
|
||||
forceHash = false;
|
||||
}
|
||||
|
||||
|
||||
@@ -13,7 +13,6 @@
|
||||
#include <xrpl/beast/unit_test/suite.h>
|
||||
#include <xrpl/json/json_value.h>
|
||||
#include <xrpl/json/to_string.h>
|
||||
#include <xrpl/protocol/ErrorCodes.h>
|
||||
#include <xrpl/protocol/Feature.h>
|
||||
#include <xrpl/protocol/Quality.h>
|
||||
#include <xrpl/protocol/SField.h>
|
||||
@@ -43,9 +42,8 @@ class ReducedOffer_test : public beast::unit_test::Suite
|
||||
{
|
||||
json::Value ledgerOffer = ledgerEntryOffer(env, acct, offerSeq);
|
||||
return !(
|
||||
ledgerOffer[jss::error].asString() == "entryNotFound" &&
|
||||
ledgerOffer[jss::error_code] == RpcEntryNotFound &&
|
||||
ledgerOffer[jss::error_message] == "Entry not found.");
|
||||
ledgerOffer.isMember(jss::error) &&
|
||||
ledgerOffer[jss::error].asString() == "entryNotFound");
|
||||
}
|
||||
|
||||
// Common code to clean up unneeded offers.
|
||||
|
||||
@@ -302,14 +302,10 @@ class SHAMapStore_test : public beast::unit_test::Suite
|
||||
}
|
||||
|
||||
static bool
|
||||
bad(json::Value const& json,
|
||||
std::string const& token,
|
||||
ErrorCodeI error,
|
||||
std::string const& message)
|
||||
bad(json::Value const& json, ErrorCodeI error = RpcLgrNotFound)
|
||||
{
|
||||
return json.isMember(jss::result) && json[jss::result][jss::error] == token &&
|
||||
json[jss::result][jss::error_code] == error &&
|
||||
json[jss::result][jss::error_message] == message;
|
||||
return json.isMember(jss::result) && rpc::containsError(json[jss::result]) &&
|
||||
json[jss::result][jss::error_code] == error;
|
||||
}
|
||||
|
||||
std::string
|
||||
@@ -887,7 +883,7 @@ public:
|
||||
std::map<std::uint32_t, json::Value const> ledgers;
|
||||
|
||||
auto ledgerTmp = env.rpc("ledger", "0");
|
||||
BEAST_EXPECT(bad(ledgerTmp, "lgrNotFound", RpcLgrNotFound, "ledgerNotFound"));
|
||||
BEAST_EXPECT(bad(ledgerTmp));
|
||||
|
||||
ledgers.emplace(1, env.rpc("ledger", "1"));
|
||||
BEAST_EXPECT(goodLedger(env, ledgers[1], "1"));
|
||||
@@ -899,10 +895,10 @@ public:
|
||||
BEAST_EXPECT(goodLedger(env, ledgerTmp, "3"));
|
||||
|
||||
ledgerTmp = env.rpc("ledger", "4");
|
||||
BEAST_EXPECT(bad(ledgerTmp, "lgrNotFound", RpcLgrNotFound, "ledgerNotFound"));
|
||||
BEAST_EXPECT(bad(ledgerTmp));
|
||||
|
||||
ledgerTmp = env.rpc("ledger", "100");
|
||||
BEAST_EXPECT(bad(ledgerTmp, "lgrNotFound", RpcLgrNotFound, "ledgerNotFound"));
|
||||
BEAST_EXPECT(bad(ledgerTmp));
|
||||
|
||||
auto const firstSeq = waitForReady(env);
|
||||
auto lastRotated = firstSeq - 1;
|
||||
@@ -991,7 +987,7 @@ public:
|
||||
// Because advisory_delete is unset,
|
||||
// "can_delete" is disabled.
|
||||
auto const canDelete = env.rpc("can_delete");
|
||||
BEAST_EXPECT(bad(canDelete, "notEnabled", RpcNotEnabled, "Not enabled in configuration."));
|
||||
BEAST_EXPECT(bad(canDelete, RpcNotEnabled));
|
||||
|
||||
// Close ledgers without triggering a rotate
|
||||
for (; ledgerSeq < lastRotated + kDeleteInterval; ++ledgerSeq)
|
||||
|
||||
@@ -20,7 +20,6 @@
|
||||
#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>
|
||||
@@ -59,7 +58,6 @@
|
||||
#include <xrpl/tx/apply.h>
|
||||
#include <xrpl/tx/applySteps.h>
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
#include <limits>
|
||||
@@ -416,59 +414,6 @@ 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
|
||||
@@ -527,24 +472,6 @@ 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
|
||||
@@ -1932,7 +1859,8 @@ public:
|
||||
{
|
||||
// existing owner objects that are outside the v1 SponsorshipTransfer
|
||||
// object allow-list
|
||||
Env env{*this, features};
|
||||
// Temporary: FixedPrecision lending is incomplete until the LoanManage PR.
|
||||
Env env{*this, features - featureLendingProtocolV1_2};
|
||||
Account const alice("alice");
|
||||
Account const sponsor("sponsor");
|
||||
env.fund(XRP(10000), alice, sponsor);
|
||||
@@ -3551,101 +3479,6 @@ public:
|
||||
BEAST_EXPECT(sponsoringOwnerCount(env, sponsor2) == 0);
|
||||
}
|
||||
|
||||
{
|
||||
// Bytecode Escrow: its reserve scales with the Bytecode size, so
|
||||
// every leg of a sponsorship has to move the whole weight.
|
||||
auto env = Env{*this, testableAmendments()};
|
||||
|
||||
env.fund(XRP(1000000), alice, bob, sponsor, sponsor2);
|
||||
env.close();
|
||||
|
||||
auto const seq = env.seq(alice);
|
||||
env(escrow::create(alice, bob, XRP(100)),
|
||||
escrow::kCondition(escrow::kCb1),
|
||||
escrow::kCancelTime(env.now() + 100s));
|
||||
env.close();
|
||||
|
||||
auto const escrowKeylet = keylet::escrow(alice, SeqProxy::rawSequence(seq));
|
||||
std::size_t const bytecodeSize = 1200;
|
||||
std::uint32_t const bytecodeWeight = 3; // ceil(1200 / 500)
|
||||
|
||||
if (!cosigning)
|
||||
{
|
||||
env(sponsor::set_reserve(sponsor, 0, bytecodeWeight), sponsor::SponseeAcc(alice));
|
||||
env(sponsor::set_reserve(sponsor2, 0, bytecodeWeight), sponsor::SponseeAcc(alice));
|
||||
env.close();
|
||||
}
|
||||
|
||||
// EscrowCreate's preclaim runs the WASM preflight, so a real
|
||||
// 1200-byte contract cannot be assembled from here; the Bytecode
|
||||
// field is injected directly and the owner's OwnerCount restored
|
||||
// to the weight such a create would have charged.
|
||||
env.app().getOpenLedger().modify([&](OpenView& view, beast::Journal) -> bool {
|
||||
auto const existing = view.read(escrowKeylet);
|
||||
if (!existing)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
auto const escrowSle = std::make_shared<SLE>(*existing);
|
||||
auto const account = std::make_shared<SLE>(*view.read(keylet::account(alice.id())));
|
||||
escrowSle->setFieldVL(sfBytecode, Blob(bytecodeSize, 0x00));
|
||||
account->setFieldU32(sfOwnerCount, bytecodeWeight);
|
||||
view.rawReplace(escrowSle);
|
||||
view.rawReplace(account);
|
||||
return true;
|
||||
});
|
||||
BEAST_EXPECT(ownerCount(env, alice) == bytecodeWeight);
|
||||
|
||||
std::optional<Sig> const sponsorSig =
|
||||
cosigning ? std::optional<Sig>(Sig(sfSponsorSignature, sponsor)) : std::nullopt;
|
||||
std::optional<Sig> const sponsor2Sig =
|
||||
cosigning ? std::optional<Sig>(Sig(sfSponsorSignature, sponsor2)) : std::nullopt;
|
||||
|
||||
// create sponsor: charges the whole bytecode weight, not 1
|
||||
if (sponsorSig)
|
||||
{
|
||||
env(sponsor::transfer(alice, tfSponsorshipCreate, escrowKeylet.key),
|
||||
sponsor::As(sponsor, spfSponsorReserve),
|
||||
*sponsorSig);
|
||||
}
|
||||
else
|
||||
{
|
||||
env(sponsor::transfer(alice, tfSponsorshipCreate, escrowKeylet.key),
|
||||
sponsor::As(sponsor, spfSponsorReserve));
|
||||
}
|
||||
|
||||
BEAST_EXPECT(sponsoredOwnerCount(env, alice) == bytecodeWeight);
|
||||
BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == bytecodeWeight);
|
||||
BEAST_EXPECT(env.le(escrowKeylet)->getAccountID(sfSponsor) == sponsor.id());
|
||||
|
||||
// transfer sponsor: moves the whole weight between the two sponsors
|
||||
if (sponsor2Sig)
|
||||
{
|
||||
env(sponsor::transfer(alice, tfSponsorshipReassign, escrowKeylet.key),
|
||||
sponsor::As(sponsor2, spfSponsorReserve),
|
||||
*sponsor2Sig);
|
||||
}
|
||||
else
|
||||
{
|
||||
env(sponsor::transfer(alice, tfSponsorshipReassign, escrowKeylet.key),
|
||||
sponsor::As(sponsor2, spfSponsorReserve));
|
||||
}
|
||||
|
||||
BEAST_EXPECT(sponsoredOwnerCount(env, alice) == bytecodeWeight);
|
||||
BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 0);
|
||||
BEAST_EXPECT(sponsoringOwnerCount(env, sponsor2) == bytecodeWeight);
|
||||
BEAST_EXPECT(env.le(escrowKeylet)->getAccountID(sfSponsor) == sponsor2.id());
|
||||
|
||||
// end sponsor: hands the whole weight back to the owner
|
||||
env(sponsor::transfer(alice, tfSponsorshipEnd, escrowKeylet.key));
|
||||
|
||||
BEAST_EXPECT(ownerCount(env, alice) == bytecodeWeight);
|
||||
BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 0);
|
||||
BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 0);
|
||||
BEAST_EXPECT(sponsoringOwnerCount(env, sponsor2) == 0);
|
||||
BEAST_EXPECT(!env.le(escrowKeylet)->isFieldPresent(sfSponsor));
|
||||
}
|
||||
|
||||
Account const gw("gw");
|
||||
auto const usd = gw["usd"];
|
||||
{
|
||||
@@ -4501,89 +4334,6 @@ public:
|
||||
BEAST_EXPECT(!sle->isFieldPresent(sfRemainingOwnerCount));
|
||||
}
|
||||
|
||||
// An escrow carrying Bytecode costs one owner count per 500 bytes, so
|
||||
// sponsoring one must move that full weight rather than a single unit.
|
||||
void
|
||||
testBytecodeEscrowReserve()
|
||||
{
|
||||
testcase("Bytecode escrow sponsorship reserve");
|
||||
using namespace test::jtx;
|
||||
using namespace std::chrono_literals;
|
||||
|
||||
auto const alice = Account{"alice"};
|
||||
auto const bob = Account{"bob"};
|
||||
auto const sponsor = Account{"sponsor"};
|
||||
|
||||
auto env = Env{*this, testableAmendments()};
|
||||
env.fund(XRP(1000000), alice, bob, sponsor);
|
||||
env.close();
|
||||
|
||||
auto const seq = env.seq(alice);
|
||||
env(escrow::create(alice, bob, XRP(100)),
|
||||
escrow::kCondition(escrow::kCb1),
|
||||
escrow::kCancelTime(env.now() + 100s));
|
||||
env.close();
|
||||
|
||||
auto const escrowKeylet = keylet::escrow(alice, SeqProxy::rawSequence(seq));
|
||||
auto const sponsorKeylet = keylet::sponsorship(sponsor.id(), alice.id());
|
||||
std::size_t const bytecodeSize = 1200;
|
||||
std::uint32_t const bytecodeWeight = 3; // ceil(1200 / 500)
|
||||
BEAST_EXPECT(ownerCount(env, alice) == 1);
|
||||
|
||||
// Pre-fund exactly the bytecode weight
|
||||
env(sponsor::set_reserve(sponsor, 0, bytecodeWeight), sponsor::SponseeAcc(alice));
|
||||
env.close();
|
||||
if (auto const sle = env.le(sponsorKeylet); BEAST_EXPECT(sle))
|
||||
{
|
||||
BEAST_EXPECT(sle->getFieldU32(sfRemainingOwnerCount) == bytecodeWeight);
|
||||
}
|
||||
|
||||
// Synthesize a bytecode escrow. EscrowCreate's preclaim runs the WASM
|
||||
// preflight, so a real 1200-byte contract cannot be assembled from
|
||||
// here; the Bytecode field is injected directly and the owner's
|
||||
// OwnerCount restored to the weight such a create would have charged.
|
||||
env.app().getOpenLedger().modify([&](OpenView& view, beast::Journal) -> bool {
|
||||
auto const existing = view.read(escrowKeylet);
|
||||
if (!existing)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
auto const escrowSle = std::make_shared<SLE>(*existing);
|
||||
auto const account = std::make_shared<SLE>(*view.read(keylet::account(alice.id())));
|
||||
escrowSle->setFieldVL(sfBytecode, Blob(bytecodeSize, 0x00));
|
||||
account->setFieldU32(sfOwnerCount, bytecodeWeight);
|
||||
view.rawReplace(escrowSle);
|
||||
view.rawReplace(account);
|
||||
return true;
|
||||
});
|
||||
BEAST_EXPECT(ownerCount(env, alice) == bytecodeWeight);
|
||||
|
||||
// Create must charge the full bytecode weight (3), not 1: counting the
|
||||
// escrow as a single unit would leave 2 pre-funded units unspent.
|
||||
env(sponsor::transfer(alice, tfSponsorshipCreate, escrowKeylet.key),
|
||||
sponsor::As(sponsor, spfSponsorReserve));
|
||||
|
||||
BEAST_EXPECT(sponsoredOwnerCount(env, alice) == bytecodeWeight);
|
||||
BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == bytecodeWeight);
|
||||
// All pre-funded units consumed (drained to absent).
|
||||
if (auto const sle = env.le(sponsorKeylet); BEAST_EXPECT(sle))
|
||||
{
|
||||
BEAST_EXPECT(!sle->isFieldPresent(sfRemainingOwnerCount));
|
||||
}
|
||||
|
||||
// End hands the same weight back; create bumped by the same amount, so
|
||||
// the counters return to 0 (counting 1 here would underflow).
|
||||
env(sponsor::transfer(alice, tfSponsorshipEnd, escrowKeylet.key));
|
||||
|
||||
BEAST_EXPECT(ownerCount(env, alice) == bytecodeWeight);
|
||||
BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 0);
|
||||
BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 0);
|
||||
if (auto const sle = env.le(escrowKeylet); BEAST_EXPECT(sle))
|
||||
{
|
||||
BEAST_EXPECT(!sle->isFieldPresent(sfSponsor));
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testSponsoredTrustLineNoFreeReserve()
|
||||
{
|
||||
@@ -5969,7 +5719,6 @@ protected:
|
||||
testTransferSponsor(jtx::testableAmendments());
|
||||
testTransferSponsor(jtx::testableAmendments() - fixCleanup3_4_0);
|
||||
testLegacySignerListReserve();
|
||||
testBytecodeEscrowReserve();
|
||||
testSponsorFee();
|
||||
testSponsorAccount();
|
||||
|
||||
|
||||
@@ -1,525 +0,0 @@
|
||||
#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
|
||||
@@ -20,7 +20,6 @@
|
||||
#include <xrpl/beast/unit_test/suite.h>
|
||||
#include <xrpl/json/json_value.h>
|
||||
#include <xrpl/json/to_string.h>
|
||||
#include <xrpl/protocol/ErrorCodes.h>
|
||||
#include <xrpl/protocol/Feature.h>
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
#include <xrpl/protocol/Protocol.h>
|
||||
@@ -216,9 +215,8 @@ struct TransactionProposalCancel_test : public beast::unit_test::Suite
|
||||
}
|
||||
|
||||
// The target account may refuse any proposal made for it, at any point in
|
||||
// its lifecycle, without owning the object: any of its signers, an
|
||||
// authorized delegate, or that delegate's signers can create one and tie
|
||||
// up one of its tickets (XLS-0103 §7.2).
|
||||
// its lifecycle, without owning the object: anyone can create a proposal
|
||||
// against any account and tie up one of its tickets (On-Chain Cosigner spec §7.2).
|
||||
void
|
||||
testTargetCancel(FeatureBitset features)
|
||||
{
|
||||
@@ -685,25 +683,6 @@ struct TransactionProposalCancel_test : public beast::unit_test::Suite
|
||||
BEAST_EXPECT(env.le(keylet::txProposal(proposalID)));
|
||||
BEAST_EXPECT(ownerCount(env, alice) == proposal::kProposalOwnerCount);
|
||||
}
|
||||
|
||||
// Paying with a ticket changes none of that. The failed cancel still
|
||||
// consumes carol's ticket, and nothing else moves: the proposal and
|
||||
// its reserve stay as they were.
|
||||
{
|
||||
uint256 const proposalID = makeProposal(env, alice, target, bob);
|
||||
std::uint32_t const cancelTicket = proposal::createTicket(env, carol);
|
||||
auto const aliceOwnerCount = ownerCount(env, alice);
|
||||
|
||||
env(proposal::cancel(carol, proposalID),
|
||||
ticket::Use(cancelTicket),
|
||||
Ter(tecNO_PERMISSION));
|
||||
env.close();
|
||||
|
||||
BEAST_EXPECT(env.le(keylet::txProposal(proposalID)));
|
||||
BEAST_EXPECT(!env.le(keylet::ticket(carol.id(), SeqProxy::rawTicket(cancelTicket))));
|
||||
BEAST_EXPECT(ownerCount(env, alice) == aliceOwnerCount);
|
||||
BEAST_EXPECT(ownerCount(env, carol) == 0);
|
||||
}
|
||||
}
|
||||
|
||||
// A Cancel works as an inner Batch transaction, where each inner runs its
|
||||
@@ -1033,25 +1012,15 @@ struct TransactionProposalCancel_test : public beast::unit_test::Suite
|
||||
{
|
||||
json::Value noTicket;
|
||||
noTicket[jss::account] = target.human();
|
||||
auto const noTicketResult = ledgerEntry(noTicket);
|
||||
BEAST_EXPECT(noTicketResult[jss::error] == "malformedRequest");
|
||||
BEAST_EXPECT(noTicketResult[jss::error_code] == RpcInvalidParams);
|
||||
BEAST_EXPECT(noTicketResult[jss::error_message] == "Missing field 'ticket_seq'.");
|
||||
BEAST_EXPECT(ledgerEntry(noTicket)[jss::error] == "malformedRequest");
|
||||
|
||||
json::Value noAccount;
|
||||
noAccount[jss::ticket_seq] = ticketSeq;
|
||||
auto const noAccountResult = ledgerEntry(noAccount);
|
||||
BEAST_EXPECT(noAccountResult[jss::error] == "malformedRequest");
|
||||
BEAST_EXPECT(noAccountResult[jss::error_code] == RpcInvalidParams);
|
||||
BEAST_EXPECT(noAccountResult[jss::error_message] == "Missing field 'account'.");
|
||||
BEAST_EXPECT(ledgerEntry(noAccount)[jss::error] == "malformedRequest");
|
||||
|
||||
json::Value badAccount = byPair;
|
||||
badAccount[jss::account] = "not an account";
|
||||
auto const badAccountResult = ledgerEntry(badAccount);
|
||||
BEAST_EXPECT(badAccountResult[jss::error] == "malformedAddress");
|
||||
BEAST_EXPECT(badAccountResult[jss::error_code] == RpcInvalidParams);
|
||||
BEAST_EXPECT(
|
||||
badAccountResult[jss::error_message] == "Invalid field 'account', not AccountID.");
|
||||
BEAST_EXPECT(ledgerEntry(badAccount)[jss::error] == "malformedAddress");
|
||||
}
|
||||
|
||||
// Listed for the proposer, who owns it, and not for the target.
|
||||
@@ -1068,8 +1037,6 @@ struct TransactionProposalCancel_test : public beast::unit_test::Suite
|
||||
{
|
||||
auto const jrr = ledgerEntry(byPair);
|
||||
BEAST_EXPECT(jrr[jss::error] == "entryNotFound");
|
||||
BEAST_EXPECT(jrr[jss::error_code] == RpcEntryNotFound);
|
||||
BEAST_EXPECT(jrr[jss::error_message] == "Entry not found.");
|
||||
BEAST_EXPECT(!jrr.isMember(jss::node));
|
||||
BEAST_EXPECT(jrr[jss::index] == to_string(proposalID));
|
||||
}
|
||||
|
||||
Loaded 100 of 207 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user