diff --git a/.github/workflows/nix.yml b/.github/workflows/nix.yml index 53c75a9f6fd..706bdbe103b 100644 --- a/.github/workflows/nix.yml +++ b/.github/workflows/nix.yml @@ -1,6 +1,25 @@ name: nix on: [push, pull_request] +# This workflow has two job matrixes. +# They can be considered phases because the second matrix ("test") +# depends on the first ("dependencies"). +# +# The first phase has a job in the matrix for each combination of +# variables that affects dependency ABI: +# platform, compiler, and configuration. +# It creates a GitHub artifact holding the Conan profile, +# and builds and caches binaries for all the dependencies. +# If an Artifactory remote is configured, they are cached there. +# If not, they are added to the GitHub artifact. +# GitHub's "cache" action has a size limit (10 GB) that is too small +# to hold the binaries if they are built locally. +# We must use the "{upload,download}-artifact" actions instead. +# +# The second phase has a job in the matrix for each test configuration. +# It installs dependency binaries from the cache, whichever was used, +# and builds and tests rippled. + jobs: dependencies: @@ -31,6 +50,8 @@ jobs: env: build_dir: .build steps: + - name: checkout + uses: actions/checkout@v3 - name: check environment run: | echo ${PATH} | tr ':' '\n' @@ -38,6 +59,8 @@ jobs: cmake --version env - name: configure Conan + env: + CONAN_URL: http://18.143.149.228:8081/artifactory/api/conan/conan-non-prod run: | conan profile new default --detect conan profile update settings.compiler.cppstd=20 default @@ -47,19 +70,39 @@ jobs: conan profile update env.CC=${{ matrix.profile.cc }} default conan profile update env.CXX=${{ matrix.profile.cxx }} default conan profile update conf.tools.build:compiler_executables='{"c": "${{ matrix.profile.cc }}", "cpp": "${{ matrix.profile.cxx }}"}' default - - name: checkout - uses: actions/checkout@v3 - - name: dependencies + # Do not quote the URL. An empty string will be accepted (with + # a non-fatal warning), but a missing argument will not. + conan remote add ripple ${{ env.CONAN_URL }} --insert 0 + - name: try to authenticate to ripple Conan remote + id: remote + run: | + echo outcome=$(conan user --remote ripple ${{ secrets.CONAN_USERNAME }} --password ${{ secrets.CONAN_TOKEN }} && echo success || echo failure) | tee ${GITHUB_OUTPUT} + - name: archive profile + # Create this archive before dependencies are added to the local cache. + run: tar -czf conan.tar -C ~/.conan . + - name: list missing binaries + id: binaries + # Print the list of dependencies that would need to be built locally. + # A non-empty list means we have "failed" to cache binaries remotely. + run: | + echo missing=$(conan info . --build missing --json 2>/dev/null | grep '^\[') | tee ${GITHUB_OUTPUT} + - name: build dependencies + if: (steps.binaries.outputs.missing != '[]') uses: ./.github/actions/dependencies with: configuration: ${{ matrix.configuration }} - - name: archive cache + - name: upload dependencies to remote + if: (steps.binaries.outputs.missing != '[]') && (steps.remote.outputs.outcome == 'success') + run: conan upload --remote ripple '*' --all --parallel --confirm + - name: recreate archive with dependencies + if: (steps.binaries.outputs.missing != '[]') && (steps.remote.outputs.outcome == 'failure') run: tar -czf conan.tar -C ~/.conan . - - name: upload cache + - name: upload archive uses: actions/upload-artifact@v3 with: name: ${{ matrix.platform }}-${{ matrix.compiler }}-${{ matrix.configuration }} path: conan.tar + if-no-files-found: error test: diff --git a/BUILD.md b/BUILD.md index 741ce2ba61d..4ef5e5aad2c 100644 --- a/BUILD.md +++ b/BUILD.md @@ -1,3 +1,12 @@ +> These instructions assume you have a C++ development environment ready +> with Git, Python, Conan, CMake, and a C++ compiler. For help setting one up +> on Linux, macOS, or Windows, see [our guide](./docs/build/environment.md). +> +> These instructions also assume a basic familiarity with Conan and CMake. +> If you are unfamiliar with Conan, +> you can read our [crash course](./docs/build/conan.md) +> or the official [Getting Started][3] walkthrough. + ## Branches For a stable release, choose the `master` branch or one of the [tagged @@ -13,256 +22,253 @@ For the latest release candidate, choose the `release` branch. git checkout release ``` -If you are contributing or want the latest set of untested features, -then use the `develop` branch. +For the latest set of untested features, or to contribute, choose the `develop` +branch. ``` git checkout develop ``` -## Platforms +## Minimum Requirements -rippled is written in the C++20 dialect and includes the `` header. -The [minimum compiler versions][2] that can compile this dialect are given -below: +- [Python 3.7](https://www.python.org/downloads/) +- [Conan 1.55](https://conan.io/downloads.html) +- [CMake 3.16](https://cmake.org/download/) -| Compiler | Minimum Version -|---|--- -| GCC | 10 -| Clang | 13 -| Apple Clang | 13.1.6 -| MSVC | 19.23 +`rippled` is written in the C++20 dialect and includes the `` header. +The [minimum compiler versions][2] required are: -We do not recommend Windows for rippled production use at this time. -As of January 2023, the Ubuntu platform has received the highest level of -quality assurance, testing, and support. -Additionally, 32-bit Windows development is not supported. +| Compiler | Version | +|-------------|---------| +| GCC | 10 | +| Clang | 13 | +| Apple Clang | 13.1.6 | +| MSVC | 19.23 | -Visual Studio 2022 is not yet supported. -This is because rippled is not compatible with [Boost][] versions 1.78 or 1.79, -but Conan cannot build Boost versions released earlier than them with VS 2022. -We expect that rippled will be compatible with Boost 1.80, which should be -released in August 2022. -Until then, we advise Windows developers to use Visual Studio 2019. +We don't recommend Windows for `rippled` production at this time. As of +January 2023, Ubuntu has the highest level of quality assurance, testing, +and support. -[Boost]: https://www.boost.org/ +Windows developers should use Visual Studio 2019. `rippled` isn't +compatible with [Boost](https://www.boost.org/) 1.78 or 1.79, and Conan +can't build earlier Boost versions. +**Note:** 32-bit Windows development isn't supported. -## Prerequisites -> **Warning** -> These instructions assume you have a C++ development environment ready -> with Git, Python, Conan, CMake, and a C++ compiler. -> For help setting one up on Linux, macOS, or Windows, -> please see [our guide](./docs/build/environment.md). -> -> These instructions further assume a basic familiarity with Conan and CMake. -> If you are unfamiliar with Conan, -> then please read our [crash course](./docs/build/conan.md) -> or the official [Getting Started][3] walkthrough. +## Steps -To build this package, you will need Python (>= 3.7), -[Conan][] (>= 1.55, < 2), and [CMake][] (>= 3.16). -[Conan]: https://conan.io/downloads.html -[CMake]: https://cmake.org/download/ +### Set Up Conan -You'll need at least one Conan profile: +1. (Optional) If you've never used Conan, use autodetect to set up a default profile. -``` -conan profile new default --detect -``` + ``` + conan profile new default --detect + ``` -You'll need to compile in the C++20 dialect: +2. Update the compiler settings. -``` -conan profile update settings.compiler.cppstd=20 default -``` + ``` + conan profile update settings.compiler.cppstd=20 default + ``` -Linux developers will commonly have a default Conan [profile][] that compiles -with GCC and links with libstdc++. -If you are linking with libstdc++ (see profile setting `compiler.libcxx`), -then you will need to choose the `libstdc++11` ABI: + Linux developers will commonly have a default Conan [profile][] that compiles + with GCC and links with libstdc++. + If you are linking with libstdc++ (see profile setting `compiler.libcxx`), + then you will need to choose the `libstdc++11` ABI. -``` -conan profile update settings.compiler.libcxx=libstdc++11 default -``` + ``` + conan profile update settings.compiler.libcxx=libstdc++11 default + ``` -We find it necessary to use the x64 native build tools on Windows. -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. + On Windows, you should use the x64 native build tools. + 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 build rippled and its dependencies for the x64 -architecture: + Windows developers must also build `rippled` and its dependencies for the x64 + architecture. -``` -conan profile update settings.arch=x86_64 default -``` + ``` + conan profile update settings.arch=x86_64 default + ``` -If you have multiple compilers installed on your platform, -then you'll need to make sure that Conan and CMake select the one you want to -use. -This setting will set the correct variables (`CMAKE__COMPILER`) in the -generated CMake toolchain file: +3. (Optional) If you have multiple compilers installed on your platform, + make sure that Conan and CMake select the one you want to use. + This setting will set the correct variables (`CMAKE__COMPILER`) + in the generated CMake toolchain file. -``` -conan profile update 'conf.tools.build:compiler_executables={"c": "", "cpp": ""}' default -``` + ``` + conan profile update 'conf.tools.build:compiler_executables={"c": "", "cpp": ""}' default + ``` -It should choose the compiler for dependencies as well, -but not all of them have a Conan recipe that respects this setting (yet). -For the rest, you can set these environment variables: + It should choose the compiler for dependencies as well, + but not all of them have a Conan recipe that respects this setting (yet). + For the rest, you can set these environment variables: -``` -conan profile update env.CC= default -conan profile update env.CXX= default -``` + ``` + conan profile update env.CC= default + conan profile update env.CXX= default + ``` -Export our [Conan recipe for Snappy](./external/snappy). -It does not explicitly link the C++ standard library, -which allows you to statically link it with GCC, if you want. +4. Export our [Conan recipe for Snappy](./external/snappy). + It doesn't explicitly link the C++ standard library, + which allows you to statically link it with GCC, if you want. -``` -conan export external/snappy snappy/1.1.9@ -``` + ``` + conan export external/snappy snappy/1.1.9@ + ``` -Export our [Conan recipe for SOCI](./external/soci). -It patches their CMake to correctly import its dependencies. +5. Export our [Conan recipe for SOCI](./external/soci). + It patches their CMake to correctly import its dependencies. -``` -conan export external/soci soci/4.0.3@ -``` + ``` + conan export external/soci soci/4.0.3@ + ``` -## How to build and test +### Build and Test -Let's start with a couple of examples of common workflows. -The first is for a single-configuration generator (e.g. Unix Makefiles) on -Linux or macOS: +1. Create a build directory and move into it. -``` -mkdir .build -cd .build -conan install .. --output-folder . --build missing --settings build_type=Release -cmake -DCMAKE_TOOLCHAIN_FILE:FILEPATH=build/generators/conan_toolchain.cmake -DCMAKE_BUILD_TYPE=Release .. -cmake --build . -./rippled --unittest -``` + ``` + mkdir .build + cd .build + ``` -The second is for a multi-configuration generator (e.g. Visual Studio) on -Windows: + You can use any directory name. Conan treats your working directory as an + install folder and generates files with implementation details. + You don't need to worry about these files, but make sure to change + your working directory to your build directory before calling Conan. -``` -mkdir .build -cd .build -conan install .. --output-folder . --build missing --settings build_type=Release --settings compiler.runtime=MT -conan install .. --output-folder . --build missing --settings build_type=Debug --settings compiler.runtime=MTd -cmake -DCMAKE_TOOLCHAIN_FILE:FILEPATH=build/generators/conan_toolchain.cmake .. -cmake --build . --config Release -cmake --build . --config Debug -./Release/rippled --unittest -./Debug/rippled --unittest -``` + **Note:** You can specify a directory for the installation files by adding + the `install-folder` or `-if` option to every `conan install` command + in the next step. -Now to explain the individual steps in each example: +2. Generate CMake files for every configuration you want to build. -1. Create a build directory (and move into it). - - You can choose any name you want. - - Conan will generate some files in what it calls the "install folder". - These files are implementation details that you don't need to worry about. - By default, the install folder is your current working directory. - If you don't move into your build directory before calling Conan, - then you may be annoyed to see it polluting your project root directory - with these files. - To make Conan put them in your build directory, - you'll have to add the option - `--install-folder` or `-if` to every `conan install` command. - -1. Generate CMake files for every configuration you want to build. + ``` + conan install .. --output-folder . --build missing --settings build_type=Release + conan install .. --output-folder . --build missing --settings build_type=Debug + ``` For a single-configuration generator, e.g. `Unix Makefiles` or `Ninja`, you only need to run this command once. For a multi-configuration generator, e.g. `Visual Studio`, you may want to run it more than once. - Each of these commands should have a different `build_type` setting. - A second command with the same `build_type` setting will just overwrite - the files generated by the first. - You can pass the build type on the command line with `--settings - build_type=$BUILD_TYPE` or in the profile itself, under the section - `[settings]`, with the key `build_type`. - - If you are using a Microsoft Visual C++ compiler, then you will need to - ensure consistency between the `build_type` setting and the - `compiler.runtime` setting. + Each of these commands should also have a different `build_type` setting. + A second command with the same `build_type` setting will overwrite the files + generated by the first. You can pass the build type on the command line with + `--settings build_type=$BUILD_TYPE` or in the profile itself, + under the section `[settings]` with the key `build_type`. + + If you are using a Microsoft Visual C++ compiler, + then you will need to ensure consistency between the `build_type` setting + and the `compiler.runtime` setting. + When `build_type` is `Release`, `compiler.runtime` should be `MT`. + When `build_type` is `Debug`, `compiler.runtime` should be `MTd`. -1. Configure CMake once. + ``` + conan install .. --output-folder . --build missing --settings build_type=Release --settings compiler.runtime=MT + conan install .. --output-folder . --build missing --settings build_type=Debug --settings compiler.runtime=MTd + ``` + +3. Configure CMake and pass the toolchain file generated by Conan, located at + `$OUTPUT_FOLDER/build/generators/conan_toolchain.cmake`. - For all choices of generator, pass the toolchain file generated by Conan. - It will be located at - `$OUTPUT_FOLDER/build/generators/conan_toolchain.cmake`. - If you are using a single-configuration generator, then pass the CMake - variable [`CMAKE_BUILD_TYPE`][build_type] and make sure it matches the - `build_type` setting you chose in the previous step. + Single-config generators: + + ``` + cmake -DCMAKE_TOOLCHAIN_FILE:FILEPATH=build/generators/conan_toolchain.cmake -DCMAKE_BUILD_TYPE=Release .. + ``` - This step is where you may pass build options for rippled. + Pass the CMake variable [`CMAKE_BUILD_TYPE`][build_type] + and make sure it matches the `build_type` setting you chose in the previous + step. -1. Build rippled. + Multi-config gnerators: - For a multi-configuration generator, you must pass the option `--config` - to select the build configuration. - For a single-configuration generator, it will build whatever configuration - you passed for `CMAKE_BUILD_TYPE`. + ``` + cmake -DCMAKE_TOOLCHAIN_FILE:FILEPATH=build/generators/conan_toolchain.cmake .. + ``` -1. Test rippled. + **Note:** You can pass build options for `rippled` in this step. - The exact location of rippled in your build directory - depends on your choice of CMake generator. - You can run unit tests by passing `--unittest`. - Pass `--help` to see the rest of the command line options. +4. Build `rippled`. + 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. -### Options + Single-config generators: -The `unity` option allows you to select between [unity][5] and non-unity -builds. -Unity builds may be faster for the first build (at the cost of much -more memory) since they concatenate sources into fewer translation -units. -Non-unity builds may be faster for incremental builds, and can be helpful for -detecting `#include` omissions. + ``` + cmake --build . + ``` -Below are the most commonly used options, -with their default values in parentheses. + Multi-config generators: + + ``` + cmake --build . --config Release + cmake --build . --config Debug + ``` -- `assert` (OFF): Enable assertions. -- `reporting` (OFF): Build the reporting mode feature. -- `tests` (ON): Build tests. -- `unity` (ON): Configure a [unity build][5]. -- `san` (): Enable a sanitizer with Clang. Choices are `thread` and `address`. +5. Test rippled. + Single-config generators: -### Troubleshooting + ``` + ./rippled --unittest + ``` -#### Conan + Multi-config generators: -If you find trouble building dependencies after changing Conan settings, -then you should retry after removing the Conan cache: + ``` + ./Release/rippled --unittest + ./Debug/rippled --unittest + ``` + + The location of `rippled` in your build directory depends on your CMake + generator. Pass `--help` to see the rest of the command line options. + + +## Options + +| Option | Default Value | Description | +| --- | ---| ---| +| `assert` | OFF | Enable assertions. +| `reporting` | OFF | Build the reporting mode feature. | +| `tests` | ON | Build tests. | +| `unity` | ON | Configure a unity build. | +| `san` | N/A | Enable a sanitizer with Clang. Choices are `thread` and `address`. | + +[Unity builds][5] may be faster for the first build +(at the cost of much more memory) since they concatenate sources into fewer +translation units. Non-unity builds may be faster for incremental builds, +and can be helpful for detecting `#include` omissions. + + +## Troubleshooting + + +### Conan + +If you have trouble building dependencies after changing Conan settings, +try removing the Conan cache. ``` rm -rf ~/.conan/data ``` -#### no std::result_of +### no std::result_of If your compiler version is recent enough to have removed `std::result_of` as -part of C++20, e.g. Apple Clang 15.0, -then you might need to add a preprocessor definition to your bulid: +part of C++20, e.g. Apple Clang 15.0, then you might need to add a preprocessor +definition to your build. ``` conan profile update 'options.boost:extra_b2_flags="define=BOOST_ASIO_HAS_STD_INVOKE_RESULT"' default @@ -273,42 +279,40 @@ conan profile update 'conf.tools.build:cxxflags+=["-DBOOST_ASIO_HAS_STD_INVOKE_R ``` -#### recompile with -fPIC +### recompile with -fPIC + +If you get a linker error suggesting that you recompile Boost with +position-independent code, such as: ``` /usr/bin/ld.gold: error: /home/username/.conan/data/boost/1.77.0/_/_/package/.../lib/libboost_container.a(alloc_lib.o): requires unsupported dynamic reloc 11; recompile with -fPIC ``` -If you get a linker error like the one above suggesting that you recompile -Boost with position-independent code, the reason is most likely that Conan -downloaded a bad binary distribution of the dependency. -For now, this seems to be a [bug][1] in Conan just for Boost 1.77.0 compiled -with GCC for Linux. -The solution is to build the dependency locally by passing `--build boost` -when calling `conan install`: +Conan most likely downloaded a bad binary distribution of the dependency. +This seems to be a [bug][1] in Conan just for Boost 1.77.0 compiled with GCC +for Linux. The solution is to build the dependency locally by passing +`--build boost` when calling `conan install`. ``` conan install --build boost ... ``` -## How to add a dependency +## Add a Dependency -If you want to experiment with a new package, here are the steps to get it -working: +If you want to experiment with a new package, follow these steps: 1. Search for the package on [Conan Center](https://conan.io/center/). -1. In [`conanfile.py`](./conanfile.py): - 1. Add a version of the package to the `requires` property. - 1. Change any default options for the package by adding them to the - `default_options` property (with syntax `'$package:$option': $value`) -1. In [`CMakeLists.txt`](./CMakeLists.txt): - 1. Add a call to `find_package($package REQUIRED)`. - 1. Link a library from the package to the target `ripple_libs` (search for - the existing call to `target_link_libraries(ripple_libs INTERFACE ...)`). -1. Start coding! Don't forget to include whatever headers you need from the - package. +2. Modify [`conanfile.py`](./conanfile.py): + - Add a version of the package to the `requires` property. + - Change any default options for the package by adding them to the + `default_options` property (with syntax `'$package:$option': $value`). +3. Modify [`CMakeLists.txt`](./CMakeLists.txt): + - Add a call to `find_package($package REQUIRED)`. + - Link a library from the package to the target `ripple_libs` + (search for the existing call to `target_link_libraries(ripple_libs INTERFACE ...)`). +4. Start coding! Don't forget to include whatever headers you need from the package. [1]: https://github.com/conan-io/conan-center-index/issues/13168 diff --git a/Builds/CMake/RippledCore.cmake b/Builds/CMake/RippledCore.cmake index db7757f9c2f..a853a6cff53 100644 --- a/Builds/CMake/RippledCore.cmake +++ b/Builds/CMake/RippledCore.cmake @@ -721,6 +721,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/SHAMapStore_test.cpp src/test/app/SetAuth_test.cpp diff --git a/README.md b/README.md index 2b6aa5512f1..45dc2005ea2 100644 --- a/README.md +++ b/README.md @@ -6,11 +6,14 @@ The [XRP Ledger](https://xrpl.org/) is a decentralized cryptographic ledger powe [XRP](https://xrpl.org/xrp.html) is a public, counterparty-free asset native to the XRP Ledger, and is designed to bridge the many different currencies in use worldwide. XRP is traded on the open-market and is available for anyone to access. The XRP Ledger was created in 2012 with a finite supply of 100 billion units of XRP. ## rippled -The server software that powers the XRP Ledger is called `rippled` and is available in this repository under the permissive [ISC open-source license](LICENSE.md). The `rippled` server software is written primarily in C++ and runs on a variety of platforms. The `rippled` server software can run in several modes depending on its [configuration](https://xrpl.org/rippled-server-modes.html). +The server software that powers the XRP Ledger is called `rippled` and is available in this repository under the permissive [ISC open-source license](LICENSE.md). The `rippled` server software is written primarily in C++ and runs on a variety of platforms. The `rippled` server software can run in several modes depending on its [configuration](https://xrpl.org/rippled-server-modes.html). + +If you are interested in running an **API Server** (including a **Full History Server**) or a **Reporting Mode** server, take a look at [Clio](https://github.com/XRPLF/clio). rippled Reporting Mode is expected to be replaced by Clio. ### Build from Source * [Read the build instructions in `BUILD.md`](BUILD.md) +* If you encounter any issues, please [open an issue](https://github.com/XRPLF/rippled/issues) ## Key Features of the XRP Ledger @@ -53,10 +56,14 @@ Some of the directories under `src` are external repositories included using git-subtree. See those directories' README files for more details. -## See Also +## Additional Documentation * [XRP Ledger Dev Portal](https://xrpl.org/) * [Setup and Installation](https://xrpl.org/install-rippled.html) * [Source Documentation (Doxygen)](https://xrplf.github.io/rippled/) + +## See Also + +* [Clio API Server for the XRP Ledger](https://github.com/XRPLF/clio) * [Mailing List for Release Announcements](https://groups.google.com/g/ripple-server) * [Learn more about the XRP Ledger (YouTube)](https://www.youtube.com/playlist?list=PLJQ55Tj1hIVZtJ_JdTvSum2qMTsedWkNi) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 08ceb5b3dd4..e83855b79db 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -8,6 +8,130 @@ This document contains the release notes for `rippled`, the reference server imp Have new ideas? Need help with setting up your node? [Please open an issue here](https://github.com/xrplf/rippled/issues/new/choose). +# Introducing XRP Ledger version 1.11.0 + +Version 1.11.0 of `rippled`, the reference server implementation of the XRP Ledger protocol, is now available. + +This release reduces memory usage, introduces the `fixNFTokenRemint` amendment, and adds new features and bug fixes. For example, the new NetworkID field in transactions helps to prevent replay attacks with side-chains. + +[Sign Up for Future Release Announcements](https://groups.google.com/g/ripple-server) + + + +## Action Required + +The `fixNFTokenRemint` amendment is now open for voting according to the XRP Ledger's [amendment process](https://xrpl.org/amendments.html), which enables protocol changes following two weeks of >80% support from trusted validators. + +If you operate an XRP Ledger server, upgrade to version 1.11.0 by July 5 to ensure service continuity. The exact time that protocol changes take effect depends on the voting decisions of the decentralized network. + + +## Install / Upgrade + +On supported platforms, see the [instructions on installing or updating `rippled`](https://xrpl.org/install-rippled.html). + + +## What's Changed + +### New Features and Improvements + +* Allow port numbers be be specified using a either a colon or a space by @RichardAH in https://github.com/XRPLF/rippled/pull/4328 +* Eliminate memory allocation from critical path: by @nbougalis in https://github.com/XRPLF/rippled/pull/4353 +* Make it easy for projects to depend on libxrpl by @thejohnfreeman in https://github.com/XRPLF/rippled/pull/4449 +* Add the ability to mark amendments as obsolete by @ximinez in https://github.com/XRPLF/rippled/pull/4291 +* Always create the FeeSettings object in genesis ledger by @ximinez in https://github.com/XRPLF/rippled/pull/4319 +* Log exception messages in several locations by @drlongle in https://github.com/XRPLF/rippled/pull/4400 +* Parse flags in account_info method by @drlongle in https://github.com/XRPLF/rippled/pull/4459 +* Add NFTokenPages to account_objects RPC by @RichardAH in https://github.com/XRPLF/rippled/pull/4352 +* add jss fields used by clio `nft_info` by @ledhed2222 in https://github.com/XRPLF/rippled/pull/4320 +* Introduce a slab-based memory allocator and optimize SHAMapItem by @nbougalis in https://github.com/XRPLF/rippled/pull/4218 +* Add NetworkID field to transactions to help prevent replay attacks on and from side-chains by @RichardAH in https://github.com/XRPLF/rippled/pull/4370 +* If present, set quorum based on command line. by @mtrippled in https://github.com/XRPLF/rippled/pull/4489 +* API does not accept seed or public key for account by @drlongle in https://github.com/XRPLF/rippled/pull/4404 +* Add `nftoken_id`, `nftoken_ids` and `offer_id` meta fields into NFT `Tx` responses by @shawnxie999 in https://github.com/XRPLF/rippled/pull/4447 + +### Bug Fixes + +* fix(gateway_balances): handle overflow exception by @RichardAH in https://github.com/XRPLF/rippled/pull/4355 +* fix(ValidatorSite): handle rare null pointer dereference in timeout by @ximinez in https://github.com/XRPLF/rippled/pull/4420 +* RPC commands understand markers derived from all ledger object types by @ximinez in https://github.com/XRPLF/rippled/pull/4361 +* `fixNFTokenRemint`: prevent NFT re-mint: by @shawnxie999 in https://github.com/XRPLF/rippled/pull/4406 +* Fix a case where ripple::Expected returned a json array, not a value by @scottschurr in https://github.com/XRPLF/rippled/pull/4401 +* fix: Ledger data returns an empty list (instead of null) when all entries are filtered out by @drlongle in https://github.com/XRPLF/rippled/pull/4398 +* Fix unit test ripple.app.LedgerData by @drlongle in https://github.com/XRPLF/rippled/pull/4484 +* Fix the fix for std::result_of by @thejohnfreeman in https://github.com/XRPLF/rippled/pull/4496 +* Fix errors for Clang 16 by @thejohnfreeman in https://github.com/XRPLF/rippled/pull/4501 +* Ensure that switchover vars are initialized before use: by @seelabs in https://github.com/XRPLF/rippled/pull/4527 +* Move faulty assert by @ximinez in https://github.com/XRPLF/rippled/pull/4533 +* Fix unaligned load and stores: (#4528) by @seelabs in https://github.com/XRPLF/rippled/pull/4531 +* fix node size estimation by @dangell7 in https://github.com/XRPLF/rippled/pull/4536 +* fix: remove redundant moves by @ckeshava in https://github.com/XRPLF/rippled/pull/4565 + +### Code Cleanup and Testing + +* Replace compare() with the three-way comparison operator in base_uint, Issue and Book by @drlongle in https://github.com/XRPLF/rippled/pull/4411 +* Rectify the import paths of boost::function_output_iterator by @ckeshava in https://github.com/XRPLF/rippled/pull/4293 +* Expand Linux test matrix by @thejohnfreeman in https://github.com/XRPLF/rippled/pull/4454 +* Add patched recipe for SOCI by @thejohnfreeman in https://github.com/XRPLF/rippled/pull/4510 +* Switch to self-hosted runners for macOS by @thejohnfreeman in https://github.com/XRPLF/rippled/pull/4511 +* [TRIVIAL] Add missing includes by @seelabs in https://github.com/XRPLF/rippled/pull/4555 + +### Docs + +* Refactor build instructions by @thejohnfreeman in https://github.com/XRPLF/rippled/pull/4381 +* Add install instructions for package managers by @thejohnfreeman in https://github.com/XRPLF/rippled/pull/4472 +* Fix typo by @solmsted in https://github.com/XRPLF/rippled/pull/4508 +* Update environment.md by @sappenin in https://github.com/XRPLF/rippled/pull/4498 +* Update BUILD.md by @oeggert in https://github.com/XRPLF/rippled/pull/4514 +* Trivial: add comments for NFToken-related invariants by @scottschurr in https://github.com/XRPLF/rippled/pull/4558 + +## New Contributors +* @drlongle made their first contribution in https://github.com/XRPLF/rippled/pull/4411 +* @ckeshava made their first contribution in https://github.com/XRPLF/rippled/pull/4293 +* @solmsted made their first contribution in https://github.com/XRPLF/rippled/pull/4508 +* @sappenin made their first contribution in https://github.com/XRPLF/rippled/pull/4498 +* @oeggert made their first contribution in https://github.com/XRPLF/rippled/pull/4514 + +**Full Changelog**: https://github.com/XRPLF/rippled/compare/1.10.1...1.11.0 + + +### GitHub + +The public source code repository for `rippled` is hosted on GitHub at . + +We welcome all contributions and invite everyone to join the community of XRP Ledger developers to help build the Internet of Value. + +### Credits + +The following people contributed directly to this release: +- Alloy Networks <45832257+alloynetworks@users.noreply.github.com> +- Brandon Wilson +- Chenna Keshava B S <21219765+ckeshava@users.noreply.github.com> +- David Fuelling +- Denis Angell +- Ed Hennis +- Elliot Lee +- John Freeman +- Mark Travis +- Nik Bougalis +- RichardAH +- Scott Determan +- Scott Schurr +- Shawn Xie <35279399+shawnxie999@users.noreply.github.com> +- drlongle +- ledhed2222 +- oeggert <117319296+oeggert@users.noreply.github.com> +- solmsted + + +Bug Bounties and Responsible Disclosures: +We welcome reviews of the rippled code and urge researchers to +responsibly disclose any issues they may find. + +To report a bug, please send a detailed report to: + + bugs@xrpl.org + + # Introducing XRP Ledger version 1.10.1 Version 1.10.1 of `rippled`, the reference server implementation of the XRP Ledger protocol, is now available. This release restores packages for Ubuntu 18.04. diff --git a/src/ripple/app/main/Application.cpp b/src/ripple/app/main/Application.cpp index 16781ac09d4..8ed328df440 100644 --- a/src/ripple/app/main/Application.cpp +++ b/src/ripple/app/main/Application.cpp @@ -602,6 +602,13 @@ class ApplicationImp : public Application, public BasicApp 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 d8cb7d31815..d2ba8f7cc75 100644 --- a/src/ripple/app/main/Application.h +++ b/src/ripple/app/main/Application.h @@ -89,6 +89,7 @@ class Overlay; class PathRequests; class PendingSaves; class PublicKey; +class ServerHandlerImp; class SecretKey; class STLedgerEntry; class TimeKeeper; @@ -231,6 +232,8 @@ class Application : public beast::PropertyStream::Source 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 fdef8c1cec8..3a5e96b0ed9 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 6be11c7dd6c..6f51f811055 100644 --- a/src/ripple/app/misc/NetworkOPs.cpp +++ b/src/ripple/app/misc/NetworkOPs.cpp @@ -65,9 +65,11 @@ #include #include #include +#include #include #include +#include #include #include #include @@ -2661,6 +2663,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/app/paths/impl/BookStep.cpp b/src/ripple/app/paths/impl/BookStep.cpp index a6b2c59611e..555d90fac8c 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 4f1d9108bca..dd01a64b5f2 100644 --- a/src/ripple/app/tx/impl/CreateOffer.cpp +++ b/src/ripple/app/tx/impl/CreateOffer.cpp @@ -824,8 +824,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/InvariantCheck.h b/src/ripple/app/tx/impl/InvariantCheck.h index c3bb0216426..fe17db44fa7 100644 --- a/src/ripple/app/tx/impl/InvariantCheck.h +++ b/src/ripple/app/tx/impl/InvariantCheck.h @@ -318,6 +318,17 @@ class ValidNewAccountRoot beast::Journal const&); }; +/** + * @brief Invariant: Validates several invariants for NFToken pages. + * + * The following checks are made: + * - The page is correctly associated with the owner. + * - The page is correctly ordered between the next and previous links. + * - The page contains at least one and no more than 32 NFTokens. + * - The NFTokens on this page do not belong on a lower or higher page. + * - The NFTokens are correctly sorted on the page. + * - Each URI, if present, is not empty. + */ class ValidNFTokenPage { bool badEntry_ = false; @@ -342,6 +353,19 @@ class ValidNFTokenPage beast::Journal const&); }; +/** + * @brief Invariant: Validates counts of NFTokens after all transaction types. + * + * The following checks are made: + * - The number of minted or burned NFTokens can only be changed by + * NFTokenMint or NFTokenBurn transactions. + * - A successful NFTokenMint must increase the number of NFTokens. + * - A failed NFTokenMint must not change the number of minted NFTokens. + * - An NFTokenMint transaction cannot change the number of burned NFTokens. + * - A successful NFTokenBurn must increase the number of burned NFTokens. + * - A failed NFTokenBurn must not change the number of burned NFTokens. + * - An NFTokenBurn transaction cannot change the number of minted NFTokens. + */ class NFTokenCountTracking { std::uint32_t beforeMintedTotal = 0; diff --git a/src/ripple/app/tx/impl/OfferStream.cpp b/src/ripple/app/tx/impl/OfferStream.cpp index 58fd209ca0b..5933d9c3838 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/basics/IOUAmount.h b/src/ripple/basics/IOUAmount.h index 764aa38aae3..2380a7d15e1 100644 --- a/src/ripple/basics/IOUAmount.h +++ b/src/ripple/basics/IOUAmount.h @@ -186,7 +186,14 @@ mulRatio( std::uint32_t den, bool roundUp); -extern LocalValue stNumberSwitchover; +// Since many uses of the number class do not have access to a ledger, +// getSTNumberSwitchover needs to be globally accessible. + +bool +getSTNumberSwitchover(); + +void +setSTNumberSwitchover(bool v); /** RAII class to set and restore the Number switchover. */ @@ -198,16 +205,16 @@ class NumberSO public: ~NumberSO() { - *stNumberSwitchover = saved_; + setSTNumberSwitchover(saved_); } NumberSO(NumberSO const&) = delete; NumberSO& operator=(NumberSO const&) = delete; - explicit NumberSO(bool v) : saved_(*stNumberSwitchover) + explicit NumberSO(bool v) : saved_(getSTNumberSwitchover()) { - *stNumberSwitchover = v; + setSTNumberSwitchover(v); } }; diff --git a/src/ripple/basics/SlabAllocator.h b/src/ripple/basics/SlabAllocator.h index c966af318b6..ece96d0b873 100644 --- a/src/ripple/basics/SlabAllocator.h +++ b/src/ripple/basics/SlabAllocator.h @@ -21,16 +21,18 @@ #define RIPPLE_BASICS_SLABALLOCATOR_H_INCLUDED #include + +#include +#include +#include + #include #include #include #include +#include #include -#include -#include -#include - #if BOOST_OS_LINUX #include #endif @@ -76,7 +78,9 @@ class SlabAllocator while (data + item <= p_ + size_) { - *reinterpret_cast(data) = l_; + // Use memcpy to avoid unaligned UB + // (will optimize to equivalent code) + std::memcpy(data, &l_, sizeof(std::uint8_t*)); l_ = data; data += item; } @@ -115,7 +119,11 @@ class SlabAllocator ret = l_; if (ret) - l_ = *reinterpret_cast(ret); + { + // Use memcpy to avoid unaligned UB + // (will optimize to equivalent code) + std::memcpy(&l_, ret, sizeof(std::uint8_t*)); + } } return ret; @@ -136,7 +144,10 @@ class SlabAllocator assert(own(ptr)); std::lock_guard l(m_); - *reinterpret_cast(ptr) = l_; + + // Use memcpy to avoid unaligned UB + // (will optimize to equivalent code) + std::memcpy(ptr, &l_, sizeof(std::uint8_t*)); l_ = ptr; } }; diff --git a/src/ripple/basics/StringUtilities.h b/src/ripple/basics/StringUtilities.h index 48de772ca41..8af81a37403 100644 --- a/src/ripple/basics/StringUtilities.h +++ b/src/ripple/basics/StringUtilities.h @@ -25,7 +25,9 @@ #include #include + #include +#include #include #include #include diff --git a/src/ripple/basics/base64.h b/src/ripple/basics/base64.h index ef34192d0b0..05a61133f83 100644 --- a/src/ripple/basics/base64.h +++ b/src/ripple/basics/base64.h @@ -57,6 +57,7 @@ #ifndef RIPPLE_BASICS_BASE64_H_INCLUDED #define RIPPLE_BASICS_BASE64_H_INCLUDED +#include #include namespace ripple { diff --git a/src/ripple/basics/impl/IOUAmount.cpp b/src/ripple/basics/impl/IOUAmount.cpp index c9b52874abd..e3c3411057b 100644 --- a/src/ripple/basics/impl/IOUAmount.cpp +++ b/src/ripple/basics/impl/IOUAmount.cpp @@ -27,7 +27,28 @@ namespace ripple { -LocalValue stNumberSwitchover(true); +namespace { + +// Use a static inside a function to help prevent order-of-initialzation issues +LocalValue& +getStaticSTNumberSwitchover() +{ + static LocalValue r{true}; + return r; +} +} // namespace + +bool +getSTNumberSwitchover() +{ + return *getStaticSTNumberSwitchover(); +} + +void +setSTNumberSwitchover(bool v) +{ + *getStaticSTNumberSwitchover() = v; +} /* The range for the mantissa when normalized */ static std::int64_t constexpr minMantissa = 1000000000000000ull; @@ -51,7 +72,7 @@ IOUAmount::normalize() return; } - if (*stNumberSwitchover) + if (getSTNumberSwitchover()) { Number const v{mantissa_, exponent_}; mantissa_ = v.mantissa(); @@ -117,7 +138,7 @@ IOUAmount::operator+=(IOUAmount const& other) return *this; } - if (*stNumberSwitchover) + if (getSTNumberSwitchover()) { *this = IOUAmount{Number{*this} + Number{other}}; return *this; diff --git a/src/ripple/basics/impl/partitioned_unordered_map.cpp b/src/ripple/basics/impl/partitioned_unordered_map.cpp index 6fb2cbec1d4..3ced32eddff 100644 --- a/src/ripple/basics/impl/partitioned_unordered_map.cpp +++ b/src/ripple/basics/impl/partitioned_unordered_map.cpp @@ -31,7 +31,11 @@ namespace ripple { static std::size_t extract(uint256 const& key) { - return *reinterpret_cast(key.data()); + std::size_t result; + // Use memcpy to avoid unaligned UB + // (will optimize to equivalent code) + std::memcpy(&result, key.data(), sizeof(std::size_t)); + return result; } static std::size_t diff --git a/src/ripple/beast/hash/impl/xxhash.cpp b/src/ripple/beast/hash/impl/xxhash.cpp index 76d5e7997f5..4a6c85db815 100644 --- a/src/ripple/beast/hash/impl/xxhash.cpp +++ b/src/ripple/beast/hash/impl/xxhash.cpp @@ -33,6 +33,8 @@ You can contact the author at : #include +#include + //************************************** // Tuning parameters //************************************** @@ -87,7 +89,7 @@ You can contact the author at : //************************************** // Includes & Memory related functions //************************************** -//#include "xxhash.h" +// #include "xxhash.h" // Modify the local functions below should you wish to use some other memory // routines for malloc(), free() #include @@ -260,7 +262,13 @@ FORCE_INLINE U64 XXH_readLE64_align(const void* ptr, XXH_endianess endian, XXH_alignment align) { if (align == XXH_unaligned) - return endian == XXH_littleEndian ? A64(ptr) : XXH_swap64(A64(ptr)); + { + // Use memcpy to avoid unaligned UB + U64 tmp_aligned; + std::memcpy(&tmp_aligned, ptr, sizeof(U64)); + return endian == XXH_littleEndian ? tmp_aligned + : XXH_swap64(tmp_aligned); + } else return endian == XXH_littleEndian ? *(U64*)ptr : XXH_swap64(*(U64*)ptr); } diff --git a/src/ripple/core/impl/Config.cpp b/src/ripple/core/impl/Config.cpp index ebeec9af36f..f835ca8df04 100644 --- a/src/ripple/core/impl/Config.cpp +++ b/src/ripple/core/impl/Config.cpp @@ -68,10 +68,8 @@ namespace detail { [[nodiscard]] std::uint64_t getMemorySize() { - struct sysinfo si; - - if (sysinfo(&si) == 0) - return static_cast(si.totalram); + if (struct sysinfo si; sysinfo(&si) == 0) + return static_cast(si.totalram) * si.mem_unit; return 0; } @@ -128,7 +126,7 @@ sizedItems {SizedItem::lgrDBCache, {{ 4, 8, 16, 32, 128 }}}, {SizedItem::openFinalLimit, {{ 8, 16, 32, 64, 128 }}}, {SizedItem::burstSize, {{ 4, 8, 16, 32, 48 }}}, - {SizedItem::ramSizeGB, {{ 8, 12, 16, 24, 32 }}}, + {SizedItem::ramSizeGB, {{ 6, 8, 12, 24, 0 }}}, {SizedItem::accountIdCacheSize, {{ 20047, 50053, 77081, 150061, 300007 }}} }}; @@ -265,7 +263,8 @@ getEnvVar(char const* name) } Config::Config() - : j_(beast::Journal::getNullSink()), ramSize_(detail::getMemorySize()) + : j_(beast::Journal::getNullSink()) + , ramSize_(detail::getMemorySize() / (1024 * 1024 * 1024)) { } @@ -290,22 +289,18 @@ Config::setupControl(bool bQuiet, bool bSilent, bool bStandalone) threshold.second.begin(), threshold.second.end(), [this](std::size_t limit) { - return (ramSize_ / (1024 * 1024 * 1024)) < limit; + return (limit == 0) || (ramSize_ < limit); }); + assert(ns != threshold.second.end()); + if (ns != threshold.second.end()) NODE_SIZE = std::distance(threshold.second.begin(), ns); // Adjust the size based on the number of hardware threads of // execution available to us: - if (auto const hc = std::thread::hardware_concurrency()) - { - if (hc == 1) - NODE_SIZE = 0; - - if (hc < 4) - NODE_SIZE = std::min(NODE_SIZE, 1); - } + if (auto const hc = std::thread::hardware_concurrency(); hc != 0) + NODE_SIZE = std::min(hc / 2, NODE_SIZE); } assert(NODE_SIZE <= 4); diff --git a/src/ripple/json/impl/json_reader.cpp b/src/ripple/json/impl/json_reader.cpp index 6686b38f49e..c92bea6d7e0 100644 --- a/src/ripple/json/impl/json_reader.cpp +++ b/src/ripple/json/impl/json_reader.cpp @@ -19,8 +19,10 @@ #include #include + #include #include +#include #include #include diff --git a/src/ripple/nodestore/impl/Shard.cpp b/src/ripple/nodestore/impl/Shard.cpp index 14bfe487303..8d0eab81153 100644 --- a/src/ripple/nodestore/impl/Shard.cpp +++ b/src/ripple/nodestore/impl/Shard.cpp @@ -688,9 +688,6 @@ Shard::finalize(bool writeSQLite, std::optional const& referenceHash) ledger->stateMap().setLedgerSeq(ledgerSeq); ledger->txMap().setLedgerSeq(ledgerSeq); - assert( - ledger->info().seq < XRP_LEDGER_EARLIEST_FEES || - ledger->read(keylet::fees())); ledger->setImmutable(); if (!ledger->stateMap().fetchRoot( SHAMapHash{ledger->info().accountHash}, nullptr)) @@ -713,6 +710,11 @@ Shard::finalize(bool writeSQLite, std::optional const& referenceHash) if (writeSQLite && !storeSQLite(ledger)) return fail("failed storing to SQLite databases"); + assert( + ledger->info().seq == ledgerSeq && + (ledger->info().seq < XRP_LEDGER_EARLIEST_FEES || + ledger->read(keylet::fees()))); + hash = ledger->info().parentHash; next = std::move(ledger); diff --git a/src/ripple/protocol/Feature.h b/src/ripple/protocol/Feature.h index d98d7d01f82..a89dcb68589 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 = 59; +static constexpr std::size_t numFeatures = 60; /** Amendments that this server supports and the default voting behavior. Whether they are enabled depends on the Rules defined in the validated @@ -345,6 +345,7 @@ extern uint256 const featureXRPFees; extern uint256 const fixUniversalNumber; extern uint256 const fixNonFungibleTokensV1_2; extern uint256 const fixNFTokenRemint; +extern uint256 const fixReducedOffersV1; extern uint256 const featurePaychanAndEscrowForTokens; } // namespace ripple diff --git a/src/ripple/protocol/Quality.h b/src/ripple/protocol/Quality.h index 9de137d8770..840d8d444e1 100644 --- a/src/ripple/protocol/Quality.h +++ b/src/ripple/protocol/Quality.h @@ -223,6 +223,29 @@ class Quality 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 06e4150b620..6c6027ad2e9 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 @@ -587,7 +604,12 @@ isAddable(STAmount const& amt1, STAmount const& amt2) // the low-level routine stAmountCanonicalize on an amendment switch. Only // transactions need to use this switchover. Outside of a transaction it's safe // to unconditionally use the new behavior. -extern LocalValue stAmountCanonicalizeSwitchover; + +bool +getSTAmountCanonicalizeSwitchover(); + +void +setSTAmountCanonicalizeSwitchover(bool v); /** RAII class to set and restore the STAmount canonicalize switchover. */ @@ -595,14 +617,14 @@ extern LocalValue stAmountCanonicalizeSwitchover; class STAmountSO { public: - explicit STAmountSO(bool v) : saved_(*stAmountCanonicalizeSwitchover) + explicit STAmountSO(bool v) : saved_(getSTAmountCanonicalizeSwitchover()) { - *stAmountCanonicalizeSwitchover = v; + setSTAmountCanonicalizeSwitchover(v); } ~STAmountSO() { - *stAmountCanonicalizeSwitchover = saved_; + setSTAmountCanonicalizeSwitchover(saved_); } private: diff --git a/src/ripple/protocol/impl/BuildInfo.cpp b/src/ripple/protocol/impl/BuildInfo.cpp index b1c1bab7fb5..734773fa03e 100644 --- a/src/ripple/protocol/impl/BuildInfo.cpp +++ b/src/ripple/protocol/impl/BuildInfo.cpp @@ -33,7 +33,7 @@ namespace BuildInfo { // and follow the format described at http://semver.org/ //------------------------------------------------------------------------------ // clang-format off -char const* const versionString = "1.10.1" +char const* const versionString = "1.11.0" // clang-format on #if defined(DEBUG) || defined(SANITIZER) diff --git a/src/ripple/protocol/impl/Feature.cpp b/src/ripple/protocol/impl/Feature.cpp index 889f3a7f5a4..f1170201d93 100644 --- a/src/ripple/protocol/impl/Feature.cpp +++ b/src/ripple/protocol/impl/Feature.cpp @@ -452,6 +452,7 @@ REGISTER_FEATURE(XRPFees, Supported::yes, VoteBehavior::De REGISTER_FIX (fixUniversalNumber, Supported::yes, VoteBehavior::DefaultNo); REGISTER_FIX (fixNonFungibleTokensV1_2, Supported::yes, VoteBehavior::DefaultNo); REGISTER_FIX (fixNFTokenRemint, Supported::yes, VoteBehavior::DefaultNo); +REGISTER_FIX (fixReducedOffersV1, Supported::yes, VoteBehavior::DefaultNo); REGISTER_FEATURE(PaychanAndEscrowForTokens, Supported::yes, VoteBehavior::DefaultNo); // The following amendments are obsolete, but must remain supported diff --git a/src/ripple/protocol/impl/Quality.cpp b/src/ripple/protocol/impl/Quality.cpp index 97e1b8a9fd7..f7b9d6b3c41 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 d1a878c8b4f..90a646787ae 100644 --- a/src/ripple/protocol/impl/STAmount.cpp +++ b/src/ripple/protocol/impl/STAmount.cpp @@ -34,7 +34,28 @@ namespace ripple { -LocalValue stAmountCanonicalizeSwitchover(true); +namespace { + +// Use a static inside a function to help prevent order-of-initialzation issues +LocalValue& +getStaticSTAmountCanonicalizeSwitchover() +{ + static LocalValue r{true}; + return r; +} +} // namespace + +bool +getSTAmountCanonicalizeSwitchover() +{ + return *getStaticSTAmountCanonicalizeSwitchover(); +} + +void +setSTAmountCanonicalizeSwitchover(bool v) +{ + *getStaticSTAmountCanonicalizeSwitchover() = v; +} static const std::uint64_t tenTo14 = 100000000000000ull; static const std::uint64_t tenTo14m1 = tenTo14 - 1; @@ -395,7 +416,7 @@ operator+(STAmount const& v1, STAmount const& v2) if (v1.native()) return {v1.getFName(), getSNValue(v1) + getSNValue(v2)}; - if (*stNumberSwitchover) + if (getSTNumberSwitchover()) { auto x = v1; x = v1.iou() + v2.iou(); @@ -717,7 +738,7 @@ STAmount::canonicalize() return; } - if (*stAmountCanonicalizeSwitchover) + if (getSTAmountCanonicalizeSwitchover()) { // log(cMaxNativeN, 10) == 17 if (mOffset > 17) @@ -725,7 +746,7 @@ STAmount::canonicalize() "Native currency amount out of range"); } - if (*stNumberSwitchover && *stAmountCanonicalizeSwitchover) + if (getSTNumberSwitchover() && getSTAmountCanonicalizeSwitchover()) { Number num( mIsNegative ? -mValue : mValue, mOffset, Number::unchecked{}); @@ -744,7 +765,7 @@ STAmount::canonicalize() while (mOffset > 0) { - if (*stAmountCanonicalizeSwitchover) + if (getSTAmountCanonicalizeSwitchover()) { // N.B. do not move the overflow check to after the // multiplication @@ -765,7 +786,7 @@ STAmount::canonicalize() mIsNative = false; - if (*stNumberSwitchover) + if (getSTNumberSwitchover()) { *this = iou(); return; @@ -1208,7 +1229,7 @@ multiply(STAmount const& v1, STAmount const& v2, Issue const& issue) return STAmount(v1.getFName(), minV * maxV); } - if (*stNumberSwitchover) + if (getSTNumberSwitchover()) return {IOUAmount{Number{v1} * Number{v2}}, issue}; std::uint64_t value1 = v1.mantissa(); @@ -1245,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) { @@ -1280,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, @@ -1344,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) { @@ -1367,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, @@ -1416,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)) @@ -1438,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/ripple/protocol/jss.h b/src/ripple/protocol/jss.h index 46f7f085fde..7a9b525d820 100644 --- a/src/ripple/protocol/jss.h +++ b/src/ripple/protocol/jss.h @@ -467,13 +467,14 @@ JSS(peers); // out: InboundLedger, handlers/Peers, Overlay JSS(peer_disconnects); // Severed peer connection counter. JSS(peer_disconnects_resources); // Severed peer connections because of // excess resource consumption. -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/ripple/rpc/handlers/AccountObjects.cpp b/src/ripple/rpc/handlers/AccountObjects.cpp index e8304c670de..65cd12f2d41 100644 --- a/src/ripple/rpc/handlers/AccountObjects.cpp +++ b/src/ripple/rpc/handlers/AccountObjects.cpp @@ -19,7 +19,6 @@ #include #include -#include #include #include #include @@ -66,7 +65,7 @@ doAccountNFTs(RPC::JsonContext& context) RPC::inject_error(rpcACT_MALFORMED, result); return result; } - auto const accountID{std::move(id.value())}; + auto const accountID{id.value()}; if (!ledger->exists(keylet::account(accountID))) return rpcError(rpcACT_NOT_FOUND); @@ -179,7 +178,7 @@ doAccountObjects(RPC::JsonContext& context) RPC::inject_error(rpcACT_MALFORMED, result); return result; } - auto const accountID{std::move(id.value())}; + auto const accountID{id.value()}; if (!ledger->exists(keylet::account(accountID))) return rpcError(rpcACT_NOT_FOUND); diff --git a/src/test/app/Offer_test.cpp b/src/test/app/Offer_test.cpp index 0d1a4326440..9d97fb692a6 100644 --- a/src/test/app/Offer_test.cpp +++ b/src/test/app/Offer_test.cpp @@ -2126,18 +2126,17 @@ class Offer_test : public beast::unit_test::suite 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 00000000000..f82efcb7fc8 --- /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 diff --git a/src/test/jtx/impl/envconfig.cpp b/src/test/jtx/impl/envconfig.cpp index 77c43f5e40a..7f8163f5ee7 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", "memory"); cfg.overwrite(ConfigSection::nodeDatabase(), "path", "main"); cfg.deprecatedClearSection(ConfigSection::importNodeDatabase()); diff --git a/src/test/rpc/AccountInfo_test.cpp b/src/test/rpc/AccountInfo_test.cpp index 6ec4740bac2..b8e479225d1 100644 --- a/src/test/rpc/AccountInfo_test.cpp +++ b/src/test/rpc/AccountInfo_test.cpp @@ -206,10 +206,7 @@ class AccountInfo_test : public beast::unit_test::suite 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/ReportingETL_test.cpp b/src/test/rpc/ReportingETL_test.cpp index 77284dd776d..ed055d0fd93 100644 --- a/src/test/rpc/ReportingETL_test.cpp +++ b/src/test/rpc/ReportingETL_test.cpp @@ -18,7 +18,6 @@ */ //============================================================================== -#include #include #include #include diff --git a/src/test/rpc/ServerInfo_test.cpp b/src/test/rpc/ServerInfo_test.cpp index 24cfd12299a..a69483cb130 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 @@ class ServerInfo_test : public beast::unit_test::suite [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 @@ class ServerInfo_test : public beast::unit_test::suite 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 @@ class ServerInfo_test : public beast::unit_test::suite 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"); + } + } } } diff --git a/src/test/rpc/Version_test.cpp b/src/test/rpc/Version_test.cpp index 360b29664a1..60ffd30fcf6 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 + ']');