From d9e7b503952480a60dc09a0b662d3dcaa230de0d Mon Sep 17 00:00:00 2001 From: "J. Scott Branson" <18340247+jscottbranson@users.noreply.github.com> Date: Tue, 17 Feb 2026 20:50:32 -0500 Subject: [PATCH 001/301] Update Build Instructions and Example Configuration File (#655) --- .gitignore | 1 + BUILD.md | 21 ++--- cfg/xahaud-example.cfg | 32 +++---- docs/build/environment.md | 4 +- docs/build/install.md | 177 ++++++-------------------------------- 5 files changed, 54 insertions(+), 181 deletions(-) diff --git a/.gitignore b/.gitignore index 5f55306f4..29af1d311 100644 --- a/.gitignore +++ b/.gitignore @@ -76,6 +76,7 @@ docs/html_doc # Xcode .DS_Store */build/* +!/docs/build/ *.pbxuser !default.pbxuser *.mode1v3 diff --git a/BUILD.md b/BUILD.md index eff169a5c..3f0aafcd1 100644 --- a/BUILD.md +++ b/BUILD.md @@ -10,7 +10,7 @@ ## Branches For a stable release, choose the `master` branch or one of the [tagged -releases](https://github.com/ripple/rippled/releases). +releases](https://github.com/Xahau/xahaud/releases). ``` git checkout master @@ -36,7 +36,7 @@ git checkout develop - [Conan 2.x](https://conan.io/downloads) - [CMake 3.16](https://cmake.org/download/) -`rippled` is written in the C++20 dialect and includes the `` header. +`xahaud` is written in the C++20 dialect and includes the `` header. The [minimum compiler versions][2] required are: | Compiler | Version | @@ -46,11 +46,11 @@ The [minimum compiler versions][2] required are: | Apple Clang | 13.1.6 | | MSVC | 19.23 | -We don't recommend Windows for `rippled` production at this time. As of -January 2023, Ubuntu has the highest level of quality assurance, testing, +We don't recommend Windows for `xahaud` production at this time. As of +November 2025, Ubuntu has the highest level of quality assurance, testing, and support. -Windows developers should use Visual Studio 2019. `rippled` isn't +Windows developers should use Visual Studio 2019. `xahaud` isn't compatible with [Boost](https://www.boost.org/) 1.78 or 1.79, and Conan can't build earlier Boost versions. @@ -100,7 +100,7 @@ can't build earlier Boost versions. An easy way to do that is to run the shortcut "x64 Native Tools Command Prompt" for the version of Visual Studio that you have installed. - Windows developers must also build `rippled` and its dependencies for the x64 + Windows developers must also build `xahaud` and its dependencies for the x64 architecture. ``` @@ -218,13 +218,14 @@ can't build earlier Boost versions. cmake -DCMAKE_TOOLCHAIN_FILE:FILEPATH=build/generators/conan_toolchain.cmake .. ``` - **Note:** You can pass build options for `rippled` in this step. + **Note:** You can pass build options for `xahaud` in this step. -4. Build `rippled`. +4. Build `xahaud`. For a single-configuration generator, it will build whatever configuration you passed for `CMAKE_BUILD_TYPE`. For a multi-configuration generator, you must pass the option `--config` to select the build configuration. + The output file is currently named 'rippled'. Single-config generators: @@ -239,7 +240,7 @@ can't build earlier Boost versions. cmake --build . --config Debug ``` -5. Test rippled. +5. Test xahaud. Single-config generators: @@ -254,7 +255,7 @@ can't build earlier Boost versions. ./Debug/rippled --unittest ``` - The location of `rippled` in your build directory depends on your CMake + The location of `xahaud` in your build directory depends on your CMake generator. Pass `--help` to see the rest of the command line options. diff --git a/cfg/xahaud-example.cfg b/cfg/xahaud-example.cfg index 42c520d4b..098bb19ce 100644 --- a/cfg/xahaud-example.cfg +++ b/cfg/xahaud-example.cfg @@ -1710,27 +1710,15 @@ secure_gateway = 127.0.0.1 # when the node has approximately two times the "online_delete" value of # ledgers. No external administrative command is required to initiate # deletion. +[ledger_history] +256 + [node_db] type=NuDB path=/opt/xahaud/db/nudb -online_delete=512 +online_delete=256 advisory_delete=0 -# This is the persistent datastore for shards. It is important for the health -# of the Xahau Network that xahaud operators shard as much as practical. -# NuDB requires SSD storage. Helpful information can be found at -# https://xrpl.org/history-sharding.html -#[shard_db] -#path=/opt/xahaud/db/shards/nudb -#max_historical_shards=50 -# -# This optional section can be configured with a list -# of paths to use for storing historical shards. Each -# path must correspond to a unique filesystem. -#[historical_shard_paths] -#/path/1 -#/path/2 - [database_path] /opt/xahaud/db @@ -1755,6 +1743,11 @@ time.apple.com time.nist.gov pool.ntp.org +# Use the following [ips] section for the main network: +[ips] +bacab.alloy.ee 21337 +hubs.xahau.as16089.net 21337 + # To use the Xahau Test Network # (see https://xahau.network/docs/infrastructure/installing-xahaud), # use the following [ips] section: @@ -1782,6 +1775,13 @@ validators-xahau.txt [ssl_verify] 1 +# Define which network xahaud is connecting to +# 21337 for the Main Xahau Network +# 21338 for the Test Xahau Network +[network_id] +21337 +# 21338 + # To run in Reporting Mode, uncomment this section and fill in the appropriate # connection info for one or more ETL sources. diff --git a/docs/build/environment.md b/docs/build/environment.md index a204cd2c1..fc6a81447 100644 --- a/docs/build/environment.md +++ b/docs/build/environment.md @@ -11,11 +11,11 @@ platforms: Linux, macOS, or Windows. Package ecosystems vary across Linux distributions, so there is no one set of instructions that will work for every Linux user. These instructions are written for Ubuntu 22.04. -They are largely copied from the [script][1] used to configure our Docker +They are largely copied from the [script][1] used to configure a Docker container for continuous integration. That script handles many more responsibilities. These instructions are just the bare minimum to build one configuration of -rippled. +xahaud. You can check that codebase for other Linux distributions and versions. If you cannot find yours there, then we hope that these instructions can at least guide you in the right diff --git a/docs/build/install.md b/docs/build/install.md index af0d6f335..b9f0e4840 100644 --- a/docs/build/install.md +++ b/docs/build/install.md @@ -1,159 +1,30 @@ -This document contains instructions for installing rippled. -The APT package manager is common on Debian-based Linux distributions like -Ubuntu, -while the YUM package manager is common on Red Hat-based Linux distributions -like CentOS. -Installing from source is an option for all platforms, -and the only supported option for installing custom builds. +Comprehensive instructions for installing and running xahaud are available on the [https://Xahau.Network](https://xahau.network/docs/infrastructure/installing-xahaud) documentation website. +## Create the Runtime Environment +xahaud can be [built from source](../../BUILD.md) or installed using the binary files available from [https://build.xahau.tech](https://build.xahau.tech/). After obtaining a working xahaud binary, users will need to provide a suitable runtime environment. The following setup can be used for Linux or Docker environments. -## From source - -From a source build, you can install rippled and libxrpl using CMake's -`--install` mode: +1. Create or download two configuration files: the main xahaud.cfg configuration file and a second validators-xahau.txt file defining which validators or UNL list publishers are trusted. The default location for these files in this xahaud repository is `cfg/`. +2. Provide a directory structure that is congruent with the contents of xahaud.cfg. This will include a location for logfiles, such as `/var/log/xahaud/`, as well as database files, `/opt/xahaud/db/`. Configuration files are, by default, sourced from `/etc/xahaud/`. It is possible to provide a symbolic link, if users wish to store configuration files elsewhere. +3. If desired, created a xahaud user and group, and change ownership of the binary and directories. Servers used for validating nodes should use the most restrictive permissions possible for `xahaud.cfg`, as the validation token is stored therein. +4. If desired, create a systemd service file: `/etc/systemd/system/xahaud.service`, enabling xahaud to run as a daemon. Alternately, run: `/path/to/binary/xahaud --conf=/path/to/xahaud.cfg`. +## Example systemd Service File ``` -cmake --install . --prefix /opt/local +[Unit] +Description=Xahaud Daemon +After=network-online.target +Wants=network-online.target + +[Service] +Type=simple +ExecStart=/path/to/xahaud --silent --conf /path/to/xahaud.cfg +Restart=on-failure +User=xahaud +Group=xahaud +LimitNOFILE=65536 + +[Install] +WantedBy=multi-user.target ``` -The default [prefix][1] is typically `/usr/local` on Linux and macOS and -`C:/Program Files/rippled` on Windows. - -[1]: https://cmake.org/cmake/help/latest/variable/CMAKE_INSTALL_PREFIX.html - - -## With the APT package manager - -1. Update repositories: - - sudo apt update -y - -2. Install utilities: - - sudo apt install -y apt-transport-https ca-certificates wget gnupg - -3. Add Ripple's package-signing GPG key to your list of trusted keys: - - sudo mkdir /usr/local/share/keyrings/ - wget -q -O - "https://repos.ripple.com/repos/api/gpg/key/public" | gpg --dearmor > ripple-key.gpg - sudo mv ripple-key.gpg /usr/local/share/keyrings - - -4. Check the fingerprint of the newly-added key: - - gpg /usr/local/share/keyrings/ripple-key.gpg - - The output should include an entry for Ripple such as the following: - - gpg: WARNING: no command supplied. Trying to guess what you mean ... - pub rsa3072 2019-02-14 [SC] [expires: 2026-02-17] - C0010EC205B35A3310DC90DE395F97FFCCAFD9A2 - uid TechOps Team at Ripple - sub rsa3072 2019-02-14 [E] [expires: 2026-02-17] - - - In particular, make sure that the fingerprint matches. (In the above example, the fingerprint is on the third line, starting with `C001`.) - -4. Add the appropriate Ripple repository for your operating system version: - - echo "deb [signed-by=/usr/local/share/keyrings/ripple-key.gpg] https://repos.ripple.com/repos/rippled-deb focal stable" | \ - sudo tee -a /etc/apt/sources.list.d/ripple.list - - The above example is appropriate for **Ubuntu 20.04 Focal Fossa**. For other operating systems, replace the word `focal` with one of the following: - - - `jammy` for **Ubuntu 22.04 Jammy Jellyfish** - - `bionic` for **Ubuntu 18.04 Bionic Beaver** - - `bullseye` for **Debian 11 Bullseye** - - `buster` for **Debian 10 Buster** - - If you want access to development or pre-release versions of `rippled`, use one of the following instead of `stable`: - - - `unstable` - Pre-release builds ([`release` branch](https://github.com/ripple/rippled/tree/release)) - - `nightly` - Experimental/development builds ([`develop` branch](https://github.com/ripple/rippled/tree/develop)) - - **Warning:** Unstable and nightly builds may be broken at any time. Do not use these builds for production servers. - -5. Fetch the Ripple repository. - - sudo apt -y update - -6. Install the `rippled` software package: - - sudo apt -y install rippled - -7. Check the status of the `rippled` service: - - systemctl status rippled.service - - The `rippled` service should start automatically. If not, you can start it manually: - - sudo systemctl start rippled.service - -8. Optional: allow `rippled` to bind to privileged ports. - - This allows you to serve incoming API requests on port 80 or 443. (If you want to do so, you must also update the config file's port settings.) - - sudo setcap 'cap_net_bind_service=+ep' /opt/ripple/bin/rippled - - -## With the YUM package manager - -1. Install the Ripple RPM repository: - - Choose the appropriate RPM repository for the stability of releases you want: - - - `stable` for the latest production release (`master` branch) - - `unstable` for pre-release builds (`release` branch) - - `nightly` for experimental/development builds (`develop` branch) - - *Stable* - - cat << REPOFILE | sudo tee /etc/yum.repos.d/ripple.repo - [ripple-stable] - name=XRP Ledger Packages - enabled=1 - gpgcheck=0 - repo_gpgcheck=1 - baseurl=https://repos.ripple.com/repos/rippled-rpm/stable/ - gpgkey=https://repos.ripple.com/repos/rippled-rpm/stable/repodata/repomd.xml.key - REPOFILE - - *Unstable* - - cat << REPOFILE | sudo tee /etc/yum.repos.d/ripple.repo - [ripple-unstable] - name=XRP Ledger Packages - enabled=1 - gpgcheck=0 - repo_gpgcheck=1 - baseurl=https://repos.ripple.com/repos/rippled-rpm/unstable/ - gpgkey=https://repos.ripple.com/repos/rippled-rpm/unstable/repodata/repomd.xml.key - REPOFILE - - *Nightly* - - cat << REPOFILE | sudo tee /etc/yum.repos.d/ripple.repo - [ripple-nightly] - name=XRP Ledger Packages - enabled=1 - gpgcheck=0 - repo_gpgcheck=1 - baseurl=https://repos.ripple.com/repos/rippled-rpm/nightly/ - gpgkey=https://repos.ripple.com/repos/rippled-rpm/nightly/repodata/repomd.xml.key - REPOFILE - -2. Fetch the latest repo updates: - - sudo yum -y update - -3. Install the new `rippled` package: - - sudo yum install -y rippled - -4. Configure the `rippled` service to start on boot: - - sudo systemctl enable rippled.service - -5. Start the `rippled` service: - - sudo systemctl start rippled.service +After the systemd service file is installed, it must be loaded with: `systemctl daemon-reload`. xahaud can then be enabled: `systemctl enable --now xahaud`. From c3e8039c5ab5c85aa74f98fa63c5ed6431b52c1a Mon Sep 17 00:00:00 2001 From: tequ Date: Wed, 18 Feb 2026 11:58:49 +0900 Subject: [PATCH 002/301] fixXahauGenesisOwnerCount (#666) --- src/ripple/app/tx/impl/Change.cpp | 18 +++++++++---- src/ripple/app/tx/impl/SetHook.cpp | 43 ++++++++++++++++++------------ src/ripple/app/tx/impl/SetHook.h | 3 +++ src/test/app/XahauGenesis_test.cpp | 27 ++++++++++++++++--- 4 files changed, 65 insertions(+), 26 deletions(-) diff --git a/src/ripple/app/tx/impl/Change.cpp b/src/ripple/app/tx/impl/Change.cpp index 1a78e0815..c2c8fe1e9 100644 --- a/src/ripple/app/tx/impl/Change.cpp +++ b/src/ripple/app/tx/impl/Change.cpp @@ -25,6 +25,7 @@ #include #include #include +#include #include #include #include @@ -584,10 +585,6 @@ Change::activateXahauGenesis() SetSignerList::removeFromLedger(ctx_.app, sb, accid, j_); // Step 4: install genesis hooks - sle->setFieldU32( - sfOwnerCount, sle->getFieldU32(sfOwnerCount) + genesis_hooks.size()); - sb.update(sle); - if (sb.exists(keylet::hook(accid))) { JLOG(j_.warn()) << "featureXahauGenesis genesis account already has " @@ -598,6 +595,7 @@ Change::activateXahauGenesis() { ripple::STArray hooks{sfHooks, static_cast(genesis_hooks.size())}; int hookCount = 0; + uint32_t hookReserve = 0; for (auto const& [hookOn, wasmBytes, params] : genesis_hooks) { @@ -703,8 +701,14 @@ Change::activateXahauGenesis() } hooks.push_back(hookObj); + + hookReserve += SetHook::computeHookReserve(hookObj); } + sle->setFieldU32( + sfOwnerCount, sle->getFieldU32(sfOwnerCount) + hookReserve); + sb.update(sle); + auto sle = std::make_shared(keylet::hook(accid)); sle->setFieldArray(sfHooks, hooks); sle->setAccountID(sfAccount, accid); @@ -745,6 +749,8 @@ Change::activateXahauGenesis() ripple::STArray hooks{sfHooks, 1}; STObject hookObj{sfHook}; hookObj.setFieldH256(sfHookHash, governHash); + + uint32_t hookReserve = 0; // parameters { std::vector vec; @@ -760,6 +766,7 @@ Change::activateXahauGenesis() sfHookParameters, STArray(vec, sfHookParameters)); } + hookReserve += SetHook::computeHookReserve(hookObj); hooks.push_back(hookObj); auto sle = std::make_shared(hookKL); @@ -786,7 +793,8 @@ Change::activateXahauGenesis() sle->setAccountID(sfRegularKey, noAccount()); sle->setFieldU32(sfFlags, lsfDisableMaster); - sle->setFieldU32(sfOwnerCount, sle->getFieldU32(sfOwnerCount) + 1); + sle->setFieldU32( + sfOwnerCount, sle->getFieldU32(sfOwnerCount) + hookReserve); sb.update(sle); } } diff --git a/src/ripple/app/tx/impl/SetHook.cpp b/src/ripple/app/tx/impl/SetHook.cpp index 72ab85be5..48ddc5ee5 100644 --- a/src/ripple/app/tx/impl/SetHook.cpp +++ b/src/ripple/app/tx/impl/SetHook.cpp @@ -1221,6 +1221,29 @@ updateHookParameters( return tesSUCCESS; } +/** + * Compute the reserve required for a hook object. + * @param hookObj The hook object to compute the reserve for.(not Transaction + * field, use the Hook object inside the ltHook object.) + * @return The reserve required for the hook object. + */ +uint32_t +SetHook::computeHookReserve(STObject const& hookObj) +{ + if (!hookObj.isFieldPresent(sfHookHash)) + return 0; + + int reserve{1}; + + if (hookObj.isFieldPresent(sfHookParameters)) + reserve += hookObj.getFieldArray(sfHookParameters).size(); + + if (hookObj.isFieldPresent(sfHookGrants)) + reserve += hookObj.getFieldArray(sfHookGrants).size(); + + return reserve; +}; + struct KeyletComparator { bool @@ -1972,28 +1995,14 @@ SetHook::setHook() int oldHookReserve = 0; int newHookReserve = 0; - auto const computeHookReserve = [](STObject const& hookObj) -> int { - if (!hookObj.isFieldPresent(sfHookHash)) - return 0; - - int reserve{1}; - - if (hookObj.isFieldPresent(sfHookParameters)) - reserve += hookObj.getFieldArray(sfHookParameters).size(); - - if (hookObj.isFieldPresent(sfHookGrants)) - reserve += hookObj.getFieldArray(sfHookGrants).size(); - - return reserve; - }; - for (int i = 0; i < hook::maxHookChainLength(); ++i) { if (oldHooks && i < oldHookCount) - oldHookReserve += computeHookReserve(((*oldHooks).get())[i]); + oldHookReserve += + SetHook::computeHookReserve(((*oldHooks).get())[i]); if (i < newHooks.size()) - newHookReserve += computeHookReserve(newHooks[i]); + newHookReserve += SetHook::computeHookReserve(newHooks[i]); } reserveDelta = newHookReserve - oldHookReserve; diff --git a/src/ripple/app/tx/impl/SetHook.h b/src/ripple/app/tx/impl/SetHook.h index ecfce2410..876aeaa3f 100644 --- a/src/ripple/app/tx/impl/SetHook.h +++ b/src/ripple/app/tx/impl/SetHook.h @@ -91,6 +91,9 @@ public: static HookSetValidation validateHookSetEntry(SetHookCtx& ctx, STObject const& hookSetObj); + static uint32_t + computeHookReserve(STObject const& hookObj); + private: TER setHook(); diff --git a/src/test/app/XahauGenesis_test.cpp b/src/test/app/XahauGenesis_test.cpp index 8353a3ce9..35cd8ca72 100644 --- a/src/test/app/XahauGenesis_test.cpp +++ b/src/test/app/XahauGenesis_test.cpp @@ -139,7 +139,9 @@ struct XahauGenesis_test : public beast::unit_test::suite false, // means the calling test already burned some of the genesis bool skipTests = false, bool const testFlag = false, - bool const badNetID = false) + bool const badNetID = false, + uint32_t const expectedOwnerCount = + 10 /** testFlag ? 10 : 14 (default) */) { using namespace jtx; @@ -247,7 +249,10 @@ struct XahauGenesis_test : public beast::unit_test::suite BEAST_EXPECT( genesisAccRoot->getFieldAmount(sfBalance) == XahauGenesis::GenesisAmount); - BEAST_EXPECT(genesisAccRoot->getFieldU32(sfOwnerCount) == 2); + BEAST_EXPECT( + genesisAccRoot->getFieldU32(sfOwnerCount) == !testFlag + ? expectedOwnerCount + : 14); // ensure the definitions are correctly set { @@ -583,7 +588,14 @@ struct XahauGenesis_test : public beast::unit_test::suite toBase58(t), membersStr); } - activate(__LINE__, env, true, false, true); + activate( + __LINE__, + env, + true, + false, + true, + {}, + 3 /* IRR,IRD,IMC */ + members.size() + tables.size()); env.close(); env.close(); @@ -2235,6 +2247,8 @@ struct XahauGenesis_test : public beast::unit_test::suite BEAST_EXPECT(!!hookLE); uint256 const ns = beast::zero; uint8_t mc = 0; + uint8_t paramsCount = 0; + if (hookLE) { auto const hooksArray = hookLE->getFieldArray(sfHooks); @@ -2242,6 +2256,9 @@ struct XahauGenesis_test : public beast::unit_test::suite hooksArray.size() == 1 && hooksArray[0].getFieldH256(sfHookHash) == governHookHash); + paramsCount = + hooksArray[0].getFieldArray(sfHookParameters).size(); + for (Account const* m : members) { auto const mVec = vecFromAcc(*m); @@ -2308,7 +2325,9 @@ struct XahauGenesis_test : public beast::unit_test::suite BEAST_EXPECT(!!root); if (root) { - BEAST_EXPECT(root->getFieldU32(sfOwnerCount) == mc * 2 + 2); + BEAST_EXPECT( + root->getFieldU32(sfOwnerCount) == + mc * 2 + 2 + paramsCount); BEAST_EXPECT(root->getFieldU32(sfFlags) & lsfDisableMaster); BEAST_EXPECT(root->getAccountID(sfRegularKey) == noAccount()); } From d18c4d9b33ad42c268ab1e97606da6368e89587d Mon Sep 17 00:00:00 2001 From: Niq Dudfield Date: Thu, 19 Feb 2026 07:20:26 +0700 Subject: [PATCH 003/301] ci: enable explicit nix CI opt-in for non-mainline PRs via labels (#684) --- .github/workflows/xahau-ga-macos.yml | 7 +++++- .github/workflows/xahau-ga-nix.yml | 36 ++++++++++++++++++++++++---- 2 files changed, 38 insertions(+), 5 deletions(-) diff --git a/.github/workflows/xahau-ga-macos.yml b/.github/workflows/xahau-ga-macos.yml index 6b3221be2..afb6ea813 100644 --- a/.github/workflows/xahau-ga-macos.yml +++ b/.github/workflows/xahau-ga-macos.yml @@ -4,7 +4,8 @@ on: push: branches: ["dev", "candidate", "release"] pull_request: - branches: ["dev", "candidate", "release"] + branches: ["**"] + types: [opened, synchronize, reopened, labeled, unlabeled] schedule: - cron: '0 0 * * *' @@ -14,6 +15,10 @@ concurrency: jobs: test: + if: > + github.event_name != 'pull_request' || + contains(fromJson('["dev","candidate","release"]'), github.base_ref) || + contains(join(github.event.pull_request.labels.*.name, ','), 'ci-full-build') strategy: matrix: generator: diff --git a/.github/workflows/xahau-ga-nix.yml b/.github/workflows/xahau-ga-nix.yml index 6477d2cbe..efce25092 100644 --- a/.github/workflows/xahau-ga-nix.yml +++ b/.github/workflows/xahau-ga-nix.yml @@ -4,9 +4,16 @@ on: push: branches: ["dev", "candidate", "release"] pull_request: - branches: ["dev", "candidate", "release"] + branches: ["**"] + types: [opened, synchronize, reopened, labeled, unlabeled] schedule: - cron: '0 0 * * *' + workflow_dispatch: + inputs: + full_matrix: + description: "Force full matrix (6 configs)" + required: false + default: "false" concurrency: group: ${{ github.workflow }}-${{ github.ref }} @@ -14,6 +21,10 @@ concurrency: jobs: matrix-setup: + if: > + github.event_name != 'pull_request' || + contains(fromJson('["dev","candidate","release"]'), github.base_ref) || + contains(join(github.event.pull_request.labels.*.name, ','), 'ci-full-build') runs-on: [self-hosted, generic, 20.04] container: python:3-slim outputs: @@ -111,6 +122,7 @@ jobs: base_ref = "${{ github.base_ref }}" # For PRs, this is the target branch event_name = "${{ github.event_name }}" pr_title = """${{ steps.escape.outputs.title }}""" + pr_labels = """${{ join(github.event.pull_request.labels.*.name, ',') }}""" pr_head_sha = "${{ github.event.pull_request.head.sha }}" # Get commit message - for PRs, fetch via API since head_commit.message is empty @@ -136,11 +148,24 @@ jobs: print(f"Base ref: {base_ref}") print(f"PR head SHA: {pr_head_sha}") print(f"PR title: {pr_title}") + print(f"PR labels: {pr_labels}") print(f"Commit message: {commit_message}") - # Check for override tags in commit message or PR title - force_full = "[ci-nix-full-matrix]" in commit_message or "[ci-nix-full-matrix]" in pr_title + # Manual trigger input to force full matrix. + manual_full = "${{ github.event.inputs.full_matrix || 'false' }}" == "true" + + # Label/manual overrides, while preserving existing title/commit behavior. + force_full = ( + manual_full + or "[ci-nix-full-matrix]" in commit_message + or "[ci-nix-full-matrix]" in pr_title + or ("ci-full-build" in pr_labels and "ci-nix-full-matrix" in pr_labels) + ) + force_min = ( + "ci-full-build" in pr_labels + ) print(f"Force full matrix: {force_full}") + print(f"Force min matrix: {force_min}") # Check if this is targeting a main branch # For PRs: check base_ref (target branch) @@ -148,8 +173,11 @@ jobs: main_branches = ["refs/heads/dev", "refs/heads/release", "refs/heads/candidate"] if force_full: - # Override: always use full matrix if tag is present + # Override: always use full matrix if forced by manual input or label. use_full = True + elif force_min: + # Override: always use minimal matrix if ci-full-build label is present. + use_full = False elif event_name == "pull_request": # For PRs, base_ref is just the branch name (e.g., "dev", not "refs/heads/dev") # Check if the PR targets release or candidate (more critical branches) From bb70739fc689a0793e9d61b4327644685a9ded0f Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Thu, 19 Feb 2026 13:52:52 +0900 Subject: [PATCH 004/301] Enable the Beta RPC API (v2) for all unit tests: (#4573) * Enable api_version 2, which is currently in beta. It is expected to be marked stable by the next stable release. * This does not change any defaults. * The only existing tests changed were one that set the same flag, which was now redundant, and a couple that tested versioning explicitly. --- src/test/jtx/impl/envconfig.cpp | 3 +++ src/test/rpc/AccountInfo_test.cpp | 5 +---- src/test/rpc/Version_test.cpp | 16 +++++++++++++--- 3 files changed, 17 insertions(+), 7 deletions(-) diff --git a/src/test/jtx/impl/envconfig.cpp b/src/test/jtx/impl/envconfig.cpp index ce2c2ae60..d7a31d140 100644 --- a/src/test/jtx/impl/envconfig.cpp +++ b/src/test/jtx/impl/envconfig.cpp @@ -49,6 +49,9 @@ setupConfigForUnitTests(Config& cfg) cfg.FEES.account_reserve = XRP(200).value().xrp().drops(); cfg.FEES.owner_reserve = XRP(50).value().xrp().drops(); + // The Beta API (currently v2) is always available to tests + cfg.BETA_RPC_API = true; + cfg.overwrite(ConfigSection::nodeDatabase(), "type", "rwdb"); cfg.overwrite(ConfigSection::nodeDatabase(), "path", "main"); cfg.overwrite(SECTION_RELATIONAL_DB, "backend", "rwdb"); diff --git a/src/test/rpc/AccountInfo_test.cpp b/src/test/rpc/AccountInfo_test.cpp index 77a1b4486..11100cf7c 100644 --- a/src/test/rpc/AccountInfo_test.cpp +++ b/src/test/rpc/AccountInfo_test.cpp @@ -206,10 +206,7 @@ public: testSignerListsApiVersion2() { using namespace jtx; - Env env{*this, envconfig([](std::unique_ptr c) { - c->loadFromString("\n[beta_rpc_api]\n1\n"); - return c; - })}; + Env env{*this}; Account const alice{"alice"}; env.fund(XRP(1000), alice); diff --git a/src/test/rpc/Version_test.cpp b/src/test/rpc/Version_test.cpp index 360b29664..60ffd30fc 100644 --- a/src/test/rpc/Version_test.cpp +++ b/src/test/rpc/Version_test.cpp @@ -76,11 +76,16 @@ class Version_test : public beast::unit_test::suite std::to_string(RPC::apiMinimumSupportedVersion - 1) + "}"); BEAST_EXPECT(badVersion(re)); + BEAST_EXPECT(env.app().config().BETA_RPC_API); re = env.rpc( "json", "version", "{\"api_version\": " + - std::to_string(RPC::apiMaximumSupportedVersion + 1) + "}"); + std::to_string( + std::max( + RPC::apiMaximumSupportedVersion, RPC::apiBetaVersion) + + 1) + + "}"); BEAST_EXPECT(badVersion(re)); re = env.rpc("json", "version", "{\"api_version\": \"a\"}"); @@ -190,20 +195,25 @@ class Version_test : public beast::unit_test::suite using namespace test::jtx; Env env{*this}; + BEAST_EXPECT(env.app().config().BETA_RPC_API); auto const without_api_verion = std::string("{ ") + "\"jsonrpc\": \"2.0\", " "\"ripplerpc\": \"2.0\", " "\"id\": 5, " "\"method\": \"version\", " "\"params\": {}}"; - auto const with_wrong_api_verion = std::string("{ ") + + auto const with_wrong_api_verion = + std::string("{ ") + "\"jsonrpc\": \"2.0\", " "\"ripplerpc\": \"2.0\", " "\"id\": 6, " "\"method\": \"version\", " "\"params\": { " "\"api_version\": " + - std::to_string(RPC::apiMaximumSupportedVersion + 1) + "}}"; + std::to_string( + std::max(RPC::apiMaximumSupportedVersion, RPC::apiBetaVersion) + + 1) + + "}}"; auto re = env.rpc( "json2", '[' + without_api_verion + ", " + with_wrong_api_verion + ']'); From 711297aa8190a276d1370c569ca441ee0923051d Mon Sep 17 00:00:00 2001 From: Scott Schurr Date: Thu, 19 Feb 2026 14:03:41 +0900 Subject: [PATCH 005/301] `fixReducedOffersV1`: prevent offers from blocking order books: (#4512) Curtail the occurrence of order books that are blocked by reduced offers with the implementation of the fixReducedOffersV1 amendment. This commit identifies three ways in which offers can be reduced: 1. A new offer can be partially crossed by existing offers, so the new offer is reduced when placed in the ledger. 2. An in-ledger offer can be partially crossed by a new offer in a transaction. So the in-ledger offer is reduced by the new offer. 3. An in-ledger offer may be under-funded. In this case the in-ledger offer is scaled down to match the available funds. Reduced offers can block order books if the effective quality of the reduced offer is worse than the quality of the original offer (from the perspective of the taker). It turns out that, for small values, the quality of the reduced offer can be significantly affected by the rounding mode used during scaling computations. This commit adjusts some rounding modes so that the quality of a reduced offer is always at least as good (from the taker's perspective) as the original offer. The amendment is titled fixReducedOffersV1 because additional ways of producing reduced offers may come to light. Therefore, there may be a future need for a V2 amendment. --- Builds/CMake/RippledCore.cmake | 1 + src/ripple/app/paths/impl/BookStep.cpp | 30 +- src/ripple/app/tx/impl/CreateOffer.cpp | 18 +- src/ripple/app/tx/impl/OfferStream.cpp | 18 +- src/ripple/protocol/Feature.h | 4 +- src/ripple/protocol/Quality.h | 23 + src/ripple/protocol/STAmount.h | 19 +- src/ripple/protocol/impl/Feature.cpp | 1 + src/ripple/protocol/impl/Quality.cpp | 29 +- src/ripple/protocol/impl/STAmount.cpp | 189 +++++++- src/test/app/Offer_test.cpp | 13 +- src/test/app/ReducedOffer_test.cpp | 622 +++++++++++++++++++++++++ 12 files changed, 939 insertions(+), 28 deletions(-) create mode 100644 src/test/app/ReducedOffer_test.cpp diff --git a/Builds/CMake/RippledCore.cmake b/Builds/CMake/RippledCore.cmake index 0ae80cea0..6afcd2c20 100644 --- a/Builds/CMake/RippledCore.cmake +++ b/Builds/CMake/RippledCore.cmake @@ -774,6 +774,7 @@ if (tests) src/test/app/PseudoTx_test.cpp src/test/app/RCLCensorshipDetector_test.cpp src/test/app/RCLValidations_test.cpp + src/test/app/ReducedOffer_test.cpp src/test/app/Regression_test.cpp src/test/app/Remit_test.cpp src/test/app/SHAMapStore_test.cpp diff --git a/src/ripple/app/paths/impl/BookStep.cpp b/src/ripple/app/paths/impl/BookStep.cpp index 0c63b8c08..20c74b8f5 100644 --- a/src/ripple/app/paths/impl/BookStep.cpp +++ b/src/ripple/app/paths/impl/BookStep.cpp @@ -531,14 +531,22 @@ limitStepOut( TOut& ownerGives, std::uint32_t transferRateIn, std::uint32_t transferRateOut, - TOut const& limit) + TOut const& limit, + Rules const& rules) { if (limit < stpAmt.out) { stpAmt.out = limit; ownerGives = mulRatio( stpAmt.out, transferRateOut, QUALITY_ONE, /*roundUp*/ false); - ofrAmt = ofrQ.ceil_out(ofrAmt, stpAmt.out); + if (rules.enabled(fixReducedOffersV1)) + // It turns out that the ceil_out implementation has some slop in + // it. ceil_out_strict removes that slop. But removing that slop + // affects transaction outcomes, so the change must be made using + // an amendment. + ofrAmt = ofrQ.ceil_out_strict(ofrAmt, stpAmt.out, /*roundUp*/ true); + else + ofrAmt = ofrQ.ceil_out(ofrAmt, stpAmt.out); stpAmt.in = mulRatio(ofrAmt.in, transferRateIn, QUALITY_ONE, /*roundUp*/ true); } @@ -577,6 +585,7 @@ BookStep::forEachOffer( sb, afView, book_, sb.parentCloseTime(), counter, j_); bool const flowCross = afView.rules().enabled(featureFlowCross); + bool const fixReduced = afView.rules().enabled(fixReducedOffersV1); bool offerAttempted = false; std::optional ofrQ; while (offers.step()) @@ -654,7 +663,16 @@ BookStep::forEachOffer( ownerGives = funds; stpAmt.out = mulRatio( ownerGives, QUALITY_ONE, ofrOutRate, /*roundUp*/ false); - ofrAmt = ofrQ->ceil_out(ofrAmt, stpAmt.out); + + // It turns out we can prevent order book blocking by (strictly) + // rounding down the ceil_out() result. This adjustment changes + // transaction outcomes, so it must be made under an amendment. + if (fixReduced) + ofrAmt = ofrQ->ceil_out_strict( + ofrAmt, stpAmt.out, /* roundUp */ false); + else + ofrAmt = ofrQ->ceil_out(ofrAmt, stpAmt.out); + stpAmt.in = mulRatio(ofrAmt.in, ofrInRate, QUALITY_ONE, /*roundUp*/ true); } @@ -770,7 +788,8 @@ BookStep::revImp( ownerGivesAdj, transferRateIn, transferRateOut, - remainingOut); + remainingOut, + afView.rules()); remainingOut = beast::zero; savedIns.insert(stpAdjAmt.in); savedOuts.insert(remainingOut); @@ -922,7 +941,8 @@ BookStep::fwdImp( ownerGivesAdjRev, transferRateIn, transferRateOut, - remainingOut); + remainingOut, + afView.rules()); if (stpAdjAmtRev.in == remainingIn) { diff --git a/src/ripple/app/tx/impl/CreateOffer.cpp b/src/ripple/app/tx/impl/CreateOffer.cpp index 79dfc93b3..a9d291aae 100644 --- a/src/ripple/app/tx/impl/CreateOffer.cpp +++ b/src/ripple/app/tx/impl/CreateOffer.cpp @@ -868,8 +868,22 @@ CreateOffer::flowCross( // what is a good threshold to check? afterCross.in.clear(); - afterCross.out = divRound( - afterCross.in, rate, takerAmount.out.issue(), true); + afterCross.out = [&]() { + // Careful analysis showed that rounding up this + // divRound result could lead to placing a reduced + // offer in the ledger that blocks order books. So + // the fixReducedOffersV1 amendment changes the + // behavior to round down instead. + if (psb.rules().enabled(fixReducedOffersV1)) + return divRoundStrict( + afterCross.in, + rate, + takerAmount.out.issue(), + false); + + return divRound( + afterCross.in, rate, takerAmount.out.issue(), true); + }(); } else { diff --git a/src/ripple/app/tx/impl/OfferStream.cpp b/src/ripple/app/tx/impl/OfferStream.cpp index 78cf623f4..0a56fcfe0 100644 --- a/src/ripple/app/tx/impl/OfferStream.cpp +++ b/src/ripple/app/tx/impl/OfferStream.cpp @@ -182,17 +182,33 @@ TOfferStreamBase::shouldRmSmallIncreasedQOffer() const } TTakerGets const ownerFunds = toAmount(*ownerFunds_); + bool const fixReduced = view_.rules().enabled(fixReducedOffersV1); auto const effectiveAmounts = [&] { if (offer_.owner() != offer_.issueOut().account && ownerFunds < ofrAmts.out) { - // adjust the amounts by owner funds + // adjust the amounts by owner funds. + // + // It turns out we can prevent order book blocking by rounding down + // the ceil_out() result. This adjustment changes transaction + // results, so it must be made under an amendment. + if (fixReduced) + return offer_.quality().ceil_out_strict( + ofrAmts, ownerFunds, /* roundUp */ false); + return offer_.quality().ceil_out(ofrAmts, ownerFunds); } return ofrAmts; }(); + // If either the effective in or out are zero then remove the offer. + // This can happen with fixReducedOffersV1 since it rounds down. + if (fixReduced && + (effectiveAmounts.in.signum() <= 0 || + effectiveAmounts.out.signum() <= 0)) + return true; + if (effectiveAmounts.in > TTakerPays::minPositiveAmount()) return false; diff --git a/src/ripple/protocol/Feature.h b/src/ripple/protocol/Feature.h index 20871c26f..8e1e8a250 100644 --- a/src/ripple/protocol/Feature.h +++ b/src/ripple/protocol/Feature.h @@ -74,7 +74,7 @@ namespace detail { // Feature.cpp. Because it's only used to reserve storage, and determine how // large to make the FeatureBitset, it MAY be larger. It MUST NOT be less than // the actual number of amendments. A LogicError on startup will verify this. -static constexpr std::size_t numFeatures = 92; +static constexpr std::size_t numFeatures = 93; /** Amendments that this server supports and the default voting behavior. Whether they are enabled depends on the Rules defined in the validated @@ -380,6 +380,8 @@ extern uint256 const fixCronStacking; extern uint256 const fixHookAPI20251128; extern uint256 const featureHookOnV2; extern uint256 const featureHooksUpdate2; +extern uint256 const fixReducedOffersV1; + } // namespace ripple #endif diff --git a/src/ripple/protocol/Quality.h b/src/ripple/protocol/Quality.h index 9de137d87..840d8d444 100644 --- a/src/ripple/protocol/Quality.h +++ b/src/ripple/protocol/Quality.h @@ -223,6 +223,29 @@ public: toAmount(stRes.in), toAmount(stRes.out)); } + Amounts + ceil_out_strict(Amounts const& amount, STAmount const& limit, bool roundUp) + const; + + template + TAmounts + ceil_out_strict( + TAmounts const& amount, + Out const& limit, + bool roundUp) const + { + if (amount.out <= limit) + return amount; + + // Use the existing STAmount implementation for now, but consider + // replacing with code specific to IOUAMount and XRPAmount + Amounts stAmt(toSTAmount(amount.in), toSTAmount(amount.out)); + STAmount stLim(toSTAmount(limit)); + auto const stRes = ceil_out_strict(stAmt, stLim, roundUp); + return TAmounts( + toAmount(stRes.in), toAmount(stRes.out)); + } + /** Returns `true` if lhs is lower quality than `rhs`. Lower quality means the taker receives a worse deal. Higher quality is better for the taker. diff --git a/src/ripple/protocol/STAmount.h b/src/ripple/protocol/STAmount.h index ecfaaa382..69288ec68 100644 --- a/src/ripple/protocol/STAmount.h +++ b/src/ripple/protocol/STAmount.h @@ -503,7 +503,7 @@ divide(STAmount const& v1, STAmount const& v2, Issue const& issue); STAmount multiply(STAmount const& v1, STAmount const& v2, Issue const& issue); -// multiply, or divide rounding result in specified direction +// multiply rounding result in specified direction STAmount mulRound( STAmount const& v1, @@ -511,6 +511,15 @@ mulRound( Issue const& issue, bool roundUp); +// multiply following the rounding directions more precisely. +STAmount +mulRoundStrict( + STAmount const& v1, + STAmount const& v2, + Issue const& issue, + bool roundUp); + +// divide rounding result in specified direction STAmount divRound( STAmount const& v1, @@ -518,6 +527,14 @@ divRound( Issue const& issue, bool roundUp); +// divide following the rounding directions more precisely. +STAmount +divRoundStrict( + STAmount const& v1, + STAmount const& v2, + Issue const& issue, + bool roundUp); + // Someone is offering X for Y, what is the rate? // Rate: smaller is better, the taker wants the most out: in/out // VFALCO TODO Return a Quality object diff --git a/src/ripple/protocol/impl/Feature.cpp b/src/ripple/protocol/impl/Feature.cpp index 8640a1d57..2e5b333bb 100644 --- a/src/ripple/protocol/impl/Feature.cpp +++ b/src/ripple/protocol/impl/Feature.cpp @@ -486,6 +486,7 @@ REGISTER_FIX (fixCronStacking, Supported::yes, VoteBehavior::De REGISTER_FIX (fixHookAPI20251128, Supported::yes, VoteBehavior::DefaultYes); REGISTER_FEATURE(HookOnV2, Supported::yes, VoteBehavior::DefaultNo); REGISTER_FEATURE(HooksUpdate2, Supported::yes, VoteBehavior::DefaultNo); +REGISTER_FIX (fixReducedOffersV1, Supported::yes, VoteBehavior::DefaultNo); // The following amendments are obsolete, but must remain supported // because they could potentially get enabled. diff --git a/src/ripple/protocol/impl/Quality.cpp b/src/ripple/protocol/impl/Quality.cpp index 97e1b8a9f..f7b9d6b3c 100644 --- a/src/ripple/protocol/impl/Quality.cpp +++ b/src/ripple/protocol/impl/Quality.cpp @@ -81,12 +81,20 @@ Quality::ceil_in(Amounts const& amount, STAmount const& limit) const return amount; } -Amounts -Quality::ceil_out(Amounts const& amount, STAmount const& limit) const +template +static Amounts +ceil_out_impl( + Amounts const& amount, + STAmount const& limit, + bool roundUp, + Quality const& quality) { if (amount.out > limit) { - Amounts result(mulRound(limit, rate(), amount.in.issue(), true), limit); + Amounts result( + MulRoundFunc(limit, quality.rate(), amount.in.issue(), roundUp), + limit); // Clamp in if (result.in > amount.in) result.in = amount.in; @@ -97,6 +105,21 @@ Quality::ceil_out(Amounts const& amount, STAmount const& limit) const return amount; } +Amounts +Quality::ceil_out(Amounts const& amount, STAmount const& limit) const +{ + return ceil_out_impl(amount, limit, /* roundUp */ true, *this); +} + +Amounts +Quality::ceil_out_strict( + Amounts const& amount, + STAmount const& limit, + bool roundUp) const +{ + return ceil_out_impl(amount, limit, roundUp, *this); +} + Quality composed_quality(Quality const& lhs, Quality const& rhs) { diff --git a/src/ripple/protocol/impl/STAmount.cpp b/src/ripple/protocol/impl/STAmount.cpp index 02e334594..90a646787 100644 --- a/src/ripple/protocol/impl/STAmount.cpp +++ b/src/ripple/protocol/impl/STAmount.cpp @@ -1266,8 +1266,28 @@ multiply(STAmount const& v1, STAmount const& v2, Issue const& issue) v1.negative() != v2.negative()); } +// This is the legacy version of canonicalizeRound. It's been in use +// for years, so it is deeply embedded in the behavior of cross-currency +// transactions. +// +// However in 2022 it was noticed that the rounding characteristics were +// surprising. When the code converts from IOU-like to XRP-like there may +// be a fraction of the IOU-like representation that is too small to be +// represented in drops. `canonicalizeRound()` currently does some unusual +// rounding. +// +// 1. If the fractional part is greater than or equal to 0.1, then the +// number of drops is rounded up. +// +// 2. However, if the fractional part is less than 0.1 (for example, +// 0.099999), then the number of drops is rounded down. +// +// The XRP Ledger has this rounding behavior baked in. But there are +// situations where this rounding behavior led to undesirable outcomes. +// So an alternative rounding approach was introduced. You'll see that +// alternative below. static void -canonicalizeRound(bool native, std::uint64_t& value, int& offset) +canonicalizeRound(bool native, std::uint64_t& value, int& offset, bool) { if (native) { @@ -1301,8 +1321,100 @@ canonicalizeRound(bool native, std::uint64_t& value, int& offset) } } -STAmount -mulRound( +// The original canonicalizeRound did not allow the rounding direction to +// be specified. It also ignored some of the bits that could contribute to +// rounding decisions. canonicalizeRoundStrict() tracks all of the bits in +// the value being rounded. +static void +canonicalizeRoundStrict( + bool native, + std::uint64_t& value, + int& offset, + bool roundUp) +{ + if (native) + { + if (offset < 0) + { + bool hadRemainder = false; + + while (offset < -1) + { + // It would be better to use std::lldiv than to separately + // compute the remainder. But std::lldiv does not support + // unsigned arguments. + std::uint64_t const newValue = value / 10; + hadRemainder |= (value != (newValue * 10)); + value = newValue; + ++offset; + } + value += + (hadRemainder && roundUp) ? 10 : 9; // Add before last divide + value /= 10; + ++offset; + } + } + else if (value > STAmount::cMaxValue) + { + while (value > (10 * STAmount::cMaxValue)) + { + value /= 10; + ++offset; + } + value += 9; // add before last divide + value /= 10; + ++offset; + } +} + +namespace { + +// saveNumberRoundMode doesn't do quite enough for us. What we want is a +// Number::RoundModeGuard that sets the new mode and restores the old mode +// when it leaves scope. Since Number doesn't have that facility, we'll +// build it here. +class NumberRoundModeGuard +{ + saveNumberRoundMode saved_; + +public: + explicit NumberRoundModeGuard(Number::rounding_mode mode) noexcept + : saved_{Number::setround(mode)} + { + } + + NumberRoundModeGuard(NumberRoundModeGuard const&) = delete; + + NumberRoundModeGuard& + operator=(NumberRoundModeGuard const&) = delete; +}; + +// We need a class that has an interface similar to NumberRoundModeGuard +// but does nothing. +class DontAffectNumberRoundMode +{ +public: + explicit DontAffectNumberRoundMode(Number::rounding_mode mode) noexcept + { + } + + DontAffectNumberRoundMode(DontAffectNumberRoundMode const&) = delete; + + DontAffectNumberRoundMode& + operator=(DontAffectNumberRoundMode const&) = delete; +}; + +} // anonymous namespace + +// Pass the canonicalizeRound function pointer as a template parameter. +// +// We might need to use NumberRoundModeGuard. Allow the caller +// to pass either that or a replacement as a template parameter. +template < + void (*CanonicalizeFunc)(bool, std::uint64_t&, int&, bool), + typename MightSaveRound> +static STAmount +mulRoundImpl( STAmount const& v1, STAmount const& v2, Issue const& issue, @@ -1365,8 +1477,15 @@ mulRound( int offset = offset1 + offset2 + 14; if (resultNegative != roundUp) - canonicalizeRound(xrp, amount, offset); - STAmount result(issue, amount, offset, resultNegative); + { + CanonicalizeFunc(xrp, amount, offset, roundUp); + } + STAmount result = [&]() { + // If appropriate, tell Number to round down. This gives the desired + // result from STAmount::canonicalize. + MightSaveRound const savedRound(Number::towards_zero); + return STAmount(issue, amount, offset, resultNegative); + }(); if (roundUp && !resultNegative && !result) { @@ -1388,7 +1507,32 @@ mulRound( } STAmount -divRound( +mulRound( + STAmount const& v1, + STAmount const& v2, + Issue const& issue, + bool roundUp) +{ + return mulRoundImpl( + v1, v2, issue, roundUp); +} + +STAmount +mulRoundStrict( + STAmount const& v1, + STAmount const& v2, + Issue const& issue, + bool roundUp) +{ + return mulRoundImpl( + v1, v2, issue, roundUp); +} + +// We might need to use NumberRoundModeGuard. Allow the caller +// to pass either that or a replacement as a template parameter. +template +static STAmount +divRoundImpl( STAmount const& num, STAmount const& den, Issue const& issue, @@ -1437,9 +1581,18 @@ divRound( int offset = numOffset - denOffset - 17; if (resultNegative != roundUp) - canonicalizeRound(isXRP(issue), amount, offset); + canonicalizeRound(isXRP(issue), amount, offset, roundUp); + + STAmount result = [&]() { + // If appropriate, tell Number the rounding mode we are using. + // Note that "roundUp == true" actually means "round away from zero". + // Otherwise round toward zero. + using enum Number::rounding_mode; + MightSaveRound const savedRound( + roundUp ^ resultNegative ? upward : downward); + return STAmount(issue, amount, offset, resultNegative); + }(); - STAmount result(issue, amount, offset, resultNegative); if (roundUp && !resultNegative && !result) { if (isXRP(issue)) @@ -1459,4 +1612,24 @@ divRound( return result; } +STAmount +divRound( + STAmount const& num, + STAmount const& den, + Issue const& issue, + bool roundUp) +{ + return divRoundImpl(num, den, issue, roundUp); +} + +STAmount +divRoundStrict( + STAmount const& num, + STAmount const& den, + Issue const& issue, + bool roundUp) +{ + return divRoundImpl(num, den, issue, roundUp); +} + } // namespace ripple diff --git a/src/test/app/Offer_test.cpp b/src/test/app/Offer_test.cpp index 23e994642..590a811e3 100644 --- a/src/test/app/Offer_test.cpp +++ b/src/test/app/Offer_test.cpp @@ -2090,18 +2090,17 @@ public: BEAST_EXPECT( jrr[jss::node][sfBalance.fieldName][jss::value] == "49.96666666666667"); + jrr = ledgerEntryState(env, bob, gw, "USD"); - if (NumberSwitchOver) + Json::Value const bobsUSD = + jrr[jss::node][sfBalance.fieldName][jss::value]; + if (!NumberSwitchOver) { - BEAST_EXPECT( - jrr[jss::node][sfBalance.fieldName][jss::value] == - "-0.9665000000333333"); + BEAST_EXPECT(bobsUSD == "-0.966500000033334"); } else { - BEAST_EXPECT( - jrr[jss::node][sfBalance.fieldName][jss::value] == - "-0.966500000033334"); + BEAST_EXPECT(bobsUSD == "-0.9665000000333333"); } } } diff --git a/src/test/app/ReducedOffer_test.cpp b/src/test/app/ReducedOffer_test.cpp new file mode 100644 index 000000000..f82efcb7f --- /dev/null +++ b/src/test/app/ReducedOffer_test.cpp @@ -0,0 +1,622 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2022 Ripple Labs Inc. + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#include +#include +#include +#include + +namespace ripple { +namespace test { + +class ReducedOffer_test : public beast::unit_test::suite +{ + static auto + ledgerEntryOffer( + jtx::Env& env, + jtx::Account const& acct, + std::uint32_t offer_seq) + { + Json::Value jvParams; + jvParams[jss::offer][jss::account] = acct.human(); + jvParams[jss::offer][jss::seq] = offer_seq; + return env.rpc( + "json", "ledger_entry", to_string(jvParams))[jss::result]; + } + + static bool + offerInLedger( + jtx::Env& env, + jtx::Account const& acct, + std::uint32_t offerSeq) + { + Json::Value ledgerOffer = ledgerEntryOffer(env, acct, offerSeq); + return !( + ledgerOffer.isMember(jss::error) && + ledgerOffer[jss::error].asString() == "entryNotFound"); + } + + // Common code to clean up unneeded offers. + static void + cleanupOldOffers( + jtx::Env& env, + jtx::Account const& acct1, + jtx::Account const& acct2, + std::uint32_t acct1OfferSeq, + std::uint32_t acct2OfferSeq) + { + env(offer_cancel(acct1, acct1OfferSeq)); + env(offer_cancel(acct2, acct2OfferSeq)); + env.close(); + } + +public: + void + testPartialCrossNewXrpIouQChange() + { + testcase("exercise partial cross new XRP/IOU offer Q change"); + + using namespace jtx; + + auto const gw = Account{"gateway"}; + auto const alice = Account{"alice"}; + auto const bob = Account{"bob"}; + auto const USD = gw["USD"]; + + // Make one test run without fixReducedOffersV1 and one with. + for (FeatureBitset features : + {supported_amendments() - fixReducedOffersV1, + supported_amendments() | fixReducedOffersV1}) + { + Env env{*this, features}; + + // Make sure none of the offers we generate are under funded. + env.fund(XRP(10'000'000), gw, alice, bob); + env.close(); + + env(trust(alice, USD(10'000'000))); + env(trust(bob, USD(10'000'000))); + env.close(); + + env(pay(gw, bob, USD(10'000'000))); + env.close(); + + // Lambda that: + // 1. Exercises one offer pair, + // 2. Collects the results, and + // 3. Cleans up for the next offer pair. + // Returns 1 if the crossed offer has a bad rate for the book. + auto exerciseOfferPair = + [this, &env, &alice, &bob]( + Amounts const& inLedger, + Amounts const& newOffer) -> unsigned int { + // Put inLedger offer in the ledger so newOffer can cross it. + std::uint32_t const aliceOfferSeq = env.seq(alice); + env(offer(alice, inLedger.in, inLedger.out)); + env.close(); + + // Now alice's offer will partially cross bob's offer. + STAmount const initialRate = Quality(newOffer).rate(); + std::uint32_t const bobOfferSeq = env.seq(bob); + STAmount const bobInitialBalance = env.balance(bob); + STAmount const bobsFee = drops(10); + env(offer(bob, newOffer.in, newOffer.out, tfSell), + fee(bobsFee)); + env.close(); + STAmount const bobFinalBalance = env.balance(bob); + + // alice's offer should be fully crossed and so gone from + // the ledger. + if (!BEAST_EXPECT(!offerInLedger(env, alice, aliceOfferSeq))) + // If the in-ledger offer was not consumed then further + // results are meaningless. + return 1; + + // bob's offer should be in the ledger, but reduced in size. + unsigned int badRate = 1; + { + Json::Value bobOffer = + ledgerEntryOffer(env, bob, bobOfferSeq); + + STAmount const reducedTakerGets = amountFromJson( + sfTakerGets, bobOffer[jss::node][sfTakerGets.jsonName]); + STAmount const reducedTakerPays = amountFromJson( + sfTakerPays, bobOffer[jss::node][sfTakerPays.jsonName]); + STAmount const bobGot = + env.balance(bob) + bobsFee - bobInitialBalance; + BEAST_EXPECT(reducedTakerPays < newOffer.in); + BEAST_EXPECT(reducedTakerGets < newOffer.out); + STAmount const inLedgerRate = + Quality(Amounts{reducedTakerPays, reducedTakerGets}) + .rate(); + + badRate = inLedgerRate > initialRate ? 1 : 0; + + // If the inLedgerRate is less than initial rate, then + // incrementing the mantissa of the reduced taker pays + // should result in a rate higher than initial. Check + // this to verify that the largest allowable TakerPays + // was computed. + if (badRate == 0) + { + STAmount const tweakedTakerPays = + reducedTakerPays + drops(1); + STAmount const tweakedRate = + Quality(Amounts{tweakedTakerPays, reducedTakerGets}) + .rate(); + BEAST_EXPECT(tweakedRate > initialRate); + } +#if 0 + std::cout << "Placed rate: " << initialRate + << "; in-ledger rate: " << inLedgerRate + << "; TakerPays: " << reducedTakerPays + << "; TakerGets: " << reducedTakerGets + << "; bob already got: " << bobGot << std::endl; +// #else + std::string_view filler = + inLedgerRate > initialRate ? "**" : " "; + std::cout << "| `" << reducedTakerGets << "` | `" + << reducedTakerPays << "` | `" << initialRate + << "` | " << filler << "`" << inLedgerRate << "`" + << filler << " |`" << std::endl; +#endif + } + + // In preparation for the next iteration make sure the two + // offers are gone from the ledger. + cleanupOldOffers(env, alice, bob, aliceOfferSeq, bobOfferSeq); + return badRate; + }; + + // bob's offer (the new offer) is the same every time: + Amounts const bobsOffer{ + STAmount(XRP(1)), STAmount(USD.issue(), 1, 0)}; + + // alice's offer has a slightly smaller TakerPays with each + // iteration. This should mean that the size of the offer bob + // places in the ledger should increase with each iteration. + unsigned int blockedCount = 0; + for (std::uint64_t mantissaReduce = 1'000'000'000ull; + mantissaReduce <= 5'000'000'000ull; + mantissaReduce += 20'000'000ull) + { + STAmount aliceUSD{ + bobsOffer.out.issue(), + bobsOffer.out.mantissa() - mantissaReduce, + bobsOffer.out.exponent()}; + STAmount aliceXRP{ + bobsOffer.in.issue(), bobsOffer.in.mantissa() - 1}; + Amounts alicesOffer{aliceUSD, aliceXRP}; + blockedCount += exerciseOfferPair(alicesOffer, bobsOffer); + } + + // If fixReducedOffersV1 is enabled, then none of the test cases + // should produce a potentially blocking rate. + // + // Also verify that if fixReducedOffersV1 is not enabled then + // some of the test cases produced a potentially blocking rate. + if (features[fixReducedOffersV1]) + { + BEAST_EXPECT(blockedCount == 0); + } + else + { + BEAST_EXPECT(blockedCount >= 170); + } + } + } + + void + testPartialCrossOldXrpIouQChange() + { + testcase("exercise partial cross old XRP/IOU offer Q change"); + + using namespace jtx; + + auto const gw = Account{"gateway"}; + auto const alice = Account{"alice"}; + auto const bob = Account{"bob"}; + auto const USD = gw["USD"]; + + // Make one test run without fixReducedOffersV1 and one with. + for (FeatureBitset features : + {supported_amendments() - fixReducedOffersV1, + supported_amendments() | fixReducedOffersV1}) + { + // Make sure none of the offers we generate are under funded. + Env env{*this, features}; + env.fund(XRP(10'000'000), gw, alice, bob); + env.close(); + + env(trust(alice, USD(10'000'000))); + env(trust(bob, USD(10'000'000))); + env.close(); + + env(pay(gw, alice, USD(10'000'000))); + env.close(); + + // Lambda that: + // 1. Exercises one offer pair, + // 2. Collects the results, and + // 3. Cleans up for the next offer pair. + auto exerciseOfferPair = + [this, &env, &alice, &bob]( + Amounts const& inLedger, + Amounts const& newOffer) -> unsigned int { + // Get the inLedger offer into the ledger so newOffer can cross + // it. + STAmount const initialRate = Quality(inLedger).rate(); + std::uint32_t const aliceOfferSeq = env.seq(alice); + env(offer(alice, inLedger.in, inLedger.out)); + env.close(); + + // Now bob's offer will partially cross alice's offer. + std::uint32_t const bobOfferSeq = env.seq(bob); + STAmount const aliceInitialBalance = env.balance(alice); + env(offer(bob, newOffer.in, newOffer.out)); + env.close(); + STAmount const aliceFinalBalance = env.balance(alice); + + // bob's offer should not have made it into the ledger. + if (!BEAST_EXPECT(!offerInLedger(env, bob, bobOfferSeq))) + { + // If the in-ledger offer was not consumed then further + // results are meaningless. + cleanupOldOffers( + env, alice, bob, aliceOfferSeq, bobOfferSeq); + return 1; + } + // alice's offer should still be in the ledger, but reduced in + // size. + unsigned int badRate = 1; + { + Json::Value aliceOffer = + ledgerEntryOffer(env, alice, aliceOfferSeq); + + STAmount const reducedTakerGets = amountFromJson( + sfTakerGets, + aliceOffer[jss::node][sfTakerGets.jsonName]); + STAmount const reducedTakerPays = amountFromJson( + sfTakerPays, + aliceOffer[jss::node][sfTakerPays.jsonName]); + STAmount const aliceGot = + env.balance(alice) - aliceInitialBalance; + BEAST_EXPECT(reducedTakerPays < inLedger.in); + BEAST_EXPECT(reducedTakerGets < inLedger.out); + STAmount const inLedgerRate = + Quality(Amounts{reducedTakerPays, reducedTakerGets}) + .rate(); + badRate = inLedgerRate > initialRate ? 1 : 0; + + // If the inLedgerRate is less than initial rate, then + // incrementing the mantissa of the reduced taker pays + // should result in a rate higher than initial. Check + // this to verify that the largest allowable TakerPays + // was computed. + if (badRate == 0) + { + STAmount const tweakedTakerPays = + reducedTakerPays + drops(1); + STAmount const tweakedRate = + Quality(Amounts{tweakedTakerPays, reducedTakerGets}) + .rate(); + BEAST_EXPECT(tweakedRate > initialRate); + } +#if 0 + std::cout << "Placed rate: " << initialRate + << "; in-ledger rate: " << inLedgerRate + << "; TakerPays: " << reducedTakerPays + << "; TakerGets: " << reducedTakerGets + << "; alice already got: " << aliceGot + << std::endl; +// #else + std::string_view filler = badRate ? "**" : " "; + std::cout << "| `" << reducedTakerGets << "` | `" + << reducedTakerPays << "` | `" << initialRate + << "` | " << filler << "`" << inLedgerRate << "`" + << filler << " | `" << aliceGot << "` |" + << std::endl; +#endif + } + + // In preparation for the next iteration make sure the two + // offers are gone from the ledger. + cleanupOldOffers(env, alice, bob, aliceOfferSeq, bobOfferSeq); + return badRate; + }; + + // alice's offer (the old offer) is the same every time: + Amounts const aliceOffer{ + STAmount(XRP(1)), STAmount(USD.issue(), 1, 0)}; + + // bob's offer has a slightly smaller TakerPays with each iteration. + // This should mean that the size of the offer alice leaves in the + // ledger should increase with each iteration. + unsigned int blockedCount = 0; + for (std::uint64_t mantissaReduce = 1'000'000'000ull; + mantissaReduce <= 4'000'000'000ull; + mantissaReduce += 20'000'000ull) + { + STAmount bobUSD{ + aliceOffer.out.issue(), + aliceOffer.out.mantissa() - mantissaReduce, + aliceOffer.out.exponent()}; + STAmount bobXRP{ + aliceOffer.in.issue(), aliceOffer.in.mantissa() - 1}; + Amounts bobsOffer{bobUSD, bobXRP}; + + blockedCount += exerciseOfferPair(aliceOffer, bobsOffer); + } + + // If fixReducedOffersV1 is enabled, then none of the test cases + // should produce a potentially blocking rate. + // + // Also verify that if fixReducedOffersV1 is not enabled then + // some of the test cases produced a potentially blocking rate. + if (features[fixReducedOffersV1]) + { + BEAST_EXPECT(blockedCount == 0); + } + else + { + BEAST_EXPECT(blockedCount > 10); + } + } + } + + void + testUnderFundedXrpIouQChange() + { + testcase("exercise underfunded XRP/IOU offer Q change"); + + // Bob places an offer that is not fully funded. + // + // This unit test compares the behavior of this situation before and + // after applying the fixReducedOffersV1 amendment. + + using namespace jtx; + auto const alice = Account{"alice"}; + auto const bob = Account{"bob"}; + auto const gw = Account{"gw"}; + auto const USD = gw["USD"]; + + // Make one test run without fixReducedOffersV1 and one with. + for (FeatureBitset features : + {supported_amendments() - fixReducedOffersV1, + supported_amendments() | fixReducedOffersV1}) + { + Env env{*this, features}; + + env.fund(XRP(10000), alice, bob, gw); + env.close(); + env.trust(USD(1000), alice, bob); + + int blockedOrderBookCount = 0; + for (STAmount initialBobUSD = USD(0.45); initialBobUSD <= USD(1); + initialBobUSD += USD(0.025)) + { + // underfund bob's offer + env(pay(gw, bob, initialBobUSD)); + env.close(); + + std::uint32_t const bobOfferSeq = env.seq(bob); + env(offer(bob, drops(2), USD(1))); + env.close(); + + // alice places an offer that would cross bob's if bob's were + // well funded. + std::uint32_t const aliceOfferSeq = env.seq(alice); + env(offer(alice, USD(1), drops(2))); + env.close(); + + // We want to detect order book blocking. If: + // 1. bob's offer is still in the ledger and + // 2. alice received no USD + // then we use that as evidence that bob's offer blocked the + // order book. + { + bool const bobsOfferGone = + !offerInLedger(env, bob, bobOfferSeq); + STAmount const aliceBalanceUSD = env.balance(alice, USD); + + // Sanity check the ledger if alice got USD. + if (aliceBalanceUSD.signum() > 0) + { + BEAST_EXPECT(aliceBalanceUSD == initialBobUSD); + BEAST_EXPECT(env.balance(bob, USD) == USD(0)); + BEAST_EXPECT(bobsOfferGone); + } + + // Track occurrences of order book blocking. + if (!bobsOfferGone && aliceBalanceUSD.signum() == 0) + { + ++blockedOrderBookCount; + } + + // In preparation for the next iteration clean up any + // leftover offers. + cleanupOldOffers( + env, alice, bob, aliceOfferSeq, bobOfferSeq); + + // Zero out alice's and bob's USD balances. + if (STAmount const aliceBalance = env.balance(alice, USD); + aliceBalance.signum() > 0) + env(pay(alice, gw, aliceBalance)); + + if (STAmount const bobBalance = env.balance(bob, USD); + bobBalance.signum() > 0) + env(pay(bob, gw, bobBalance)); + + env.close(); + } + } + + // If fixReducedOffersV1 is enabled, then none of the test cases + // should produce a potentially blocking rate. + // + // Also verify that if fixReducedOffersV1 is not enabled then + // some of the test cases produced a potentially blocking rate. + if (features[fixReducedOffersV1]) + { + BEAST_EXPECT(blockedOrderBookCount == 0); + } + else + { + BEAST_EXPECT(blockedOrderBookCount > 15); + } + } + } + + void + testUnderFundedIouIouQChange() + { + testcase("exercise underfunded IOU/IOU offer Q change"); + + // Bob places an IOU/IOU offer that is not fully funded. + // + // This unit test compares the behavior of this situation before and + // after applying the fixReducedOffersV1 amendment. + + using namespace jtx; + using namespace std::chrono_literals; + auto const alice = Account{"alice"}; + auto const bob = Account{"bob"}; + auto const gw = Account{"gw"}; + + auto const USD = gw["USD"]; + auto const EUR = gw["EUR"]; + + STAmount const tinyUSD(USD.issue(), /*mantissa*/ 1, /*exponent*/ -81); + + // Make one test run without fixReducedOffersV1 and one with. + for (FeatureBitset features : + {supported_amendments() - fixReducedOffersV1, + supported_amendments() | fixReducedOffersV1}) + { + Env env{*this, features}; + + env.fund(XRP(10000), alice, bob, gw); + env.close(); + env.trust(USD(1000), alice, bob); + env.trust(EUR(1000), alice, bob); + + STAmount const eurOffer( + EUR.issue(), /*mantissa*/ 2957, /*exponent*/ -76); + STAmount const usdOffer( + USD.issue(), /*mantissa*/ 7109, /*exponent*/ -76); + + STAmount const endLoop( + USD.issue(), /*mantissa*/ 50, /*exponent*/ -81); + + int blockedOrderBookCount = 0; + for (STAmount initialBobUSD = tinyUSD; initialBobUSD <= endLoop; + initialBobUSD += tinyUSD) + { + // underfund bob's offer + env(pay(gw, bob, initialBobUSD)); + env(pay(gw, alice, EUR(100))); + env.close(); + + // This offer is underfunded + std::uint32_t bobOfferSeq = env.seq(bob); + env(offer(bob, eurOffer, usdOffer)); + env.close(); + env.require(offers(bob, 1)); + + // alice places an offer that crosses bob's. + std::uint32_t aliceOfferSeq = env.seq(alice); + env(offer(alice, usdOffer, eurOffer)); + env.close(); + + // Examine the aftermath of alice's offer. + { + bool const bobsOfferGone = + !offerInLedger(env, bob, bobOfferSeq); + STAmount aliceBalanceUSD = env.balance(alice, USD); +#if 0 + std::cout + << "bobs initial: " << initialBobUSD + << "; alice final: " << aliceBalanceUSD + << "; bobs offer: " << bobsOfferJson.toStyledString() + << std::endl; +#endif + // Sanity check the ledger if alice got USD. + if (aliceBalanceUSD.signum() > 0) + { + BEAST_EXPECT(aliceBalanceUSD == initialBobUSD); + BEAST_EXPECT(env.balance(bob, USD) == USD(0)); + BEAST_EXPECT(bobsOfferGone); + } + + // Track occurrences of order book blocking. + if (!bobsOfferGone && aliceBalanceUSD.signum() == 0) + { + ++blockedOrderBookCount; + } + } + + // In preparation for the next iteration clean up any + // leftover offers. + cleanupOldOffers(env, alice, bob, aliceOfferSeq, bobOfferSeq); + + // Zero out alice's and bob's IOU balances. + auto zeroBalance = [&env, &gw]( + Account const& acct, IOU const& iou) { + if (STAmount const balance = env.balance(acct, iou); + balance.signum() > 0) + env(pay(acct, gw, balance)); + }; + + zeroBalance(alice, EUR); + zeroBalance(alice, USD); + zeroBalance(bob, EUR); + zeroBalance(bob, USD); + env.close(); + } + + // If fixReducedOffersV1 is enabled, then none of the test cases + // should produce a potentially blocking rate. + // + // Also verify that if fixReducedOffersV1 is not enabled then + // some of the test cases produced a potentially blocking rate. + if (features[fixReducedOffersV1]) + { + BEAST_EXPECT(blockedOrderBookCount == 0); + } + else + { + BEAST_EXPECT(blockedOrderBookCount > 20); + } + } + } + + void + run() override + { + testPartialCrossNewXrpIouQChange(); + testPartialCrossOldXrpIouQChange(); + testUnderFundedXrpIouQChange(); + testUnderFundedIouIouQChange(); + } +}; + +BEAST_DEFINE_TESTSUITE_PRIO(ReducedOffer, tx, ripple, 2); + +} // namespace test +} // namespace ripple From 045b07f9797640922728f7a8982fb523b755a5a6 Mon Sep 17 00:00:00 2001 From: drlongle Date: Thu, 19 Feb 2026 14:03:45 +0900 Subject: [PATCH 006/301] Add RPC/WS ports to server_info (#4427) Enhance the /crawl endpoint by publishing WebSocket/RPC ports in the server_info response. The function processing requests to the /crawl endpoint actually calls server_info internally, so this change enables a server to advertise its WebSocket/RPC port(s) to peers via the /crawl endpoint. `grpc` and `peer` ports are included as well. The new `ports` array contains objects, each containing a `port` for the listening port (number string), and an array `protocol` listing the supported protocol(s). This allows crawlers to build a richer topology without needing to port-scan nodes. For non-admin users (including peers), the info about *admin* ports is excluded. Also increase test coverage for RPC ServerInfo. Fix #2837. --- src/ripple/app/main/Application.cpp | 7 ++++ src/ripple/app/main/Application.h | 3 ++ src/ripple/app/main/GRPCServer.cpp | 2 +- src/ripple/app/misc/NetworkOPs.cpp | 48 +++++++++++++++++++++++ src/ripple/protocol/jss.h | 5 ++- src/test/rpc/ServerInfo_test.cpp | 61 ++++++++++++++++++++++++++++- 6 files changed, 122 insertions(+), 4 deletions(-) diff --git a/src/ripple/app/main/Application.cpp b/src/ripple/app/main/Application.cpp index 20427107b..036374aaf 100644 --- a/src/ripple/app/main/Application.cpp +++ b/src/ripple/app/main/Application.cpp @@ -605,6 +605,13 @@ public: return *m_networkOPs; } + virtual ServerHandlerImp& + getServerHandler() override + { + assert(serverHandler_); + return *serverHandler_; + } + boost::asio::io_service& getIOService() override { diff --git a/src/ripple/app/main/Application.h b/src/ripple/app/main/Application.h index a6e58b19e..f10003c80 100644 --- a/src/ripple/app/main/Application.h +++ b/src/ripple/app/main/Application.h @@ -88,6 +88,7 @@ class Overlay; class PathRequests; class PendingSaves; class PublicKey; +class ServerHandlerImp; class SecretKey; class STLedgerEntry; class TimeKeeper; @@ -230,6 +231,8 @@ public: getOPs() = 0; virtual OrderBookDB& getOrderBookDB() = 0; + virtual ServerHandlerImp& + getServerHandler() = 0; virtual TransactionMaster& getMasterTransaction() = 0; virtual perf::PerfLog& diff --git a/src/ripple/app/main/GRPCServer.cpp b/src/ripple/app/main/GRPCServer.cpp index fdef8c1ce..3a5e96b0e 100644 --- a/src/ripple/app/main/GRPCServer.cpp +++ b/src/ripple/app/main/GRPCServer.cpp @@ -429,7 +429,7 @@ GRPCServerImpl::GRPCServerImpl(Application& app) // if present, get endpoint from config if (app_.config().exists("port_grpc")) { - Section section = app_.config().section("port_grpc"); + const auto& section = app_.config().section("port_grpc"); auto const optIp = section.get("ip"); if (!optIp) diff --git a/src/ripple/app/misc/NetworkOPs.cpp b/src/ripple/app/misc/NetworkOPs.cpp index 0e5b8ef5f..a98143b8d 100644 --- a/src/ripple/app/misc/NetworkOPs.cpp +++ b/src/ripple/app/misc/NetworkOPs.cpp @@ -68,10 +68,13 @@ #include #include #include +#include #include #include #include #include + +#include #include #include #include @@ -2709,6 +2712,51 @@ NetworkOPsImp::getServerInfo(bool human, bool admin, bool counters) info["reporting"] = app_.getReportingETL().getInfo(); } + // This array must be sorted in increasing order. + static constexpr std::array protocols{ + "http", "https", "peer", "ws", "ws2", "wss", "wss2"}; + static_assert(std::is_sorted(std::begin(protocols), std::end(protocols))); + { + Json::Value ports{Json::arrayValue}; + for (auto const& port : app_.getServerHandler().setup().ports) + { + // Don't publish admin ports for non-admin users + if (!admin && + !(port.admin_nets_v4.empty() && port.admin_nets_v6.empty() && + port.admin_user.empty() && port.admin_password.empty())) + continue; + std::vector proto; + std::set_intersection( + std::begin(port.protocol), + std::end(port.protocol), + std::begin(protocols), + std::end(protocols), + std::back_inserter(proto)); + if (!proto.empty()) + { + auto& jv = ports.append(Json::Value(Json::objectValue)); + jv[jss::port] = std::to_string(port.port); + jv[jss::protocol] = Json::Value{Json::arrayValue}; + for (auto const& p : proto) + jv[jss::protocol].append(p); + } + } + + if (app_.config().exists("port_grpc")) + { + auto const& grpcSection = app_.config().section("port_grpc"); + auto const optPort = grpcSection.get("port"); + if (optPort && grpcSection.get("ip")) + { + auto& jv = ports.append(Json::Value(Json::objectValue)); + jv[jss::port] = *optPort; + jv[jss::protocol] = Json::Value{Json::arrayValue}; + jv[jss::protocol].append("grpc"); + } + } + info[jss::ports] = std::move(ports); + } + return info; } diff --git a/src/ripple/protocol/jss.h b/src/ripple/protocol/jss.h index 3d7dd66fa..2a049db25 100644 --- a/src/ripple/protocol/jss.h +++ b/src/ripple/protocol/jss.h @@ -573,13 +573,14 @@ JSS(peer_disconnects_resources); // Severed peer connections because of // excess resource consumption. JSS(percent_complete); JSS(phash); -JSS(port); // in: Connect +JSS(port); // in: Connect, out: NetworkOPs +JSS(ports); // out: NetworkOPs JSS(previous); // out: Reservations JSS(previous_ledger); // out: LedgerPropose JSS(proof); // in: BookOffers JSS(propose_seq); // out: LedgerPropose JSS(proposers); // out: NetworkOPs, LedgerConsensus -JSS(protocol); // out: PeerImp +JSS(protocol); // out: NetworkOPs, PeerImp JSS(proxied); // out: RPC ping JSS(pubkey_node); // out: NetworkOPs JSS(pubkey_publisher); // out: ValidatorList diff --git a/src/test/rpc/ServerInfo_test.cpp b/src/test/rpc/ServerInfo_test.cpp index a746aec2e..97c12d250 100644 --- a/src/test/rpc/ServerInfo_test.cpp +++ b/src/test/rpc/ServerInfo_test.cpp @@ -17,6 +17,7 @@ */ //============================================================================== +#include #include #include #include @@ -55,6 +56,16 @@ public: [validators] %2% + +[port_grpc] +ip = 0.0.0.0 +port = 50051 + +[port_admin] +ip = 0.0.0.0 +port = 50052 +protocol = wss2 +admin = 127.0.0.1 )rippleConfig"); p->loadFromString(boost::str( @@ -77,8 +88,30 @@ public: BEAST_EXPECT(result[jss::result][jss::status] == "success"); BEAST_EXPECT(result[jss::result].isMember(jss::info)); } + { - Env env(*this, makeValidatorConfig()); + Env env(*this); + + // Call NetworkOPs directly and set the admin flag to false. + // Expect that the admin ports are not included in the result. + auto const result = + env.app().getOPs().getServerInfo(true, false, 0); + auto const& ports = result[jss::ports]; + BEAST_EXPECT(ports.isArray() && ports.size() == 0); + } + + { + auto config = makeValidatorConfig(); + auto const rpc_port = + (*config)["port_rpc"].get("port"); + auto const grpc_port = + (*config)["port_grpc"].get("port"); + auto const ws_port = (*config)["port_ws"].get("port"); + BEAST_EXPECT(grpc_port); + BEAST_EXPECT(rpc_port); + BEAST_EXPECT(ws_port); + + Env env(*this, std::move(config)); auto const result = env.rpc("server_info"); BEAST_EXPECT(!result[jss::result].isMember(jss::error)); BEAST_EXPECT(result[jss::result][jss::status] == "success"); @@ -86,6 +119,32 @@ public: BEAST_EXPECT( result[jss::result][jss::info][jss::pubkey_validator] == validator_data::public_key); + + auto const& ports = result[jss::result][jss::info][jss::ports]; + BEAST_EXPECT(ports.isArray() && ports.size() == 3); + for (auto const& port : ports) + { + auto const& proto = port[jss::protocol]; + BEAST_EXPECT(proto.isArray()); + auto const p = port[jss::port].asUInt(); + BEAST_EXPECT(p == rpc_port || p == ws_port || p == grpc_port); + if (p == grpc_port) + { + BEAST_EXPECT(proto.size() == 1); + BEAST_EXPECT(proto[0u].asString() == "grpc"); + } + if (p == rpc_port) + { + BEAST_EXPECT(proto.size() == 2); + BEAST_EXPECT(proto[0u].asString() == "http"); + BEAST_EXPECT(proto[1u].asString() == "ws2"); + } + if (p == ws_port) + { + BEAST_EXPECT(proto.size() == 1); + BEAST_EXPECT(proto[0u].asString() == "ws"); + } + } } } From 1fa8f8f08735d8383469fbf64e267af952fa4bfd Mon Sep 17 00:00:00 2001 From: Howard Hinnant Date: Thu, 19 Feb 2026 14:03:45 +0900 Subject: [PATCH 007/301] refactor: remove TypedField's move constructor (#4567) Apply a minor cleanup in `TypedField`: * Remove a non-working and unused move constructor. * Constrain the remaining constructor to not be overly generic enough as to be used as a copy or move constructor. --- src/ripple/protocol/SField.h | 8 +------- src/ripple/protocol/impl/SField.cpp | 7 +++++++ 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/src/ripple/protocol/SField.h b/src/ripple/protocol/SField.h index aa4d9ec52..8b013f698 100644 --- a/src/ripple/protocol/SField.h +++ b/src/ripple/protocol/SField.h @@ -275,13 +275,7 @@ struct TypedField : SField using type = T; template - explicit TypedField(Args&&... args) : SField(std::forward(args)...) - { - } - - TypedField(TypedField&& u) : SField(std::move(u)) - { - } + explicit TypedField(private_access_tag_t pat, Args&&... args); }; /** Indicate std::optional field semantics. */ diff --git a/src/ripple/protocol/impl/SField.cpp b/src/ripple/protocol/impl/SField.cpp index da8521a74..41c3a5881 100644 --- a/src/ripple/protocol/impl/SField.cpp +++ b/src/ripple/protocol/impl/SField.cpp @@ -38,6 +38,13 @@ struct SField::private_access_tag_t static SField::private_access_tag_t access; +template +template +TypedField::TypedField(private_access_tag_t pat, Args&&... args) + : SField(pat, std::forward(args)...) +{ +} + // Construct all compile-time SFields, and register them in the knownCodeToField // database: From 154d07a5cc1e358296e049fcd33d32b0ebbe9386 Mon Sep 17 00:00:00 2001 From: Denis Angell Date: Thu, 19 Feb 2026 14:05:52 +0900 Subject: [PATCH 008/301] refactor: replace hand-rolled lexicalCast (#4473) Replace hand-rolled code with std::from_chars for better maintainability. The C++ std::from_chars function is intended to be as fast as possible, so it is unlikely to be slower than the code it replaces. This change is a net gain because it reduces the amount of hand-rolled code. --- .../app/rdb/backend/detail/impl/Node.cpp | 13 +- src/ripple/beast/core/LexicalCast.h | 140 +++--------------- 2 files changed, 22 insertions(+), 131 deletions(-) diff --git a/src/ripple/app/rdb/backend/detail/impl/Node.cpp b/src/ripple/app/rdb/backend/detail/impl/Node.cpp index c80038ef7..120f0b0d3 100644 --- a/src/ripple/app/rdb/backend/detail/impl/Node.cpp +++ b/src/ripple/app/rdb/backend/detail/impl/Node.cpp @@ -522,7 +522,7 @@ getHashByIndex(soci::session& session, LedgerIndex ledgerIndex) std::string sql = "SELECT LedgerHash FROM Ledgers INDEXED BY SeqLedger WHERE LedgerSeq='"; - sql.append(beast::lexicalCastThrow(ledgerIndex)); + sql.append(std::to_string(ledgerIndex)); sql.append("';"); std::string hash; @@ -585,9 +585,9 @@ getHashesByIndex( { std::string sql = "SELECT LedgerSeq,LedgerHash,PrevHash FROM Ledgers WHERE LedgerSeq >= "; - sql.append(beast::lexicalCastThrow(minSeq)); + sql.append(std::to_string(minSeq)); sql.append(" AND LedgerSeq <= "); - sql.append(beast::lexicalCastThrow(maxSeq)); + sql.append(std::to_string(maxSeq)); sql.append(";"); std::uint64_t ls; @@ -765,8 +765,7 @@ transactionsSQL( boost::format("SELECT %s FROM AccountTransactions " "WHERE Account = '%s' %s %s LIMIT %u, %u;") % selection % toBase58(options.account) % maxClause % minClause % - beast::lexicalCastThrow(options.offset) % - beast::lexicalCastThrow(numberOfResults)); + options.offset % numberOfResults); else sql = boost::str( boost::format( @@ -779,9 +778,7 @@ transactionsSQL( "LIMIT %u, %u;") % selection % toBase58(options.account) % maxClause % minClause % (descending ? "DESC" : "ASC") % (descending ? "DESC" : "ASC") % - (descending ? "DESC" : "ASC") % - beast::lexicalCastThrow(options.offset) % - beast::lexicalCastThrow(numberOfResults)); + (descending ? "DESC" : "ASC") % options.offset % numberOfResults); JLOG(j.trace()) << "txSQL query: " << sql; return sql; } diff --git a/src/ripple/beast/core/LexicalCast.h b/src/ripple/beast/core/LexicalCast.h index de1a6a396..f4c78341b 100644 --- a/src/ripple/beast/core/LexicalCast.h +++ b/src/ripple/beast/core/LexicalCast.h @@ -23,8 +23,8 @@ #include #include #include +#include #include -#include #include #include #include @@ -32,111 +32,10 @@ #include #include -#include - namespace beast { namespace detail { -#if BOOST_COMP_MSVC -#pragma warning(push) -#pragma warning(disable : 4800) -#pragma warning(disable : 4804) -#endif - -template -bool -parse_integral(Int& num, FwdIt first, FwdIt last, Accumulator accumulator) -{ - num = 0; - - if (first == last) - return false; - - while (first != last) - { - auto const c = *first++; - if (c < '0' || c > '9') - return false; - if (!accumulator(num, Int(c - '0'))) - return false; - } - - return true; -} - -template -bool -parse_negative_integral(Int& num, FwdIt first, FwdIt last) -{ - Int limit_value = std::numeric_limits::min() / 10; - Int limit_digit = std::numeric_limits::min() % 10; - - if (limit_digit < 0) - limit_digit = -limit_digit; - - return parse_integral( - num, first, last, [limit_value, limit_digit](Int& value, Int digit) { - assert((digit >= 0) && (digit <= 9)); - if (value < limit_value || - (value == limit_value && digit > limit_digit)) - return false; - value = (value * 10) - digit; - return true; - }); -} - -template -bool -parse_positive_integral(Int& num, FwdIt first, FwdIt last) -{ - Int limit_value = std::numeric_limits::max() / 10; - Int limit_digit = std::numeric_limits::max() % 10; - - return parse_integral( - num, first, last, [limit_value, limit_digit](Int& value, Int digit) { - assert((digit >= 0) && (digit <= 9)); - if (value > limit_value || - (value == limit_value && digit > limit_digit)) - return false; - value = (value * 10) + digit; - return true; - }); -} - -template -bool -parseSigned(IntType& result, FwdIt first, FwdIt last) -{ - static_assert( - std::is_signed::value, - "You may only call parseSigned with a signed integral type."); - - if (first != last && *first == '-') - return parse_negative_integral(result, first + 1, last); - - if (first != last && *first == '+') - return parse_positive_integral(result, first + 1, last); - - return parse_positive_integral(result, first, last); -} - -template -bool -parseUnsigned(UIntType& result, FwdIt first, FwdIt last) -{ - static_assert( - std::is_unsigned::value, - "You may only call parseUnsigned with an unsigned integral type."); - - if (first != last && *first == '+') - return parse_positive_integral(result, first + 1, last); - - return parse_positive_integral(result, first, last); -} - -//------------------------------------------------------------------------------ - // These specializatons get called by the non-member functions to do the work template struct LexicalCast; @@ -148,7 +47,7 @@ struct LexicalCast explicit LexicalCast() = default; template - std::enable_if_t::value, bool> + std::enable_if_t, bool> operator()(std::string& out, Arithmetic in) { out = std::to_string(in); @@ -156,7 +55,7 @@ struct LexicalCast } template - std::enable_if_t::value, bool> + std::enable_if_t, bool> operator()(std::string& out, Enumeration in) { out = std::to_string( @@ -172,21 +71,24 @@ struct LexicalCast explicit LexicalCast() = default; static_assert( - std::is_integral::value, + std::is_integral_v, "beast::LexicalCast can only be used with integral types"); template - std::enable_if_t::value, bool> + std::enable_if_t< + std::is_integral_v && !std::is_same_v, + bool> operator()(Integral& out, std::string const& in) const { - return parseUnsigned(out, in.begin(), in.end()); - } + auto first = in.data(); + auto last = in.data() + in.size(); - template - std::enable_if_t::value, bool> - operator()(Integral& out, std::string const& in) const - { - return parseSigned(out, in.begin(), in.end()); + if (first != last && *first == '+') + ++first; + + auto ret = std::from_chars(first, last, out); + + return ret.ec == std::errc() && ret.ptr == last; } bool @@ -242,10 +144,6 @@ struct LexicalCast } }; -#if BOOST_COMP_MSVC -#pragma warning(pop) -#endif - } // namespace detail //------------------------------------------------------------------------------ @@ -278,9 +176,7 @@ template Out lexicalCastThrow(In in) { - Out out; - - if (lexicalCastChecked(out, in)) + if (Out out; lexicalCastChecked(out, in)) return out; throw BadLexicalCast(); @@ -295,9 +191,7 @@ template Out lexicalCast(In in, Out defaultValue = Out()) { - Out out; - - if (lexicalCastChecked(out, in)) + if (Out out; lexicalCastChecked(out, in)) return out; return defaultValue; From 9554f1efb9db0848ef9db14e536a4828a043b1dd Mon Sep 17 00:00:00 2001 From: Chenna Keshava B S <21219765+ckeshava@users.noreply.github.com> Date: Thu, 19 Feb 2026 14:05:53 +0900 Subject: [PATCH 009/301] fix: remove deprecated fields in `ledger` method (#4244) Remove deprecated fields from the ledger command: * accepted * hash (use ledger_hash instead) * seqNum (use ledger_index instead) * totalCoins (use total_coins instead) Update SHAMapStore unit tests to use `jss:ledger_hash` instead of the deprecated `hash` field. Fix #3214 --- src/ripple/app/ledger/impl/LedgerToJson.cpp | 5 ----- src/ripple/protocol/jss.h | 2 -- src/test/app/SHAMapStore_test.cpp | 9 +++++---- src/test/rpc/LedgerRPC_test.cpp | 8 -------- 4 files changed, 5 insertions(+), 19 deletions(-) diff --git a/src/ripple/app/ledger/impl/LedgerToJson.cpp b/src/ripple/app/ledger/impl/LedgerToJson.cpp index 558757d51..8234ba16f 100644 --- a/src/ripple/app/ledger/impl/LedgerToJson.cpp +++ b/src/ripple/app/ledger/impl/LedgerToJson.cpp @@ -53,7 +53,6 @@ fillJson(Object& json, bool closed, LedgerInfo const& info, bool bFull) { json[jss::parent_hash] = to_string(info.parentHash); json[jss::ledger_index] = to_string(info.seq); - json[jss::seqNum] = to_string(info.seq); // DEPRECATED if (closed) { @@ -70,10 +69,6 @@ fillJson(Object& json, bool closed, LedgerInfo const& info, bool bFull) json[jss::account_hash] = to_string(info.accountHash); json[jss::total_coins] = to_string(info.drops); - // These next three are DEPRECATED. - json[jss::hash] = to_string(info.hash); - json[jss::totalCoins] = to_string(info.drops); - json[jss::accepted] = closed; json[jss::close_flags] = info.closeFlags; // Always show fields that contribute to the ledger hash diff --git a/src/ripple/protocol/jss.h b/src/ripple/protocol/jss.h index 2a049db25..6fa8f871c 100644 --- a/src/ripple/protocol/jss.h +++ b/src/ripple/protocol/jss.h @@ -637,7 +637,6 @@ JSS(send_max); // in: PathRequest, RipplePathFind JSS(seq); // in: LedgerEntry; // out: NetworkOPs, RPCSub, AccountOffers, // ValidatorList, ValidatorInfo, Manifest -JSS(seqNum); // out: LedgerToJson JSS(sequence); // in: UNL JSS(sequence_count); // out: AccountInfo JSS(server_domain); // out: NetworkOPs @@ -694,7 +693,6 @@ JSS(timeouts); // out: InboundLedger JSS(track); // out: PeerImp JSS(traffic); // out: Overlay JSS(total); // out: counters -JSS(totalCoins); // out: LedgerToJson JSS(total_bytes_recv); // out: Peers JSS(total_bytes_sent); // out: Peers JSS(total_coins); // out: LedgerToJson diff --git a/src/test/app/SHAMapStore_test.cpp b/src/test/app/SHAMapStore_test.cpp index 8a3ca0f89..d459ac539 100644 --- a/src/test/app/SHAMapStore_test.cpp +++ b/src/test/app/SHAMapStore_test.cpp @@ -85,7 +85,8 @@ class SHAMapStore_test : public beast::unit_test::suite const std::string outTxHash = to_string(info.txHash); auto const& ledger = json[jss::result][jss::ledger]; - return outHash == ledger[jss::hash].asString() && outSeq == seq && + return outHash == ledger[jss::ledger_hash].asString() && + outSeq == seq && outParentHash == ledger[jss::parent_hash].asString() && outDrops == ledger[jss::total_coins].asString() && outCloseTime == ledger[jss::close_time].asUInt() && @@ -111,9 +112,9 @@ class SHAMapStore_test : public beast::unit_test::suite BEAST_EXPECT( json.isMember(jss::result) && json[jss::result].isMember(jss::ledger) && - json[jss::result][jss::ledger].isMember(jss::hash) && - json[jss::result][jss::ledger][jss::hash].isString()); - return json[jss::result][jss::ledger][jss::hash].asString(); + json[jss::result][jss::ledger].isMember(jss::ledger_hash) && + json[jss::result][jss::ledger][jss::ledger_hash].isString()); + return json[jss::result][jss::ledger][jss::ledger_hash].asString(); } void diff --git a/src/test/rpc/LedgerRPC_test.cpp b/src/test/rpc/LedgerRPC_test.cpp index 0f4b1c823..4442800c5 100644 --- a/src/test/rpc/LedgerRPC_test.cpp +++ b/src/test/rpc/LedgerRPC_test.cpp @@ -84,10 +84,6 @@ public: env.rpc("json", "ledger", to_string(jvParams))[jss::result]; BEAST_EXPECT(jrr[jss::ledger][jss::closed] == true); BEAST_EXPECT(jrr[jss::ledger][jss::ledger_index] == "1"); - BEAST_EXPECT(jrr[jss::ledger][jss::accepted] == true); - BEAST_EXPECT( - jrr[jss::ledger][jss::totalCoins] == - env.balance(env.master).value().getText()); } { @@ -97,10 +93,6 @@ public: env.rpc("json", "ledger", to_string(jvParams))[jss::result]; BEAST_EXPECT(jrr[jss::ledger][jss::closed] == true); BEAST_EXPECT(jrr[jss::ledger][jss::ledger_index] == "1"); - BEAST_EXPECT(jrr[jss::ledger][jss::accepted] == true); - BEAST_EXPECT( - jrr[jss::ledger][jss::totalCoins] == - env.balance(env.master).value().getText()); } { From ff17f3190aa4ca263fca8bb400af05aef7eb45f1 Mon Sep 17 00:00:00 2001 From: Scott Schurr Date: Thu, 19 Feb 2026 14:05:54 +0900 Subject: [PATCH 010/301] refactor: rename ServerHandlerImp to ServerHandler (#4516) Rename `ServerHandlerImp` to `ServerHandler`. There was no other ServerHandler definition despite the existence of a header suggesting that there was. This resolves a piece of historical confusion in the code, which was identified during a code review. The changes in the diff may look more extensive than they actually are. The contents of `impl/ServerHandlerImp.h` were merged into `ServerHandler.h`, making the latter file appear to have undergone significant modifications. However, this is a non-breaking refactor that only restructures code. --- Builds/CMake/RippledCore.cmake | 2 +- src/ripple/rpc/ServerHandler.h | 214 +++++++++++++++++- ...ServerHandlerImp.cpp => ServerHandler.cpp} | 47 ++-- 3 files changed, 232 insertions(+), 31 deletions(-) rename src/ripple/rpc/impl/{ServerHandlerImp.cpp => ServerHandler.cpp} (98%) diff --git a/Builds/CMake/RippledCore.cmake b/Builds/CMake/RippledCore.cmake index 6afcd2c20..d6d60de76 100644 --- a/Builds/CMake/RippledCore.cmake +++ b/Builds/CMake/RippledCore.cmake @@ -691,7 +691,7 @@ target_sources (rippled PRIVATE src/ripple/rpc/impl/RPCHandler.cpp src/ripple/rpc/impl/RPCHelpers.cpp src/ripple/rpc/impl/Role.cpp - src/ripple/rpc/impl/ServerHandlerImp.cpp + src/ripple/rpc/impl/ServerHandler.cpp src/ripple/rpc/impl/ShardArchiveHandler.cpp src/ripple/rpc/impl/ShardVerificationScheduler.cpp src/ripple/rpc/impl/Status.cpp diff --git a/src/ripple/rpc/ServerHandler.h b/src/ripple/rpc/ServerHandler.h index 54cccdff6..fe7f418cb 100644 --- a/src/ripple/rpc/ServerHandler.h +++ b/src/ripple/rpc/ServerHandler.h @@ -20,21 +20,215 @@ #ifndef RIPPLE_RPC_SERVERHANDLER_H_INCLUDED #define RIPPLE_RPC_SERVERHANDLER_H_INCLUDED -#include -#include -#include +#include #include -#include -#include -#include -#include -#include -#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include #include namespace ripple { -using ServerHandler = ServerHandlerImp; +inline bool +operator<(Port const& lhs, Port const& rhs) +{ + return lhs.name < rhs.name; +} + +class ServerHandler +{ +public: + struct Setup + { + explicit Setup() = default; + + std::vector ports; + + // Memberspace + struct client_t + { + explicit client_t() = default; + + bool secure = false; + std::string ip; + std::uint16_t port = 0; + std::string user; + std::string password; + std::string admin_user; + std::string admin_password; + }; + + // Configuration when acting in client role + client_t client; + + // Configuration for the Overlay + struct overlay_t + { + explicit overlay_t() = default; + + boost::asio::ip::address ip; + std::uint16_t port = 0; + }; + + overlay_t overlay; + + void + makeContexts(); + }; + +private: + using socket_type = boost::beast::tcp_stream; + using stream_type = boost::beast::ssl_stream; + + Application& app_; + Resource::Manager& m_resourceManager; + beast::Journal m_journal; + NetworkOPs& m_networkOPs; + std::unique_ptr m_server; + Setup setup_; + JobQueue& m_jobQueue; + beast::insight::Counter rpc_requests_; + beast::insight::Event rpc_size_; + beast::insight::Event rpc_time_; + std::mutex mutex_; + std::condition_variable condition_; + bool stopped_{false}; + std::map, int> count_; + + // A private type used to restrict access to the ServerHandler constructor. + struct ServerHandlerCreator + { + explicit ServerHandlerCreator() = default; + }; + + // Friend declaration that allows make_ServerHandler to access the + // private type that restricts access to the ServerHandler ctor. + friend std::unique_ptr + make_ServerHandler( + Application& app, + boost::asio::io_service&, + JobQueue&, + NetworkOPs&, + Resource::Manager&, + CollectorManager& cm); + +public: + // Must be public so make_unique can call it. + ServerHandler( + ServerHandlerCreator const&, + Application& app, + boost::asio::io_service& io_service, + JobQueue& jobQueue, + NetworkOPs& networkOPs, + Resource::Manager& resourceManager, + CollectorManager& cm); + + ~ServerHandler(); + + using Output = Json::Output; + + void + setup(Setup const& setup, beast::Journal journal); + + Setup const& + setup() const + { + return setup_; + } + + void + stop(); + + // + // Handler + // + + bool + onAccept(Session& session, boost::asio::ip::tcp::endpoint endpoint); + + Handoff + onHandoff( + Session& session, + std::unique_ptr&& bundle, + http_request_type&& request, + boost::asio::ip::tcp::endpoint const& remote_address); + + Handoff + onHandoff( + Session& session, + http_request_type&& request, + boost::asio::ip::tcp::endpoint const& remote_address) + { + return onHandoff( + session, + {}, + std::forward(request), + remote_address); + } + + void + onRequest(Session& session); + + void + onWSMessage( + std::shared_ptr session, + std::vector const& buffers); + + void + onUDPMessage( + std::string const& message, + boost::asio::ip::tcp::endpoint const& remoteEndpoint, + std::function sendResponse); + + void + onClose(Session& session, boost::system::error_code const&); + + void + onStopped(Server&); + +private: + Json::Value + processSession( + std::shared_ptr const& session, + std::shared_ptr const& coro, + Json::Value const& jv); + + Json::Value + processUDP( + Json::Value const& jv, + Role const& role, + std::shared_ptr const& coro, + std::optional> sendResponse, + boost::asio::ip::tcp::endpoint const& remoteEndpoint); + + void + processSession( + std::shared_ptr const&, + std::shared_ptr coro); + + void + processRequest( + Port const& port, + std::string const& request, + beast::IP::Endpoint const& remoteIPAddress, + Output&&, + std::shared_ptr coro, + boost::string_view forwardedFor, + boost::string_view user); + + Handoff + statusResponse(http_request_type const& request) const; +}; ServerHandler::Setup setup_ServerHandler(Config const& c, std::ostream&& log); diff --git a/src/ripple/rpc/impl/ServerHandlerImp.cpp b/src/ripple/rpc/impl/ServerHandler.cpp similarity index 98% rename from src/ripple/rpc/impl/ServerHandlerImp.cpp rename to src/ripple/rpc/impl/ServerHandler.cpp index 5c9f43538..d5dc4832f 100644 --- a/src/ripple/rpc/impl/ServerHandlerImp.cpp +++ b/src/ripple/rpc/impl/ServerHandler.cpp @@ -17,6 +17,8 @@ */ //============================================================================== +#include + #include #include #include @@ -35,9 +37,7 @@ #include #include #include -#include #include -#include #include #include #include @@ -101,7 +101,8 @@ authorized(Port const& port, std::map const& h) return strUser == port.user && strPassword == port.password; } -ServerHandlerImp::ServerHandlerImp( +ServerHandler::ServerHandler( + ServerHandlerCreator const&, Application& app, boost::asio::io_service& io_service, JobQueue& jobQueue, @@ -121,13 +122,13 @@ ServerHandlerImp::ServerHandlerImp( rpc_time_ = group->make_event("time"); } -ServerHandlerImp::~ServerHandlerImp() +ServerHandler::~ServerHandler() { m_server = nullptr; } void -ServerHandlerImp::setup(Setup const& setup, beast::Journal journal) +ServerHandler::setup(Setup const& setup, beast::Journal journal) { setup_ = setup; m_server->ports(setup.ports); @@ -136,7 +137,7 @@ ServerHandlerImp::setup(Setup const& setup, beast::Journal journal) //------------------------------------------------------------------------------ void -ServerHandlerImp::stop() +ServerHandler::stop() { m_server->close(); { @@ -148,7 +149,7 @@ ServerHandlerImp::stop() //------------------------------------------------------------------------------ bool -ServerHandlerImp::onAccept( +ServerHandler::onAccept( Session& session, boost::asio::ip::tcp::endpoint endpoint) { @@ -170,7 +171,7 @@ ServerHandlerImp::onAccept( } Handoff -ServerHandlerImp::onHandoff( +ServerHandler::onHandoff( Session& session, std::unique_ptr&& bundle, http_request_type&& request, @@ -272,7 +273,7 @@ buffers_to_string(ConstBufferSequence const& bs) } void -ServerHandlerImp::onRequest(Session& session) +ServerHandler::onRequest(Session& session) { // Make sure RPC is enabled on the port if (session.port().protocol.count("http") == 0 && @@ -312,7 +313,7 @@ ServerHandlerImp::onRequest(Session& session) } void -ServerHandlerImp::onWSMessage( +ServerHandler::onWSMessage( std::shared_ptr session, std::vector const& buffers) { @@ -362,7 +363,7 @@ ServerHandlerImp::onWSMessage( } void -ServerHandlerImp::onUDPMessage( +ServerHandler::onUDPMessage( std::string const& message, boost::asio::ip::tcp::endpoint const& remoteEndpoint, std::function sendResponse) @@ -423,14 +424,14 @@ ServerHandlerImp::onUDPMessage( } void -ServerHandlerImp::onClose(Session& session, boost::system::error_code const&) +ServerHandler::onClose(Session& session, boost::system::error_code const&) { std::lock_guard lock(mutex_); --count_[session.port()]; } void -ServerHandlerImp::onStopped(Server&) +ServerHandler::onStopped(Server&) { std::lock_guard lock(mutex_); stopped_ = true; @@ -459,7 +460,7 @@ logDuration( } Json::Value -ServerHandlerImp::processUDP( +ServerHandler::processUDP( Json::Value const& jv, Role const& role, std::shared_ptr const& coro, @@ -593,7 +594,7 @@ ServerHandlerImp::processUDP( } Json::Value -ServerHandlerImp::processSession( +ServerHandler::processSession( std::shared_ptr const& session, std::shared_ptr const& coro, Json::Value const& jv) @@ -740,7 +741,7 @@ ServerHandlerImp::processSession( // Run as a coroutine. void -ServerHandlerImp::processSession( +ServerHandler::processSession( std::shared_ptr const& session, std::shared_ptr coro) { @@ -781,7 +782,7 @@ Json::Int constexpr forbidden = -32605; Json::Int constexpr wrong_version = -32606; void -ServerHandlerImp::processRequest( +ServerHandler::processRequest( Port const& port, std::string const& request, beast::IP::Endpoint const& remoteIPAddress, @@ -1217,7 +1218,7 @@ ServerHandlerImp::processRequest( is reported, meaning the server can accept more connections. */ Handoff -ServerHandlerImp::statusResponse(http_request_type const& request) const +ServerHandler::statusResponse(http_request_type const& request) const { using namespace boost::beast::http; Handoff handoff; @@ -1447,8 +1448,14 @@ make_ServerHandler( Resource::Manager& resourceManager, CollectorManager& cm) { - return std::make_unique( - app, io_service, jobQueue, networkOPs, resourceManager, cm); + return std::make_unique( + ServerHandler::ServerHandlerCreator(), + app, + io_service, + jobQueue, + networkOPs, + resourceManager, + cm); } } // namespace ripple From 6e1a7638068ad29929767a6bee210e0359b3d586 Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Thu, 19 Feb 2026 14:05:54 +0900 Subject: [PATCH 011/301] Fix build references to deleted ServerHandlerImp: (#4592) * Commits 0b812cd (#4427) and 11e914f (#4516) conflict. The first added references to `ServerHandlerImp` in files outside of that class's organizational unit (which is technically incorrect). The second removed `ServerHandlerImp`, but was not up to date with develop. This results in the build failing. * Fixes the build by changing references to `ServerHandlerImp` to the more correct `ServerHandler`. --- src/ripple/app/main/Application.cpp | 2 +- src/ripple/app/main/Application.h | 4 ++-- src/ripple/app/misc/NetworkOPs.cpp | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/ripple/app/main/Application.cpp b/src/ripple/app/main/Application.cpp index 036374aaf..6f86dee06 100644 --- a/src/ripple/app/main/Application.cpp +++ b/src/ripple/app/main/Application.cpp @@ -605,7 +605,7 @@ public: return *m_networkOPs; } - virtual ServerHandlerImp& + virtual ServerHandler& getServerHandler() override { assert(serverHandler_); diff --git a/src/ripple/app/main/Application.h b/src/ripple/app/main/Application.h index f10003c80..c1697a4a7 100644 --- a/src/ripple/app/main/Application.h +++ b/src/ripple/app/main/Application.h @@ -88,7 +88,7 @@ class Overlay; class PathRequests; class PendingSaves; class PublicKey; -class ServerHandlerImp; +class ServerHandler; class SecretKey; class STLedgerEntry; class TimeKeeper; @@ -231,7 +231,7 @@ public: getOPs() = 0; virtual OrderBookDB& getOrderBookDB() = 0; - virtual ServerHandlerImp& + virtual ServerHandler& getServerHandler() = 0; virtual TransactionMaster& getMasterTransaction() = 0; diff --git a/src/ripple/app/misc/NetworkOPs.cpp b/src/ripple/app/misc/NetworkOPs.cpp index a98143b8d..f40b9028b 100644 --- a/src/ripple/app/misc/NetworkOPs.cpp +++ b/src/ripple/app/misc/NetworkOPs.cpp @@ -67,8 +67,8 @@ #include #include #include +#include #include -#include #include #include #include From e0566cc22a137a4e6efd06caddd605aab8b081be Mon Sep 17 00:00:00 2001 From: Peter Chen <34582813+PeterChen13579@users.noreply.github.com> Date: Thu, 19 Feb 2026 14:05:55 +0900 Subject: [PATCH 012/301] APIv2: add error messages for account_tx (#4571) Certain inputs for the AccountTx method should return an error. In other words, an invalid request from a user or client now results in an error message. Since this can change the response from the API, it is an API breaking change. This commit maintains backward compatibility by keeping the existing behavior for existing requests. When clients specify "api_version": 2, they will be able to get the updated error messages. Update unit tests to check the error based on the API version. * Fix #4288 * Fix #4545 --- src/ripple/rpc/handlers/AccountTx.cpp | 29 ++- src/test/rpc/AccountTx_test.cpp | 251 +++++++++++++++++--------- 2 files changed, 192 insertions(+), 88 deletions(-) diff --git a/src/ripple/rpc/handlers/AccountTx.cpp b/src/ripple/rpc/handlers/AccountTx.cpp index f65657d92..bfbc76362 100644 --- a/src/ripple/rpc/handlers/AccountTx.cpp +++ b/src/ripple/rpc/handlers/AccountTx.cpp @@ -53,9 +53,23 @@ using LedgerSpecifier = RelationalDatabase::LedgerSpecifier; // parses args into a ledger specifier, or returns a Json object on error std::variant, Json::Value> -parseLedgerArgs(Json::Value const& params) +parseLedgerArgs(RPC::Context& context, Json::Value const& params) { Json::Value response; + // if ledger_index_min or max is specified, then ledger_hash or ledger_index + // should not be specified. Error out if it is + if (context.apiVersion > 1) + { + if ((params.isMember(jss::ledger_index_min) || + params.isMember(jss::ledger_index_max)) && + (params.isMember(jss::ledger_hash) || + params.isMember(jss::ledger_index))) + { + RPC::Status status{rpcINVALID_PARAMS, "invalidParams"}; + status.inject(response); + return response; + } + } if (params.isMember(jss::ledger_index_min) || params.isMember(jss::ledger_index_max)) { @@ -145,6 +159,17 @@ getLedgerRange( using T = std::decay_t; if constexpr (std::is_same_v) { + // if ledger_index_min or ledger_index_max is out of + // valid ledger range, error out. exclude -1 as + // it is a valid input + if (context.apiVersion > 1) + { + if ((ls.max > uValidatedMax && ls.max != -1) || + (ls.min < uValidatedMin && ls.min != 0)) + { + return rpcLGR_IDX_MALFORMED; + } + } if (ls.min > uValidatedMin) { uLedgerMin = ls.min; @@ -379,7 +404,7 @@ doAccountTxJson(RPC::JsonContext& context) args.account = *account; - auto parseRes = parseLedgerArgs(params); + auto parseRes = parseLedgerArgs(context, params); if (auto jv = std::get_if(&parseRes)) { return *jv; diff --git a/src/test/rpc/AccountTx_test.cpp b/src/test/rpc/AccountTx_test.cpp index aaed4defe..4d112a19b 100644 --- a/src/test/rpc/AccountTx_test.cpp +++ b/src/test/rpc/AccountTx_test.cpp @@ -111,7 +111,7 @@ class AccountTx_test : public beast::unit_test::suite }; void - testParameters() + testParameters(unsigned int apiVersion) { using namespace test::jtx; @@ -146,104 +146,178 @@ class AccountTx_test : public beast::unit_test::suite }; Json::Value jParms; + jParms[jss::api_version] = apiVersion; - BEAST_EXPECT(isErr( - env.rpc("json", "account_tx", to_string(jParms)), - rpcINVALID_PARAMS)); - - jParms[jss::account] = "0xDEADBEEF"; - - BEAST_EXPECT(isErr( - env.rpc("json", "account_tx", to_string(jParms)), - rpcACT_MALFORMED)); - - jParms[jss::account] = A1.human(); - BEAST_EXPECT(hasTxs(env.rpc("json", "account_tx", to_string(jParms)))); - - // Ledger min/max index + if (apiVersion < 2) { - Json::Value p{jParms}; - p[jss::ledger_index_min] = -1; - p[jss::ledger_index_max] = -1; - BEAST_EXPECT(hasTxs(env.rpc("json", "account_tx", to_string(p)))); - - p[jss::ledger_index_min] = 0; - p[jss::ledger_index_max] = 100; - BEAST_EXPECT(hasTxs(env.rpc("json", "account_tx", to_string(p)))); - - p[jss::ledger_index_min] = 1; - p[jss::ledger_index_max] = 2; - BEAST_EXPECT(noTxs(env.rpc("json", "account_tx", to_string(p)))); - - p[jss::ledger_index_min] = 2; - p[jss::ledger_index_max] = 1; BEAST_EXPECT(isErr( - env.rpc("json", "account_tx", to_string(p)), - (RPC::apiMaximumSupportedVersion == 1 ? rpcLGR_IDXS_INVALID - : rpcINVALID_LGR_RANGE))); - } + env.rpc("json", "account_tx", to_string(jParms)), + rpcINVALID_PARAMS)); - // Ledger index min only - { - Json::Value p{jParms}; - p[jss::ledger_index_min] = -1; - BEAST_EXPECT(hasTxs(env.rpc("json", "account_tx", to_string(p)))); + jParms[jss::account] = "0xDEADBEEF"; - p[jss::ledger_index_min] = 1; - BEAST_EXPECT(hasTxs(env.rpc("json", "account_tx", to_string(p)))); - - p[jss::ledger_index_min] = env.current()->info().seq; BEAST_EXPECT(isErr( - env.rpc("json", "account_tx", to_string(p)), - (RPC::apiMaximumSupportedVersion == 1 ? rpcLGR_IDXS_INVALID - : rpcINVALID_LGR_RANGE))); + env.rpc("json", "account_tx", to_string(jParms)), + rpcACT_MALFORMED)); + + jParms[jss::account] = A1.human(); + BEAST_EXPECT( + hasTxs(env.rpc("json", "account_tx", to_string(jParms)))); + + // Ledger min/max index + { + Json::Value p{jParms}; + p[jss::ledger_index_min] = -1; + p[jss::ledger_index_max] = -1; + BEAST_EXPECT( + hasTxs(env.rpc("json", "account_tx", to_string(p)))); + + p[jss::ledger_index_min] = 0; + p[jss::ledger_index_max] = 100; + BEAST_EXPECT( + hasTxs(env.rpc("json", "account_tx", to_string(p)))); + + p[jss::ledger_index_min] = 1; + p[jss::ledger_index_max] = 2; + BEAST_EXPECT( + noTxs(env.rpc("json", "account_tx", to_string(p)))); + + p[jss::ledger_index_min] = 2; + p[jss::ledger_index_max] = 1; + BEAST_EXPECT(isErr( + env.rpc("json", "account_tx", to_string(p)), + (RPC::apiMaximumSupportedVersion == 1 + ? rpcLGR_IDXS_INVALID + : rpcINVALID_LGR_RANGE))); + } + + // Ledger index min only + { + Json::Value p{jParms}; + p[jss::ledger_index_min] = -1; + BEAST_EXPECT( + hasTxs(env.rpc("json", "account_tx", to_string(p)))); + + p[jss::ledger_index_min] = 1; + BEAST_EXPECT( + hasTxs(env.rpc("json", "account_tx", to_string(p)))); + + p[jss::ledger_index_min] = env.current()->info().seq; + BEAST_EXPECT(isErr( + env.rpc("json", "account_tx", to_string(p)), + (RPC::apiMaximumSupportedVersion == 1 + ? rpcLGR_IDXS_INVALID + : rpcINVALID_LGR_RANGE))); + } + + // Ledger index max only + { + Json::Value p{jParms}; + p[jss::ledger_index_max] = -1; + BEAST_EXPECT( + hasTxs(env.rpc("json", "account_tx", to_string(p)))); + + p[jss::ledger_index_max] = env.current()->info().seq; + BEAST_EXPECT( + hasTxs(env.rpc("json", "account_tx", to_string(p)))); + + p[jss::ledger_index_max] = 3; + BEAST_EXPECT( + hasTxs(env.rpc("json", "account_tx", to_string(p)))); + + p[jss::ledger_index_max] = env.closed()->info().seq; + BEAST_EXPECT( + hasTxs(env.rpc("json", "account_tx", to_string(p)))); + + p[jss::ledger_index_max] = env.closed()->info().seq - 1; + BEAST_EXPECT( + noTxs(env.rpc("json", "account_tx", to_string(p)))); + } + + // Ledger Sequence + { + Json::Value p{jParms}; + + p[jss::ledger_index] = env.closed()->info().seq; + BEAST_EXPECT( + hasTxs(env.rpc("json", "account_tx", to_string(p)))); + + p[jss::ledger_index] = env.closed()->info().seq - 1; + BEAST_EXPECT( + noTxs(env.rpc("json", "account_tx", to_string(p)))); + + p[jss::ledger_index] = env.current()->info().seq; + BEAST_EXPECT(isErr( + env.rpc("json", "account_tx", to_string(p)), + rpcLGR_NOT_VALIDATED)); + + p[jss::ledger_index] = env.current()->info().seq + 1; + BEAST_EXPECT(isErr( + env.rpc("json", "account_tx", to_string(p)), + rpcLGR_NOT_FOUND)); + } + + // Ledger Hash + { + Json::Value p{jParms}; + + p[jss::ledger_hash] = to_string(env.closed()->info().hash); + BEAST_EXPECT( + hasTxs(env.rpc("json", "account_tx", to_string(p)))); + + p[jss::ledger_hash] = + to_string(env.closed()->info().parentHash); + BEAST_EXPECT( + noTxs(env.rpc("json", "account_tx", to_string(p)))); + } } - - // Ledger index max only + else { - Json::Value p{jParms}; - p[jss::ledger_index_max] = -1; - BEAST_EXPECT(hasTxs(env.rpc("json", "account_tx", to_string(p)))); + // Ledger index max/min/index all specified + // ERRORS out with invalid Parenthesis + { + jParms[jss::account] = "0xDEADBEEF"; + jParms[jss::account] = A1.human(); + Json::Value p{jParms}; - p[jss::ledger_index_max] = env.current()->info().seq; - BEAST_EXPECT(hasTxs(env.rpc("json", "account_tx", to_string(p)))); + p[jss::ledger_index_max] = -1; + p[jss::ledger_index_min] = -1; + p[jss::ledger_index] = -1; - p[jss::ledger_index_max] = env.closed()->info().seq; - BEAST_EXPECT(hasTxs(env.rpc("json", "account_tx", to_string(p)))); + BEAST_EXPECT(isErr( + env.rpc("json", "account_tx", to_string(p)), + rpcINVALID_PARAMS)); + } - p[jss::ledger_index_max] = env.closed()->info().seq - 1; - BEAST_EXPECT(noTxs(env.rpc("json", "account_tx", to_string(p)))); - } + // Ledger index min/max only + { + Json::Value p{jParms}; + p[jss::ledger_index_max] = 100; + p[jss::ledger_index_min] = 0; + BEAST_EXPECT(isErr( + env.rpc("json", "account_tx", to_string(p)), + rpcLGR_IDX_MALFORMED)); - // Ledger Sequence - { - Json::Value p{jParms}; + p[jss::ledger_index_max] = -1; + p[jss::ledger_index_min] = -1; + BEAST_EXPECT( + hasTxs(env.rpc("json", "account_tx", to_string(p)))); - p[jss::ledger_index] = env.closed()->info().seq; - BEAST_EXPECT(hasTxs(env.rpc("json", "account_tx", to_string(p)))); + p[jss::ledger_index_min] = 2; + p[jss::ledger_index_max] = 1; + BEAST_EXPECT(isErr( + env.rpc("json", "account_tx", to_string(p)), + rpcINVALID_LGR_RANGE)); + } - p[jss::ledger_index] = env.closed()->info().seq - 1; - BEAST_EXPECT(noTxs(env.rpc("json", "account_tx", to_string(p)))); - - p[jss::ledger_index] = env.current()->info().seq; - BEAST_EXPECT(isErr( - env.rpc("json", "account_tx", to_string(p)), - rpcLGR_NOT_VALIDATED)); - - p[jss::ledger_index] = env.current()->info().seq + 1; - BEAST_EXPECT(isErr( - env.rpc("json", "account_tx", to_string(p)), rpcLGR_NOT_FOUND)); - } - - // Ledger Hash - { - Json::Value p{jParms}; - - p[jss::ledger_hash] = to_string(env.closed()->info().hash); - BEAST_EXPECT(hasTxs(env.rpc("json", "account_tx", to_string(p)))); - - p[jss::ledger_hash] = to_string(env.closed()->info().parentHash); - BEAST_EXPECT(noTxs(env.rpc("json", "account_tx", to_string(p)))); + // Ledger index max only + { + Json::Value p{jParms}; + p[jss::ledger_index_max] = env.current()->info().seq; + BEAST_EXPECT(isErr( + env.rpc("json", "account_tx", to_string(p)), + rpcLGR_IDX_MALFORMED)); + } } } @@ -778,7 +852,12 @@ public: void run() override { - testParameters(); + for (auto testVersion = RPC::apiMinimumSupportedVersion; + testVersion <= RPC::apiBetaVersion; + ++testVersion) + { + testParameters(testVersion); + } testContents(); testAccountDelete(); } From 21cacbfd5f775e88ef7b7e2f7e8fcdb4917cb148 Mon Sep 17 00:00:00 2001 From: Chenna Keshava B S <21219765+ckeshava@users.noreply.github.com> Date: Thu, 19 Feb 2026 14:05:55 +0900 Subject: [PATCH 013/301] fix: Update Handler::Condition enum values #3417 (#4239) - Use powers of two to clearly indicate the bitmask - Replace bitmask with explicit if-conditions to better indicate predicates Change enum values to be powers of two (fix #3417) #4239 Implement the simplified condition evaluation removes the complex bitwise and(&) operator Implement the second proposed solution in Nik Bougalis's comment - Software does not distinguish between different Conditions (Version: 1.5) #3417 (comment) I have tested this code change by performing RPC calls with the commands server_info, server_state, peers and validation_info. These commands worked as expected. --- src/ripple/rpc/impl/Handler.h | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/src/ripple/rpc/impl/Handler.h b/src/ripple/rpc/impl/Handler.h index 73f2232d5..e2188ef51 100644 --- a/src/ripple/rpc/impl/Handler.h +++ b/src/ripple/rpc/impl/Handler.h @@ -39,8 +39,8 @@ namespace RPC { enum Condition { NO_CONDITION = 0, NEEDS_NETWORK_CONNECTION = 1, - NEEDS_CURRENT_LEDGER = 2 + NEEDS_NETWORK_CONNECTION, - NEEDS_CLOSED_LEDGER = 4 + NEEDS_NETWORK_CONNECTION, + NEEDS_CURRENT_LEDGER = 1 << 1, + NEEDS_CLOSED_LEDGER = 1 << 2, }; struct Handler @@ -94,20 +94,18 @@ conditionMet(Condition condition_required, T& context) } if (context.app.getOPs().isAmendmentBlocked() && - (condition_required & NEEDS_CURRENT_LEDGER || - condition_required & NEEDS_CLOSED_LEDGER)) + (condition_required != NO_CONDITION)) { return rpcAMENDMENT_BLOCKED; } if (context.app.getOPs().isUNLBlocked() && - (condition_required & NEEDS_CURRENT_LEDGER || - condition_required & NEEDS_CLOSED_LEDGER)) + (condition_required != NO_CONDITION)) { return rpcEXPIRED_VALIDATOR_LIST; } - if ((condition_required & NEEDS_NETWORK_CONNECTION) && + if ((condition_required != NO_CONDITION) && (context.netOps.getOperatingMode() < OperatingMode::SYNCING)) { JLOG(context.j.info()) << "Insufficient network mode for RPC: " @@ -119,7 +117,7 @@ conditionMet(Condition condition_required, T& context) } if (!context.app.config().standalone() && - condition_required & NEEDS_CURRENT_LEDGER) + condition_required != NO_CONDITION) { if (context.ledgerMaster.getValidatedLedgerAge() > Tuning::maxValidatedLedgerAge) @@ -143,7 +141,7 @@ conditionMet(Condition condition_required, T& context) } } - if ((condition_required & NEEDS_CLOSED_LEDGER) && + if ((condition_required != NO_CONDITION) && !context.ledgerMaster.getClosedLedger()) { if (context.apiVersion == 1) From 4085036e2529b75752fc7fc56996b13ff43886c6 Mon Sep 17 00:00:00 2001 From: Peter Chen <34582813+PeterChen13579@users.noreply.github.com> Date: Thu, 19 Feb 2026 14:05:56 +0900 Subject: [PATCH 014/301] APIv2(account_info): handle invalid "signer_lists" value (#4585) When requesting `account_info` with an invalid `signer_lists` value, the API should return an "invalidParams" error. `signer_lists` should have a value of type boolean. If it is not a boolean, then it is invalid input. The response now indicates that. * This is an API breaking change, so the change is only reflected for requests containing `"api_version": 2` * Fix #4539 --- src/ripple/rpc/handlers/AccountInfo.cpp | 9 +++++++++ src/test/rpc/AccountInfo_test.cpp | 11 +++++++++++ 2 files changed, 20 insertions(+) diff --git a/src/ripple/rpc/handlers/AccountInfo.cpp b/src/ripple/rpc/handlers/AccountInfo.cpp index ed5bc8a4e..fbb826256 100644 --- a/src/ripple/rpc/handlers/AccountInfo.cpp +++ b/src/ripple/rpc/handlers/AccountInfo.cpp @@ -137,6 +137,15 @@ doAccountInfo(RPC::JsonContext& context) result[jss::account_flags] = std::move(acctFlags); + // The document states that signer_lists is a bool, however + // assigning any string value works. Do not allow this. + // This check is for api Version 2 onwards only + if (!params[jss::signer_lists].isBool() && context.apiVersion > 1) + { + RPC::inject_error(rpcINVALID_PARAMS, result); + return result; + } + // Return SignerList(s) if that is requested. if (params.isMember(jss::signer_lists) && params[jss::signer_lists].asBool()) diff --git a/src/test/rpc/AccountInfo_test.cpp b/src/test/rpc/AccountInfo_test.cpp index 11100cf7c..331611cf2 100644 --- a/src/test/rpc/AccountInfo_test.cpp +++ b/src/test/rpc/AccountInfo_test.cpp @@ -217,6 +217,10 @@ public: "\"api_version\": 2, \"account\": \"" + alice.human() + "\", " + "\"signer_lists\": true }"; + auto const withSignersAsString = std::string("{ ") + + "\"api_version\": 2, \"account\": \"" + alice.human() + "\", " + + "\"signer_lists\": asdfggh }"; + // Alice has no SignerList yet. { // account_info without the "signer_lists" argument. @@ -263,6 +267,13 @@ public: auto const& entry0 = signerEntries[0u][sfSignerEntry.jsonName]; BEAST_EXPECT(entry0[sfSignerWeight.jsonName] == 3); } + { + // account_info with "signer_lists" as not bool should error out + auto const info = + env.rpc("json", "account_info", withSignersAsString); + BEAST_EXPECT(info[jss::status] == "error"); + BEAST_EXPECT(info[jss::error] == "invalidParams"); + } // Give alice a big signer list Account const demon{"demon"}; From 706b0d2e31d565667cc05fd296d330b1d639920e Mon Sep 17 00:00:00 2001 From: Chenna Keshava B S <21219765+ckeshava@users.noreply.github.com> Date: Thu, 19 Feb 2026 14:06:18 +0900 Subject: [PATCH 015/301] refactor: change the return type of mulDiv to std::optional (#4243) - Previously, mulDiv had `std::pair` as the output type. - This is an error-prone interface as it is easy to ignore when overflow occurs. - Using a return type of `std::optional` should decrease the likelihood of ignoring overflow. - It also allows for the use of optional::value_or() as a way to explicitly recover from overflow. - Include limits.h header file preprocessing directive in order to satisfy gcc's numeric_limits incomplete_type requirement. Fix #3495 --------- Co-authored-by: John Freeman --- src/ripple/app/misc/DatagramMonitor.h | 3 +- src/ripple/app/misc/NetworkOPs.cpp | 4 +-- src/ripple/app/misc/TxQ.h | 15 +++------ src/ripple/app/misc/impl/LoadFeeTrack.cpp | 4 +-- src/ripple/app/misc/impl/TxQ.cpp | 32 +++++++++++--------- src/ripple/basics/FeeUnits.h | 32 +++++++++++--------- src/ripple/basics/impl/mulDiv.cpp | 14 +++------ src/ripple/basics/mulDiv.h | 14 ++++----- src/ripple/rpc/handlers/Fee1.cpp | 1 - src/ripple/rpc/impl/TransactionSign.cpp | 4 +-- src/test/app/TxQ_test.cpp | 1 - src/test/basics/FeeUnits_test.cpp | 37 +++++++++++++++-------- src/test/basics/mulDiv_test.cpp | 18 +++++------ 13 files changed, 95 insertions(+), 84 deletions(-) diff --git a/src/ripple/app/misc/DatagramMonitor.h b/src/ripple/app/misc/DatagramMonitor.h index 033090581..bf78f97c6 100644 --- a/src/ripple/app/misc/DatagramMonitor.h +++ b/src/ripple/app/misc/DatagramMonitor.h @@ -11,6 +11,7 @@ #include #include #include +#include #include #include #include @@ -834,7 +835,7 @@ private: escalationMetrics.openLedgerFeeLevel, loadBaseServer, escalationMetrics.referenceFeeLevel) - .second; + .value_or(muldiv_max); header->load_factor = std::max( safe_cast(loadFactorServer), diff --git a/src/ripple/app/misc/NetworkOPs.cpp b/src/ripple/app/misc/NetworkOPs.cpp index f40b9028b..0e5f88e7b 100644 --- a/src/ripple/app/misc/NetworkOPs.cpp +++ b/src/ripple/app/misc/NetworkOPs.cpp @@ -2055,7 +2055,7 @@ NetworkOPsImp::pubServer() f.em->openLedgerFeeLevel, f.loadBaseServer, f.em->referenceFeeLevel) - .second); + .value_or(ripple::muldiv_max)); jvObj[jss::load_factor] = trunc32(loadFactor); jvObj[jss::load_factor_fee_escalation] = @@ -2549,7 +2549,7 @@ NetworkOPsImp::getServerInfo(bool human, bool admin, bool counters) escalationMetrics.openLedgerFeeLevel, loadBaseServer, escalationMetrics.referenceFeeLevel) - .second; + .value_or(ripple::muldiv_max); auto const loadFactor = std::max( safe_cast(loadFactorServer), diff --git a/src/ripple/app/misc/TxQ.h b/src/ripple/app/misc/TxQ.h index 65bcecde7..a50221878 100644 --- a/src/ripple/app/misc/TxQ.h +++ b/src/ripple/app/misc/TxQ.h @@ -504,7 +504,7 @@ private: @param seriesSize Total number of transactions in the series to be processed. - @return A `std::pair` as returned from @ref `mulDiv` indicating + @return A `std::pair` indicating whether the calculation result overflows. */ static std::pair @@ -875,20 +875,15 @@ template XRPAmount toDrops(FeeLevel const& level, XRPAmount baseFee) { - if (auto const drops = mulDiv(level, baseFee, TxQ::baseLevel); drops.first) - return drops.second; - - return XRPAmount(STAmount::cMaxNativeN); + return mulDiv(level, baseFee, TxQ::baseLevel) + .value_or(XRPAmount(STAmount::cMaxNativeN)); } inline FeeLevel64 toFeeLevel(XRPAmount const& drops, XRPAmount const& baseFee) { - if (auto const feeLevel = mulDiv(drops, TxQ::baseLevel, baseFee); - feeLevel.first) - return feeLevel.second; - - return FeeLevel64(std::numeric_limits::max()); + return mulDiv(drops, TxQ::baseLevel, baseFee) + .value_or(FeeLevel64(std::numeric_limits::max())); } } // namespace ripple diff --git a/src/ripple/app/misc/impl/LoadFeeTrack.cpp b/src/ripple/app/misc/impl/LoadFeeTrack.cpp index 11679c9a6..86d145c85 100644 --- a/src/ripple/app/misc/impl/LoadFeeTrack.cpp +++ b/src/ripple/app/misc/impl/LoadFeeTrack.cpp @@ -109,9 +109,9 @@ scaleFeeLoad( auto const result = mulDiv( fee, feeFactor, safe_cast(feeTrack.getLoadBase())); - if (!result.first) + if (!result) Throw("scaleFeeLoad"); - return result.second; + return *result; } } // namespace ripple diff --git a/src/ripple/app/misc/impl/TxQ.cpp b/src/ripple/app/misc/impl/TxQ.cpp index 5c4af9f8b..db27604d6 100644 --- a/src/ripple/app/misc/impl/TxQ.cpp +++ b/src/ripple/app/misc/impl/TxQ.cpp @@ -61,12 +61,8 @@ getFeeLevelPaid(ReadView const& view, STTx const& tx) return FeeLevel64(0); } - if (std::pair const feeLevelPaid = - mulDiv(effectiveFeePaid, TxQ::baseLevel, baseFee); - feeLevelPaid.first) - return feeLevelPaid.second; - - return FeeLevel64(std::numeric_limits::max()); + return mulDiv(effectiveFeePaid, TxQ::baseLevel, baseFee) + .value_or(FeeLevel64(std::numeric_limits::max())); } static std::optional @@ -88,7 +84,8 @@ getFirstLedgerSequence(STTx const& tx) static FeeLevel64 increase(FeeLevel64 level, std::uint32_t increasePercent) { - return mulDiv(level, 100 + increasePercent, 100).second; + return mulDiv(level, 100 + increasePercent, 100) + .value_or(static_cast(ripple::muldiv_max)); } ////////////////////////////////////////////////////////////////////////// @@ -132,16 +129,19 @@ TxQ::FeeMetrics::update( // upperLimit must be >= minimumTxnCount_ or std::clamp can give // unexpected results auto const upperLimit = std::max( - mulDiv(txnsExpected_, cutPct, 100).second, minimumTxnCount_); + mulDiv(txnsExpected_, cutPct, 100).value_or(ripple::muldiv_max), + minimumTxnCount_); txnsExpected_ = std::clamp( - mulDiv(size, cutPct, 100).second, minimumTxnCount_, upperLimit); + mulDiv(size, cutPct, 100).value_or(ripple::muldiv_max), + minimumTxnCount_, + upperLimit); recentTxnCounts_.clear(); } else if (size > txnsExpected_ || size > targetTxnCount_) { recentTxnCounts_.push_back( mulDiv(size, 100 + setup.normalConsensusIncreasePercent, 100) - .second); + .value_or(ripple::muldiv_max)); auto const iter = std::max_element(recentTxnCounts_.begin(), recentTxnCounts_.end()); BOOST_ASSERT(iter != recentTxnCounts_.end()); @@ -199,7 +199,8 @@ TxQ::FeeMetrics::scaleFeeLevel(Snapshot const& snapshot, OpenView const& view) { // Compute escalated fee level // Don't care about the overflow flag - return mulDiv(multiplier, current * current, target * target).second; + return mulDiv(multiplier, current * current, target * target) + .value_or(static_cast(ripple::muldiv_max)); } return baseLevel; @@ -282,7 +283,7 @@ TxQ::FeeMetrics::escalatedSeriesFeeLevel( auto const totalFeeLevel = mulDiv( multiplier, sumNlast.second - sumNcurrent.second, target * target); - return totalFeeLevel; + return {totalFeeLevel.has_value(), *totalFeeLevel}; } LedgerHash TxQ::MaybeTx::parentHashComp{}; @@ -2057,8 +2058,11 @@ TxQ::getTxRequiredFeeAndSeq( std::uint32_t const accountSeq = sle ? (*sle)[sfSequence] : 0; std::uint32_t const availableSeq = nextQueuableSeqImpl(sle, lock).value(); - - return {mulDiv(fee, baseFee, baseLevel).second, accountSeq, availableSeq}; + return { + mulDiv(fee, baseFee, baseLevel) + .value_or(XRPAmount(std::numeric_limits::max())), + accountSeq, + availableSeq}; } std::vector diff --git a/src/ripple/basics/FeeUnits.h b/src/ripple/basics/FeeUnits.h index c74524c7c..c0f1afbe6 100644 --- a/src/ripple/basics/FeeUnits.h +++ b/src/ripple/basics/FeeUnits.h @@ -409,7 +409,7 @@ template < class Source2, class Dest, class = enable_muldiv_t> -std::pair +std::optional mulDivU(Source1 value, Dest mul, Source2 div) { // Fees can never be negative in any context. @@ -420,7 +420,7 @@ mulDivU(Source1 value, Dest mul, Source2 div) assert(value.value() >= 0); assert(mul.value() >= 0); assert(div.value() >= 0); - return {false, Dest{0}}; + return std::nullopt; } using desttype = typename Dest::value_type; @@ -428,12 +428,12 @@ mulDivU(Source1 value, Dest mul, Source2 div) // Shortcuts, since these happen a lot in the real world if (value == div) - return {true, mul}; + return mul; if (mul.value() == div.value()) { if (value.value() > max) - return {false, Dest{max}}; - return {true, Dest{static_cast(value.value())}}; + return std::nullopt; + return Dest{static_cast(value.value())}; } using namespace boost::multiprecision; @@ -447,9 +447,9 @@ mulDivU(Source1 value, Dest mul, Source2 div) auto quotient = product / div.value(); if (quotient > max) - return {false, Dest{max}}; + return std::nullopt; - return {true, Dest{static_cast(quotient)}}; + return Dest{static_cast(quotient)}; } } // namespace feeunit @@ -464,7 +464,7 @@ template < class Source2, class Dest, class = feeunit::enable_muldiv_t> -std::pair +std::optional mulDiv(Source1 value, Dest mul, Source2 div) { return feeunit::mulDivU(value, mul, div); @@ -475,7 +475,7 @@ template < class Source2, class Dest, class = feeunit::enable_muldiv_commute_t> -std::pair +std::optional mulDiv(Dest value, Source1 mul, Source2 div) { // Multiplication is commutative @@ -483,7 +483,7 @@ mulDiv(Dest value, Source1 mul, Source2 div) } template > -std::pair +std::optional mulDiv(std::uint64_t value, Dest mul, std::uint64_t div) { // Give the scalars a non-tag so the @@ -492,7 +492,7 @@ mulDiv(std::uint64_t value, Dest mul, std::uint64_t div) } template > -std::pair +std::optional mulDiv(Dest value, std::uint64_t mul, std::uint64_t div) { // Multiplication is commutative @@ -503,20 +503,24 @@ template < class Source1, class Source2, class = feeunit::enable_muldiv_sources_t> -std::pair +std::optional mulDiv(Source1 value, std::uint64_t mul, Source2 div) { // Give the scalars a dimensionless unit so the // unit-handling version gets called. auto unitresult = feeunit::mulDivU(value, feeunit::scalar(mul), div); - return {unitresult.first, unitresult.second.value()}; + + if (!unitresult) + return std::nullopt; + + return unitresult->value(); } template < class Source1, class Source2, class = feeunit::enable_muldiv_sources_t> -std::pair +std::optional mulDiv(std::uint64_t value, Source1 mul, Source2 div) { // Multiplication is commutative diff --git a/src/ripple/basics/impl/mulDiv.cpp b/src/ripple/basics/impl/mulDiv.cpp index 433c7dddd..a991b8bde 100644 --- a/src/ripple/basics/impl/mulDiv.cpp +++ b/src/ripple/basics/impl/mulDiv.cpp @@ -17,15 +17,13 @@ */ //============================================================================== -#include #include #include -#include -#include +#include namespace ripple { -std::pair +std::optional mulDiv(std::uint64_t value, std::uint64_t mul, std::uint64_t div) { using namespace boost::multiprecision; @@ -35,12 +33,10 @@ mulDiv(std::uint64_t value, std::uint64_t mul, std::uint64_t div) result /= div; - auto constexpr limit = std::numeric_limits::max(); + if (result > ripple::muldiv_max) + return std::nullopt; - if (result > limit) - return {false, limit}; - - return {true, static_cast(result)}; + return static_cast(result); } } // namespace ripple diff --git a/src/ripple/basics/mulDiv.h b/src/ripple/basics/mulDiv.h index 30579c255..e338f87c8 100644 --- a/src/ripple/basics/mulDiv.h +++ b/src/ripple/basics/mulDiv.h @@ -21,9 +21,12 @@ #define RIPPLE_BASICS_MULDIV_H_INCLUDED #include +#include +#include #include namespace ripple { +auto constexpr muldiv_max = std::numeric_limits::max(); /** Return value*mul/div accurately. Computes the result of the multiplication and division in @@ -31,14 +34,11 @@ namespace ripple { Throws: None Returns: - `std::pair`: - `first` is `false` if the calculation overflows, - `true` if the calculation is safe. - `second` is the result of the calculation if - `first` is `false`, max value of `uint64_t` - if `true`. + `std::optional`: + `std::nullopt` if the calculation overflows. Otherwise, `value * mul + / div`. */ -std::pair +std::optional mulDiv(std::uint64_t value, std::uint64_t mul, std::uint64_t div); } // namespace ripple diff --git a/src/ripple/rpc/handlers/Fee1.cpp b/src/ripple/rpc/handlers/Fee1.cpp index 5a9a5f0bf..c8b993ac3 100644 --- a/src/ripple/rpc/handlers/Fee1.cpp +++ b/src/ripple/rpc/handlers/Fee1.cpp @@ -24,7 +24,6 @@ #include #include #include -#include #include #include #include diff --git a/src/ripple/rpc/impl/TransactionSign.cpp b/src/ripple/rpc/impl/TransactionSign.cpp index c903c26f8..7610682fd 100644 --- a/src/ripple/rpc/impl/TransactionSign.cpp +++ b/src/ripple/rpc/impl/TransactionSign.cpp @@ -738,9 +738,9 @@ checkFee( auto const limit = [&]() { // Scale fee units to drops: auto const result = mulDiv(feeDefault, mult, div); - if (!result.first) + if (!result) Throw("mulDiv"); - return result.second; + return *result; }(); if (fee > limit) diff --git a/src/test/app/TxQ_test.cpp b/src/test/app/TxQ_test.cpp index a849418d3..83310f50f 100644 --- a/src/test/app/TxQ_test.cpp +++ b/src/test/app/TxQ_test.cpp @@ -22,7 +22,6 @@ #include #include #include -#include #include #include #include diff --git a/src/test/basics/FeeUnits_test.cpp b/src/test/basics/FeeUnits_test.cpp index 85527423c..3ded58129 100644 --- a/src/test/basics/FeeUnits_test.cpp +++ b/src/test/basics/FeeUnits_test.cpp @@ -50,12 +50,17 @@ private: FeeLevel32 f{10}; FeeLevel32 baseFee{100}; - auto drops = mulDiv(baseFee, x, f).second; + auto drops = mulDiv(baseFee, x, f); + BEAST_EXPECT(drops); BEAST_EXPECT(drops.value() == 1000); - BEAST_EXPECT( - (std::is_same_v)); - BEAST_EXPECT((std::is_same_v)); + BEAST_EXPECT((std::is_same_v< + std::remove_reference_t::unit_type, + feeunit::dropTag>)); + + BEAST_EXPECT((std::is_same_v< + std::remove_reference_t, + XRPAmount>)); } { XRPAmount x{100}; @@ -70,12 +75,16 @@ private: FeeLevel64 f{10}; FeeLevel64 baseFee{100}; - auto drops = mulDiv(baseFee, x, f).second; + auto drops = mulDiv(baseFee, x, f); + BEAST_EXPECT(drops); BEAST_EXPECT(drops.value() == 1000); - BEAST_EXPECT( - (std::is_same_v)); - BEAST_EXPECT((std::is_same_v)); + BEAST_EXPECT((std::is_same_v< + std::remove_reference_t::unit_type, + feeunit::dropTag>)); + BEAST_EXPECT((std::is_same_v< + std::remove_reference_t, + XRPAmount>)); } { FeeLevel64 x{1024}; @@ -91,12 +100,16 @@ private: XRPAmount basefee{10}; FeeLevel64 referencefee{256}; - auto drops = mulDiv(x, basefee, referencefee).second; + auto drops = mulDiv(x, basefee, referencefee); + BEAST_EXPECT(drops); BEAST_EXPECT(drops.value() == 40); - BEAST_EXPECT( - (std::is_same_v)); - BEAST_EXPECT((std::is_same_v)); + BEAST_EXPECT((std::is_same_v< + std::remove_reference_t::unit_type, + feeunit::dropTag>)); + BEAST_EXPECT((std::is_same_v< + std::remove_reference_t, + XRPAmount>)); } } diff --git a/src/test/basics/mulDiv_test.cpp b/src/test/basics/mulDiv_test.cpp index 2dfc97606..f51b91fec 100644 --- a/src/test/basics/mulDiv_test.cpp +++ b/src/test/basics/mulDiv_test.cpp @@ -32,27 +32,27 @@ struct mulDiv_test : beast::unit_test::suite const std::uint64_t max32 = std::numeric_limits::max(); auto result = mulDiv(85, 20, 5); - BEAST_EXPECT(result.first && result.second == 340); + BEAST_EXPECT(result && *result == 340); result = mulDiv(20, 85, 5); - BEAST_EXPECT(result.first && result.second == 340); + BEAST_EXPECT(result && *result == 340); result = mulDiv(0, max - 1, max - 3); - BEAST_EXPECT(result.first && result.second == 0); + BEAST_EXPECT(result && *result == 0); result = mulDiv(max - 1, 0, max - 3); - BEAST_EXPECT(result.first && result.second == 0); + BEAST_EXPECT(result && *result == 0); result = mulDiv(max, 2, max / 2); - BEAST_EXPECT(result.first && result.second == 4); + BEAST_EXPECT(result && *result == 4); result = mulDiv(max, 1000, max / 1000); - BEAST_EXPECT(result.first && result.second == 1000000); + BEAST_EXPECT(result && *result == 1000000); result = mulDiv(max, 1000, max / 1001); - BEAST_EXPECT(result.first && result.second == 1001000); + BEAST_EXPECT(result && *result == 1001000); result = mulDiv(max32 + 1, max32 + 1, 5); - BEAST_EXPECT(result.first && result.second == 3689348814741910323); + BEAST_EXPECT(result && *result == 3689348814741910323); // Overflow result = mulDiv(max - 1, max - 2, 5); - BEAST_EXPECT(!result.first && result.second == max); + BEAST_EXPECT(!result); } }; From ac415e24fb30e1fbd659e4594ff4315596a1a343 Mon Sep 17 00:00:00 2001 From: Arihant Kothari Date: Thu, 19 Feb 2026 14:14:30 +0900 Subject: [PATCH 016/301] APIv2(ledger_entry): return "invalidParams" when fields missing (#4552) Improve error handling for ledger_entry by returning an "invalidParams" error when one or more request fields are specified incorrectly, or one or more required fields are missing. For example, if none of of the following fields is provided, then the API should return an invalidParams error: * index, account_root, directory, offer, ripple_state, check, escrow, payment_channel, deposit_preauth, ticket Prior to this commit, the API returned an "unknownOption" error instead. Since the error was actually due to invalid parameters, rather than unknown options, this error was misleading. Since this is an API breaking change, the "invalidParams" error is only returned for requests using api_version: 2 and above. To maintain backward compatibility, the "unknownOption" error is still returned for api_version: 1. Related: #4573 Fix #4303 --- src/ripple/rpc/handlers/LedgerEntry.cpp | 7 ++++++- src/test/rpc/LedgerRPC_test.cpp | 23 +++++++++++++++++++---- 2 files changed, 25 insertions(+), 5 deletions(-) diff --git a/src/ripple/rpc/handlers/LedgerEntry.cpp b/src/ripple/rpc/handlers/LedgerEntry.cpp index bf861333a..ee810e9f8 100644 --- a/src/ripple/rpc/handlers/LedgerEntry.cpp +++ b/src/ripple/rpc/handlers/LedgerEntry.cpp @@ -550,7 +550,12 @@ doLedgerEntry(RPC::JsonContext& context) } } else - jvResult[jss::error] = "unknownOption"; + { + if (context.apiVersion < 2u) + jvResult[jss::error] = "unknownOption"; + else + jvResult[jss::error] = "invalidParams"; + } } if (uNodeIndex.isNonZero()) diff --git a/src/test/rpc/LedgerRPC_test.cpp b/src/test/rpc/LedgerRPC_test.cpp index 4442800c5..64e8caf28 100644 --- a/src/test/rpc/LedgerRPC_test.cpp +++ b/src/test/rpc/LedgerRPC_test.cpp @@ -24,6 +24,7 @@ #include #include #include +#include #include #include @@ -1916,9 +1917,11 @@ public: } void - testLedgerEntryUnknownOption() + testLedgerEntryInvalidParams(unsigned int apiVersion) { - testcase("ledger_entry Request Unknown Option"); + testcase( + "ledger_entry Request With Invalid Parameters v" + + std::to_string(apiVersion)); using namespace test::jtx; Env env{*this}; @@ -1926,11 +1929,16 @@ public: // "features" is not an option supported by ledger_entry. Json::Value jvParams; + jvParams[jss::api_version] = apiVersion; jvParams[jss::features] = ledgerHash; jvParams[jss::ledger_hash] = ledgerHash; Json::Value const jrr = env.rpc("json", "ledger_entry", to_string(jvParams))[jss::result]; - checkErrorValue(jrr, "unknownOption", ""); + + if (apiVersion < 2u) + checkErrorValue(jrr, "unknownOption", ""); + else + checkErrorValue(jrr, "invalidParams", ""); } /// @brief ledger RPC requests as a way to drive @@ -2442,11 +2450,18 @@ public: testLedgerEntryURIToken(); testLedgerEntryImportVLSeq(); testLedgerEntryCron(); - testLedgerEntryUnknownOption(); testLookupLedger(); testNoQueue(); testQueue(); testLedgerAccountsOption(); + + // version specific tests + for (auto testVersion = RPC::apiMinimumSupportedVersion; + testVersion <= RPC::apiBetaVersion; + ++testVersion) + { + testLedgerEntryInvalidParams(testVersion); + } } }; From 233f060b0b6d44de7ffa5ca7f5facd9f05007ac6 Mon Sep 17 00:00:00 2001 From: Gregory Tsipenyuk Date: Thu, 19 Feb 2026 15:01:11 +0900 Subject: [PATCH 017/301] Introduce AMM support (XLS-30d): (#4294) Add AMM functionality: - InstanceCreate - Deposit - Withdraw - Governance - Auctioning - payment engine integration To support this functionality, add: - New RPC method, `amm_info`, to fetch pool and LPT balances - AMM Root Account - trust line for each IOU AMM token - trust line to track Liquidity Provider Tokens (LPT) - `ltAMM` object The `ltAMM` object tracks: - fee votes - auction slot bids - AMM tokens pair - total outstanding tokens balance - `AMMID` to AMM `RootAccountID` mapping Add new classes to facilitate AMM integration into the payment engine. `BookStep` uses these classes to infer if AMM liquidity can be consumed. The AMM formula implementation uses the new Number class added in #4192. IOUAmount and STAmount use Number arithmetic. Add AMM unit tests for all features. AMM requires the following amendments: - featureAMM - fixUniversalNumber - featureFlowCross Notes: - Current trading fee threshold is 1% - AMM currency is generated by: 0x03 + 152 bits of sha256{cur1, cur2} - Current max AMM Offers is 30 --------- Co-authored-by: Howard Hinnant --- Builds/CMake/RippledCore.cmake | 24 +- Builds/levelization/results/ordering.txt | 2 + hook/sfcodes.h | 17 + hook/tts.h | 5 + src/ripple/app/ledger/OrderBookDB.cpp | 18 +- src/ripple/app/misc/AMMHelpers.h | 311 ++ src/ripple/app/misc/AMMUtils.h | 98 + src/ripple/app/misc/impl/AMMHelpers.cpp | 206 + src/ripple/app/misc/impl/AMMUtils.cpp | 191 + src/ripple/app/paths/AMMContext.h | 119 + src/ripple/app/paths/AMMLiquidity.h | 148 + src/ripple/app/paths/AMMOffer.h | 149 + src/ripple/app/paths/Flow.cpp | 10 + src/ripple/app/paths/impl/AMMLiquidity.cpp | 223 + src/ripple/app/paths/impl/AMMOffer.cpp | 143 + src/ripple/app/paths/impl/BookStep.cpp | 303 +- src/ripple/app/paths/impl/PaySteps.cpp | 7 + src/ripple/app/paths/impl/Steps.h | 31 +- src/ripple/app/paths/impl/StrandFlow.h | 85 +- src/ripple/app/tx/impl/AMMBid.cpp | 353 ++ src/ripple/app/tx/impl/AMMBid.h | 86 + src/ripple/app/tx/impl/AMMCreate.cpp | 381 ++ src/ripple/app/tx/impl/AMMCreate.h | 82 + src/ripple/app/tx/impl/AMMDeposit.cpp | 837 ++++ src/ripple/app/tx/impl/AMMDeposit.h | 230 + src/ripple/app/tx/impl/AMMVote.cpp | 243 + src/ripple/app/tx/impl/AMMVote.h | 71 + src/ripple/app/tx/impl/AMMWithdraw.cpp | 813 ++++ src/ripple/app/tx/impl/AMMWithdraw.h | 230 + src/ripple/app/tx/impl/Escrow.cpp | 12 + src/ripple/app/tx/impl/Escrow.h | 3 + src/ripple/app/tx/impl/InvariantCheck.cpp | 15 +- src/ripple/app/tx/impl/Offer.h | 77 +- src/ripple/app/tx/impl/PayChan.cpp | 3 + src/ripple/app/tx/impl/Payment.cpp | 5 + src/ripple/app/tx/impl/Remit.cpp | 10 +- src/ripple/app/tx/impl/URIToken.cpp | 8 +- src/ripple/app/tx/impl/applySteps.cpp | 55 + src/ripple/ledger/View.h | 47 +- src/ripple/ledger/impl/View.cpp | 72 +- src/ripple/net/impl/RPCCall.cpp | 1 + src/ripple/protocol/AMMCore.h | 134 + src/ripple/protocol/AmountConversions.h | 55 + src/ripple/protocol/ErrorCodes.h | 5 +- src/ripple/protocol/Feature.h | 3 +- src/ripple/protocol/Indexes.h | 7 + src/ripple/protocol/Issue.h | 18 +- src/ripple/protocol/LedgerFormats.h | 9 + src/ripple/protocol/QualityFunction.h | 107 + src/ripple/protocol/SField.h | 25 + src/ripple/protocol/STIssue.h | 138 + src/ripple/protocol/STObject.h | 3 + src/ripple/protocol/TER.h | 16 +- src/ripple/protocol/TxFlags.h | 17 + src/ripple/protocol/TxFormats.h | 15 + src/ripple/protocol/impl/AMMCore.cpp | 131 + src/ripple/protocol/impl/ErrorCodes.cpp | 1 + src/ripple/protocol/impl/Feature.cpp | 1 + src/ripple/protocol/impl/Indexes.cpp | 19 + .../protocol/impl/InnerObjectFormats.cpp | 18 + src/ripple/protocol/impl/Issue.cpp | 87 + src/ripple/protocol/impl/LedgerFormats.cpp | 13 + src/ripple/protocol/impl/QualityFunction.cpp | 59 + src/ripple/protocol/impl/SField.cpp | 28 +- src/ripple/protocol/impl/STAmount.cpp | 15 +- src/ripple/protocol/impl/STIssue.cpp | 113 + src/ripple/protocol/impl/STObject.cpp | 6 + src/ripple/protocol/impl/STParsedJSON.cpp | 12 + src/ripple/protocol/impl/STVar.cpp | 7 + src/ripple/protocol/impl/TER.cpp | 6 + src/ripple/protocol/impl/TxFormats.cpp | 58 + src/ripple/protocol/jss.h | 39 +- src/ripple/rpc/handlers/AMMInfo.cpp | 208 + src/ripple/rpc/handlers/Handlers.h | 2 + src/ripple/rpc/handlers/LedgerEntry.cpp | 33 + src/ripple/rpc/handlers/ServerDefinitions.cpp | 29 +- src/ripple/rpc/impl/Handler.cpp | 1 + src/ripple/rpc/impl/RPCHelpers.cpp | 3 +- src/test/app/AMMCalc_test.cpp | 457 ++ src/test/app/AMMExtended_test.cpp | 3914 ++++++++++++++++ src/test/app/AMM_test.cpp | 4169 +++++++++++++++++ src/test/app/BaseFee_test.cpp | 7 +- src/test/app/CrossingLimits_test.cpp | 20 +- src/test/app/Escrow_test.cpp | 1886 ++++---- src/test/app/Flow_test.cpp | 1 + src/test/app/Freeze_test.cpp | 1 + src/test/app/Offer_test.cpp | 5 +- src/test/app/Path_test.cpp | 97 +- src/test/app/PayChan_test.cpp | 27 +- src/test/app/PayStrand_test.cpp | 32 +- src/test/app/SetHookTSH_test.cpp | 147 +- src/test/app/SetRemarks_test.cpp | 9 +- src/test/app/TheoreticalQuality_test.cpp | 3 + src/test/app/Touch_test.cpp | 19 +- src/test/app/TrustAndBalance_test.cpp | 18 +- src/test/jtx.h | 1 - src/test/jtx/AMM.h | 355 ++ src/test/jtx/AMMTest.h | 156 + src/test/jtx/TestHelpers.h | 337 +- src/test/jtx/escrow.h | 149 - src/test/jtx/impl/AMM.cpp | 727 +++ src/test/jtx/impl/AMMTest.cpp | 278 ++ src/test/jtx/impl/TestHelpers.cpp | 315 +- src/test/jtx/impl/escrow.cpp | 126 - src/test/jtx/impl/paths.cpp | 8 +- src/test/jtx/impl/pay.cpp | 11 +- src/test/jtx/paths.h | 3 + src/test/jtx/pay.h | 2 + src/test/rpc/AMMInfo_test.cpp | 177 + src/test/rpc/AccountOffers_test.cpp | 8 +- src/test/rpc/LedgerData_test.cpp | 8 +- src/test/rpc/NoRipple_test.cpp | 7 +- 112 files changed, 18961 insertions(+), 1684 deletions(-) create mode 100644 src/ripple/app/misc/AMMHelpers.h create mode 100644 src/ripple/app/misc/AMMUtils.h create mode 100644 src/ripple/app/misc/impl/AMMHelpers.cpp create mode 100644 src/ripple/app/misc/impl/AMMUtils.cpp create mode 100644 src/ripple/app/paths/AMMContext.h create mode 100644 src/ripple/app/paths/AMMLiquidity.h create mode 100644 src/ripple/app/paths/AMMOffer.h create mode 100644 src/ripple/app/paths/impl/AMMLiquidity.cpp create mode 100644 src/ripple/app/paths/impl/AMMOffer.cpp create mode 100644 src/ripple/app/tx/impl/AMMBid.cpp create mode 100644 src/ripple/app/tx/impl/AMMBid.h create mode 100644 src/ripple/app/tx/impl/AMMCreate.cpp create mode 100644 src/ripple/app/tx/impl/AMMCreate.h create mode 100644 src/ripple/app/tx/impl/AMMDeposit.cpp create mode 100644 src/ripple/app/tx/impl/AMMDeposit.h create mode 100644 src/ripple/app/tx/impl/AMMVote.cpp create mode 100644 src/ripple/app/tx/impl/AMMVote.h create mode 100644 src/ripple/app/tx/impl/AMMWithdraw.cpp create mode 100644 src/ripple/app/tx/impl/AMMWithdraw.h create mode 100644 src/ripple/protocol/AMMCore.h create mode 100644 src/ripple/protocol/QualityFunction.h create mode 100644 src/ripple/protocol/STIssue.h create mode 100644 src/ripple/protocol/impl/AMMCore.cpp create mode 100644 src/ripple/protocol/impl/QualityFunction.cpp create mode 100644 src/ripple/protocol/impl/STIssue.cpp create mode 100644 src/ripple/rpc/handlers/AMMInfo.cpp create mode 100644 src/test/app/AMMCalc_test.cpp create mode 100644 src/test/app/AMMExtended_test.cpp create mode 100644 src/test/app/AMM_test.cpp create mode 100644 src/test/jtx/AMM.h create mode 100644 src/test/jtx/AMMTest.h delete mode 100644 src/test/jtx/escrow.h create mode 100644 src/test/jtx/impl/AMM.cpp create mode 100644 src/test/jtx/impl/AMMTest.cpp delete mode 100644 src/test/jtx/impl/escrow.cpp create mode 100644 src/test/rpc/AMMInfo_test.cpp diff --git a/Builds/CMake/RippledCore.cmake b/Builds/CMake/RippledCore.cmake index d6d60de76..b63bb22de 100644 --- a/Builds/CMake/RippledCore.cmake +++ b/Builds/CMake/RippledCore.cmake @@ -86,6 +86,7 @@ target_sources (xrpl_core PRIVATE subdir: protocol #]===============================] src/ripple/protocol/impl/AccountID.cpp + src/ripple/protocol/impl/AMMCore.cpp src/ripple/protocol/impl/Book.cpp src/ripple/protocol/impl/BuildInfo.cpp src/ripple/protocol/impl/ErrorCodes.cpp @@ -93,10 +94,12 @@ target_sources (xrpl_core PRIVATE src/ripple/protocol/impl/Indexes.cpp src/ripple/protocol/impl/InnerObjectFormats.cpp src/ripple/protocol/impl/Issue.cpp + src/ripple/protocol/impl/STIssue.cpp src/ripple/protocol/impl/Keylet.cpp src/ripple/protocol/impl/LedgerFormats.cpp src/ripple/protocol/impl/PublicKey.cpp src/ripple/protocol/impl/Quality.cpp + src/ripple/protocol/impl/QualityFunction.cpp src/ripple/protocol/impl/Rate2.cpp src/ripple/protocol/impl/Rules.cpp src/ripple/protocol/impl/SField.cpp @@ -224,6 +227,7 @@ install ( install ( FILES src/ripple/protocol/AccountID.h + src/ripple/protocol/AMMCore.h src/ripple/protocol/AmountConversions.h src/ripple/protocol/Book.h src/ripple/protocol/BuildInfo.h @@ -240,12 +244,14 @@ install ( src/ripple/protocol/Protocol.h src/ripple/protocol/PublicKey.h src/ripple/protocol/Quality.h + src/ripple/protocol/QualityFunction.h src/ripple/protocol/Rate.h src/ripple/protocol/Rules.h src/ripple/protocol/SField.h src/ripple/protocol/SOTemplate.h src/ripple/protocol/STAccount.h src/ripple/protocol/STAmount.h + src/ripple/protocol/STIssue.h src/ripple/protocol/STArray.h src/ripple/protocol/STBase.h src/ripple/protocol/STBitString.h @@ -404,6 +410,8 @@ target_sources (rippled PRIVATE src/ripple/app/reporting/ReportingETL.cpp src/ripple/app/reporting/ETLSource.cpp src/ripple/app/reporting/P2pProxy.cpp + src/ripple/app/misc/impl/AMMHelpers.cpp + src/ripple/app/misc/impl/AMMUtils.cpp src/ripple/app/misc/CanonicalTXSet.cpp src/ripple/app/misc/FeeVoteImpl.cpp src/ripple/app/misc/HashRouter.cpp @@ -430,6 +438,8 @@ target_sources (rippled PRIVATE src/ripple/app/paths/RippleCalc.cpp src/ripple/app/paths/RippleLineCache.cpp src/ripple/app/paths/TrustLine.cpp + src/ripple/app/paths/impl/AMMLiquidity.cpp + src/ripple/app/paths/impl/AMMOffer.cpp src/ripple/app/paths/impl/BookStep.cpp src/ripple/app/paths/impl/DirectStep.cpp src/ripple/app/paths/impl/PaySteps.cpp @@ -446,6 +456,11 @@ target_sources (rippled PRIVATE src/ripple/app/rdb/impl/UnitaryShard.cpp src/ripple/app/rdb/impl/Vacuum.cpp src/ripple/app/rdb/impl/Wallet.cpp + src/ripple/app/tx/impl/AMMBid.cpp + src/ripple/app/tx/impl/AMMCreate.cpp + src/ripple/app/tx/impl/AMMDeposit.cpp + src/ripple/app/tx/impl/AMMVote.cpp + src/ripple/app/tx/impl/AMMWithdraw.cpp src/ripple/app/tx/impl/ApplyContext.cpp src/ripple/app/tx/impl/BookTip.cpp src/ripple/app/tx/impl/CancelCheck.cpp @@ -625,6 +640,7 @@ target_sources (rippled PRIVATE src/ripple/rpc/handlers/AccountOffers.cpp src/ripple/rpc/handlers/AccountNamespace.cpp src/ripple/rpc/handlers/AccountTx.cpp + src/ripple/rpc/handlers/AMMInfo.cpp src/ripple/rpc/handlers/BlackList.cpp src/ripple/rpc/handlers/BookOffers.cpp src/ripple/rpc/handlers/CanDelete.cpp @@ -734,6 +750,9 @@ if (tests) src/test/app/AccountDelete_test.cpp src/test/app/AccountTxPaging_test.cpp src/test/app/AmendmentTable_test.cpp + src/test/app/AMM_test.cpp + src/test/app/AMMCalc_test.cpp + src/test/app/AMMExtended_test.cpp src/test/app/BaseFee_test.cpp src/test/app/Check_test.cpp src/test/app/ClaimReward_test.cpp @@ -894,9 +913,12 @@ if (tests) src/test/jtx/Env_test.cpp src/test/jtx/WSClient_test.cpp src/test/jtx/impl/Account.cpp + src/test/jtx/impl/AMM.cpp + src/test/jtx/impl/AMMTest.cpp src/test/jtx/impl/Env.cpp src/test/jtx/impl/JSONRPCClient.cpp src/test/jtx/impl/ManualTimeKeeper.cpp + src/test/jtx/impl/TestHelpers.cpp src/test/jtx/impl/WSClient.cpp src/test/jtx/impl/hook.cpp src/test/jtx/impl/acctdelete.cpp @@ -908,7 +930,6 @@ if (tests) src/test/jtx/impl/delivermin.cpp src/test/jtx/impl/deposit.cpp src/test/jtx/impl/envconfig.cpp - src/test/jtx/impl/escrow.cpp src/test/jtx/impl/fee.cpp src/test/jtx/impl/flags.cpp src/test/jtx/impl/genesis.cpp @@ -1033,6 +1054,7 @@ if (tests) src/test/rpc/AccountSet_test.cpp src/test/rpc/AccountTx_test.cpp src/test/rpc/AmendmentBlocked_test.cpp + src/test/rpc/AMMInfo_test.cpp src/test/rpc/Book_test.cpp src/test/rpc/Catalogue_test.cpp src/test/rpc/DepositAuthorized_test.cpp diff --git a/Builds/levelization/results/ordering.txt b/Builds/levelization/results/ordering.txt index e409855a7..e154c6d96 100644 --- a/Builds/levelization/results/ordering.txt +++ b/Builds/levelization/results/ordering.txt @@ -140,6 +140,8 @@ test.jtx > ripple.json test.jtx > ripple.ledger test.jtx > ripple.net test.jtx > ripple.protocol +test.jtx > ripple.resource +test.jtx > ripple.rpc test.jtx > ripple.server test.ledger > ripple.app test.ledger > ripple.basics diff --git a/hook/sfcodes.h b/hook/sfcodes.h index e9d6072fa..7c0d3096e 100644 --- a/hook/sfcodes.h +++ b/hook/sfcodes.h @@ -10,6 +10,8 @@ #define sfTransactionType ((1U << 16U) + 2U) #define sfSignerWeight ((1U << 16U) + 3U) #define sfTransferFee ((1U << 16U) + 4U) +#define sfTradingFee ((1U << 16U) + 5U) +#define sfDiscountedFee ((1U << 16U) + 6U) #define sfVersion ((1U << 16U) + 16U) #define sfHookStateChangeCount ((1U << 16U) + 17U) #define sfHookEmitCount ((1U << 16U) + 18U) @@ -61,6 +63,7 @@ #define sfBurnedNFTokens ((2U << 16U) + 44U) #define sfHookStateCount ((2U << 16U) + 45U) #define sfEmitGeneration ((2U << 16U) + 46U) +#define sfVoteWeight ((2U << 16U) + 48U) #define sfLockCount ((2U << 16U) + 49U) #define sfFirstNFTokenSequence ((2U << 16U) + 50U) #define sfStartTime ((2U << 16U) + 93U) @@ -110,6 +113,7 @@ #define sfEmitNonce ((5U << 16U) + 12U) #define sfEmitHookHash ((5U << 16U) + 13U) #define sfObjectID ((5U << 16U) + 14U) +#define sfAMMID ((5U << 16U) + 15U) #define sfBookDirectory ((5U << 16U) + 16U) #define sfInvoiceID ((5U << 16U) + 17U) #define sfNickname ((5U << 16U) + 18U) @@ -148,6 +152,9 @@ #define sfFee ((6U << 16U) + 8U) #define sfSendMax ((6U << 16U) + 9U) #define sfDeliverMin ((6U << 16U) + 10U) +#define sfAmount2 ((6U << 16U) + 11U) +#define sfBidMin ((6U << 16U) + 12U) +#define sfBidMax ((6U << 16U) + 13U) #define sfMinimumOffer ((6U << 16U) + 16U) #define sfRippleEscrow ((6U << 16U) + 17U) #define sfDeliveredAmount ((6U << 16U) + 18U) @@ -157,6 +164,11 @@ #define sfBaseFeeDrops ((6U << 16U) + 22U) #define sfReserveBaseDrops ((6U << 16U) + 23U) #define sfReserveIncrementDrops ((6U << 16U) + 24U) +#define sfLPTokenOut ((6U << 16U) + 25U) +#define sfLPTokenIn ((6U << 16U) + 26U) +#define sfEPrice ((6U << 16U) + 27U) +#define sfPrice ((6U << 16U) + 28U) +#define sfLPTokenBalance ((6U << 16U) + 31U) #define sfPublicKey ((7U << 16U) + 1U) #define sfMessageKey ((7U << 16U) + 2U) #define sfSigningPubKey ((7U << 16U) + 3U) @@ -223,6 +235,9 @@ #define sfHookDefinition ((14U << 16U) + 22U) #define sfHookParameter ((14U << 16U) + 23U) #define sfHookGrant ((14U << 16U) + 24U) +#define sfVoteEntry ((14U << 16U) + 25U) +#define sfAuctionSlot ((14U << 16U) + 26U) +#define sfAuthAccount ((14U << 16U) + 27U) #define sfRemark ((14U << 16U) + 97U) #define sfGenesisMint ((14U << 16U) + 96U) #define sfActiveValidator ((14U << 16U) + 95U) @@ -239,11 +254,13 @@ #define sfMemos ((15U << 16U) + 9U) #define sfNFTokens ((15U << 16U) + 10U) #define sfHooks ((15U << 16U) + 11U) +#define sfVoteSlots ((15U << 16U) + 12U) #define sfMajorities ((15U << 16U) + 16U) #define sfDisabledValidators ((15U << 16U) + 17U) #define sfHookExecutions ((15U << 16U) + 18U) #define sfHookParameters ((15U << 16U) + 19U) #define sfHookGrants ((15U << 16U) + 20U) +#define sfAuthAccounts ((15U << 16U) + 25U) #define sfRemarks ((15U << 16U) + 97U) #define sfGenesisMints ((15U << 16U) + 96U) #define sfActiveValidators ((15U << 16U) + 95U) diff --git a/hook/tts.h b/hook/tts.h index bd03b0399..19a234632 100644 --- a/hook/tts.h +++ b/hook/tts.h @@ -29,6 +29,11 @@ #define ttNFTOKEN_CANCEL_OFFER 28 #define ttNFTOKEN_ACCEPT_OFFER 29 #define ttCLAWBACK 30 +#define ttAMM_CREATE 35 +#define ttAMM_DEPOSIT 36 +#define ttAMM_WITHDRAW 37 +#define ttAMM_VOTE 38 +#define ttAMM_BID 39 #define ttURITOKEN_MINT 45 #define ttURITOKEN_BURN 46 #define ttURITOKEN_BUY 47 diff --git a/src/ripple/app/ledger/OrderBookDB.cpp b/src/ripple/app/ledger/OrderBookDB.cpp index 343e7f626..45262c4d8 100644 --- a/src/ripple/app/ledger/OrderBookDB.cpp +++ b/src/ripple/app/ledger/OrderBookDB.cpp @@ -20,6 +20,7 @@ #include #include #include +#include #include #include #include @@ -93,7 +94,7 @@ OrderBookDB::update(std::shared_ptr const& ledger) JLOG(j_.debug()) << "Beginning update (" << ledger->seq() << ")"; - // walk through the entire ledger looking for orderbook entries + // walk through the entire ledger looking for orderbook/AMM entries int cnt = 0; try @@ -126,6 +127,21 @@ OrderBookDB::update(std::shared_ptr const& ledger) ++cnt; } + else if (sle->getType() == ltAMM) + { + auto const issue1 = (*sle)[sfAsset]; + auto const issue2 = (*sle)[sfAsset2]; + auto addBook = [&](Issue const& in, Issue const& out) { + allBooks[in].insert(out); + + if (isXRP(out)) + xrpBooks.insert(in); + + ++cnt; + }; + addBook(issue1, issue2); + addBook(issue2, issue1); + } } } catch (SHAMapMissingNode const& mn) diff --git a/src/ripple/app/misc/AMMHelpers.h b/src/ripple/app/misc/AMMHelpers.h new file mode 100644 index 000000000..24c259228 --- /dev/null +++ b/src/ripple/app/misc/AMMHelpers.h @@ -0,0 +1,311 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2023 Ripple Labs Inc. + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#ifndef RIPPLE_APP_MISC_AMMHELPERS_H_INCLUDED +#define RIPPLE_APP_MISC_AMMHELPERS_H_INCLUDED + +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +namespace ripple { + +/** Calculate LP Tokens given AMM pool reserves. + * @param asset1 AMM one side of the pool reserve + * @param asset2 AMM another side of the pool reserve + * @return LP Tokens as IOU + */ +STAmount +ammLPTokens( + STAmount const& asset1, + STAmount const& asset2, + Issue const& lptIssue); + +/** Calculate LP Tokens given asset's deposit amount. + * @param asset1Balance current AMM asset1 balance + * @param asset1Deposit requested asset1 deposit amount + * @param lptAMMBalance AMM LPT balance + * @param tfee trading fee in basis points + * @return tokens + */ +STAmount +lpTokensIn( + STAmount const& asset1Balance, + STAmount const& asset1Deposit, + STAmount const& lptAMMBalance, + std::uint16_t tfee); + +/** Calculate asset deposit given LP Tokens. + * @param asset1Balance current AMM asset1 balance + * @param lpTokens LP Tokens + * @param lptAMMBalance AMM LPT balance + * @param tfee trading fee in basis points + * @return + */ +STAmount +ammAssetIn( + STAmount const& asset1Balance, + STAmount const& lptAMMBalance, + STAmount const& lpTokens, + std::uint16_t tfee); + +/** Calculate LP Tokens given asset's withdraw amount. Return 0 + * if can't calculate. + * @param asset1Balance current AMM asset1 balance + * @param asset1Withdraw requested asset1 withdraw amount + * @param lptAMMBalance AMM LPT balance + * @param tfee trading fee in basis points + * @return tokens out amount + */ +STAmount +lpTokensOut( + STAmount const& asset1Balance, + STAmount const& asset1Withdraw, + STAmount const& lptAMMBalance, + std::uint16_t tfee); + +/** Calculate asset withdrawal by tokens + * @param assetBalance balance of the asset being withdrawn + * @param lptAMMBalance total AMM Tokens balance + * @param lpTokens LP Tokens balance + * @param tfee trading fee in basis points + * @return calculated asset amount + */ +STAmount +withdrawByTokens( + STAmount const& assetBalance, + STAmount const& lptAMMBalance, + STAmount const& lpTokens, + std::uint16_t tfee); + +/** Check if the relative distance between the qualities + * is within the requested distance. + * @param calcQuality calculated quality + * @param reqQuality requested quality + * @param dist requested relative distance + * @return true if within dist, false otherwise + */ +inline bool +withinRelativeDistance( + Quality const& calcQuality, + Quality const& reqQuality, + Number const& dist) +{ + if (calcQuality == reqQuality) + return true; + auto const [min, max] = std::minmax(calcQuality, reqQuality); + // Relative distance is (max - min)/max. Can't use basic operations + // on Quality. Have to use Quality::rate() instead, which + // is inverse of quality: (1/max.rate - 1/min.rate)/(1/max.rate) + return ((min.rate() - max.rate()) / min.rate()) < dist; +} + +/** Check if the relative distance between the amounts + * is within the requested distance. + * @param calc calculated amount + * @param req requested amount + * @param dist requested relative distance + * @return true if within dist, false otherwise + */ +// clang-format off +template + requires( + std::is_same_v || std::is_same_v || + std::is_same_v) +bool +withinRelativeDistance(Amt const& calc, Amt const& req, Number const& dist) +{ + if (calc == req) + return true; + auto const [min, max] = std::minmax(calc, req); + return ((max - min) / max) < dist; +} +// clang-format on + +/** Finds takerPays (i) and takerGets (o) such that given pool composition + * poolGets(I) and poolPays(O): (O - o) / (I + i) = quality. + * Where takerGets is calculated as the swapAssetIn (see below). + * The above equation produces the quadratic equation: + * i^2*(1-fee) + i*I*(2-fee) + I^2 - I*O/quality, + * which is solved for i, and o is found with swapAssetIn(). + * @param pool AMM pool balances + * @param quality requested quality + * @param tfee trading fee in basis points + * @return seated in/out amounts if the quality can be changed + */ +template +std::optional> +changeSpotPriceQuality( + TAmounts const& pool, + Quality const& quality, + std::uint16_t tfee) +{ + auto const f = feeMult(tfee); // 1 - fee + auto const& a = f; + auto const b = pool.in * (1 + f); + Number const c = pool.in * pool.in - pool.in * pool.out * quality.rate(); + if (auto const res = b * b - 4 * a * c; res < 0) + return std::nullopt; + else if (auto const nTakerPaysPropose = (-b + root2(res)) / (2 * a); + nTakerPaysPropose > 0) + { + auto const nTakerPays = [&]() { + // The fee might make the AMM offer quality less than CLOB quality. + // Therefore, AMM offer has to satisfy this constraint: o / i >= q. + // Substituting o with swapAssetIn() gives: + // i <= O / q - I / (1 - fee). + auto const nTakerPaysConstraint = + pool.out * quality.rate() - pool.in / f; + if (nTakerPaysPropose > nTakerPaysConstraint) + return nTakerPaysConstraint; + return nTakerPaysPropose; + }(); + if (nTakerPays <= 0) + return std::nullopt; + auto const takerPays = toAmount( + getIssue(pool.in), nTakerPays, Number::rounding_mode::upward); + // should not fail + if (auto const amounts = + TAmounts{ + takerPays, swapAssetIn(pool, takerPays, tfee)}; + Quality{amounts} < quality && + !withinRelativeDistance(Quality{amounts}, quality, Number(1, -7))) + Throw("changeSpotPriceQuality failed"); + else + return amounts; + } + return std::nullopt; +} + +/** AMM pool invariant - the product (A * B) after swap in/out has to remain + * at least the same: (A + in) * (B - out) >= A * B + * XRP round-off may result in a smaller product after swap in/out. + * To address this: + * - if on swapIn the out is XRP then the amount is round-off + * downward, making the product slightly larger since out + * value is reduced. + * - if on swapOut the in is XRP then the amount is round-off + * upward, making the product slightly larger since in + * value is increased. + */ + +/** Swap assetIn into the pool and swap out a proportional amount + * of the other asset. Implements AMM Swap in. + * @see [XLS30d:AMM + * Swap](https://github.com/XRPLF/XRPL-Standards/discussions/78) + * @param pool current AMM pool balances + * @param assetIn amount to swap in + * @param tfee trading fee in basis points + * @return + */ +template +TOut +swapAssetIn( + TAmounts const& pool, + TIn const& assetIn, + std::uint16_t tfee) +{ + return toAmount( + getIssue(pool.out), + pool.out - (pool.in * pool.out) / (pool.in + assetIn * feeMult(tfee)), + Number::rounding_mode::downward); +} + +/** Swap assetOut out of the pool and swap in a proportional amount + * of the other asset. Implements AMM Swap out. + * @see [XLS30d:AMM + * Swap](https://github.com/XRPLF/XRPL-Standards/discussions/78) + * @param pool current AMM pool balances + * @param assetOut amount to swap out + * @param tfee trading fee in basis points + * @return + */ +template +TIn +swapAssetOut( + TAmounts const& pool, + TOut const& assetOut, + std::uint16_t tfee) +{ + return toAmount( + getIssue(pool.in), + ((pool.in * pool.out) / (pool.out - assetOut) - pool.in) / + feeMult(tfee), + Number::rounding_mode::upward); +} + +/** Return square of n. + */ +Number +square(Number const& n); + +/** Adjust LP tokens to deposit/withdraw. + * Amount type keeps 16 digits. Maintaining the LP balance by adding deposited + * tokens or subtracting withdrawn LP tokens from LP balance results in + * losing precision in LP balance. I.e. the resulting LP balance + * is less than the actual sum of LP tokens. To adjust for this, subtract + * old tokens balance from the new one for deposit or vice versa for withdraw + * to cancel out the precision loss. + * @param lptAMMBalance LPT AMM Balance + * @param lpTokens LP tokens to deposit or withdraw + * @param isDeposit true if deposit, false if withdraw + */ +STAmount +adjustLPTokens( + STAmount const& lptAMMBalance, + STAmount const& lpTokens, + bool isDeposit); + +/** Calls adjustLPTokens() and adjusts deposit or withdraw amounts if + * the adjusted LP tokens are less than the provided LP tokens. + * @param amountBalance asset1 pool balance + * @param amount asset1 to deposit or withdraw + * @param amount2 asset2 to deposit or withdraw + * @param lptAMMBalance LPT AMM Balance + * @param lpTokens LP tokens to deposit or withdraw + * @param tfee trading fee in basis points + * @param isDeposit true if deposit, false if withdraw + * @return + */ +std::tuple, STAmount> +adjustAmountsByLPTokens( + STAmount const& amountBalance, + STAmount const& amount, + std::optional const& amount2, + STAmount const& lptAMMBalance, + STAmount const& lpTokens, + std::uint16_t tfee, + bool isDeposit); + +/** Positive solution for quadratic equation: + * x = (-b + sqrt(b**2 + 4*a*c))/(2*a) + */ +Number +solveQuadraticEq(Number const& a, Number const& b, Number const& c); + +} // namespace ripple + +#endif // RIPPLE_APP_MISC_AMMHELPERS_H_INCLUDED diff --git a/src/ripple/app/misc/AMMUtils.h b/src/ripple/app/misc/AMMUtils.h new file mode 100644 index 000000000..1718df496 --- /dev/null +++ b/src/ripple/app/misc/AMMUtils.h @@ -0,0 +1,98 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2023 Ripple Labs Inc. + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== +#ifndef RIPPLE_APP_MISC_AMMUTILS_H_INLCUDED +#define RIPPLE_APP_MISC_AMMUTILS_H_INLCUDED + +#include +#include +#include +#include +#include +#include + +namespace ripple { + +class ReadView; +class ApplyView; +class Sandbox; +class NetClock; + +/** Get AMM pool balances. + */ +std::pair +ammPoolHolds( + ReadView const& view, + AccountID const& ammAccountID, + Issue const& issue1, + Issue const& issue2, + FreezeHandling freezeHandling, + beast::Journal const j); + +/** Get AMM pool and LP token balances. If both optIssue are + * provided then they are used as the AMM token pair issues. + * Otherwise the missing issues are fetched from ammSle. + */ +Expected, TER> +ammHolds( + ReadView const& view, + SLE const& ammSle, + std::optional const& optIssue1, + std::optional const& optIssue2, + FreezeHandling freezeHandling, + beast::Journal const j); + +/** Get the balance of LP tokens. + */ +STAmount +ammLPHolds( + ReadView const& view, + Currency const& cur1, + Currency const& cur2, + AccountID const& ammAccount, + AccountID const& lpAccount, + beast::Journal const j); + +STAmount +ammLPHolds( + ReadView const& view, + SLE const& ammSle, + AccountID const& lpAccount, + beast::Journal const j); + +/** Get AMM trading fee for the given account. The fee is discounted + * if the account is the auction slot owner or one of the slot's authorized + * accounts. + */ +std::uint16_t +getTradingFee( + ReadView const& view, + SLE const& ammSle, + AccountID const& account); + +/** Returns total amount held by AMM for the given token. + */ +STAmount +ammAccountHolds( + ReadView const& view, + AccountID const& ammAccountID, + Issue const& issue); + +} // namespace ripple + +#endif // RIPPLE_APP_MISC_AMMUTILS_H_INLCUDED diff --git a/src/ripple/app/misc/impl/AMMHelpers.cpp b/src/ripple/app/misc/impl/AMMHelpers.cpp new file mode 100644 index 000000000..736743eaa --- /dev/null +++ b/src/ripple/app/misc/impl/AMMHelpers.cpp @@ -0,0 +1,206 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2023 Ripple Labs Inc. + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#include + +namespace ripple { + +STAmount +ammLPTokens( + STAmount const& asset1, + STAmount const& asset2, + Issue const& lptIssue) +{ + auto const tokens = root2(asset1 * asset2); + return toSTAmount(lptIssue, tokens); +} + +/* + * Equation 3: + * t = T * [(b/B - (sqrt(f2**2 - b/(B*f1)) - f2)) / + * (1 + sqrt(f2**2 - b/(B*f1)) - f2)] + * where f1 = 1 - tfee, f2 = (1 - tfee/2)/f1 + */ +STAmount +lpTokensIn( + STAmount const& asset1Balance, + STAmount const& asset1Deposit, + STAmount const& lptAMMBalance, + std::uint16_t tfee) +{ + auto const f1 = feeMult(tfee); + auto const f2 = feeMultHalf(tfee) / f1; + Number const r = asset1Deposit / asset1Balance; + auto const c = root2(f2 * f2 + r / f1) - f2; + auto const t = lptAMMBalance * (r - c) / (1 + c); + return toSTAmount(lptAMMBalance.issue(), t); +} + +/* Equation 4 solves equation 3 for b: + * Let f1 = 1 - tfee, f2 = (1 - tfee/2)/f1, t1 = t/T, t2 = 1 + t1, R = b/B + * then + * t1 = [R - sqrt(f2**2 + R/f1) + f2] / [1 + sqrt(f2**2 + R/f1] - f2] => + * sqrt(f2**2 + R/f1)*(t1 + 1) = R + f2 + t1*f2 - t1 => + * sqrt(f2**2 + R/f1)*t2 = R + t2*f2 - t1 => + * sqrt(f2**2 + R/f1) = R/t2 + f2 - t1/t2, let d = f2 - t1/t2 => + * sqrt(f2**2 + R/f1) = R/t2 + d => + * f2**2 + R/f1 = (R/t2)**2 +2*d*R/t2 + d**2 => + * (R/t2)**2 + R*(2*d/t2 - 1/f1) + d**2 - f2**2 = 0 + */ +STAmount +ammAssetIn( + STAmount const& asset1Balance, + STAmount const& lptAMMBalance, + STAmount const& lpTokens, + std::uint16_t tfee) +{ + auto const f1 = feeMult(tfee); + auto const f2 = feeMultHalf(tfee) / f1; + auto const t1 = lpTokens / lptAMMBalance; + auto const t2 = 1 + t1; + auto const d = f2 - t1 / t2; + auto const a = 1 / (t2 * t2); + auto const b = 2 * d / t2 - 1 / f1; + auto const c = d * d - f2 * f2; + return toSTAmount( + asset1Balance.issue(), asset1Balance * solveQuadraticEq(a, b, c)); +} + +/* Equation 7: + * t = T * (c - sqrt(c**2 - 4*R))/2 + * where R = b/B, c = R*fee + 2 - fee + */ +STAmount +lpTokensOut( + STAmount const& asset1Balance, + STAmount const& asset1Withdraw, + STAmount const& lptAMMBalance, + std::uint16_t tfee) +{ + Number const fr = asset1Withdraw / asset1Balance; + auto const f1 = getFee(tfee); + auto const c = fr * f1 + 2 - f1; + auto const t = lptAMMBalance * (c - root2(c * c - 4 * fr)) / 2; + return toSTAmount(lptAMMBalance.issue(), t); +} + +/* Equation 8 solves equation 7 for b: + * c - 2*t/T = sqrt(c**2 - 4*R) => + * c**2 - 4*c*t/T + 4*t**2/T**2 = c**2 - 4*R => + * -4*c*t/T + 4*t**2/T**2 = -4*R => + * -c*t/T + t**2/T**2 = -R -=> + * substitute c = R*f + 2 - f => + * -(t/T)*(R*f + 2 - f) + (t/T)**2 = -R, let t1 = t/T => + * -t1*R*f -2*t1 +t1*f +t1**2 = -R => + * R = (t1**2 + t1*(f - 2)) / (t1*f - 1) + */ +STAmount +withdrawByTokens( + STAmount const& assetBalance, + STAmount const& lptAMMBalance, + STAmount const& lpTokens, + std::uint16_t tfee) +{ + auto const f = getFee(tfee); + Number const t1 = lpTokens / lptAMMBalance; + auto const b = assetBalance * (t1 * t1 - t1 * (2 - f)) / (t1 * f - 1); + return toSTAmount(assetBalance.issue(), b); +} + +Number +square(Number const& n) +{ + return n * n; +} + +STAmount +adjustLPTokens( + STAmount const& lptAMMBalance, + STAmount const& lpTokens, + bool isDeposit) +{ + // Force rounding downward to ensure adjusted tokens are less or equal + // to requested tokens. + saveNumberRoundMode rm(Number::setround(Number::rounding_mode::downward)); + if (isDeposit) + return (lptAMMBalance + lpTokens) - lptAMMBalance; + return (lpTokens - lptAMMBalance) + lptAMMBalance; +} + +std::tuple, STAmount> +adjustAmountsByLPTokens( + STAmount const& amountBalance, + STAmount const& amount, + std::optional const& amount2, + STAmount const& lptAMMBalance, + STAmount const& lpTokens, + std::uint16_t tfee, + bool isDeposit) +{ + auto const lpTokensActual = + adjustLPTokens(lptAMMBalance, lpTokens, isDeposit); + + if (lpTokensActual == beast::zero) + { + auto const amount2Opt = + amount2 ? std::make_optional(STAmount{}) : std::nullopt; + return std::make_tuple(STAmount{}, amount2Opt, lpTokensActual); + } + + if (lpTokensActual < lpTokens) + { + // Equal trade + if (amount2) + { + Number const fr = lpTokensActual / lpTokens; + auto const amountActual = toSTAmount(amount.issue(), fr * amount); + auto const amount2Actual = + toSTAmount(amount2->issue(), fr * *amount2); + return std::make_tuple( + amountActual < amount ? amountActual : amount, + amount2Actual < amount2 ? amount2Actual : amount2, + lpTokensActual); + } + + // Single trade + auto const amountActual = [&]() { + if (isDeposit) + return ammAssetIn( + amountBalance, lptAMMBalance, lpTokensActual, tfee); + else + return withdrawByTokens( + amountBalance, lptAMMBalance, lpTokens, tfee); + }(); + return amountActual < amount + ? std::make_tuple(amountActual, std::nullopt, lpTokensActual) + : std::make_tuple(amount, std::nullopt, lpTokensActual); + } + + assert(lpTokensActual == lpTokens); + + return {amount, amount2, lpTokensActual}; +} + +Number +solveQuadraticEq(Number const& a, Number const& b, Number const& c) +{ + return (-b + root2(b * b - 4 * a * c)) / (2 * a); +} + +} // namespace ripple diff --git a/src/ripple/app/misc/impl/AMMUtils.cpp b/src/ripple/app/misc/impl/AMMUtils.cpp new file mode 100644 index 000000000..7156c77f2 --- /dev/null +++ b/src/ripple/app/misc/impl/AMMUtils.cpp @@ -0,0 +1,191 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2023 Ripple Labs Inc. + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== +#include +#include +#include +#include +#include +#include + +namespace ripple { + +std::pair +ammPoolHolds( + ReadView const& view, + AccountID const& ammAccountID, + Issue const& issue1, + Issue const& issue2, + FreezeHandling freezeHandling, + beast::Journal const j) +{ + auto const assetInBalance = + accountHolds(view, ammAccountID, issue1, freezeHandling, j); + auto const assetOutBalance = + accountHolds(view, ammAccountID, issue2, freezeHandling, j); + return std::make_pair(assetInBalance, assetOutBalance); +} + +Expected, TER> +ammHolds( + ReadView const& view, + SLE const& ammSle, + std::optional const& optIssue1, + std::optional const& optIssue2, + FreezeHandling freezeHandling, + beast::Journal const j) +{ + auto const issues = [&]() -> std::optional> { + auto const issue1 = ammSle[sfAsset]; + auto const issue2 = ammSle[sfAsset2]; + if (optIssue1 && optIssue2) + { + if (invalidAMMAssetPair( + *optIssue1, + *optIssue2, + std::make_optional(std::make_pair(issue1, issue2)))) + { + JLOG(j.debug()) << "ammHolds: Invalid optIssue1 or optIssue2 " + << *optIssue1 << " " << *optIssue2; + return std::nullopt; + } + return std::make_optional(std::make_pair(*optIssue1, *optIssue2)); + } + auto const singleIssue = + [&issue1, &issue2, &j]( + Issue checkIssue, + const char* label) -> std::optional> { + if (checkIssue == issue1) + return std::make_optional(std::make_pair(issue1, issue2)); + else if (checkIssue == issue2) + return std::make_optional(std::make_pair(issue2, issue1)); + JLOG(j.debug()) + << "ammHolds: Invalid " << label << " " << checkIssue; + return std::nullopt; + }; + if (optIssue1) + { + return singleIssue(*optIssue1, "optIssue1"); + } + else if (optIssue2) + { + return singleIssue(*optIssue2, "optIssue2"); + } + return std::make_optional(std::make_pair(issue1, issue2)); + }(); + if (!issues) + return Unexpected(tecAMM_INVALID_TOKENS); + auto const [asset1, asset2] = ammPoolHolds( + view, + ammSle.getAccountID(sfAccount), + issues->first, + issues->second, + freezeHandling, + j); + return std::make_tuple(asset1, asset2, ammSle[sfLPTokenBalance]); +} + +STAmount +ammLPHolds( + ReadView const& view, + Currency const& cur1, + Currency const& cur2, + AccountID const& ammAccount, + AccountID const& lpAccount, + beast::Journal const j) +{ + return accountHolds( + view, + lpAccount, + ammLPTCurrency(cur1, cur2), + ammAccount, + FreezeHandling::fhZERO_IF_FROZEN, + j); +} + +STAmount +ammLPHolds( + ReadView const& view, + SLE const& ammSle, + AccountID const& lpAccount, + beast::Journal const j) +{ + return ammLPHolds( + view, + ammSle[sfAsset].currency, + ammSle[sfAsset2].currency, + ammSle[sfAccount], + lpAccount, + j); +} + +std::uint16_t +getTradingFee(ReadView const& view, SLE const& ammSle, AccountID const& account) +{ + using namespace std::chrono; + if (ammSle.isFieldPresent(sfAuctionSlot)) + { + auto const& auctionSlot = + static_cast(ammSle.peekAtField(sfAuctionSlot)); + // Not expired + if (auto const expiration = auctionSlot[~sfExpiration]; + duration_cast( + view.info().parentCloseTime.time_since_epoch()) + .count() < expiration) + { + if (auctionSlot[~sfAccount] == account) + return auctionSlot[sfDiscountedFee]; + if (auctionSlot.isFieldPresent(sfAuthAccounts)) + { + for (auto const& acct : + auctionSlot.getFieldArray(sfAuthAccounts)) + if (acct[~sfAccount] == account) + return auctionSlot[sfDiscountedFee]; + } + } + } + return ammSle[sfTradingFee]; +} + +STAmount +ammAccountHolds( + ReadView const& view, + AccountID const& ammAccountID, + Issue const& issue) +{ + if (isXRP(issue)) + { + if (auto const sle = view.read(keylet::account(ammAccountID))) + return (*sle)[sfBalance]; + } + else if (auto const sle = view.read( + keylet::line(ammAccountID, issue.account, issue.currency)); + sle && + !isFrozen(view, ammAccountID, issue.currency, issue.account)) + { + auto amount = (*sle)[sfBalance]; + if (ammAccountID > issue.account) + amount.negate(); + amount.setIssuer(issue.account); + return amount; + } + + return STAmount{issue}; +} + +} // namespace ripple diff --git a/src/ripple/app/paths/AMMContext.h b/src/ripple/app/paths/AMMContext.h new file mode 100644 index 000000000..06835189b --- /dev/null +++ b/src/ripple/app/paths/AMMContext.h @@ -0,0 +1,119 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2023 Ripple Labs Inc. + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#ifndef RIPPLE_APP_PATHS_AMMCONTEXT_H_INCLUDED +#define RIPPLE_APP_PATHS_AMMCONTEXT_H_INCLUDED + +#include + +#include + +namespace ripple { + +/** Maintains AMM info per overall payment engine execution and + * individual iteration. + * Only one instance of this class is created in Flow.cpp::flow(). + * The reference is percolated through calls to AMMLiquidity class, + * which handles AMM offer generation. + */ +class AMMContext +{ +public: + // Restrict number of AMM offers. If this restriction is removed + // then need to restrict in some other way because AMM offers are + // not counted in the BookStep offer counter. + constexpr static std::uint8_t MaxIterations = 30; + +private: + // Tx account owner is required to get the AMM trading fee in BookStep + AccountID account_; + // true if payment has multiple paths + bool multiPath_{false}; + // Is true if AMM offer is consumed during a payment engine iteration. + bool ammUsed_{false}; + // Counter of payment engine iterations with consumed AMM + std::uint16_t ammIters_{0}; + +public: + AMMContext(AccountID const& account, bool multiPath) + : account_(account), multiPath_(multiPath) + { + } + ~AMMContext() = default; + AMMContext(AMMContext const&) = delete; + AMMContext& + operator=(AMMContext const&) = delete; + + bool + multiPath() const + { + return multiPath_; + } + + void + setMultiPath(bool fs) + { + multiPath_ = fs; + } + + void + setAMMUsed() + { + ammUsed_ = true; + } + + void + update() + { + if (ammUsed_) + ++ammIters_; + ammUsed_ = false; + } + + bool + maxItersReached() const + { + return ammIters_ >= MaxIterations; + } + + std::uint16_t + curIters() const + { + return ammIters_; + } + + AccountID + account() const + { + return account_; + } + + /** Strand execution may fail. Reset the flag at the start + * of each payment engine iteration. + */ + void + clear() + { + ammUsed_ = false; + } +}; + +} // namespace ripple + +#endif // RIPPLE_APP_PATHS_AMMCONTEXT_H_INCLUDED diff --git a/src/ripple/app/paths/AMMLiquidity.h b/src/ripple/app/paths/AMMLiquidity.h new file mode 100644 index 000000000..7757bd468 --- /dev/null +++ b/src/ripple/app/paths/AMMLiquidity.h @@ -0,0 +1,148 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2023 Ripple Labs Inc. + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#ifndef RIPPLE_APP_TX_AMMLIQUIDITY_H_INCLUDED +#define RIPPLE_APP_TX_AMMLIQUIDITY_H_INCLUDED + +#include "ripple/app/misc/AMMHelpers.h" +#include "ripple/app/misc/AMMUtils.h" +#include "ripple/app/paths/AMMContext.h" +#include "ripple/basics/Log.h" +#include "ripple/ledger/ReadView.h" +#include "ripple/ledger/View.h" +#include "ripple/protocol/Quality.h" +#include "ripple/protocol/STLedgerEntry.h" + +namespace ripple { + +template +class AMMOffer; + +/** AMMLiquidity class provides AMM offers to BookStep class. + * The offers are generated in two ways. If there are multiple + * paths specified to the payment transaction then the offers + * are generated based on the Fibonacci sequence with + * a limited number of payment engine iterations consuming AMM offers. + * These offers behave the same way as CLOB offers in that if + * there is a limiting step, then the offers are adjusted + * based on their quality. + * If there is only one path specified in the payment transaction + * then the offers are generated based on the competing CLOB offer + * quality. In this case the offer's size is set in such a way + * that the new AMM's pool spot price quality is equal to the CLOB's + * offer quality. + */ +template +class AMMLiquidity +{ +private: + inline static const Number InitialFibSeqPct = Number(5) / 20000; + AMMContext& ammContext_; + AccountID const ammAccountID_; + std::uint32_t const tradingFee_; + Issue const issueIn_; + Issue const issueOut_; + // Initial AMM pool balances + TAmounts const initialBalances_; + beast::Journal const j_; + +public: + AMMLiquidity( + ReadView const& view, + AccountID const& ammAccountID, + std::uint32_t tradingFee, + Issue const& in, + Issue const& out, + AMMContext& ammContext, + beast::Journal j); + ~AMMLiquidity() = default; + AMMLiquidity(AMMLiquidity const&) = delete; + AMMLiquidity& + operator=(AMMLiquidity const&) = delete; + + /** Generate AMM offer. Returns nullopt if clobQuality is provided + * and it is better than AMM offer quality. Otherwise returns AMM offer. + * If clobQuality is provided then AMM offer size is set based on the + * quality. + */ + std::optional> + getOffer(ReadView const& view, std::optional const& clobQuality) + const; + + AccountID const& + ammAccount() const + { + return ammAccountID_; + } + + bool + multiPath() const + { + return ammContext_.multiPath(); + } + + std::uint32_t + tradingFee() const + { + return tradingFee_; + } + + AMMContext& + context() const + { + return ammContext_; + } + + Issue const& + issueIn() const + { + return issueIn_; + } + + Issue const& + issueOut() const + { + return issueOut_; + } + +private: + /** Fetches current AMM balances. + */ + TAmounts + fetchBalances(ReadView const& view) const; + + /** Generate AMM offers with the offer size based on Fibonacci sequence. + * The sequence corresponds to the payment engine iterations with AMM + * liquidity. Iterations that don't consume AMM offers don't count. + * The number of iterations with AMM offers is limited. + * If the generated offer exceeds the pool balance then the function + * throws overflow exception. + */ + TAmounts + generateFibSeqOffer(TAmounts const& balances) const; + + /** Generate max offer + */ + AMMOffer + maxOffer(TAmounts const& balances) const; +}; + +} // namespace ripple + +#endif // RIPPLE_APP_TX_AMMLIQUIDITY_H_INCLUDED diff --git a/src/ripple/app/paths/AMMOffer.h b/src/ripple/app/paths/AMMOffer.h new file mode 100644 index 000000000..10e6017dd --- /dev/null +++ b/src/ripple/app/paths/AMMOffer.h @@ -0,0 +1,149 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2023 Ripple Labs Inc. + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#ifndef RIPPLE_APP_AMMOFFER_H_INCLUDED +#define RIPPLE_APP_AMMOFFER_H_INCLUDED + +#include +#include +#include +#include + +namespace ripple { + +template +class AMMLiquidity; +class QualityFunction; + +/** Represents synthetic AMM offer in BookStep. AMMOffer mirrors TOffer + * methods for use in generic BookStep methods. AMMOffer amounts + * are changed indirectly in BookStep limiting steps. + */ +template +class AMMOffer +{ +private: + AMMLiquidity const& ammLiquidity_; + // Initial offer amounts. It is fibonacci seq generated for multi-path. + // If the offer size is set based on the competing CLOB offer then + // the AMM offer size is such that if the offer is consumed then + // the updated AMM pool SP quality is going to be equal to competing + // CLOB offer quality. If there is no competing CLOB offer then + // the initial size is set to in=cMax[Native,Value],balances.out. + // While this is not a "real" offer it simulates the case of + // the swap out of the entire side of the pool, in which case + // the swap in amount is infinite. + TAmounts const amounts_; + // If seated then current pool balances. Used in one-path limiting steps + // to swap in/out. + std::optional> const balances_; + // The Spot Price quality if balances != amounts + // else the amounts quality + Quality const quality_; + // AMM offer can be consumed once at a given iteration + bool consumed_; + +public: + AMMOffer( + AMMLiquidity const& ammLiquidity, + TAmounts const& amounts, + std::optional> const& balances, + Quality const& quality); + + Quality + quality() const noexcept + { + return quality_; + } + + Issue const& + issueIn() const; + + Issue const& + issueOut() const; + + AccountID const& + owner() const; + + std::optional + key() const + { + return std::nullopt; + } + + TAmounts const& + amount() const; + + void + consume(ApplyView& view, TAmounts const& consumed); + + bool + fully_consumed() const + { + return consumed_; + } + + /** Limit out of the provided offer. If one-path then swapOut + * using current balances. If multi-path then ceil_out using + * current quality. + */ + TAmounts + limitOut( + TAmounts const& offrAmt, + TOut const& limit, + bool fixReducedOffers, + bool roundUp) const; + + /** Limit in of the provided offer. If one-path then swapIn + * using current balances. If multi-path then ceil_in using + * current quality. + */ + TAmounts + limitIn(TAmounts const& offrAmt, TIn const& limit) const; + + QualityFunction + getQualityFunc() const; + + /** Send funds without incurring the transfer fee + */ + template + static TER + send(Args&&... args) + { + return accountSend(std::forward(args)..., WaiveTransferFee::Yes); + } + + bool + isFunded() const + { + // AMM offer is fully funded by the pool + return true; + } + + static std::pair + adjustRates(std::uint32_t ofrInRate, std::uint32_t ofrOutRate) + { + // AMM doesn't pay transfer fee on Payment tx + return {ofrInRate, QUALITY_ONE}; + } +}; + +} // namespace ripple + +#endif // RIPPLE_APP_AMMOFFER_H_INCLUDED diff --git a/src/ripple/app/paths/Flow.cpp b/src/ripple/app/paths/Flow.cpp index 374eed284..a39068ba4 100644 --- a/src/ripple/app/paths/Flow.cpp +++ b/src/ripple/app/paths/Flow.cpp @@ -17,6 +17,7 @@ */ //============================================================================== +#include #include #include #include @@ -84,6 +85,8 @@ flow( if (sendMax) sendMaxIssue = sendMax->issue(); + AMMContext ammContext(src, false); + // convert the paths to a collection of strands. Each strand is the // collection of account->account steps and book steps that may be used in // this payment. @@ -98,6 +101,7 @@ flow( defaultPaths, ownerPaysTransferFee, offerCrossing, + ammContext, j); if (!isTesSuccess(toStrandsTer)) @@ -107,6 +111,8 @@ flow( return result; } + ammContext.setMultiPath(strands.size() > 1); + if (j.trace()) { j.trace() << "\nsrc: " << src << "\ndst: " << dst @@ -145,6 +151,7 @@ flow( limitQuality, sendMax, j, + ammContext, flowDebugInfo)); } @@ -163,6 +170,7 @@ flow( limitQuality, sendMax, j, + ammContext, flowDebugInfo)); } @@ -181,6 +189,7 @@ flow( limitQuality, sendMax, j, + ammContext, flowDebugInfo)); } @@ -198,6 +207,7 @@ flow( limitQuality, sendMax, j, + ammContext, flowDebugInfo)); } diff --git a/src/ripple/app/paths/impl/AMMLiquidity.cpp b/src/ripple/app/paths/impl/AMMLiquidity.cpp new file mode 100644 index 000000000..3f22ebace --- /dev/null +++ b/src/ripple/app/paths/impl/AMMLiquidity.cpp @@ -0,0 +1,223 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2023 Ripple Labs Inc. + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== +#include + +#include + +namespace ripple { + +template +AMMLiquidity::AMMLiquidity( + ReadView const& view, + AccountID const& ammAccountID, + std::uint32_t tradingFee, + Issue const& in, + Issue const& out, + AMMContext& ammContext, + beast::Journal j) + : ammContext_(ammContext) + , ammAccountID_(ammAccountID) + , tradingFee_(tradingFee) + , issueIn_(in) + , issueOut_(out) + , initialBalances_{fetchBalances(view)} + , j_(j) +{ +} + +template +TAmounts +AMMLiquidity::fetchBalances(ReadView const& view) const +{ + auto const assetIn = ammAccountHolds(view, ammAccountID_, issueIn_); + auto const assetOut = ammAccountHolds(view, ammAccountID_, issueOut_); + // This should not happen. + if (assetIn < beast::zero || assetOut < beast::zero) + Throw("AMMLiquidity: invalid balances"); + + return TAmounts{get(assetIn), get(assetOut)}; +} + +template +TAmounts +AMMLiquidity::generateFibSeqOffer( + TAmounts const& balances) const +{ + TAmounts cur{}; + + cur.in = toAmount( + getIssue(balances.in), + InitialFibSeqPct * initialBalances_.in, + Number::rounding_mode::upward); + cur.out = swapAssetIn(initialBalances_, cur.in, tradingFee_); + + if (ammContext_.curIters() == 0) + return cur; + + // clang-format off + constexpr std::uint32_t fib[AMMContext::MaxIterations] = { + 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610, 987, + 1597, 2584, 4181, 6765, 10946, 17711, 28657, 46368, 75025, 121393, + 196418, 317811, 514229, 832040, 1346269}; + // clang-format on + + assert(!ammContext_.maxItersReached()); + + cur.out = toAmount( + getIssue(balances.out), + cur.out * fib[ammContext_.curIters() - 1], + Number::rounding_mode::downward); + // swapAssetOut() returns negative in this case + if (cur.out >= balances.out) + Throw( + "AMMLiquidity: generateFibSeqOffer exceeds the balance"); + + cur.in = swapAssetOut(balances, cur.out, tradingFee_); + + return cur; +} + +template +constexpr T +maxAmount() +{ + if constexpr (std::is_same_v) + return XRPAmount(STAmount::cMaxNative); + else if constexpr (std::is_same_v) + return IOUAmount(STAmount::cMaxValue / 2, STAmount::cMaxOffset); + else if constexpr (std::is_same_v) + return STAmount(STAmount::cMaxValue / 2, STAmount::cMaxOffset); +} + +template +AMMOffer +AMMLiquidity::maxOffer(TAmounts const& balances) const +{ + return AMMOffer( + *this, + {maxAmount(), + swapAssetIn(balances, maxAmount(), tradingFee_)}, + balances, + Quality{balances}); +} + +template +std::optional> +AMMLiquidity::getOffer( + ReadView const& view, + std::optional const& clobQuality) const +{ + // Can't generate more offers if multi-path. + if (ammContext_.maxItersReached()) + return std::nullopt; + + auto const balances = fetchBalances(view); + + // Frozen accounts + if (balances.in == beast::zero || balances.out == beast::zero) + { + JLOG(j_.debug()) << "AMMLiquidity::getOffer, frozen accounts"; + return std::nullopt; + } + + JLOG(j_.trace()) << "AMMLiquidity::getOffer balances " + << to_string(initialBalances_.in) << " " + << to_string(initialBalances_.out) << " new balances " + << to_string(balances.in) << " " + << to_string(balances.out); + + // Can't generate AMM with a better quality than CLOB's + // quality if AMM's Spot Price quality is less than CLOB quality or is + // within a threshold. + // Spot price quality (SPQ) is calculated within some precision threshold. + // On the next iteration, after SPQ is changed, the new SPQ might be close + // to the requested clobQuality but not exactly and potentially SPQ may keep + // on approaching clobQuality for many iterations. Checking for the quality + // threshold prevents this scenario. + if (auto const spotPriceQ = Quality{balances}; clobQuality && + (spotPriceQ <= clobQuality || + withinRelativeDistance(spotPriceQ, *clobQuality, Number(1, -7)))) + { + JLOG(j_.trace()) << "AMMLiquidity::getOffer, higher clob quality"; + return std::nullopt; + } + + auto offer = [&]() -> std::optional> { + try + { + if (ammContext_.multiPath()) + { + auto const amounts = generateFibSeqOffer(balances); + if (clobQuality && Quality{amounts} < clobQuality) + return std::nullopt; + return AMMOffer( + *this, amounts, std::nullopt, Quality{amounts}); + } + else if (!clobQuality) + { + // If there is no CLOB to compare against, return the largest + // amount, which doesn't overflow. The size is going to be + // changed in BookStep per either deliver amount limit, or + // sendmax, or available output or input funds. + return maxOffer(balances); + } + else if ( + auto const amounts = + changeSpotPriceQuality(balances, *clobQuality, tradingFee_)) + { + return AMMOffer( + *this, *amounts, balances, Quality{*amounts}); + } + } + catch (std::overflow_error const& e) + { + JLOG(j_.error()) << "AMMLiquidity::getOffer overflow " << e.what(); + return maxOffer(balances); + } + catch (std::exception const& e) + { + JLOG(j_.error()) << "AMMLiquidity::getOffer exception " << e.what(); + } + return std::nullopt; + }(); + + if (offer) + { + if (offer->amount().in > beast::zero && + offer->amount().out > beast::zero) + { + JLOG(j_.trace()) + << "AMMLiquidity::getOffer, created " + << to_string(offer->amount().in) << "/" << issueIn_ << " " + << to_string(offer->amount().out) << "/" << issueOut_; + return offer; + } + + JLOG(j_.error()) << "AMMLiquidity::getOffer, failed"; + } + + return std::nullopt; +} + +template class AMMLiquidity; +template class AMMLiquidity; +template class AMMLiquidity; +template class AMMLiquidity; + +} // namespace ripple diff --git a/src/ripple/app/paths/impl/AMMOffer.cpp b/src/ripple/app/paths/impl/AMMOffer.cpp new file mode 100644 index 000000000..10b75b785 --- /dev/null +++ b/src/ripple/app/paths/impl/AMMOffer.cpp @@ -0,0 +1,143 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2023 Ripple Labs Inc. + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//==============================================================================/ +#include + +#include +#include + +namespace ripple { + +template +AMMOffer::AMMOffer( + AMMLiquidity const& ammLiquidity, + TAmounts const& amounts, + std::optional> const& balances, + Quality const& quality) + : ammLiquidity_(ammLiquidity) + , amounts_(amounts) + , balances_(balances) + , quality_(quality) + , consumed_(false) +{ +} + +template +Issue const& +AMMOffer::issueIn() const +{ + return ammLiquidity_.issueIn(); +} + +template +Issue const& +AMMOffer::issueOut() const +{ + return ammLiquidity_.issueOut(); +} + +template +AccountID const& +AMMOffer::owner() const +{ + return ammLiquidity_.ammAccount(); +} + +template +TAmounts const& +AMMOffer::amount() const +{ + return amounts_; +} + +template +void +AMMOffer::consume( + ApplyView& view, + TAmounts const& consumed) +{ + // Consumed offer must be less or equal to the original + if (consumed.in > amounts_.in || consumed.out > amounts_.out) + Throw("Invalid consumed AMM offer."); + // AMM pool is updated when the amounts are transferred + // in BookStep::consumeOffer(). + + consumed_ = true; + + // Let the context know AMM offer is consumed + ammLiquidity_.context().setAMMUsed(); +} + +template +TAmounts +AMMOffer::limitOut( + TAmounts const& offrAmt, + TOut const& limit, + bool fixReducedOffers, + bool roundUp) const +{ + // Change the offer size proportionally to the original offer quality + // to keep the strands quality order unchanged. The taker pays slightly + // more for the offer in this case, which results in a slightly higher + // pool product than the original pool product. I.e. if the original + // pool is poolPays, poolGets and the offer is assetIn, assetOut then + // poolPays * poolGets < (poolPays - assetOut) * (poolGets + assetIn) + if (ammLiquidity_.multiPath()) + { + if (fixReducedOffers) + // It turns out that the ceil_out implementation has some slop in + // it. ceil_out_strict removes that slop. But removing that slop + // affects transaction outcomes, so the change must be made using + // an amendment. + return quality().ceil_out_strict(offrAmt, limit, roundUp); + return quality().ceil_out(offrAmt, limit); + } + // Change the offer size according to the conservation function. The offer + // quality is increased in this case, but it doesn't matter since there is + // only one path. + return {swapAssetOut(*balances_, limit, ammLiquidity_.tradingFee()), limit}; +} + +template +TAmounts +AMMOffer::limitIn( + TAmounts const& offrAmt, + TIn const& limit) const +{ + // See the comments above in limitOut(). + if (ammLiquidity_.multiPath()) + return quality().ceil_in(offrAmt, limit); + return {limit, swapAssetIn(*balances_, limit, ammLiquidity_.tradingFee())}; +} + +template +QualityFunction +AMMOffer::getQualityFunc() const +{ + if (ammLiquidity_.multiPath()) + return QualityFunction{quality(), QualityFunction::CLOBLikeTag{}}; + return QualityFunction{ + *balances_, ammLiquidity_.tradingFee(), QualityFunction::AMMTag{}}; +} + +template class AMMOffer; +template class AMMOffer; +template class AMMOffer; +template class AMMOffer; + +} // namespace ripple diff --git a/src/ripple/app/paths/impl/BookStep.cpp b/src/ripple/app/paths/impl/BookStep.cpp index 20c74b8f5..f1fce99c0 100644 --- a/src/ripple/app/paths/impl/BookStep.cpp +++ b/src/ripple/app/paths/impl/BookStep.cpp @@ -17,7 +17,9 @@ */ //============================================================================== -#include +#include +#include +#include #include #include #include @@ -42,6 +44,8 @@ template class BookStep : public StepImp> { protected: + enum class OfferType { AMM, CLOB }; + uint32_t const maxOffersToConsume_; Book book_; AccountID strandSrc_; @@ -59,6 +63,10 @@ protected: be partially consumed multiple times during a payment. */ std::uint32_t offersUsed_ = 0; + // If set, AMM liquidity might be available + // if AMM offer quality is better than CLOB offer + // quality or there is no CLOB offer. + std::optional> ammLiquidity_; beast::Journal const j_; struct Cache @@ -91,6 +99,15 @@ public: , ownerPaysTransferFee_(ctx.ownerPaysTransferFee) , j_(ctx.j) { + if (auto const ammSle = ctx.view.read(keylet::amm(in, out))) + ammLiquidity_.emplace( + ctx.view, + (*ammSle)[sfAccount], + getTradingFee(ctx.view, *ammSle, ctx.ammContext.account()), + in, + out, + ctx.ammContext, + ctx.j); } Book const& @@ -132,6 +149,9 @@ public: qualityUpperBound(ReadView const& v, DebtDirection prevStepDir) const override; + std::pair, DebtDirection> + getQualityFunc(ReadView const& v, DebtDirection prevStepDir) const override; + std::uint32_t offersUsed() const override; @@ -205,13 +225,36 @@ private: DebtDirection prevStepDebtDir, Callback& callback) const; + // Offer is either TOffer or AMMOffer + template