From 63d8772f69fc8fff90bc6b5f982978d7fd7b1df1 Mon Sep 17 00:00:00 2001 From: Sergey Kuznetsov Date: Mon, 10 Aug 2026 12:58:49 +0100 Subject: [PATCH 01/28] chore: Remove corrosion from nix (#7982) --- nix/packages.nix | 1 - 1 file changed, 1 deletion(-) diff --git a/nix/packages.nix b/nix/packages.nix index 01ab2ecf9a..3dfc6dff98 100644 --- a/nix/packages.nix +++ b/nix/packages.nix @@ -146,7 +146,6 @@ in cargo-audit cargo-llvm-cov cargo-nextest - corrosion rustToolchain ]; } From 07b9c59b89aae10aec31e012f8782fec388c84fa Mon Sep 17 00:00:00 2001 From: Ayaz Salikhov Date: Mon, 10 Aug 2026 14:08:15 +0100 Subject: [PATCH 02/28] build: Remove protobuf dependencies from Nix (#7984) --- nix/packages.nix | 9 --------- 1 file changed, 9 deletions(-) diff --git a/nix/packages.nix b/nix/packages.nix index 3dfc6dff98..0623ff51b9 100644 --- a/nix/packages.nix +++ b/nix/packages.nix @@ -129,15 +129,6 @@ in perl # needed for openssl pkg-config pre-commit - # protoc generates the Go gRPC bindings and embeds its own version string into every committed - # .pb.go file. To allow CI to verify those files with a plain `git diff`, we pin the version to - # `protobuf_34` rather than the rolling `protobuf` to keep regeneration reproducible across the - # Nix frequently changing unstable channel. The protoc-gen-go* plugins have no versioned - # attributes in nixpkgs; protoc-gen-go's version is in turn constrained by the go.mod require - # on google.golang.org/protobuf. - protobuf_34 # provides protoc - protoc-gen-go # protoc plugin for the Go message bindings - protoc-gen-go-grpc # protoc plugin for the Go gRPC service stubs python3 runClangTidy vim From a24caaa6eae9fb9d1cdfa80a4ba2e26ccaa55753 Mon Sep 17 00:00:00 2001 From: Ayaz Salikhov Date: Mon, 10 Aug 2026 16:00:30 +0100 Subject: [PATCH 03/28] docs: Rearrange & simplify build/nix/environment docs (#7985) --- BUILD.md | 54 +++++++++----------------- docs/build/environment.md | 81 +++++++++++++++++++++++++-------------- docs/build/nix.md | 16 +++----- 3 files changed, 77 insertions(+), 74 deletions(-) diff --git a/BUILD.md b/BUILD.md index 238c10e17c..ad4666b141 100644 --- a/BUILD.md +++ b/BUILD.md @@ -4,34 +4,14 @@ ## Minimum Requirements -See [System Requirements](https://xrpl.org/system-requirements.html). +For the hardware needed to run a node, see +[System Requirements](https://xrpl.org/system-requirements.html). -Building xrpld generally requires Git, Python, Conan, CMake, and a C++ -compiler. - -- [Python](https://www.python.org/downloads/) -- [Conan](https://conan.io/downloads.html) -- [CMake](https://cmake.org/download/) - -You can verify that the required tools are installed and runnable with: - -```bash -./bin/check-tools.sh -``` - -`xrpld` is written in the C++23 dialect. The [tested compiler versions][cpp23-support] are: - -| Compiler | Version | -| ----------- | --------------- | -| GCC | 15.2 | -| Clang | 22 | -| Apple Clang | 21 | -| MSVC | 19.44[^windows] | +For the software needed to build xrpld, see the +[environment setup guide](./docs/build/environment.md). ## Operating Systems -Please see the [environment setup guide](./docs/build/environment.md) for detailed instructions for all platforms. - ### Linux The Ubuntu Linux distribution has received the highest level of quality @@ -47,9 +27,8 @@ CI testing is done in macOS 26 (Tahoe), but the build defaults `CMAKE_OSX_DEPLOY ### Windows -Windows is used by some engineers for development only. - -[^windows]: Windows is not recommended for production use. +Windows is used by some engineers for development only, and is not recommended +for production use. ## Steps @@ -74,12 +53,8 @@ releases](https://github.com/XRPLF/rippled/releases). ### Set Up Conan -After you have a [C++ development environment](./docs/build/environment.md) ready with Git, Python, -Conan, CMake, and a C++ compiler, you may need to set up your Conan profile. - -These instructions assume a basic familiarity with Conan and CMake. If you are -unfamiliar with Conan, then please read [this crash course](./docs/build/conan.md) or the official -[Getting Started][conan-getting-started] walkthrough. +Once your [development environment](./docs/build/environment.md) is ready, you +may need to set up your Conan profile. #### Profiles @@ -269,10 +244,14 @@ which is only enabled when the `coverage` option is set, e.g. with Prerequisites for the coverage report: - [gcovr tool][gcovr] (can be installed e.g. with [pip][python-pip]) -- `gcov` for GCC (installed with the compiler by default) or -- `llvm-cov` for Clang (installed with the compiler by default) +- `gcov` for GCC or `llvm-cov` for Clang, usually installed with the compiler - `Debug` build type +> [!NOTE] +> Clang coverage is not available in the [Nix development shell](./docs/build/nix.md#building-xrpld-in-the-nix-shell): +> its `clang` shells do not ship `llvm-cov`. Use a `gcc` shell instead (`.#gcc`, +> or `.#gcc-plain` on Linux), which provides a `gcov` matching its compiler. + A coverage report is created when the following steps are completed, in order: 1. `xrpld` binary built with instrumentation data, enabled by the `coverage` @@ -389,6 +368,10 @@ After any updates or changes to dependencies, you may need to do the following: 4. [Regenerate lockfile](./docs/build/advanced_conan.md#conan-lockfile). 5. Re-run [conan install](#build-and-test). +If you are using the Nix development shell, prebuilt Conan binaries may be +incompatible with it — see +[Building xrpld in the Nix shell](./docs/build/nix.md#building-xrpld-in-the-nix-shell). + #### ERROR: Package not resolved If you're seeing an error like `ERROR: Package 'snappy/1.1.10' not resolved: Unable to find 'snappy/1.1.10#968fef506ff261592ec30c574d4a7809%1756234314.246' in remotes.`, @@ -412,7 +395,6 @@ For example, if you want to build Debug: 1. For conan install, pass `--settings build_type=Debug` 2. For cmake, pass `-DCMAKE_BUILD_TYPE=Debug` -[cpp23-support]: https://en.cppreference.com/w/cpp/compiler_support/23 [conan-getting-started]: https://docs.conan.io/en/latest/getting_started.html [unity-build]: https://en.wikipedia.org/wiki/Unity_build [gcovr]: https://gcovr.com/en/stable/getting-started.html diff --git a/docs/build/environment.md b/docs/build/environment.md index e639ed2d5f..5616f32f37 100644 --- a/docs/build/environment.md +++ b/docs/build/environment.md @@ -6,22 +6,52 @@ This document explains how to set one up. ## Tested compiler versions -`xrpld` is built in the **C++23** dialect by default. -Make sure your toolchain is recent enough — the compiler versions currently tested in CI are: +`xrpld` is built in the **C++23** dialect by default, so your toolchain has to +support it — see [compiler support for C++23][cpp23-support]. +The versions currently tested in CI are: -| Compiler | Version | -| ----------- | ------- | -| GCC | 15.2 | -| Clang | 22 | -| Apple Clang | 17 | -| MSVC | 19.44 | +| Compiler | Version | +| ----------- | ------------------ | +| GCC | 15.2 | +| Clang | 22 | +| Apple Clang | 21 | +| MSVC | Visual Studio 2026 | LLVM tools (`clang-tidy` and `clang-format`) are also pinned to version 22. +### Older compilers + Older compilers may fail to build the latest `develop` code: the codebase now relies on C++23 features and has been adjusted for `clang-tidy`. If the latest code doesn't build for you, update your build toolchain first. +If updating isn't an option for you, we do accept pull requests that fix builds +on older compilers, as long as the change is small and doesn't make the code +harder to read. What we can't promise is that older compilers will keep working: +only the versions in the table above are tested in CI, and we won't hold back +the use of C++23 features or add invasive workarounds to keep an untested +compiler building. Treat support for anything outside the table as best-effort. + +## Required tools + +Besides a compiler, building `xrpld` requires: + +| Tool | Minimum version | +| ------------------------------------------- | --------------- | +| [Git](https://git-scm.com/downloads) | any recent | +| [Python](https://www.python.org/downloads/) | 3.11 | +| [Conan](https://conan.io/downloads.html) | 2.17 | +| [CMake](https://cmake.org/download/) | 3.16 | + +On Linux and macOS, the [Nix development shell](./nix.md) provides all of them +(see below). On Windows they have to be installed manually. + +Once they are in place, verify that everything is installed and runnable with: + +```bash +./bin/check-tools.sh +``` + ## Linux and macOS The **recommended way** to get a development environment on Linux and macOS is @@ -39,20 +69,15 @@ Clang. If you instead opt to use your system-wide Apple Clang (via below). See [Using the Nix development shell](./nix.md) for installation and usage -details, including how to select a different compiler. - -> [!NOTE] -> Using Nix is not mandatory. Any custom environment (Homebrew packages or -> anything else) will continue to work, but then it is up to you to keep it in -> sync with the environment used in CI. Nix unifies the development environment -> for everyone and synchronizes updates, which is why we recommend it. +details, including how to select a different compiler and why we recommend Nix +over a hand-maintained environment. ### macOS: managing the Apple Clang version If you use your system-wide Apple Clang on macOS (via `nix develop .#apple-clang`), the compiler version is whatever your installed Xcode (or Command Line Tools) provides. The following command should return a version greater than or equal to -the [minimum required](#tested-compiler-versions): +the [tested one](#tested-compiler-versions): ```bash clang --version @@ -89,23 +114,23 @@ building xrpld. You may want to install and pin a specific version of Xcode: Nix is not available on Windows, so the required tools have to be installed manually: -- [Visual Studio 2022](https://visualstudio.microsoft.com/) with the +- [Visual Studio 2026](https://visualstudio.microsoft.com/) with the **"Desktop development with C++"** workload — this provides MSVC and the - "x64 Native Tools Command Prompt". + "x64 Native Tools Command Prompt". CI configures CMake with the + `Visual Studio 18 2026` generator. - [Git for Windows](https://git-scm.com/download/win) -- [Python 3.11](https://www.python.org/downloads/), or higher -- [Conan 2.17](https://conan.io/downloads.html), or higher -- [CMake 3.22](https://cmake.org/download/), or higher - -> [!NOTE] -> Windows is used for development only and is not recommended for production. +- Python, Conan, and CMake, at the versions listed in + [Required tools](#required-tools). ## Clang-tidy `clang-tidy` is required to run static analysis checks locally (see [CONTRIBUTING.md](../../CONTRIBUTING.md)). It is not required to build the -project. This project currently uses `clang-tidy` version 22. +project. The version this project uses is listed in +[Tested compiler versions](#tested-compiler-versions). -On Linux and macOS, the [Nix development shell](./nix.md) provides `clang-tidy` -22 out of the box — run it via `run-clang-tidy`. No separate installation is -needed. +On Linux and macOS, the [Nix development shell](./nix.md) provides that exact +version out of the box — run it via `run-clang-tidy`. No separate installation +is needed. + +[cpp23-support]: https://en.cppreference.com/w/cpp/compiler_support/23 diff --git a/docs/build/nix.md b/docs/build/nix.md index d0001294e3..fad8bc701d 100644 --- a/docs/build/nix.md +++ b/docs/build/nix.md @@ -120,11 +120,15 @@ nix develop -c "$SHELL" > > If it doesn't, either adjust your shell configuration so it doesn't override `$PATH`, or use [direnv](#automatic-activation-with-direnv) (below), which loads the environment _after_ your shell config and so takes precedence regardless of the shell you use. -## Building xrpld with Nix +## Building xrpld in the Nix shell Once inside the Nix development shell, follow the standard [build instructions](../../BUILD.md#steps). The Nix shell provides all necessary tools (CMake, Ninja, Conan, etc.). -Coverage builds (`-Dcoverage=ON`) work in the `gcc` shell (and `gcc-plain` on Linux): +Two things differ from a system environment: + +**Prebuilt Conan packages.** There is no guarantee that binaries from the Conan cache will work when using Nix. If you encounter any errors, add `--build '*'` to the `conan install` command in [Build and Test](../../BUILD.md#build-and-test) to force Conan to compile everything from source. Keep the rest of the command as it is there, so it rebuilds the `build_type` you are actually configuring. + +**Coverage builds.** `-Dcoverage=ON` works in the `gcc` shell (and `gcc-plain` on Linux): each ships a `gcov` matching its compiler, since Nix's cc-wrapper does not expose one. The `clang` shells do not include `llvm-cov`, so use a `gcc` shell for coverage. @@ -142,14 +146,6 @@ The repository already ships an `.envrc` at its root that activates the Nix flak > [!NOTE] > direnv only caches the `.direnv` directory (already listed in `.gitignore`); no other repository files are affected. -## Conan and Prebuilt Packages - -Please note that there is no guarantee that binaries from conan cache will work when using nix. If you encounter any errors, please use `--build '*'` to force conan to compile everything from source: - -```bash -conan install .. --output-folder . --build '*' --settings build_type=Release -``` - ## Updating `flake.lock` file To update `flake.lock` to the latest revision use `nix flake update` command. From 71e972cbed9006a475ae326cf41b8959ef8d25f9 Mon Sep 17 00:00:00 2001 From: Mayukha Vadari Date: Mon, 10 Aug 2026 13:05:18 -0400 Subject: [PATCH 04/28] refactor: Act on TODOs that are unblocked by C++23 (#7990) --- src/libxrpl/protocol/ErrorCodes.cpp | 5 ++- src/test/app/Invariants_test.cpp | 10 ++---- src/test/jtx/TestHelpers.h | 13 ++------ src/xrpld/app/misc/FeeVoteImpl.cpp | 28 +++++++--------- src/xrpld/overlay/detail/ProtocolVersion.cpp | 33 ++++++------------- .../server_info/ServerDefinitions.cpp | 1 - 6 files changed, 29 insertions(+), 61 deletions(-) diff --git a/src/libxrpl/protocol/ErrorCodes.cpp b/src/libxrpl/protocol/ErrorCodes.cpp index e81f975844..802bae100d 100644 --- a/src/libxrpl/protocol/ErrorCodes.cpp +++ b/src/libxrpl/protocol/ErrorCodes.cpp @@ -105,10 +105,9 @@ static constexpr ErrorInfo kUnorderedErrorInfos[]{ }; // clang-format on -// Sort and validate unorderedErrorInfos at compile time. Should be -// converted to consteval when get to C++20. +// Sort and validate unorderedErrorInfos at compile time. template -constexpr auto +consteval auto sortErrorInfos(ErrorInfo const (&unordered)[N]) -> std::array { std::array ret = {}; diff --git a/src/test/app/Invariants_test.cpp b/src/test/app/Invariants_test.cpp index ffdfe6bc83..ed09b7b660 100644 --- a/src/test/app/Invariants_test.cpp +++ b/src/test/app/Invariants_test.cpp @@ -437,16 +437,10 @@ class Invariants_test : public beast::unit_test::Suite XRPAmount{}, STTx{ttACCOUNT_DELETE, [](STObject& tx) {}}); - for (auto const& keyletInfo : kDirectAccountKeylets) + for (auto const& [keyletfunc, type, includeInTests] : kDirectAccountKeylets) { - // TODO: Use structured binding once LLVM 16 is the minimum - // supported version. See also: - // https://github.com/llvm/llvm-project/issues/48582 - // https://github.com/llvm/llvm-project/commit/127bf44385424891eb04cff8e52d3f157fc2cb7c - if (!keyletInfo.includeInTests) + if (!includeInTests) continue; - auto const& keyletfunc = keyletInfo.function; - auto const& type = keyletInfo.expectedLEName; using namespace std::string_literals; diff --git a/src/test/jtx/TestHelpers.h b/src/test/jtx/TestHelpers.h index 5c8486e6c5..801c3627b8 100644 --- a/src/test/jtx/TestHelpers.h +++ b/src/test/jtx/TestHelpers.h @@ -43,6 +43,7 @@ #include #include #include +#include #include #include #include @@ -315,19 +316,11 @@ auto const kData = JTxFieldWrapper(sfData); auto const kAmount = JTxFieldWrapper(sfAmount); -// TODO We only need this long "requires" clause as polyfill, for C++20 -// implementations which are missing header. Replace with -// `std::ranges::range`, and accordingly use std::ranges::begin/end -// when we have moved to better compilers. -template +template auto makeVector(Input const& input) - requires requires(Input& v) { - std::begin(v); - std::end(v); - } { - return std::vector(std::begin(input), std::end(input)); + return std::vector(std::ranges::begin(input), std::ranges::end(input)); } // Functions used in debugging diff --git a/src/xrpld/app/misc/FeeVoteImpl.cpp b/src/xrpld/app/misc/FeeVoteImpl.cpp index 76a4d8f186..f1cb944a52 100644 --- a/src/xrpld/app/misc/FeeVoteImpl.cpp +++ b/src/xrpld/app/misc/FeeVoteImpl.cpp @@ -260,39 +260,35 @@ FeeVoteImpl::doVoting( } // choose our positions - // TODO: Use structured binding once LLVM 16 is the minimum supported - // version. See also: https://github.com/llvm/llvm-project/issues/48582 - // https://github.com/llvm/llvm-project/commit/127bf44385424891eb04cff8e52d3f157fc2cb7c - auto const baseFee = baseFeeVote.getVotes(); - auto const baseReserve = baseReserveVote.getVotes(); - auto const incReserve = incReserveVote.getVotes(); + auto const [baseFee, baseFeeChanged] = baseFeeVote.getVotes(); + auto const [baseReserve, baseReserveChanged] = baseReserveVote.getVotes(); + auto const [incReserve, incReserveChanged] = incReserveVote.getVotes(); auto const seq = lastClosedLedger->header().seq + 1; // add transactions to our position - if (baseFee.second || baseReserve.second || incReserve.second) + if (baseFeeChanged || baseReserveChanged || incReserveChanged) { - JLOG(journal_.warn()) << "We are voting for a fee change: " << baseFee.first << "/" - << baseReserve.first << "/" << incReserve.first; + JLOG(journal_.warn()) << "We are voting for a fee change: " << baseFee << "/" << baseReserve + << "/" << incReserve; STTx const feeTx(ttFEE, [=, &rules](auto& obj) { obj[sfAccount] = AccountID(); obj[sfLedgerSequence] = seq; if (rules.enabled(featureXRPFees)) { - obj[sfBaseFeeDrops] = baseFee.first; - obj[sfReserveBaseDrops] = baseReserve.first; - obj[sfReserveIncrementDrops] = incReserve.first; + obj[sfBaseFeeDrops] = baseFee; + obj[sfReserveBaseDrops] = baseReserve; + obj[sfReserveIncrementDrops] = incReserve; } else { // Without the featureXRPFees amendment, these fields are // required. - obj[sfBaseFee] = baseFee.first.dropsAs(baseFeeVote.current()); - obj[sfReserveBase] = - baseReserve.first.dropsAs(baseReserveVote.current()); + obj[sfBaseFee] = baseFee.dropsAs(baseFeeVote.current()); + obj[sfReserveBase] = baseReserve.dropsAs(baseReserveVote.current()); obj[sfReserveIncrement] = - incReserve.first.dropsAs(incReserveVote.current()); + incReserve.dropsAs(incReserveVote.current()); obj[sfReferenceFeeUnits] = kFeeUnitsDeprecated; } }); diff --git a/src/xrpld/overlay/detail/ProtocolVersion.cpp b/src/xrpld/overlay/detail/ProtocolVersion.cpp index 2d5d0a56f7..93d4fae156 100644 --- a/src/xrpld/overlay/detail/ProtocolVersion.cpp +++ b/src/xrpld/overlay/detail/ProtocolVersion.cpp @@ -14,6 +14,7 @@ #include #include #include +#include #include #include @@ -32,31 +33,17 @@ constexpr ProtocolVersion const kSupportedProtocolList[]{ {2, 3}, }; -// This ugly construct ensures that supportedProtocolList is sorted in strictly -// ascending order and doesn't contain any duplicates. -// FIXME: With C++20 we can use std::is_sorted with an appropriate comparator +// There should be at least one protocol we're willing to speak. static_assert( - []() constexpr -> bool { - auto const len = - std::distance(std::begin(kSupportedProtocolList), std::end(kSupportedProtocolList)); + !std::ranges::empty(kSupportedProtocolList), + "There must be at least one supported protocol."); - // There should be at least one protocol we're willing to speak. - if (len == 0) - return false; - - // A list with only one entry is, by definition, sorted so we don't - // need to check it. - if (len != 1) - { - for (auto i = 0; i != len - 1; ++i) - { - if (kSupportedProtocolList[i] >= kSupportedProtocolList[i + 1]) - return false; - } - } - - return true; - }(), +// Searching for an adjacent pair where the first element is not less than the +// second one proves the list is sorted in strictly ascending order, which in +// turn means it holds no duplicates. +static_assert( + std::ranges::adjacent_find(kSupportedProtocolList, std::ranges::greater_equal{}) == + std::ranges::end(kSupportedProtocolList), "The list of supported protocols isn't properly sorted."); std::string diff --git a/src/xrpld/rpc/handlers/server_info/ServerDefinitions.cpp b/src/xrpld/rpc/handlers/server_info/ServerDefinitions.cpp index b561ce6d38..32a084a833 100644 --- a/src/xrpld/rpc/handlers/server_info/ServerDefinitions.cpp +++ b/src/xrpld/rpc/handlers/server_info/ServerDefinitions.cpp @@ -64,7 +64,6 @@ ServerDefinitions::translate(std::string const& inp) return out; }; - // TODO: use string::contains with C++23 auto contains = [&](std::string_view s) -> bool { return inp.contains(s); }; if (contains("UINT")) From 2967f1f0ccac0e573ffc0a3ff6c8eaca928505c3 Mon Sep 17 00:00:00 2001 From: Mayukha Vadari Date: Mon, 10 Aug 2026 13:06:29 -0400 Subject: [PATCH 05/28] chore: Remove unreferenced legacy documents (#7989) --- docs/NodeStoreRefactoringCaseStudy.pdf | Bin 393922 -> 0 bytes docs/sample_chart.doc | 24 ------------------------ 2 files changed, 24 deletions(-) delete mode 100644 docs/NodeStoreRefactoringCaseStudy.pdf delete mode 100644 docs/sample_chart.doc diff --git a/docs/NodeStoreRefactoringCaseStudy.pdf b/docs/NodeStoreRefactoringCaseStudy.pdf deleted file mode 100644 index 6cde8a2eedd968662ee2b3c49598705be4321628..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 393922 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�MI6-|wp1>+Wf*QHH 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

CH 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

}ku;=$~rH0d$c}Tu=i0qPihxjlNJ(Gm%B@!ckLH zfxq1JFv4`mUDvE=$>f1ECsmQ#&(o(S8a}^Upcw3#sO^g=CRS;(0sw zBv_L}e(N7d;QeDwAMD#AOcKr6U*fx>82*|z2w2$1Bg;%YZNiAylcosm79cYB@d>-v=t^{ei()-+(~ z>CxF}1se8QSKa>OA#|O8C=OTvf2Czw?}yAcSSBp6DbUQ| z_@|tYAvFaJ+z+%4H~oPAVHAvdcZ|%iX%kn^(A1iDHgm-8vz?*x_yoEk%ETbn>rT8Oh=odO`GPd;_o$W`#p4yd{_JYunsly zk7DNRkU@Xu;OTx+cq~SH!75 zPBp({0;`gm4=o-N2#qOi2SVG=4z)PGO+FfsPMb;NH}-nAkZL<1S|F$7wKB`?Sk&B{ zuPW><9{A^z8ixhGkY-u#OWLR4W^cjSM?)9=Jr`We_s{{NO}^C<%us!~xGpctQ1(RZ z^o@&$sTLS7a&U%G zH$HJ8!maYI7A@4uY**y>{hp6*zy4mvPPP5~iNBM)f$yCBb;$Oh^pc_Bd{9M37_U^V zcHKLDof!`u4)O0ZYh}_F)X1*BOA`Ge3=g4)hyrqEAn=x%+(y_(0Wa*ahWCVsQnLkC zQK8L1mLJBW1I!T+m@<8;XC6>^`u0eLXl}VjvR%zlQ&vRWo`D;%R*whG@D)-qaz$Xh ztCY*?1BO3R&Tzf>Mo6~@qep9}g1#LBtlnPWJ#%P#@c_gbKJ^Iiy9?0h0;YEWxAEy^ zO1))g0D+7|7*7J06Z4+p@OuvPc2%NLAvfbK9Wazl2lh((xv_vkZ*2e%K@S}@RXB)l?G%x2{^{1FDy%{{9ImHu$qgHS9Vn__GZY{Cr_ij zor$F=`W(^XH}=I#Rd*Lh-}Jk6h06{NEMCzYx*gAAo3&+~&=-q0MU@47Mi(p(C#|yA z#1KW?6Lc$r1w?s;eC$$Rb(-{tb4JGr2;5TXe#L!s%3ONZT)o3Uc`oI;8L##rf4@mQ4hbJ~`W>4`kn0iFP_buhq5mkbtXj+<>0`VA4^r($+z|aL zXVt5p`?|f*-@B4Z>v??8)jWG)JLcjXo9k(us8O@-O1;Sr&L`&NTgtqw56bSY$D$P4 z-ulWhB3s?So}^9UOwpf(>Y|MP-&R$DNi92Hy?fLcagLh_|b( z(R`a{v3SLq(!7p2Xq9%YslVLLCVscBA-fD;RCjwvC;5*KI{%QRTg$8}NUnQ=6)f|41QP|mXMp5SVsVehq~c!A zu(Cr29?ly&V+gg@>cYnNNm7nnD>ex#uiQyoNxWF9w;ov5;P3CrcRPqEL9K+sw_QnX zaHdxB&lCshI6LRO0X~whN=_<^(6#aq`091bwn2~SH$a|ln4X@0+l4T3oICA2M*@G| z+cl0#6DGOn^#_#;&d>OZ8PVO7b+9go7w6@7t@+AbN4A4CkhGBIFUl|T9w;idJOp|| z-s9CZWwoeOcX5FW*E872fYtixug_CLKQj%&h_ZlYJQ~jTvJTKwH)QeRf1e)HIDL$f z+4UwJSnbf9J@g1Zb#HwOWVrwcX<$n^K~dzI)Wj&;o|uq+7vh1YoHyuBz648x5nS;p z%Oj5s^OV^i2-l^8*)l?9s?-l*y$X@k{uSi@bLIZ>0+iOM#f%d_Y*;@W3nEaW>bLtU zwC-V|G0z3FJPH<29z)dIU)|f{8yjsiWMM#g!cycV=ftXKhy)zFKYXoDHkJpk;5f{BA%d zje9A4t*P2|aTWp~w&9xd^^C>ltrNB!K86{#hZl+s_lZA}-%Cr73#{Kxm-N)1WLc8A zZ1L@#R&`@-BP>RXqX5^F zTNO25)Sg*EC?^LR;@Ub*N_W}9Dgr*HM3`Z?CEprX!28PTjod7tX+|3%z8 zhF8{Y+rmjIso1tFwr$(CZQHhO+jga5JE_E2pz-BKxzv<|>X1@K-0NV?}9z!wQXJ^>o;wUoh&NB1?^ZvbNLt4wp+`h6P=FA6sx zzacfELyQP(DdjNtO95?BnG`1KK{0@x*b;-~!=~Wn5m4As_(;@+qXHu!asVb4W9|o! zqZ04Ic(t%nGiDR@ykgS|&`Nvp<$pxXy9IoYD(bOIpWpO%r4j(tRHDF(jDrCDI6hQ` zPW$+VzpjQ?2IR9OiRI9gw;NSsPMgS9ae#Lmxwc8$YF21T2*fv9B|}T>AJ0ggG(cWR zr3By-R6?}}g1u3#{7$*D8`-dYn>|%(*{Y|>zaHDn5|R|8fTAPdaEhoGQDHZf_u^jV zS(iqqr`^4D-%#3}87)8J23KXh0+py0Dt;VNM7eao5ggw(Kvo*_N9@K?W#L6p!i5Ip z5)g#XLqy*;=gIJkjwCpO#+V92eHTIdE?YEeWN$yRpw_#nLdwXa0J_WXdh=s3jp-Xs zo{o4eEpH1?CB2{dZp9qC7piS%U24&Q|)D|D&d*@7^!{#>DS#1H&ryGf1 zW?dcViL!;?!Em&li0B$l`{1mGaw!`}#$SJ>GoMvqZ+OK>PS0yWFSy}Efh{Wj#xC&` z^cbR6Y(m|i!ZddNR8vucr4#QBQu!eBb5Bd7GXTpzV)p?uj@+}DsJ!*1kXUBcl?DVe zA6))!P>$UgQ}gu3GFs~i**wh6K}&PZ42HXwjOR$CFxu51O29n8@g+@U`$!eBR$*9Z zrpnTK)0&8CKTd^)v@W^d(Q+|MJ+$R0Hg^}pa=2gOB?DZ*fR{MizjR)U(>`gHFcG>% zKL*k^6y-&lRDQmR=-E#|Hmv5Erx5Ti-)1_8KH!U2FIgVbL( z3NXO6X9aNX{dL^`^`5kT2Yk-~zC-}uu$i?zRBWgP1bY>BNf#g^f9Rjn7gtbCCUzss zx3sF~80!JvN&v~{E^3^d!VLtOsCCB;Z?$T4Yw!UDJz!r+5pYQ1dnre|gNk^LdXVuV zg^++zScdhKPOd-n%fHcYy;L?#+ z=>Y@=0dnh!Is6lR)&vL&!u|CRvW8l|fvPSA@XTAn0bEEB0B>LCTpIwsqF4aPV-Bm! zWy@=NBnd6H<;V9Qw~*kk0bXvTzJT15jsV9q0Km|2+5Uy00WeF9N<4KE3uq|$w_U-RDyba+n6<5%*6;IN)my{X zwjH4P+Pt;xOS!#N@(3WG_Niw5trA_f0|p4w_g1m_0~|~~s((whuhm=Mu7-6UG`hjJ z0ctEs?(lu6R{yRh}^!9)OnvGsr)@b9&)9nB2f z3Yh@?{)Y5E3cCBOeHVj%<;##(_9_yq9>e*`;<-XV4)aC)1ylhK)Ah$Ig~Y?3-c}-n z1H-z3sZKX2`+(f`S(xtt+mYlLEmrER73o{JY!`=VZ;!&J_;~dtZcl0*ZK$^JnH?Orhhi7eGQoj{CKYF6Z@Sz0jTwj>$ui?rdBB``~3*m zy7mL#(TnaKjdE_Gwj2Eu8dcNzEbnilePWyLG=L8_)f~pN0jo~{M0=VR)c6H7U|5Ug zvK{#5tzMNS-9_x8^f`g5_<`#7UIhqDWl-rWtUYV2dq)5$D;$xfws6(JFSe;qF3*#c zYxI{PYpZ0KOA(#2e%9~^w%GnC@_F4W_ha73Xx7Sv>bCkrH^oc%+Mt|tKL~*Jirv3} z=F8Hso*n?yNaZN^$OC1JKs#ZV`0=Jh={^YI`2w zj^qEU+sm*zmg;PjHA4p6TLl-c&UW9mU|8`(^p@iiiP`cB5W2EnuAb~=SoxRKVc&Z0 zp!_2S5%HF|_PNpua38X5uTlrxq5$+b0L}X^7vkGLQKbMDI3S#~+3jBg14zT02W_BQ zmZ6%<0~Ev=l)txFJ;bo?5x(5x9Ma z$nMfMytGBOhrTUNzZp+}q#$5IXRp@rItoV$PjAM`wMZgtpgNi7LP14=L6IAV6}IcIO1_j_US*DB0bA0`PY=D!mt6{P(5EuKQ1bV!+=b{=bWVYq9@b+w$rs z_ytszQuiJk_h~r{_rX`-_m5R#f32$hPw~S+HMS2>_X}Y0KXs{na^`*y4lQ3m33C8N zA%MZnbg%8}fB^suuBE(Mct!uvw(&b~_PbN1EO4na-^$o8*FxU;5e>MpsT|;^yEWBa^be;0aF!&^4kTl4_%O?D1E$j9zC*N6Qac5~$sT5J=7XP^Hfp{?BDap{Z zbk7|Ay{{E~%lz+{dUcd0F3IgK{tjvdRkChF(8M%KzU}aW8BjE|*O~fWKl6}&(1wfA zUUoLDk&>6md`W2B5h2$Fpwugz9pNH|y=~g^I+g(verlGsm2evw(kx4Rg)W)l4y-0;Cw=(jZZuR*E#=FIW^*|qrsA};!Duj-RE&wH>3JgomU^K}cI zWFOizeg9bHcDjlDS&R0sA+?`n{-?(3{xS+Mbm)H#UFX$F1aRO(+5iXs6>!U9`%iDG zfcE?Tm-e^InDt)Uwfz3;z-H3@cW+krNB_Sz?S%i`rW_z??W@)Q)&l?>^!cy;IEwuK zt5D;cHP?683m7UO6ROUu9bm82H@g>{JirI~87B?j7NCF)I9jTJ*SoNHzYV}Cw)l4z zWX!j3x9tFPqV&2~sdR=VBs}qO<|HrnQ~@{*lEyVtg3*vfcqSE46Q08oRP%ES%TQ%E z)aUsQJGVbY0mndHt_0x2e(-UA?W;QT>|P0RSGsUM13Z1#piFFyotzy_3~c`Xvoo}W zf?}a3peOkIkDHrL*~8w1PF>E>(!|IaicZPJ(E0BQQ9DN)C^|twJ9jMt1~vjF4i*9y z4(7j0S_T3gC^|7mI~RMv-b!@Jjs~_)_6Ck7wniRwLP~VPCaxAnCW>N$bW#M)jxHv= zyijz~CbnkI<^=3ebfOm4&L)m@qSgk^Cc-90cE%=9fDQg_o_qE;Aixj!f3le2KNbrK z5YW@IKoJlS&>0#0^B2&>e-Dl6KO6qLTNY-(D#iby>;JXq3!X7sQjS02Uy;E-G9;R9N<(e_=_4R%o z?q0kN)#1(QeZ9x)cl&%@u;OBTeh`#+bDC?Z#(DQZMwG*ofU{c=P`w*`)=bqgy?3VT zsXe<0z0c>fedSKoxjjD&zrR}gc8Bo!F!e>AKC6UZQ|%yqYJo8LMv;$}vaVsuj3|bS zQ1OL8mJ+};Jn!ebv+$X`?#IFVZNpccRg?;`R-o$q+5h?2e6+6$$cRT@zPmC7or6f% zkkyBCOQ!H=SdD51z_`b7YTNVje%|9_-t#%hf0-4h`t0Xe=Uam>0VW9{18x!0$kFT* z&zL9u2vc<5D|zAdhUg^3vSgr*)_ zrZe?550=uDl-6(jR#NLWe994n!GKJnGz9nJCze}~1Tpqiso<0no)9JvU8-vn zDgMyht1syuMJ~evvF;+*lH9{TCX${b^WM)#B~y|~8C~M-%#d8!1={6P?t{d|&@~x^ zl43$g)OXIStQ5)ZG)sKfDvLO$#6!RoO92MqtQ2mx56blEV_2R+ zNOgoph_m`XtWH9!231Q7yQkJ7t;Zq{^z=*`T>O8PZz7imp?>_@Z5`C+-vz)8|!poncOlOb`4wp9f=15u$yFYqlozKIj0hmyGa^~FY{@B-p9G=cM>rWPSg z8l88wJflee+Vqlz#fj4?GZ22c9;KnL>fN4(Tdp-<&+4{dCWpgd?7bbqTZ3*sKH6XC zhv7J={dB#DR+wjD&o_%Qi{7{+^_M;%NX;cR>&)6(HOK%R8u3`+DzM$8<=k`q^t(|I zmU+3^%O!(lCu_k$%M>_QTD09WSFKqU-wGQVB~_W&k{@}2z|+oGRj4ctn%+rVHR0AXt4cZ^CizVkwd&gKzi^!Wder+whCj<1qUt zim3FxCmo&2jx>$>LzI&J{RFyRsGEP_qExy-5c&uv0K3Jx8PuO^T`%m*YP-T_21&%>VS08&bsLtGxv#XNI4lXfSDbmrXk1bsHoA^1#~b8ZR#v~Ex{ z63*3gloBp8b&u7z(N1d29}mxucvd{7G7BPKps#aUBD?pnBNl`{bH@)UG9f@}NA0@* zLlI>(zRqb38?H{(RLg2NhDG@_2k-W>NzNjk&EdPK4RVAgU9^wF26>$87$gPl)DbMl z2}U2k`MRl2v4@x2$R|9e?FBRhjzA#tOFXtmvx^%n!F9#PN)591wT z)r}1K!TD=cjCOEj!usfG&&X*+yr4C^v7%eGj}P6FM@I3VXw0G2UEo+QBNY9#Py=@V zX*Z7gy;{0^h^{75fn>;(ciG)Qa$>PM8$ON5mZptEtTR=}JHq;$8zURtd{DI+=y;*6 zq63}G`AbM84iL6*-B1Wn{6NiEFM6p4y7^nE(WM$MutbB|2%a@}u!G<~y1-;4MsG2h zvwax+m);Oi97{Q~$O1T0>q!L|Yyv1dcQ^WwHK_23&l)s=#Z6lZdD^JW84T)^vK)e$ zHKXy;dOF)M7)k_HkwT+v!;!x%z}A22Gi!Ikl17U{ z&>mGm?jQ{g7gjMA7Tzj~o;SU=r>2=r4jeWwG1>q0ADma0Nt;vESU4Aw5iO4wq>plh zSEW4+1`=iMcHF9k zb3%iip)y;jLY}{~PxUv{6&jiwNTWlun5Nz$OoN(vf`SKF&CTzKZOd+;aa>Fzt8;8+ zyIA9CpWC2vy^H#Y?ObzpiS6gxEMNOh%ucmfHgsr{bivPrO~yh48Pppo1g;aKNwJYA zlo}ISvsmXEV;ggrZAyfi*J1?{t6IyIEH(Bd-&~?~Ec6_kM^KGVDl64gMa|ndP!b<0 z2G5>Ic!9J`JtHhnkq2rhGxlwhU8>PJ1OuiD;vG@n_q+0ZtEz)q5(%M>GVqYbIU6;W zG~DR#V4EdW%=SxpQkPZ?3uvJ5am^{jN|YyJS~vixKdqE}iej^$<)=E^{bZPS2B=pA z1=5&eS=AfH?g2F4vJ1KYEy$iWcwp?@HDZnw2nQ9>k1`4PT^mSOyrlbFnImcN$*M6N zs9!TlS`-ylrfQ>*K9&1ccag-+ncYM~-f1BT4zWxMC6-90BRaJLQihfc*n+n;$tv?{=kiReU$by6_`nol9xoqxJ^D_gA6z!JFZ{Jmd zf47T+ZI9h+u%Vn?5`%K3l&ZsyaBUOG4azs)XAIy{2Bj^>f-s;_CnI1{i4__PfoUB@ z#H2Livea}^uzwLB?83=8K8%P$@4Uvz$A+dCd(*1|jrc*ZB)gF>mJxy3zlmLET~b;8 z9AgL0{v>>lA`pufhTQOmu_Xk^1?MPi4qymK&POi_2HVIpw-{bby8GAHy&7pr%leIe zhb&EqBD(Xo!+3rp9n}oxPogKsJvl=Te>nlt{y?j+u&+-dp7vsXBdTlFR3h-r9L*=G zMa76;cmZyKyS^72Wf|c^?eSlMYCbt>6PFMQO2J{+(uB6<4akIt>7T&g#s1MIZYcMC z^^F@gH;j@;3goA{yu6^fM=I#}K>SW%SHwC8f76m|@ai7nAED3< zJIb=HRG(K_saX8aKl(d*g-Ozwt)8Uj=xR%KK=kF_hSCpE&YaE>V6qO8YBg6+NrB+5 zkXvg`np2g}trujqd=eOP0T@O;kOc$YiGSqH+@8wm*;g)BPetto4RW#ibt-_xs>q4n zXcyC?rFaAhB7q}jxpx21xU+YUl*qXDhHyIc?5dWmYdghp4YUESLu4H@QS&)a5wRZ| zZ@<2SSJaO20vpOdLPOB3k;xBIYGLEsW*J`4jNbtnCYwd-TrB*=MlqrbAtMdk$ncD-F-%KR5C1; z){Q~j(exmwqbvO^RkU`%-29e=H~MIPeBRi|ViiG`qUZ$fHv zk)$JJr5%dLTuB*iy7MD@A5T2eg+GX}*V=evapdCShT~Cf5;LD&aErfs-;hJj2p2@s zNgh!D$fx~=nX7_oGRFXdx10!F@WlF4XJL&X1}`k;c3r*K?a-3N+=cr4M>eo2%9|UZ zq$f|w75D=-gEz)Jyv7|5Ym6bKK}@)hGuvuNl{HwyewoK>Cd1MnOVo4_#0pdAipUG& znjAawfE!835p3~uK7mdjH6bU4u=nx`@ohbSwZ-5IuJmAK|H_eQQSm`1(PDq?KP?O@ z>m3pjiE?3-G#&FnIjrlTU7(h{d2pngh^+<^X(XwXk}8#BTqRnlKe>XI?W8_tmL7;k_E z3W()7yKdVX)0`bsVQbrQ2|==QM^o-V$Omgtmj^NTih)q=JO_&DW_Z`GV)n(fH)~W? zd6pz^TU9NXS~wKnmG!x^t@dID8{r)FY9e<(%`m?A#7k;2z%(5=J5JK~<2g*xBM?ku zhk~e2&?BsZ!wP*fvA}>xPJb_s=q6-ZBq8Qwvy^~o+ZGdbyqUg6Nuuki%E>&jKc``+ zlK@?yuUn>tspp^hAtqUg$X-liX9~JBZefjb6j5P0c5I<+X66nY_4D;6)mvAr#bb-P zEDKgD_lH2L4=vXs9jSn1_yfskV@1`7J_W+O8w0X&mLX_loCyiyW__=FoxH4UalsQvvES`4E`OsKjCd#G~GHq7eydvrd zLZGy0Xl0_2pqQs|5u>VGOzJ}=Jvz{Rln_nfN=al@W=AQoQ=AZ7vYyci1G$C&M_Ex# zT_x&l^R^t+D?2DnUs}DviqRJ+J*=?wPSJQctis~NlGX(U0uL`7n$}`&y;{?V(oXKQ ztmBxV?N^vK+p}#gVv#uc_Okf68|4{~6O#>X?0is2i=zVsf({&cuJhQm_Na26usCa= zQ03lArnmZ$*0pz<{=+>r)2VsK>Y>7cejB^&K$L7BO=jda!%$k|y^qmYare5VRKQ7X zi0fWoIYAKqGFo(gTZ=xYuP154=KC8}j0*C?l2H;$_@~O*(AQlxmVlkrQZXHNCtB!; zs?it>Sp}^Mp9F&U$AbHa!w1u|Y(k0l=FjV%oTn3k8k!4^7xT4V_D#| zNE$)RbckA3=(ND!2$$R~z7_Ca;{qwnG2^7bkZSV|fOL69RP8;>9C?FEY*w+Y64850 zLdwjq=o;_eyZm|8_%juTZk5q}+@68600Dm;&&?oMqx}FAI~~2#U&Mrxhb6AC2ovOQ@VkRjQLBxYiu>iFo*X6C#HZRu}i?eAt%?>L)=R6a!>u(F7z)4V z!DnR*lYPIkllMr;2z4ov$`9WXd}G@#zn+G)Wv}R7S)j?Funv znV=bnory)TspvffL#|-a(F>wub&*+1lUO%yUa?t{STaw=xKy`?oRET&S;Jr-zR7vUkDaP7fc*MO@nkLGK+L z%@7wDE?@w!rfn@#AoyM-LUf;IdgZN8QR{F_$w@(qi#i835{xx>yzGCkP~8M~B2a6Z zba&z&KePc(lbl)0yCN8-3K4n%O33#v=WzsXfl~!VuTR?rr8hJddu-@<2 zeh=Gf8@EkAHbI^(Kd}V4R74`>sE)UykjN!&bF89SLlS(pJ+E&VYgAb7YxC`k+ zi|z3Z9@>*25Al#xPPkAv;JwlI**mVNdBKq*Bm_`a(0am2LU32*ANvbyY>(ut&I+R3 zAya~qcnum&L^vcm_|-7U(vD~vlr{*mDcAnI+2y0BrkPW)cmR}ep4 zKt0G~t>ENqABK^^gGM5*h_!*v%|agxm5W7icraqCS5Fl!AItWhhk&FM&rL;A%03ne zu^OqtGttgOB&?Pq11;;K$h^c#(=jI35fmTz2Arh|+1g@(NQ{-G3N6-Qp^$_t2&s_mTfrsm7^z5WDUx?vVIyeGF5AqIN|?E5Mwa zvbBN?k4ZufcwS1hJ8Ba{ip^WzTQ6_bMcVGiGC7C_>_dG2t)xZ>7qiX^Y4#c0GWoEU zVz<*w{yA*=?h~+LA%P8#cU{ac4%p5I(n>x_n{R`$}&(vY5`IckV|_r zixRqc(ZpOAR9stuIz znH}5t<7oyoJaTG^N_sT>?vho6OYX7tkh8M!#~ue4zt;yBUcai`p4ra6Y0#(e6bQec zyahhmXb+tBum~x|xBbrWXVO!t?jt06Ln5dxahWlh!%I)=kPvE@(H`0h6L-em0TRyb zcxrAAz4;J4OqC8a5(4%$f%FRBe&cCS5S9}%b47q&1;%#SjC6yUj%WhfX1;1D^qYdQSNAcsx?pg>=EIdfiuByzDwVyo_OO?Aoo9fIYC$`F4laT2i5Ocgr-?oUiA<+AXaf$fK9_c;JN$Q%p zI7}7$(%U3|r)N6+Dl2m=)NG&~$Q5KAxY-`g&>P_OI{v?fN=S zC*r=|?>|P$wjqC+^moh5oLThGY=6HSem8y3b9lbB&dzLmcVK;R2^|JJp@t%kNdY$!kT=jLA(|3d4%i#N#m+;l%$L?7i#sJqg z?brUb*(c|8OShTsYcU^o-*ZELEgU@`)eLTmbAv{iMNd0l8%KwC*+yKy=YG7zJ7XsA z%7bh7D;Au3|IQigppEPECaYDP{)w2$9gY)ron)&b<_DJh%zmEQ`Ug!e@^9fZk?_%D}!A>Z!@*$~!EIr*%>2su3uURbB|beq|( z(^w8%VtxYWS;1sVgUuIDbzw-MiAeo8FReE>$fUDjT;PZZnPFyOrb%wLxi{hVqTPga z$5e7}9oq)$q^A&KZF=Rj%D-}kX$O6KNgA*A=-$GLWQ|eZLjy3~394blAR41rVSIKxXF5?Gf(1tQ-6!nRD;mzY%FkXoaW z`3v)#cz4`|Damsqu~^}Xk);|U+PhUp7_slB$A=!MvAZ-0K@2XJ7HuKPu}JM%V5&#o zSK6Fxzwq{7QRAbj1G2HjpP&3)nec9rf4}{N(d`9g`6BokQ>G`hend^ke(_73OhqR2 zkXP`UbTd7YmDk29kCnGZH>N`lTg+*cb9OiDX0WnXa-##*mraWO*4DOZTN;t+j)nqN zPljf zW>6Wwi7#4q>ljMaN1~gzN#bB#6H(v|g@4DXxvGNti60w5Pv`n-dm%!m8P#(9kbtO#34sKLXy{asq0xywR68%8~VfdMA98F^S=)_dLl=Hg=-C zA*`QtaifU@>Gwdv#RBjp1Q!H{DV%0Ut)6yD9Q%(Eb{rh9U5vS(YfWi7E~)AlTJJ`B z^K5E%h}g1(to8`f0`d&Hrm=i`-`uCDFo=xcT?cPTJzs`iNPK819t>&%9@6iG0oks9 z*2RtFfJ8@|EgmlorAy~s*WE4HW9aW`5z+0+qBf~qp#*+?T}i_3F6KU97!4;$qSv_| z|F(B{zrQS!9DPzoV3Qiz{n}8O!j2wAELo zk0DJ&r)F+{Q+S-^Wv_SKuZQq1_0HSqe-KO5FUI+}jk5qB9-AcVQ*5-PFhIuIjs{sp zsc0T+E|_5~P|+?uy%BpT+=x1SqXC-9?%uiqV)pLIZmuE=w`H&%66sb-GtSDK;XJS( zvLm_mxN}ok0L86~u(U$0#SNKx4xKV2Or_m+blRBy2c-{>Ys3@oCrpxfno;in2tNGe z=k>%%4E-2pf_lh#%nkw#zt+9j^C&XTyiX3bhK`PxY0j|FWsf`x_2rI3ehcEvKpYtCNnm&zz8(EgEt&A_7RTUnvGB$YF@fC%X5K=LIfni^RD34 zqwDxQUF}v1q|f(DCE{^RS>U;L)E~kkOTpP^+P4MvoU)K_1h-9$1ZLazzsqsY*)iDp zkLgbM9IT|6jfSRs>Kn0qar8EcGwZbpO8Vs3i;++pj@;#8u z%i>_Y@IBFQKnvzP45VX9F5Z9MJ;HI9N^qPKlF8&UhaAvaJV39SSlvLEkCSBUT`5LevTPtz9IrrE=c+oJmiSfa3?zw_|AW` z+5$q0-_A@<-GCvO(;uzF3@ue6CgH-;RWNzsRFe>oQG&KBeoiTfh|zf(Q3OGZRLnU< zXp)?8%r~L72Eyfrncj=4;z7y366x{$aS6yXpQjpe0mf6OQ`OIqXcCw?Ik zM|#)9(B}9c2jX53Mh=WN2q9rddgPLISDWGHT|&WBidpTr1qQ8#eU=xU8G$yT1k87T z80S}$d*QrB%fW2)rd=y6-;(o**U6*E*&vyQQ;hM%(G=z~N2saGo8&NmU*Sz$vlyjl z7w)ddec#B-nKh=&+!VzM7|oM`M$QUX0u+~UB3I~N5|nXh~7Xn)=mQl$KkYBvSGr!vnW}c zGM2j*yk~KomEc$!3$xOkAjc_^se~GYK$AI$A>k8~`(J83eZ1f>!M+cX zDKMGx*`p_R{pvF$Z%m+v7E&JE232G{rkZ)N4Rg*YI7|1oMnK9ggmtt=AWBQhd`)Bw z!rKV>e%646rH)EtN4> z<^P99;68%R1pYfRRqi2zM)Ra729Z$4u77$zSLrtnh7}T1Y@u(kxgLHj*7&7_ zgWu`|du3XpjDJm=gCh70 z3P={x`Wpd*tIKT%$20TCa232orjTS0@6Y_nba{IZ{J381%@-GE*PL5Qo5zyt>+I<% z(T5Hznea*-$Id2q>?WJ6gkxp7B=e)|iEmPJEE(V@?}Ue?$Ed%C%gs7u-8L={aabxs z@_m~Z1=+dfEem86O3=6?yh>Ez(F8052w5srf)o=p^`Cvalq*6AREt$tRO=T(ge(=S zprNE_oUhv?YVdXhEF+YkVgajUQZ*SfFBGjBa)!bD7f_u>Qw?^U^@FvoBE*b&e|}@PgNROD@cd2;gQ(%(HowyAbFIpWN3<~-%Hd~2 zM%xGh=iyu$F_S$4N?pp}d6mu|H`n15i70wJJs?rX-i)9T|8f(u#340Pt(mq2SHS;B}84y#rQ2woVW<7vM^oSugM(s$fHihoT)wrhl^ z3I!Tg9BO!i@tJStr=Qv!5Zxcw&FT|gse6PUu9ZaA-41hxpaQ01jP?pHAWWe(0Gv%5 zWH93~YM9FaKP}OYvqyX55g8Z03Uy zM(mX#CIuBn$egRzo=fO0IJVjJI5Wa3eamSUuQG7AMjfyFQPcm*E2@d=e>M*VQz1ds z+zp?qO1i9K5mMhPqN(zTAwdP{oYpLuRyyBhkZ~g(vQqsWSy8T`$*`>H9cQ^^^K10B z31>ce)^gf=ZeLeI*Z5^E$RIusVki_G7wVFI(g$ZbQ}L|+vj?DXH(OYqLZwjB zlWIG^@F1>HJS~6R7$gI0dpj?M$t?yvt{W`q_ z$#RTse7#pjWQA!x8!L`EAA?TBSFPg&Hsqi)&pPIsP@2GGb}=J!1-+G%&t7km)yssi zZB+Mc1n>PB3@lvFh~l<&Lk1nVW*r*4VZAGuIJ1z+ z*0YgVR&l($RBH_XdtNBU_Wk7!5^)F#xhmuefqyLp&W61i`iUdRR*2mVyw|1lxCOCR z*tf_LV9p^vst0I^=MIS+*%~4Ma$dSE=^CS4#`RrGU+}W)N%idoL!f=xeD&PDKA2=`26`w!p3LB9QD&Nt%;OLaEfl8pi^@b%J z-_?_?3P;oE7>;my0zaUJ$d9@qBp+!AJA(|<>kBcXl_@maB%^b!Cj31zT7zq4L=mR? zDrZBG8qW~EwDhpsrbAit8lhc}-@3ZeGOC>P z`TbfqULE!6L^iMQhRL>=yXp;y-%}Km`_RuKGU%G?d>#xto_n+ ze!7AlRQk6>q0^+7Zxwl_TQRO99t6Tb4w$BdONwm=#G&3Tvt#lf6nS9s?;8AuITs7b zD@f`{D|*l$;I6?@*tbN*KuH~V%?* zLZbLF*1PuGSpTUoiI&Hv;P7+YvCCn1<>OBO1=|3`+Bd<5 zU@Vx9@RcSbdQIC*ev>|k+*PK4b$izf5Y&%AL`j=XMp} zgBVx+3IGA|dzpNT@v(!Lf=Q0{%B&8Wj!{p0eRNC=LMJV#j%EGu->Kb803#Rzu&M(Q z9fyK3aVyb01@HRgsz5Domx6HEn9(W@Vnrc~IUn-}<@PMf%MCzqvcDK_jlSIE6Z7%hO9x;7HqfTaVQU6-Zm?5OevS_dH%yStx zbk!*nCbq5GEHj=PqS{Y+WzWl63!?5b3lSRD39$w`%&mU^VZN`iVi6sOH~5$k{EmSo zyDD!HXFsxx@L)Vu8wd945xmlvbqyV+!(J{+Lnv)+#ji{6L{3g3nYPBk0>>XqbCxoJ zA6D328OOa@y7$WDWabtX_P8ad&O66Xtjgx#avvz5*EAO>rk(LmH}i&2J?#3WmW3l; z(zD(0<|Xp7RUMMuPGGdx1~Dj|*9DRw!82TK3MT@IUE~BLS()UE{HHu92&;TD&|vk> zEkT3UM{X3M{C8Ww`3kbDkw68%EnY%GY-VMW^?z8$TNHJ*xd?KEY;hRiMZZozwJ;1c zi<53ANDfyc4qkEU-eF#Z1KZ=zCi3^z{75=4vE@i1kCGh%v(7tWY_{*x&`pMG=!PtP zoqnL8{?It1I0+JQnkiUAeE&3CoirloF0_25E{CgE=FN(7cNv#s0u=KHvSL1XIp zm@%5vGLrVv&0e`F{7W6M^wPO=*6BS1DmsYFoFn15LkcvoLkXI;@mDnxHq_Lx5)#3WqPU!ls8$_eUT^MsK1H;HV`RVzyL`|Z}TCTX{5BIG6#w#56&K@fS%>^^c z!Xq=i0V_e)vCC7pgz`*G{IR>RVFfoz0a01dW|%>DB=z(%q#W3 zvETTWes9!KmPo>}R!IsLMqp4}!&C{&7Dz?X^y6~$6*@dNQ&eiA-|MJ+>*E8k@?LZT$y?X8o5H1F)3#BP}kb%v>RH+CC*G<4+dM7zG8{w1W$83HAxbjHMq>u*@B& zH(5X4NlJxeW<6DhWGsFj^4n;75g7%8SNtY`LgRnNTCh<(r2!MWLb5$X=`^JtXw*QDG*F?OAwc7sao z1${w_4U8fzj4Mz8EI+x^YUfsiwh`+BOa)o{u9L zPt}4F19UIOHL3R8Fk3?Y-hD=4UozzTvyTO>2dku_&ZJ9PwDpLOR9hmEF7N#km#Hm)q zTZGW7;hTht^=d#u_A9Ao=L#t8zNWQ1jEC-x@&b)#{jRk%VxD{ghw$`II9np&aV5r1 zhv5_%7VlzhIXzxJq2E7t5m#x`WThlC5+_bg#ov$dVAaTOc>@z$`FUYOAhpZ);>PK1 zZc#epUu4oU-uIlKC)Q8}wZYkmaC)F^o8}pAPl9`uPljb}9Iq%G>vIpw5{j`~Lwz{^ zKkD8g$g{BB_DtJWrES}`ZQHhO|I@a!(zb2ec4j3i^VaF;KG6~P48ETAoxFn`d+C0}wU)v*YYf>lx=?nK8@ZxdN_fr9oB>DMnYH zJs}pF(-oSrX~x|vG^+A>p?DdQ+7O6_oWPYXThfzcA951sbTedvkk!-VL6n(Fyr|S# zjXLtKq9RAINm)pnwc_P8jx%!)2YleH!}LH_Y{?VT#}x`tX!-&*v6FHTnMC!=W^o<% z%p#WT^f$h&mg|Jw7eYO*5LvR+&p5$vuCOp|$;0d=?(sx|W#Z|*oo9ha!faoV_?}&r}+ggbBCsEbG zj$Xo4Ugj1`Sadvgk#g18t9rAgfctIfTwU8Z@i>NF0Ik!M!z~SIGqt>5}n3-gt_{6R?v~S@%fjwZR-wd_)5Zuy61852+f%Re_HHzrR;YZ+A;6XbJ`a z->)G#`l=rfUYkDwSAc-`k1+*}r|MuxIlx{wWb`*qIZ^fHq+5ed#vZZ0$iM6(tVRl!UX;pph zQB_`Mu5<*J(XdBGn>ANEu*RB66f@jYN$2uHSnw>-3vI?-cc{A~zc zvi$VeZl+@NDKs~-ZT(o{nP)Gx3pEo1>P14zn%gDn@$~ z3R)NgL;t;jf}8HDiriJgy2Y9sq;4cQZ7t!++}c(EYbx1d1_)EJbbMBo@gVl^oCSEISSM_J_|-iHT11JbM)-tfkl91+2B@aZG%}(wNN$9nTlEDM*?V@DU6|q(q|Oh zqwlY(Qy*JdM3jR8!|9NG6=3iWG$Vbi5@s511mT!84ZR|HcSTll2l7-=hkv=%SHVOs zFi;841+kQT>c5#rUyzaM3ik1FlKdWW+VCQRnNIg6X&aOZ2FA|F&La06Cpvea2WF8g zuVxRu!$U&^-5ww2JUCNDEZNw{<1gCYUbaE0_Kb60aF7Ca9z+gaBdsxC9KKUakjXM! zmuEP(^QkmqZ^Lw543XjDY!ew@8}}qiG-ykeXaO~^LFn)nRwi##IvKGun6z%?Y(Ent#p{*5uxKIdLo1WW)B^`iag8Al+wn4E-MzAqUY8j{1O6x&O;Ncq$KB7Wr2~f!#mi(wsSPI zI3y6MPtXv}ebrIznGH3V2hDWvsi29IfIlFiy>2+cvpV9yGwpA)qZ|CbFHPzt=w~OX zaUi{gEMUOFq_cH#o~8QzhMDF*pxF)6@mF^tnn0=7c6{+!rH!i(s+=JzE2lN+=WYYi zZ?%}DnM8M8E)5wA9#j3(Ee6h1y?an%IId4*FBp?I*wu}SE`HhDS6!(>xX~4CC3$ygTTdM_4jTE)+^txli)ZLjV-k#%O#9sA2_B-64MmaDQBBgPkm0Fg~ z##d4$%et}naNAc0EmKN(`qcO=o@OpNbzo`ajLJ=bXq$Nhx|7r{Dv9l^6YI!Pr=NCg z@)uD6IP?-7qn3 z6Koe{Iny%KC)FeWP4Mrxwb8W@@wC%y5x`w(N>A@ehB-;R}zrb zeRS`()qZksselcwv^kx8zwKR<-xTdF^-*<5%@dRrj^{-4bVW?->KCc%nzu@@`sqeD zYWt^`hHj*A_NgFa4I^0UrQ?PcJi?~;dA3HkC2Cv=iq8-kw;(rg*cVx(tClEuJVp&| z^1H}iI*uXA4RmK2As4?L5_dZHq`AlO2i!-Wb`a2(_pv&{jG?*ri1sw7ygChsbp~J@ zi??fro!!z3!WRUoEw-AV8zgP8;C&b<{5r$pPq-jBUvny#olor=4aUWY`7OF*rcZNZ zeUcvLW)5}DQBl&^*kbu3@|0{r#)^bW(yl2)um>{x!3i7-cf1)yDQr^I$Z`4?o@4rC zSxXFi{vF-JfLu(I^zODz%(U;Aoc2fZ!m^lbZdUjr6{`jAGWC5In$B=y(peSy3s6Wf zsZ9<7MhZv5(mQ2Eux5zg!cekRP5N#_tx?5UDKE|YLw$b@-kvc{`!6>u6iy^FTe~S+cIQpihTQu{w-VwR-E9#1 z0(n3UB5xB^by;1N-{h}hG@$a{7)Vu*IRgIRf^MS+tbyu0aBxG;foCJ;-*vmE6DjWy z)5Ku`U>v8YP!H{eJb$qpuN&V`ZAN!1y1ccodg-x(Omo%4eq;!77OJTX)!ZYQ7F~B% zaZ{hZ+e6A+uEP(xi(RXS*o@kr`$iaZFm>oWORdI~Lu^A8hhAxN+akGz&I?xnP)l7K zC87qfWJAf$x^?Yk$cez^?>pK}TY}lFlSSVAHLu@|6Br-)HdZz5P1!``G$LBCV)y8p z(B(N+NSUf>+vO-J@!$vaRR69Qs6H$gUM!&?t+&=aYDauD8ZL)N#F2I#!)zFMtLG;{i)W ztwl`tfkgWeE5(C9@y?0`NbdwH6JQDya*+96ineOXu{m>9F<&%AiO;SRFpEGdAi~ix(}X8WHfchQ44%XiwPdul zm6Msl3gsbr%5arErCb5uYXHm=kcyaoELt)hHD1i{C$Eoot~T(Nf+fXzwv$t$Cn*sn zMd!tUEGDOc0?TM`kBI*i0sDxr>qPqu1#}_hPt_RPqdsMA2m5OS4rM+SG+9p4#CsWz zvpwIKf;rEN>RLOV6ns%*U0jb003G73Qx{(M-`8X|Oyui?n!8Vz%r@W4CYtaBurPk! zX3zqa*cOj`&V#4#QU48L4{lRPoWkb#2Tf#4dKHc1THedgM}OhL=Uhe5((LF>>#9q) zJ$Qi6kF^Fu+=nmGM8ZPt)@m%*qI^-YM9lFqR#hV}ksD-xs$l7`j!!W*=zIg`$|uAoz+QWQRT?~PpY@1FnO z(EhIBUiCohE1zpH$JSf;d5|As@!9O>E>x1y-caQA=9_lKA3sD9pI%AiOCXYzHDy$s zCF#Qz6!c~d1!Y)Z*Oa}y_uxyUGX~ah;i?C`7p(u7S?}!l4i0a^YBPw*P-#s%; z!fLU5#S|3s`G`k3zu<1IEXpBP9o-qPLo^B;vd!Z%EzY>#X>#*$M2BvaJ~o4SAP%Q> z7!~cSf)-#gCgsk-pg2Nyd3KNv?e0VwMTDgmFj-~x?s!6}DN)!ba>}Uf!KZLKTY^i4 zV`5K@xA0%jY+9>r+VuRpDrCZwr?+CL8EzSMjN`6hZkz;m& zY;)PY>nns^F{8M)eNjt~(X{HD-W^8iOj6D?-j`k~Zm3h<4Z2%fjl zeAT|w^1g17ugAd4w%MOMO$MmEf2lo$%GdnPFr^rpZ819YJge)DtbmwLnc<4JK6XZ; zR!J(p0_nTHsUFg?9<%oxW+<@b=_XndNO2?~W9gQ>felOw|dl-L#$Rmu{Ig~dacp!KTbuZ+KT ztjv_P;yie|&;1NK) z74uiZ&Kip`n1F?joUM7va;(QAi_W8mMxWm*TyR_6CdZBL=XtYe^p$kW>JQzLg$Y|B znt&k!G?vkvqDf1X+2pDg4UPu{i-F=9F%Ob(wJGP$AU zA)B>m>b>XFF=uOoS%8aLZ^`t*>efBn;X!h>5Sj!fY+)>0WeswtI$nel+M3sAPN_I{ zm@c2kEN+F#^Yv_-EYCV4!;F=BeVk#*DeYDokZejyT5;fG90Zd`HC>0>?8?I!Z~1UL z5I7X+Je>M$+I=TxC#@JMG)u{>k$9z>Tzd#+U)+ib8{k6M_3M->UB}0Vrw>KnofWVk z4g)CJf_F2?5l?#c6nt4-8!7Z}7Pl%5dMphiBbQ>UY9wtQu-9JLIm@51B60UnQ}Y=L zTo4Yv+~we!sDvI7PWsu}z_6EIK3D-nV}d#SR@$J&97Iqy3((IHnDZAw1>0hnCqRao z>hN7uZ?qVw7S9cvG2oj5>NjM4#pqY=p~`=_u1ft~)vhdIkLZ@Sowjj6df4|l>BArU zW@Jq4Ir=>(hZW~nA{)VK!akDEz((hK@Rm3vYS)_ZJAdR9^b&TQTX8_0I?F2{R>uK_ zsOx7_+`C&>%!4?@yVbFb5W(L6w(zNZKAP}lI92{Nd!uYUnwp;=utvu@c&R|fDbRq$ z7PP1f-Lu!BtSHzskcvq79^97XFdK1c6YK1Z>$>MFP5A;awHezX2L~%jp8qMMtE0Iyj@~t+LzQI~804$wDs!KLI`>Obv|&&>pSm=KaFizcgMm9^FyT4 zUFdHWU9Sxpw~O2;(TXSLM)eJ*&9bqEI}L1`q}ZN$R~0-7W-rxGN-c5oydiP17!qmXECX)p0EoyNUxkULrHcVr&w2(t%S2i zIybem>tB7BD3QpfT{wu_w!%s7MmP_gDkh&N%!+4qM@TpJ3ilUCZtF=NP3f21q_ekX z*A%ka5jvEG{S`{=JnYy36U^;e7&zTMo3!}r0nW2GSnkwgTLA~-wRgkUrydk41Y%Qo zwHNZRt+dSM(_QhdyPQ97w9h9*+?%gP@T&LLk-vlI>M+r`ecN`hBQ@Hz z^VV)hh4}oGozJN;ki`0ZQWOi~Yboo!wL$&^X(=9+3o&do zHl4RS8FYI?=#fmSXC1f0i`84vBrGkChn0Oo!b*viL}F_Xe|VDk^0fP(eRX6wg3O07 z9Wn9H%AL^oc5)E?ML^t|QEV=cY0nfRx5)Sh*7s}o&wayM8I9~%P0O{J*0?6^4!wYB zc|!h@I8`_6B0L{2i?CeXY{1P75Zg!x3jEoc`tz$quFBfb2+1M>=45P-*>%}x&Y)=j zci+;zSqWy84Ry_=e8mCi>OM9XG>_QEty+isB>AK+aeX@-y8@Rhx;?|mC{_0JB64$U z1kYUvn&_AFl{KVNPp>VL6{iGBz{Ck?dRLB`NWXheloy_OcHBKNx$xhe{7&Zt8olKT z`944N)?AYQ)!vY4^e3c_?7#b)nf}eI0Wk%CJ?r(~$6T-bm%q@xU*4``Gz9qmemwsL z2z*U+lpB1%JpWzW%klI6JX=!8{{}r4as2)tW6G=M{&?~l}`McOy-=F8dPRG;7H)6l>7 z1Q=Y&zRoVPE~HdK0~VK_+5$!~B$J)+^U6Y!pCc_+QVKgN8Dw0TxNru4Y=}tpj{wH* z0T2cJ>C01fl5G51(cLMOZ@p-e)?ys*-zn|f>Gf*nqqK4#8Sq7mqM4KD<3_sTzZ0e^ z&dH||dp+qO$f{{5wYnH~2rj&7`vQ{>T;AJ#7V9F8M*OVs$SSwbY;Z`h)_m5CE`uh~PI5r% zuwcWS!pr5_DvbtQtZnOxa;4`N_d+D)#xqQ=WDIH@XznUoaW1Vd5_kjwY&TYx-+4-OLcV`UEZFJ(l{kNUk2ZWS~t zT=L?Uw1eZCmM#zFnxbinPt*8BNtv?2rLcIuulRMB8FXvF;cFC2Rndt*~6pv)~cNc4VT{hZuh6xC^uwT*hrNV-la)Q&;zb{YP0Ajyl$sju}}bzg>EA z^ZrkUG&xbGrz9dQ%T1AVFLGw z?gj`&t)oifoGpeOx_#%A2H2YINOG4)0+gK-ZwU$PuOq!ftn`X57)46kyFr#2x~L1| z-+7^ye1{OCBu__J;QeiHAuI{{zLca$TKs}43+P&yz1 zkEv4 z6F#x)48hnNLuZcF-Nubl7@WnM7)SM<8UjFS$ACVh$fBkpK0S zcSu)XV?m%T8E=OlD8w^*nKM6ANSOY-+5co)^AdcLm|`ync{$?b zoRjn6;LYs3vO&Y6Y#U|^9h)RxTkd;uhh*fR=@-FJ^4e7nwl;Zv#$pP(fKEH)UV2wC9r5sZfJvTDf7UxyoytSe>trEK;kgdO58SQE9IN zL%5k-fLv@y*|FARyF8H8wHjuMd489PM%I-kTQt%nG4dHnUm;_7dRH+g_=EZ@EqH~Z z2H$8-X+SwHCc5U}k?4nrgN*M4m{^Xo*DEM%x4cf96E zQx%%d(hHg}=CA1M5XmSXY$tE4k5x$WTM5Q9w#8+6J5t6nq7(*+c=M|n+oos&Iry46 ztD*ZDR<@V+V--fw1fR&{ts04EtB8VuIxHr|?F}5MfHdbi;C}XOaTb+P4j`p6d6}gb z4TSk2QK87KcQbcRc#Buw;Cp72b909V>7l6^P)zV8Fo~vWb2ELkUfqGt3rEhw(?^Qq>QlDr1akRvF81ToDMT{I8zPcvGQ1Mv z*j*3EWvwsmFAm}t6^md!{aO#nOTs)KS_70dgJ!em9ofztk_Iu)Ur9K4DX3JnnDicv_{gw#aR z@VVm~{?BImE@uBw za&8Yqr;sar5m@(VoW7NI@TqO~iqq8aqr&)aP3`KDG<-?kP$Ft&8^Y~IvEx_E%qDC= zZH>>c@~ASr`kIDF(U!?gV zgH}XjIq&XB%kRy5lI@ZUDZanI(c#A~9vi3Mw#=9&a~d25bl`{v?Pwv8?RDT5O2&*G zUC3}SL|Cn~WEJ{s<%WFj+tT5*!Hp6se?3+TEy;BjDpa`?`wk{TBfH}~7SrKhwzD!q zGeQk#FhyF`ux*%~(^tn@+Uj3wnVE)sl%g}uA5CB-^=S$ITcc)({HCHlB5w_0Un}Et znQXVnwxwCF*-WP!nZU1IB0BCqO)C4IP2J9AQVv5;oP_rzXHO684kCJMbRjd2B$we< z=+Tc)-x~_I!GuCPSz|RqDkm{^kvPH?U!|$eJ{ztF!;{xAF}_Jg77T^`{^duros7-p zFk%{z+qxg!|0-Xmusf?2=BQU8$yVcF>sfG%G9Tok#%thJN(q5lrtWS#rh*JPSE&Px zBjbj)zBgpvfveKmlq#%)b!BzcRxh(DwWsW?y{z%jROOzG1jeb(iw z@Rj-pmx_$c?T&Zj6zD61pr6bEocT&H^=)>|)e1W;UBra>cvbu4%gJa(90jnzsYN&n z(KVTuT2s;%Sc`oZ3B`CeQ6~Locbugg4jowxFp-iICccu}e&!Kbpy=-5QU!kn0YG3Vf4$BM`<=`?%qPvBf_bQ6Aa`7r_Dt zPP%bceZKGeX9(+gzk+*68BJyHTsl z;Q2<-^H-j>ygr^f)GY)hTW+xktSp>Pldb|QKFXCAIa50xmdT|&r3Ep z)5GW}F}Yl=5YN52OH)WvthwS&!lq65>Ja$z__)C@{qlGsZ;|#SLZ2s}x+)oLQ*)Q9a|HeN9{g8rRLw=2x)YbP>9(if~k7 z<3ycXm*#*r;QGhSrpC#{{Uq&y)9ECQNu7|nYI&1p3E%ZI7kHVGNgt>luEJwlM2ZiS zXdJa%A@kC04RTY@>Sn1oFWl^67v=X&1I8dAB`3^SJiT>lq&vD(&)cv6JOr7=N31D3 zB7?zXwZTKOWj)OuXe&}xz{KtVOXid3iYq;xh_z2eY1XRlJRl)1_ORPo1+9r^O?r@4 zr$VA-pN33nhg*@dQbx5RMU?sb2q1N-6k4AwN^NYBRDy5cD~>WzTP>QbC!Ft-7BQk9 zk@3KkzB1GS-AxGyP7bV9Y}PxB1&dlmiMVD@TigUvr4sm4hDbK>pe&N1O(RUk=9n)m z!Z-NNEuwbtnu?8sx!m*PMeBhVPPXsom2(ZL(OX1%-VGNc)$E&T>OCbaQv6H4-E7+g zUq~%g*Kci|5t`oEnF(W|$SJCJB&2Uh+-=c;Z8TT<6b5%7t7m{cp1@!Ue9vq|`B&$7 zC)tuBc>iZaFf@@6Lk1y=9AjZdo~o*)E@^uJs}qm~We2wtM3Utpoc+RJCPV_nEK=?> z=Tj@F1*6SwM+p}Z&pFT{lJl=29MMRPamE-#`HM2rb5UH`qA+&SC6*~D_*WKk22|yj zfthqcSljt0_=iTYzU9LG4+i4y zPo;eD&u6D#_YZ1~mkYrIjkE%;s>7`$2tVI0))(_&3K=ZS=#K`m1DHHUY2@1QmOiDgEHIp<`)O*i4DXo++)VVC4?S<7AGgG z$@KD(I47k{M(-^-Otk5|JI>gRu z4*8v%2>gjJ9n}e?jboM%j$MZ*Y+P_fa=`H-+1xRT4yTv(C>_{FB9pyQSGb<)^D*MgQ5H7jV7gJI`?$UO4G!(N8)C3x@P*kps6|@yU0KgvX4N+ zPhVIF_PGeuhc9up0Gr7!Z+*Y9>)A`><}QLJ0}L{W!}q!+r&N1=F<=1SI??7ey@ZA+ z4Ku>qsxb3lp2r$8%TWjy-^J4{xAOF?WbCjnHbu~|WYx&`zazh~q~GM-+OEE3<#q9c zU1f78zDvd_W-FBE$(FsuP2%Sms;yRi>&SurBzc=j@0-0S+|rFsqBlVkL*xi{8i=mP zB&4ZK_D#B)E~xh0nXXf~{>UU><0-U_*iLv~L@J!h_>C%-+J@T5mRg~ImvnTHi687j z+2dT^X^C&UGkArQ<MS++XmNHy&+E4`nu&_fU7-@H)t)YrnKgbADnS$UN(i-MjTzmp zN}J?{Uj6)0VleRowl6mOa)YDqykyItUV0ssBS^CN&OdUg-;_B1jn4~baj&V3|~ zI>dChu5Hl}>sXK%1#eDS#f{YS1eln#F-J(pmT4(Lb$XYKru9mhGDRJL@*|r>lbP$3 z9*2vO4yFgxmgMbL%)|-2?c2!*I101GPNTRgz~rQ=QCA8?!?2*-$G$k%dO^qCPs?J* za0y~SK&%Zou(;YNc56j#e<$%$FyNeG?;+g>!}xw`8yDQs;cJk?-XqNzV~v%X&Pv_= zR;IXzY)t5I%n8bH5%58S$WW;5JVtX+pvhcV2fB|!vv+%j54(r*6>>15_OAiX z{;gAtzEvdNfGB{F{0ZIlwXYX{(*B75b=X7^^6>RGYW&fH;Byx+8^-^~?b!Epx(<-! z;zaqPeD^EJa_nk|2&EKEj%;O*>mWI-#AE{X&1|;$$YLYWXz9WHxnsA9&qKe3H-~3X zt`2NtNQOqVy76~W=kn2b*RI2eHxluFwPA+wY<=!D&&f@I*3$m%HWI^7C%MZs8R?Y# zkMH}<{{)lS7&#dKJDAMze_$|~ z<39~1bNuhYWRCwNm^{Nb5pT+6|Mn5hi>SDvFtrsZ42+(Z2H|Vd4nzRs(qsWdU@QB% zzvmIT{ECk$)o6+c5qS{}my{~1ye%qMe|qx6|M$qO^V{KJD@N<)^#~d+i(=*F8OJ>j@&*4p~dp6cO#M9xFuJAc zvw{hmM%u4P19a>4(ko@%2OkBDVco`TZ=v)Vy|p)}=~*vk*kE`WUnv>izbG0L{tpeG zdqWa`N`d+Bjp%57L+u*x8Zh<-1LQ9N?ja1Ia9>jx{PZt?O?LX~j>iSRElDF^C!Lmi zw2SV(PIhG;!Kl4DB(HvQ+7+Kb;I2zU#M9=1hHeNB@~`okMej^2SP)UCaa;4)vj|Z; zMla+{J^eKy)7eLnIvYnJJ(PEPm=$mUi9gmpHmc@)`{mhz%zkSF{8IQOb=0MVR&r>yezQd`@*mT=Gv#`SgA?d8ba!%O&a-P8U!1kX=Zgd*4~SJ+xH$LLV6oS8uZ6}o+cDVk-Q8sS*bdQ4q6h$5?^!^Ede}}Z1_GGaDWx^IkKul zH0)~2th}fDsxO|y@FRU($UjI#DTBsH$3?{_Et&&grVp7PqKPKSQ&_^WQp_ZEWwHJK zsG?lriC`Uu5t1nSzK7Tfk|Vq_82KG9)N*tCED(+``c+99+zsenW@m|Z z$&*izYJre5d5E$q&GmS-Lze4MR-<+WWulwYN+e`61@`IiN+lLK8eonj=iIn!s24YH zaNv)1+`~`no<>vYBdXh6BlM%~l;C@@F~6VYV0y`1QaWZYhcFTbAZ|+;1{xj8+cWrZ z+V^vd{(k1=ihjk=P~3{K85-ZmDi?>%Vfag;6gp(qOnTg`1NGh1zYehn4u$J?NZI@X zP1%<<-BXoc1FyC^0J>6NqMpWET|66tF($>L5L7@&7DVDr+|k+IA&O=X%0o-$)O4bH zP4bNvxh5&qNhVGcuA0J2-GNFX>LIi(goS3Iz&Cey`5Nbco75kh;WeJig-KOpO%~No49x z(^gtxa9^$jTzHZfg7nACY^mNrW&OVPKs8%#p5|7s3F51B_qrxlGhHd4d0n0;3`>N$ z9n2dhJy^vRqs(yQeKSPeI#)2JkL!1xwlms2ra-mNS@{{5zE*!D`|#-^l2Ov3JStLj zTSriD35n<#;u9XN{PNuW{19;yuPV~{J-F`Q* zh2Sc;>K@8xco6p@hxi#d1^a0&F{3CPE4%FxTRiGgbO^j6oSW<=(Tj96Q2--!GT&iq zEw-&l$L=fW_45)I#yq8AnTKY1zL&7mYqS^!79Lo#;9 zq_)mJk2G_4BR2;IQDkleX+0b)tf;dhQ}LNrGm%csr8r$G97D7)>-8p*WkHi^8hmS9 zW&H(fE3uj@tU?BScjBPGEd{gJ9(xk|hP>WSP|>HSOeEg4nH8X!R6*W?$kxmJWVNHU zc4K+#Gp{9X%Y225IM{ey@^cRzn*E6M+2x#BOt|qiy}#dyOf&F}*Y@)(;W*M~5@hMp zXQotq^f$hsG8=7s#mo~lXSZgoxb=jw`Wv_*&qy>N(+W7~%disS&H2FhSg*LEm4`Ja zgla>^i+C4^em_L4ZXsdFb~kl-I5#NiT3<|kDajbbuhov-1lC*#eLP~XrSl80SCjf} z*GL;76ADP?&kVk4(awnw2U5>(aDjql{G3K} zkXXrp8}@|?#@L1}J`r^4Y%b4ECJ~iTQ7*wjOrK0fU8+nUqGjcFzQZt#E zOi)eKf^Z z+T^8J6b0(Mo;L64TCxk{o5yq7PHe`sqMt%7;8HQE-@l`Ovfvwm`EHROpYByt%=GT& zl;9fhB5dVrFo{+!C?miM9PdUGHO(kgdlX~aBW(S`wuh;^plf{kHR+hW!bg!S49vm2 zJt`mus@!l9tM#spYJr~f*^}e8f1N{onZe+Ul9sJ-Wf5k3V?<1xpQsYz zOEqWz-!_SJORuT>SEv{Ub~k7|bQX%o(n>Qp8@NJ7kyIX1McKK1Ak#JKJyfU7Nt%L-sI73xEzaxc;jfNN`Fc^e zI+aV#4jn7l!o|wAS@2P_Y0{0Gp5;|6=VF_Wx zKl0|iU8->*N~Jwj;#}e!A4J8%g%i^u_8>fv`@d8koHpS~YM#HSQFzi~lst7#Q|DMp z>tNJzFd+|d9WEG{lrJXg2vYOhzBXy3=WY+(^^%`g^_Gpi$UMH`$>|fow+JLDbgJ-Y^HaWFwt(FUQ(yT`Z!xW{1 z%ts(-;!59jkQ4Ej=hrQvUQo0}nMh1Aah=meI9IhYbyqeoKG!l<@Aq8T30lFU>6P9u zBUbo7^E6|S)P+`!1XaWCSKtpkj)~ zR$+x*;H)Vo7-|3;dCpS7A=#?ynLaJ;++j;l)hwXT6(3L#k-C#AG=Ns=8SZ5~AwK5T zH&E2yg`H&NCIweViR+1zvf`CzEIKsHcFL#{h+mkNFeV z>@^86&8QM*M~5Uem~x`pS$KWng2l-|tW@nnJeh-=to?HDXI#v%T*u0z!Z%H|y487t-kf{14cjSIJSgN_FS*w| zQ)aN5aX3!dy*pEX2!&99!`PVGhr-pzoU0&#%>|=^| zC)>|NXn85!7Y^4w7A_rgBjPH48fbwLLc$bm2xMz~8q$W(mVTE~&bW%SZOS8D?}8UH z-7z^ zx!t1!eqck=GA$m2E|vHMFJzI@9h;tFf!5C#hYk>N?||3S7wNUs1Qz!y*2(#8==6w)V{b1*LhkLv>;HTY>8IWWNl@HCwN2c&PGG z;W25yf(Qv$2-n>j;LEP&F=BcOPT&g0;vQ_Ml#d&?%%?U8h@;4Zzy@NetA#)$B4TFd ztEo)ZXnoqr@VbdivcyRTX`h3Q__K>$(G7>RXH5POiSeRnx~l-Ao~ELmcjhaS65L54c z!7IqibIUyZRGOzR(`v7Cb8c_ZYWFkDh?I|>nZZQ?OtXoL6REBJRMAo*h>Cw_5W%wj zlnqQXXd^eSC&j?;qh|*kwfbe=Nt1?IGX^Al=eS{~lD4`n+m=84v>r0SW0H>J$SXgf za)%IMZBe2Os}9Mf4^Z?nux_qF>M_u?5b;y9Ti$^W6XFvz1`_en>fXezE0#RIHS}u& z`LT%Hd|brLLvWZ*vR>`d)yM~@A5@hHM9-e*1ddS080^?VHV~^3oUt7qxwYltu zKB=Mg8pGj9)UC!Pi9KJF(sr&s>|5l$N!34;-Q%iH7&8s$pVSIJ?fV-nrWNFwdtLaE zYtK}WAogbK%4mEJs_0CM7#)&bAy^BIO#fXY*w44@~$L!$y?uv)35@tpcUxPFNOVq1s ztMX4{B{V?=O9*D|v%)xQa0nHpKE;L&K^O!Jfkk-Mq4zWIlLiDmuWlo=@JSC@x;LMd zVK<+(?5(Wj2ExnR*@^JLF|#UT;qEa&Ob*3DVi+&J*wXk>qpRL;(@A>MmB~J(yY(We zZUMNK1iYY&g}gZ%|CX^+2_OD)o+p~TDaby+vYd6Z5tiiPCB-&j&0kSe&<~?b8*ha@Amu&d+oLNdY-CJRoxd)>Qs`aETWeM zG!@)wOL`O-nrR&W841tE?J}2tO9+@NGPCdg%5|a(o$hz_)^^tswwwzaglzX))cP%G z+rTdZqO)2^2!bG;l2ip>5q69_yvV$xIk6U$zsOS-8~WJROH@ry2Ks{Ge`L#- z2Kkyo-^1_XS&Y^^P_`LS%Whfjn29|JEMD^Tccvz9<;|8bi)IA%U)i=%8G`p}hlQ$= zO1Tec_NOeVr(p3C^gbvFKcc@zP5qcc?FOc>ePI<5MmB!7$_rg%rq@)_3Ue}vHG=&t zD1nPEDb=LBp#P&Cb>tr|32c>U-WF{@2=`P^#NiX~kKxNnSC#DEd05C+S~h$n@;*`W z2=N~Vp>QK(m^O3@^Rlm9v@JCmL}a(+1w5@;nOUJV;}Fs6kb?-@k`toq4dIfO=_B#c z#fLdHdb8;Os<#7}k2ku=j;M4!vxiEU`+JAKjFFrEQ4t@u8I)>`OFDalR>DW(j|%B4fiRh;|4rVkLn3y4LMP_49&&v_p^gGZceQWJ%5yns4YY~)4 zlt&tPNxi-IDmK`|mxOmOr{XB5AML8r`7!<=Btnzd$}y8C8!`=8HY9!zuvmtZn#Fpr z!>Xu8vW!jeg%>MW`HK0hbl5lVR4Eg23f)ofBeeN-AnWxbSz=GbiiP=MG#!w7(9t~yZ2ien8*NMLL#OwwMCvc9ZOo7 zW?6b!CXq3s2%uSl@Rj%%*Cj90_vFmyBWs5`q_d19F-l{sUs;|!Vwj0^Xc5=OI}9LL zLC+y#V2zA;KAMC>6w$L*l5>s!tnzpPK{@<~u)5rZqu#(FHWI$dDe8Q-F>L=J+QvPA zrYlv*Z>wa}J?casmlEwoTRQ$eU$Oq{-6Fc}Z{}9iyRpuilH!&TGciH1Lh%o_Rt;rZOxrNrxsZVG7wC z+TKujBGzZ{zw*}?L2<=F&&iks-*If#SttZfhv73mS#M)(El*%Yf6nj?0G}dv6KSW9 zRJ-JOjiA?3eTK=ncvW5k6?_`PG{X|3DW(4rX1B!k=VPD(5vr+$NgWPQofGpCImcN> z;_r!c3MQ>5nPgnd(mXV(TH2SzXoqTMGQ%Ms3(5MDz5!Z;sNkI;foACI1Uf!3^`Zw+ z{BHf8K26(>D_#6omJ|8DR^VP1@ly1NrMjvzAIzvdVS01%m^Ms!!F3Tjg2(_UIzkf( zeCVLvS~rYE2#hMHi>S;VD}WUZ0spG^3?{sPRgn|43JDzne_olBlv8n!@xwQJ09&Zr zGRE!qK<4MP=Zs_MAdxp9)X0bDHb#PJ;$SgwiWy+HjpLBLnbk?4FCBC?2m_a%fVAPg zlOj|j=ePX=_AVL@gN*rZnR$+uf6cE#9W!yXNfF zrx_A}VdIDv&qrZV<_Sh8UT_9PZrj}mY6>?eE0VJ&LYS0&QbrCWWgph>IvMPYr9slC zZA5Ng`ohl{p8JcEd|!`fY@RX2g_q-GYt$10OygiCtfRx+CL?0817oNK_HxVBu{iPD%;pmy`^eRqlL0 zNrqg`r%3-kR~Ovxj%b1Vc*~fwN76UpOmeR^DMBrdN5(3?nDV~7U;|HCsxih{No($& zqHr2{qqmyZl?SD5I6+3MU?AltED$8wU?`$8J)oe78=#0r2}cVcA=P#iL27wlpbDu@ zAZ8LXU?itVaKXNQAkqd_xy+@_mKqYKpJGz!Lo2d-s0iu zThK)IC_?mTrcXgupw9@Q#nE_eA6$XQOM9I>>(4&bb_4-ydA(0TNv~HXGV*=M?ja6K z!CDCPtC;xrKLaqO1Fn6_&gX*B>;pgUG?x9v4Jz72jAjK$H`HO^|E@?NnoGRiQExKe6FM8F5%?P`h?%}A$Ea}>)(q5hF}N0R#66ZWRunir z%9{Ay94Evka$X{40+y)UMt#fyn=P;0y9xku;#d(TC*s(5Rk7sD*|f`|yG$ zN>q(ad&>Nb1fvxJc+!Pb3dQ;jq@kM#<(iz73=A$6oqW=QNB0J7YZCP6%cIo9y1UJx zP1j)Yu)5+&P^pZ+rXmbXPRK}J-2i8VDfF<}Umjj3z@dHH~=xh|A5T$wGD zGX{BnqET8f%5mnsTccAwxd$*eKV}UMylhGCXn+tO2t+=a3}cmwwe)Eo%SesVjaz%G z)e*GS>X6;1_TfkiQ_7uEF;jmHB{Ldyhg!A~AdEgfT`Ur6i0Wp7oyA!0jeM3gmGw;~ zV!NEoUOct*Vl1ZC3D`a_)hzcG{+8~YqNf~(phqbwv9f@z7zm?};+U|cz5%EZl!>@z zNNQ!6bory%VwZXNW`B89H7PXTMa2o>$wic-B;O~g(=p>m1i7eR@QnaEo>05?CQ&g- zVJwZxlIX!=%`JE1K$Y%qUcI(WaY?}dwU+QB9I}#9w6_8uBlh z9T$ZB(5c1+mfA6?s&C;jwhLS)#T1E_4tdqL-o-=-xetZ>Z2Xh@ zzk;U4VLWtLV(+0DzKI_7p{;?Ufq6)l~s_+=z+J4zWPxemye9z;k?pPJW&0%Qibf~vpy`P3#rnz<8^kp2}X z&xX!_3t~=NrzjbZwh_^XWB!x|Mk%XoUlthio?b(fDk;h2_#jfRECSgfWtj3u0gF4Z8Rfpk1%IUgWqBVRIp5;fD%$s`9!>gF-K2EmMjNnR=^| z=F`RkkyHUA?=@se(WQGVbcOO;#BKCv={WF2Ozdb8t!@?yf!$2~dMyEev%Po&!*ivN zKU9Z4xtTp9R(Q+SUhB9aoEWH)AjXme{k`=;etu;(A(wDg!u9tefQFKi~S6_LV!4c`GC^9 zp6>lJZ2R)#@i93)xiLL!3?)G49?d_5*|y#LmCf;fp}=7ql-^c{+0^^>5u0s$ZW$Q& z+^d^c5vu}tX;aC`;Ncg3_l`0+@p8Kg>~j40Sq=>P>)qs;Kf304$){}kZal8Q{^K&8 z@!-U9VY1$wq0z}X-@x_G!lSb03FD6HV&sHLJVWOz`_d-!>GFB-F(~k2F)x=z=yb%z`-Z?HPm8NRzl_pNMd{qsTW=Pg->Yx(T~IQqVYCg2~9Pk+2+ z^Ya~MBM`tx)Vmzh$_VlVX^^Yq89h1LEofr&#N1Ipl*2g{Wpt})l6IQXV(h$fbFF6s zpTH+{>5 zHPx#z2TLwFw8gk>QUVJ>y&b z_QZLSdLvSr=`c^*oyX%Rs`I9Jn%BmJ>U6{-$vHj1`>-=xZ-Jug*|A6Hk@^`6->Bv2-n37_8)QV5(g5ES#kIdUeWbX2dimmc*n-#veLbKsczr zp&c9ykWN<`$7CD^vV{{u(eLWn4xXLc^##bwIhORnDK3^v=7vt99B`BXS|oe0L95nA zv~jY^C*Hqiz0)mRt6fs6)seQMhf7+=9o3A)DfAQB%Z2$0o#l;^x@d`irrr=a=V)#@ znA~ZQ#cP8Xc=H0Yw03{>V^Q|^lzc#cPENUw;SXvf^=r?tRv7r&jkowRfuxvD-U@!x zH_eEyGb{A9@}m!!qc#~iH+*XoVialjrSOc-=Lygf-D2tOpBs|}hPBsBk@dIM*5i(; zBB+bTSVtI?y)tsjBRNbm%Mn6-495%`%>`Jn-p9P6g#_)ZXf6@&sAQHD+nddG0P_N( z=fM!47cA$Ru#7W)1)Oc3M8-W5FHs5>$*u#jrvvxb>)08pHmbZGA}mV#L4ZC`5(vvy z1DK&ks@53iI3K46@fcU8Zi#v_X;ZKM^lwk4*45Q6)ouB09@`|BK)|Un7e)!hIPbBN z$Wwuw8hk8j%3UqnkcRPtwo}DejYL{A?bb#;{4wchdFEVd0E&N;V;q@yiy4AoPtd)e zEJ|2Hhj20kRVqaXnuwu+5V`N-u8qz46`>%cw#lBn3Tc?kB7(~98NU_vCBAM{&SJSaFoabV7?4E53;HJ3 zfjI$s1b}@cDW+YI?w<}Q^b?KD=U8j4J~9<>d6Z$}z9T0oxGBzRGLxM6B9lw9s&<3B zQqk^0aEC)|0_{exrEE>~Sy+q4_W=}L+OsM76&SsMy%FjOOkB5A2B^^LSGOR0<2*6I z`B{KnB{fO%18!zORH>;)@X37hz49r1hfa|lSo-2jg|#&)5K<&0*4kji79w(jM1KRi zT}>=mX>g?Tgx4-tktb(~VWBg|`6FRSh+#A$k~F?!&OwXF$jr-y=qNneVk5c_3AfmQ zW(T=76Lr0R51DU3^!G_fUHWDZ#pk?1IRs~0K+Bo7g9Yi4xm+cnWbIJ6>yaGq>o;UD z8Trx;5O<@)A}knHhrZB5yWEPLC9e%6ck6`74utrz+_@1Eh+)(|sLEbl zxqY4Dz}p~}^R)gRAqgM|$``f>rea7GH(Z~?Eah^@Z`%0-2|qtC2Ik?IvpR% ztIC*Z%_)*K0kb|JmE z#i&`vh!HN?SzMWjJCF2U&D!ycoB+X1;SeiQN7WfyA~)Pzd6`DyjzqZ>Aw|CoT^Lb| zA!XbI90)};(hE?U0E}pzBG|83bSN)9v0^wHL=XnK3W8D>RclPGIwWa!iW2(gRoM_M zG^lUs7w{ap$iP})K)42dTj$LBSflp2^r~!%MG_lmkRdPD3#wid=oy${j>$;3p3{Zq zNUGU0_MPdp>aYS-F6~)F0m^xLBz0?RddMH~wZQh@SvMj|6s7p;DGExH!{?^h@n|+v zLlC6=E}5EI5lTMLe)4mZk=Sh_o^qK^LRUhM|9<|-Tp3VVpbMv|GT>n*#YTpi=&r8bBo0jGvz#fGs8A$-N$r_&Z% z)*-dSe`tB=>!%6^LTg|i#Mh=q|1IwbIL6+`?93ag<90a*0aeicB^qdrRSCCJN=9d- z!Vugwr6zf8sl+sFdChl3w6f0ikcF+{j?Q%8Zr{T_DMjX70!-=gUKY5`KTjo_qDTz` zk^xMLE|yjSoPq%vVX9xAAsAP;Oe{cfU=aXvsrYg(WphyUR}z4a8H3;<{5%`U_Xv|> zOXNzdVSX6Vv7BnBAd4>LLZ%3>C6D1AZddlPqB}9T1YAcPJ_~QI*cof}A26!!36yVh zArbUTG1_0c9y_dh==P=Lk+mMZ&F8=|kcWGt%5$tDka%&)7JJ*lo_q+L#lHMLCV$3j_0IE!@E5?$BadbL>Y0=9My{? zaYs4-l(WW+D!S&L<*e$|Gr94ivp3z`ZZ+1<9C*-gBj;ah8Q+Bj4%)a6ezBrJT&_cv zL?;Az`I|PyE}1?X#;uE?9v256gB|Z?Iwh%Nu_EH}TS`FHV60JizyK%GK8o^5=uu~d z(0~#@6Z0cei|F?+{f)B976{}gTnIoi5QXCMBuj&@4i=F&;u z2Px!c=-YQ-gAVqpx#2xMWea5;{lf6g$>m!znqKPv=TMAuDUa*x538D1#P%*5i+i$$ zP8Z>53hVGz`l}TaVPBE#{xW^fNkVUKc=r|JA{6Bnl}}4_=>TFRxzxy8ML9v`h^HsF z;{_jCIXV`wud?WjJZGYljZ=^)ME1UIWe8hP2B_L|xpf=z&Nbk zuK|AmLZ1oTSoG2Pux9%vD#nTlO$NrlU2=*A$`C41Bci{+)mCM0a76LEcE(mYDgBZ5 z7 z%FRUZh8q}hGqd{Zv5%<|Maw~MY6PWOj=92V*=%kX*y1#PL`(f*s!kOi%;%H4I zmGx7Y?$~nGd2&iC_8fSV!}drJHM2*lqSwkLOqjB&s^BCm5{~WclR7Jk!*t91l5>j1 za{Wktt6<46{T}}%450^qf1t-IF3XWW7ilL5%Y0gvLz6~5q!R5^qThcYLcXj=)REOP z7_<-!bh;6TOO)7R(gLZ=;$8(VTp#RtN<5!qXw%D0ILkEv7v<3JYJJJOM;v<2{B31C@V z5kg1!zD?^wcT@o)SB6uyR;Ws4F8ub#IzUTHXAGvb{`zyqxcZ?`Wx(nX9} zyNf5>vy)MkhF(f>L2`5Nb)C<}!nP20lelKYvwWrc325wA$EM>h9W=8Ay58ZS)C`*x zQ4Vx!+1>eVO7BHUSy>EUnYxX+vCFZavfhjd;x-=%u(J=;zFVJ!@vcu-;msGEwz7GY z)9>4hS?V<_uFm}`^>=}w$p-1H?4V(qz^(l^sl9}8Bf8!JVO8Z;fMZ4}%MeX@NVlr9R*&8MLB8VFthB z-FN|^e@lBu;}e@m=t?6NNC3GrKxmYwI7GwI?l4%?2ALH}C@^T*$(`7FUS#96`DRg==+W zXzXeXUcp46${kZx&au>`mfadKBER}ytro?yaM<(&9>C#bsNS=n;Ovdi5_}NR1|y6Vd)=sXw?-Ct#S}#<5AG zORl@{7`yR5#3e37Y;!2b+Ho`XXW|-b96{VE%w=2m+I7DTJeh}0Xw%nyFfeka9Svp$ zNFWa>lxq@nr4hbgW-8K@BJBScrk#tYM69!Kc_ZLoZzZn}`5C^3nCkT@?sSKYDt zwRMEX$lINu$`cCblodqHL0t{vO3$l^ck5CeC!eTYkTp9OUE!VKs5e5mjk|O}s+w(; zdaEuZi~1cbUo4g-k34t-Q_YoLmca+IG-hKqpc&(f3*z=+jLBuL?5o_A)eDdTF3{%N z1XKsfe9apcyMHGht-rXv>_^qwfKvPkW6tL0dR}Qlmo6OTE6S!~qQrhGgI35C&TJuW z4tmpOqc~t4FWq`1XkHO2=9((iv0UxU|c*f4#ALEPki1OP!@{ ziUhAR?Jhq`2ZHC?yr<(->hma}qHaN$;WYPL-ePqYWZ8_!W^dy-^Zrf%Wt-@e!{@5= z3Z`hHgJH@HS+#iUAmatT6!tHjwYu2t#R8C*QEf3dukmCmwOj>+dN7J$M^lT@xlS&i z2#z`@voL7nb=I6Cq!2(Lu!IJd1M0y3*#CSf4z@kiTX=_mcZY6gLezw}CR5SRH(|9W z{&eBRDm(rbr#a>S`Xz+%thntgO2f~G`_=cYT=V(A1DBHu{yHcB0+)Y2U!Prb>1Ths zKYbqjr&hyL{u?Rse~WPcpLxv6#me&kq{#n)<}v4gY94d`SM!+jKQWK5@F(r*+U#!X zZn^thAG0t4Mt!3_^mBT1ur5X}s}zEmbV>B@ANYJy(n}XP2PDx%0S7z;N@!AvXyXM# z$v&SqgLU!ypAKvvMVm|CuNwjc-|vG5y#yiNpBoH_e+>xzzlq@-{kwhqk$z=7#6SD* z{CvM2d^}SQAH4h)=$W|EDj_sr|9EiN`T4xP`WRIB72&K1QZSWV*~jSidhYLmbzbGy z@`G`LSa6AG^0V*fig=W`t!iKFEmayW{NX>^t9xniXyor3&zR~|*E@vHDQHy4CwHi* zc_~uzS%;iMSUiV!C#Qh)ay$L~dTdDV=!UphC%AfgB-ul7EcGH)`x-t$_e5^wOUC+{ z%@9x@frvl-DH2aJH`PZ?&zKk?L+%pqV2y8qxFnBiC6UR8ydrn_FW2_9C5LD6;(Umx zCd^b%7a0|Whg0<2DL|*8;_XSik2EK_6gL&o4&A&hL$V+p%5op8h|3uj{wwn7&{M|v zGK{o?>|=O76Xr9`@_c6#<@aVC1rr{0mxoxR{LyGnPX_x|`jHiW;UAF*Ae!bS>!n5Z zl9G@?C>tt-u#+H3Q=VHC8mOB6aljlc$j(pX8S;sNhTqVrM(WU{LS^}3`%cP9iO_nP z6tyew)glZZsT%M>)%N`Ugw)Zyi*Cm_bE#Onxj-rz;+jXGqd<~XLy9gQY9c`|S){nJ zuMvBImkjHUP*VViSSLy=VFpbh<3qaY0z6o0$7?SVd;ojpVy7YPeA;tkec%&MQmx>+ zKqgHn96-DDgKjLlsa!&*GNc31o7~I+nw=A5{+!lDBFCm8-!npG8WZRu`(9JwX^1Gl zRd!X)&TJ|Ed9RI(U&vDvw*6ND)V4ghD|soShVnh_yn1paCctJpQT$H`56Qzq?F6UPyG*x0ytS--3)tB{@jxA0j%6v9mmHv3j=9mBt1Yn&ESDYqq5ztHUOk zyhJadaE^C}Ys*|hu%H`t_xzaxEWYYML|1=|DY_SajE}G14DemJexi%`mt)RTAp~c4 ztlvpIR9P&mF9Uk?H?`)iIt+E>)+t(yAImA!tq`ryBfHcXAkwWNGIy0zMb5pL+Eq~> zTOASH#$3IaI#p3GmbC9hBJ|-p0@1i!jIS-fK}O^1;qN(U+HpE&U?P;F2tjZQN@`<~ zyL{NpS#87yTb5h?LNE(X9|}POCnawGEMyL7SywJtfI0=Rdn{=8SCl|3G(SLugYsGd zq>=c72j#qS`}dBov2qAHWBQFEQ(8@UQMIhw5XJIRkJ}|Un|SL10j}IsEWrL02%p!h zDlCS-z%}d(`N6q1cQv)ld1_`?3ySpXfECEDpClbG1;Ai z@ZeY&+xS}(A3U4-LPCMH1bd$_erP^l5B7epj8uTwxiS+lN`L^2l~#slvXNVOV)U<9 znPkS@U)$ZsMc4+avSj=058vA`k(vO{H`d#GJY`*`P~5g!4#zc}2*%Ap#s;1FJ3NpY zh)w>IImu9&bj7;>dCKzYMo=@z4J!jX`|VD0uv%u>tHdr8Pn9TosG|+XAl{xx;RT)) zSwYA7L|WErfu(Cn=1m3-LlmtibZQ|PTXUJY`lGERvTG_Y_&~MLD8bFkt9J@t zoHd)RNN4TxQV~}-qUwZ<7#|~11cFYEix$a!nVGcl)F9%wXh6tFdczHI-&fC+2|jxaPWfllk<) z0S!8X+h(n_@uZ=JGI}GI^UD+^jaTKr#c*(KD!jnEqLe|cwZlxjv9Ol8l3{d*?IIOK z7(V{CCcsTSBR1=w`9+?S>WM1icUB-QDVt{Ox7JtT2*W)wS&KU&xY`I2w(*Zd9mSo1~5EVaU>bxzlurp25F3W>^SY4$L32iKJ>?YFsPn zYcv?c`V^s?X1bBE_UANpBlmgmn$~UM&WUKJ1%qDVH=LZitHZ3Ct)0uJa-VJJ?Udxp+^YkPyjV_mvu^=w<1T(hJ zyp=KC&c{q)b_*nfqA-hm zjPFz>KWm(7n73v>*#IY>gY}=T;@}Vy&CB0Pn&entP06TL?S}inaY|9AvAPb>-U8Ov zHj10_bfV)4ZI3x1gebllZW(}*ORywpi zHZP?Tzjs?%hP0$>`j`IBz29 z@5^E5lo08L4NU9usuqSnt-;3$`Td>iS#@wBQ3dJ$H$!$vp^R-r{1#I_D~`jKk7k@C zmvr&eLx52VR|uwvt3pyjXiC7H4#Yx3WhS5>x=X**AEW72@CZ-16NlwWJj^%YKgmJ| z`D*=f6<jy z_4=1UQ3>#oIzGqW3^(n25DwGqPSEk(_XwCINB@Gg4@km;4`-hN-;eN8O_9y|W0-_O zT5IDR??l8ot7~g@hoB_qJ3Qk4@f1UYlX(*Q;9|a5w2tT;EHVt42UC0{t$*s|?YX1c zqf~6*?FzgrO~Mr(seg+1-4?trh0=0Z9Z=lnW(dQmXH!_1DZj!Wn4_w#yM6>8IOMew zY1=xqRLvvTI*AD{U+G1m8%5xgqbFnmryG1{RenOklt`f;uqOJIPDV~;$2BD8o_S3|iHrX^V8rtPFfQqZu zA5a_s)1`WVM9Rdc+LQ)aWMU_YR!vR?vxHGygB;t*-Hi6mQxHQBlYHy+Xh5X8C@G(! zwI~fhnVSGkzlIe9mgysv1Tp$3yyl8xMcI8o2mUJ01p_;*!ge9*W+a}}DOc)9#9Yeu zI5#G;7M4Hjs(uyVdLv_6rg4sQ13uuK2q!^nNv4{nm20!T5_2U(L5?G@8Ayw=bVWQE zFYVyG*O`Fs$?LmphcpKf-zKtsp3_KlJ$aBs$VC%T5yW9uB%`NJ1;a!nhwja2y=x|% zYsY$-GMZr}wNW~q$7BAqn4CyNlKDX`j5A28*k_6Do_Rtx_`CcQQ0Ww<6Lwbm#wL$fSo83l@ldBC{3EpF3T01VHZ&EOj@~Y-SZ>lfRN$vjhNWE5Lc1f=?78$Ikm)3y)!fZ;(}8<>{$4xoTlfspin2ka#h~$y}d<%408Xx zaSTQC5497|(Hp`SSdfdyG>F%RtynUm?s;C?%>S`<92@Sp-yCdw}oNV3Yo zhb5966G)#kxpQx}YBDQQgzM#&1oHa#T6b&;H#MGp17qX6Sg~mJeU@`l=khD~p6ie0 zw|8uc&UJkIMqC+p=>&L|{RKBQG71=XuckCtp3Ul~SJCM!YZs6MO)UxOjtA<=&q?$L zhSs8CI2xZiLHqAO3QJaJi$%cH(oM2J#aK zg)^L_Rg0gMmb~3)Y;7rpo(i+MC<{x6{bZR>{cJzJq(<$(Q^n2bfQI2-W2y}2n!*uv z(?uhG+%PgYebxorRf?X$lb5Ss@i+~{b#6TCwmjMcD{E^T>Sh!}p4@mZ^BLqhNiIJR z+cF4r$HsjpRk0gE$*DL6BFdjcC2AL*yyHnW^ZL7G9NI`SGcD3^vTU+OwUkLT<(`Nn z@&ylL8Ly+Sl2rt2%dL05x0f_j{$@ubRyCMW5j`-7F`re$FU^mE+F}XfAiW?C zUNH|?o0>tSl7r>0cdO1Ew64CN$T`E?e|mX*sY!XI$xL+o**5*}5gj?S?~%pImkLg{ z6X0ZhDi!G*XWBLMpj4C|szHZM?>uX2dg{`9szDXbou;^=0DPDxKN!WN$c1a3WD&Ym zQyfZ;CYK-KB-yuu#&~9xY#JWjKl&Xo=Sck)hNZX|ZR)G3fwYft3GO=pF2C$ank9p% zXUbXEfz@7&(i)U(^Pq)bPf2H%iqRHzDc$SkA{44!>oRmR6mBv1aA$C2^zgvL7+LbBfS%}p_14r|eh zK#(gttnR@7HKhP`Ps#)jAz;a*x+vM8Zo|47ilhr&D+S!YhnC!&W-8YeVM@v~hX?YE z=YR;ub4{g`XOlAiYCVYFh}U>plU3hHS#wIq7WFkjO*fBIEQ@!MD=g@Q06s~q&^_8C}Aj+p}hNJJ19*twzljlge^ehwN*^I@a?$^_}Yr0~*ZhDVII>K?dvFq@wi*!WvcPSl< zb?>BEs@P*jD3AKOGrz6rYe-EqO#ug7C~OqqHJ{>W@NW+STicO}S636@q>@yJ%oU8f z)KVh09ocN>WpC3ky!JxxI-d4yqAzwm1B&dgRt?goQByJbTABxUpgEFbGi!OYn7Vzw z71@da>ldtbDi?praX@JHHJUi#z7U(^k~YRGkYn&B`U7kEg*5`_l-H6O)9eISBL8)Q z?#|Id?*O|qAUv4wXji^IJiA6hT%@;35GK+`S186YJWYD1NI1Wk9MnB0ZFrY}F7vNf zvuGA{qb0ua_-w~(Y%qYzq>?u^dpGgodOR(lIb z4G#9!mMC}*H@5Gfw)wu~h;)YfF-K=BRqL!Tq$nzLYp)!zVX??Mvx|(<#pExHAwoGN zKLJFY%he_h2&VK_Mj~!GC0z}cS}N%_cwlx~s`ng{wYSwClql?13c%*C*z$LZAFri? zwO*Z4F@WO-n-m^YD_Py?gJ7Jgr}hjBv|e9=JOM#XbzQhE8BW0zaUG70|ABx@Fp>=m zwVXojTYWnba3^lqO2_V6t$Y$%NJ!2y+52Y}#!(>!cKbJkyNzPx1Q`)^fwTAsxX&nT zk0YI!6;OONYSizSeA&mngtpuIegRDFo;3ey9HHJa51~h-a32pccRzeayw`ByMp)milTFFTe6C0h zN}}{QqyN;r&*!pUy!*TKAKvfRuQn0hp0AI)vhW|^pfa}A|4xYgKTIea6Boz- z6=J#m2Qr~t|0xs7^7gYTKG%V-M3PbYXns>d=kxA-M!FAgzgl&E72 z%0*W1Jhr1CMg6bsPFhWGIV>3qVx4E1E{cTep7g1FPm1sRWA9|t=*yvTvMB56{&jMP z;QMpyK!70Z_BH)CL6pP4_h)Z|(&PK(fu+uTk^b#Z=n&Z6?~21*mBm0z!G+^j*~<4`)sQIK7-O9YHwls&Y;ob;Us_WBU-70?2Hu03`Dm@J>uaSeMbrfM_{WGD^taZfT zbFiA!O+%X@igi=))yX_3DZBzpvzC7oKdVEZt))p{+CY3zYE3tJMHrzNO*AV!E8^68 z>6zTYD{C(1o?>(YsV%9<9w{FN^t*#3BQ6Tud!Siw&1YVfsb8NkQ_pp^ULq_##ZY9@E zVUn#=rQ;fF;B;yrUmY8&dYXIcC@SPq{p83BO}uDs1(SK)Ww1tKzWi)l?*JQSUNywq z-mQIE1mDOW)oUv~Zr)NY*BZvw#acnOCqk)~1Ad0qS_@p*5P(x=;Xql%cNI~p0Uvrs z*ZGTa?UZ^6L|%Lt@kT(`31~09J5lZ+FTr{-CUIL{HL|NteC zB;kBKr9An6n&nmyJ2aw+0|sB^WJwlT8utPP+eAvjfo)-Ys5g{!)b!w{l&`rPp{|K- z?L5xNM1l>u{QG*oEa==nI}%eogDV29cOizO1V@i~!RZkC-5imvvr707#!B4LYGiXR z9NAmQATfY-4iC$WD0W)QSd<{zs27NEXrnt=P+5LqDc^O~IMH?iKxLsSKgyMH1{b%M zsIF~S8}aCBJ%But&uQ3$bmgFu(pQP=v#*r=sC!!x^F|1CG8LoGTZb4534HTlgWsF z#@JezgT7>#%@8OXBHGU)xfaC@;x+aN27$WspD#`Om&VG>Rscp|#J`xEF@a~P%Dzhr zVc!<>i|&=`V&L_~`##2SYOWM|JtvP0>6;w7SCkMxaB>zh{yoX| zTqsN7-p=+n`MQ;C&AF8g6ivn36(^g&{2j=_uKcm&0RH8nSs}+eSWY%?(eKG{e>%L8sU^>g+3kD^`+i*(cllP)k`JaQwldPfg4SuTye0 zw+K>+E1m+Qis~X`#Y4~#1!evDi>d9rx>9=9Ew)PssoKN#7nnT~IHvOvct;2&y1x`g zt0Djg(k^Ke63>&phWS7_x6oik2Vr@F+Dk(BoIrgARsHiD7Hdv}HTlJ7X2i)Mj?8L0 zU0wM;NhA@cz9n`_uk3Gv=2$K$w_l^;8v-Gz4=TTME*5QD4fxU0vRZsOBu+B0%fDg7 zF1Q!YC0Z+Cnd!}me)6}x{NtmUP%O6d>|hn=&|vaSt-TCqn>I5sbk?#l7|3F`4Yu8hcB_D16z% zYAY#RPQ=qXklSMbv8wA_dg>6mySJK^B~=4jIC4&1Q1hes>b9j-lg2-UYN0~#9Cev& zsLk?)U1fQaV!Fk8PUYgLBqd?}iQyfic2T1pZ?X-Qzf(K3^pImJi(^VQ#>a!gB%y%| z(X?{NE)u9;Y{OEx0UQYQbz<(CA&pag$)rq6b&UvkK5Vvt^!wld8ZDYJb9sC4)FgFV zXPXFHav!`c=Qn*hC3$k;&R>o7Qsqr*LUo+A8&+v%;LRid3M!Qiofmc2-~vPame8}_*~t~wGq5NXK0%@wPCS?|-0FlZ~? zo~fQ)ozmH~JhbDOnBuzhA~K^hmk?x5Y^W zbI4_UK=VYXTPZjumxd1Oo1d)#>@s@-jy8c?LGNhrJU1Ye(qMW+DxyF(X*b4OHU+~@W z@#lkf+=g0S)wvRyBV~lyfC-rNqRYi+qa28%sMUXKnL8esCpxnN#WGievjfFCb#;#i zh{yBLs)DhOIP4v2M<KH2F>Z8#x{JTqf)m9Z$1uq+Nc#;Fnt9|8qvNM-ob$h^yN4jr zq99GcRrSiYZQHhO+qP}nwr$(CZQFP?FM1+oCZc=Mv+P~2&*H|p|M@a=l!_IxTrwJE z=QZHcV|gu*Phji-ntE+xrd@=h3$aRKl7OiYHX*`ioCKTt;w5t+*uJ)y>3Og}lLQGu zusmbMjEFLk4dz6QCShLisBa^8j zsFQ8!``8`gjq?6cvH9?a1!k!*!q@buc3nvM zOFr~A_UP^jQtGaObav6?ZG8&RN|w|#vd8uL+PSPhTx)2_gj-#ATuNi>6IA*)Bv>r* zsZD>{er}7&TyCjRyVu!o$f64Q0YC;BCC8_PNE@j(?rMH>$`uCSwqbC7D|Be7xUPrs zNcw?R=+|YGKQ-L0dP_j8ZxGUHfGk73oK;%0b+;{1L&{)Pr>At!OTE~=5*_w|#{rCz z=eELN$P)237Tad=ly~Y*J66>;Z{k!gN`hs(ZJoXIgLXtT_O`>&NJb<6-hH(LR5D z{i>Ok)fH4Xc>P)sE+T28HsKqdEGmC}|H7dF=ncWf*4oWxG}4RCDB}*it#^8?<()K` zu2q#d?I)VETue{dNNe1L&uVqg6-JrzDo-Vl1LEn*bnk9z&VJUJqlNNo=WWz_z`$-Txk5s)B02}3*IGQZQUFjoEwR$dQ`DwE)?qIm^uhZbFs}eb4t-dZ^7|LbsiZ5 zEkm80=VtCR4b4KEgow(9EN*n$Y&RjxhT@#@;KG`IkgC>cwgIF0U zW0hqq>V17JQ`I|tO)~_Cv+JOz=(js@v{}+xY}c~9U$ma8SuY_ZPNvfm8JG5foB_9c zRcKBICxut!Zwy|Z8;tJ#(ZNEK+>ZY<{K{eQsd#OCgF%;yPzO*6i~x?+OBC;ZTRl

<3)UKdpRxU(`))#j?Z==4FVl$E1NVUb7aNXG`a|a34K1;A? zu!y?reVIb$vtC;!wJV`G;2L|&v97Ch-R~L;BIC-kkj?E^7T7+F>6V0hKzvJjqCRok zQdOYPo!=Z6h>5O_cere%Upo@|>@czk&y-7OhKLju))AJjj$L(Wr$W8RT>ppfMk*z6 zK+EmLVRY|QsNw$Hil!P_lcFXLu3Zd}3Ir3)qal$5t>zo@clbG1OSsi<9kX@h;b*96 zkFwvH<_<3xXHSq>ujxIIcFfwM7qpbg$8Jg&i?ywiv{`h)TIitU$;?9)X+y@ei(<5+LuamYYX>fIcrF6?5(#uRnJqP9j2^=w4biD;|lgj|m!9D_1ts3Blr z_Ho78UfI^FtF52HoNP&=2o?NRZ4Jq9Q08u_QL6X?m6^kxuE)=L1zYzFnfzRD#8>Wv zxCopfk(&(YU~3QC{V+vi67^W-SJd;GMf%ke)@CUX3DWg^FamWdqye=@PtKWD*6mtVhsEDmS~ zq=z3()XfISd4cevuy2pZf>;qzLc;8t6fhEC_QhS;Q8?00VyUR}cW!6yZRw?N#P-c{ zW$TB|ukULL@AlV&?PIL(+ga`V6f@$zrDY%zdK)^b){c+ ze7@dwt7JL8+|?g<8$%x-pGJ(gW2e64Z)m@APno;fu5V>-Z$o{&U3h43Xh*+>OF}6C zR^Oi9>41*@aN?YRl5i;V936g_;@<8~a(1BBQto3s$`1zb)f_z0bRwKTXtK9%HwAou zanF9@)4T|HOM=GI^dO-Rg3NewTpI+4M{dInOy}1R8ESR#zn~?~u98aJ^ ziIAb3&ti9;AiYfIc`v8P<9R0sp&yhYN!vq!P{<~1C+9F|&r_QVWz+xh5?oF%m*Ls0 zjd+Z1Ls!={hFEXpnYGc!0M8fO5}WC-2WsaO0nNyPV--nYo>ALPlFtFu`}1c?c({-HIXv#pW)a_) z7gj+yr_)&45op~~{-F)XA} z3RCr85n|kjf<_Ug^4_|M0ZM8LU5|}mqVr`n1 zi*^6nfUA+vJ_89;SmV{;)TL|DuWH#7jdq%y9c|$z1SIXtW&bo(PNI(JjL*AQb`J^v z27*holZo3qjy}lD22tmv@RitNQUMGnA*{ zuG_7HB7L-kUR`9}&LFdsZJ8UPReX1)_REV(V)zC`RgS-08uQnFpb7Rdwd*~BSh=F9 zlWR!!o|ODl;}}OUwR`q3d{Y4dMrzegQP{mYs;7Sr2DZRp@`b~P@Ri^o{>>Cg!-pWv z7$Xu;VAkg+MgHPuqAv7`t9GI4QR!Vlqq;A^7Of@U5TVrS+6YoA4qaHTg4-gM|YTSGMR5 z|7cdzLFyb^%`hgzI)_Ddd_^uIJ(lKeZo}^Zr-FMdq%wETZFtZNUJkX=5ag|6 z9}n&HI9l|K5|C`B&=@uErE*1UM!YvlRz+E(;Cmttk;ZR)HVa{Et0s7oM5;^;URt&! z%p5zWn&)^p!-*89vmRcyDMx18*OyRg`&)(tddv)azL{~G%|l~QF3?VnR5C^)>wXTP z1wqQvWqA$qVkKhakF1T1@(&o|)uwnF%~KfU?1VCR6`l^t1qu=adD)7IzGr(DC2<_1 z6>bW9E1A)C*o6acI<}nx@v$cwBD$yC0VRqd-9L&)eM@Et@eQ;#@a!J@e<|QQ0m+U6 zlYJ54=Y#R($T5J1y@p%)p;e^mVoR>*7D*gu(~(?`|I&r5Eo2rgDH+4s9u#W?<%KTm zV!$4JN7W88BSUFNo_N|B;VDRKT zAnFl{*&i&(WlI!)&b`?sUe%`93KoP{t32CsImuQJY0(-o50r0emU8S7eKlCY2>_u3 z(jykzn;h^1M;8M|#+AR?Cc!Oa=9*kJteYJM)I%Fk3{jF0nHmQw$nuo1$zHx$R-ihn z3Qwys<)Yi(w?#QwzC<`K@}q&W_`DDs)0V1^WaWP}qEsyf$~46oYD~lAEMBw>;~up5 za4x$xOLk1qSex=mb*}d9ayfD0804xNee`pVR}o}Sv!wxi0N@*ESpiW;)+=RtctT`5 zz&#jB0(+LBI(M!E4vXk+aSHZr!ieyit%O994uIj-AmJxEKu$9@j{7p^2y2lcyBb}n zud#kHer!O`^tEr}u5ud;C&$Dv(%#p?>jlYhqS-G0WgjLlGPCRlGlE!m49qY~JICYi zCU^92-7cfiN&<-+y&h1u7Xxb1N$wkZa30Bn&Dkng&D7#eoZ3?>*`YMNsz zd^{rgx2pC#W@Hy^8_{Anw@@nyC5Q<Tb8o8==sLV)oWqK8wF$q<~W;;^tLi{jx!d_BCp42_Rua0Ef_%$*d}$#tBTc=mxS zZ$cH<#{!DWf14J~J!ksJl)SoAywsJnQf3GHi$%Pacbm`aY9(M#@fpGX4NHOM6~MS^ z^rr%$NK4dul%dNW9nL(lzOAA{Kv;CAknoSZWEfB*gRynu<`1y#uzu7@_C2hw=zV!* zA(8E1<&xada=cmx%+*-(qrQg2coD83-T>&Q31 z4-5I*VFT}Ro=247*8C!Jyl*k=9aSo2#)(dG9p3N}lVi#ySI=5&kV5)?|I3&scw4&n z_QKYp5O|f__9{s#Vx77eQ6+@ufy0_|^1X(ciBGa682gT&JxRS95o_sKnpm{CKV|Bt zl!a~F3qNcspu>#h9TluuleG^SKb#nW{UCs~H|kt>DJ=ZdmunlS?uhYh%nCPA%l{}k zXyQtN2+EXY&;%KfE@J~~vO(CgoU$B#a9sKdkxj5%RVY?}A(Lj5yOpiam`!jX9dnwP z?dcSOXBeD*vVF6NgeY-Aj|jYBa+6GE%`o5-f`p$`B%BNm*)iMMSGHtZug(%Ogxckll8jJ zM3sWzON-cE)&&!G#G-RrP0M?K!gOFt*$w`fz|h!9jmJ9Dyab^0_2=gDW=6sty=_uY zfo>ia8_=-Ck*a_-23ufo_ejVda3k}AK@K7`0gHvahXZy?xkjrw-J(~bzYKGy8M=te24p{lr`@X7ThCOC<>Hke}5RU z3fQb+h%QoGLVGa@B4>ao_r}Dd_nQPf{g`b=oivBgAj%i2SL-8DnKi7^f38ct5Qpz% ztc8wXNCdOT;hWb8T6sCZCNjM0TY8*(LU|y+_)JD(??HQO`c`)#k z9w)pX$lHU6827J{X~J}LyP9~APa1CwdSBQP32SwB_cvOE7MY5h2t;(~-JfLR&Y9e& zrmLY=_iG|?2{5}&wq{fL#Nqqcg4(K|Hr6wM9Ar_};IS&xNec8(+U!>=(*-YEVncqo z2UHue!7p9FTh!`^*EY+9Z;Z;jX-P%z5gt|0*gVFlz1`H2SyHuLC)=D6i5gAf#M~X) zyihhZ^Gdzv);^QrmP#f4tuDEv)$zI_4Di@CJN$|BPHR1$?c52P>hBJo3F0Q& z6lF#p1*QM|M4XFgT=Cyh>rlv>u&vzI8cZD6Wz*%1P%(=A8Yj>DV86Ag>6#h?UU#ceaD3q zH#IF86-M<4N9aN*9r=nQ#)2GqZvu1}7&g zl01cC6MDA6zmQSVu=s{>Z8LKfGjx!%?P)(7r`^gJ)gucqFj{^%B5)OQTZp>q>g>wU z2+eq1N5XPSTpko!*HUx#$+u+8-d3?ag|i~P@=GG!y3^=$X}n=#*!Y_pS9?&(?he*W z+y+q^6fhE?7QZuf`IZoHf(fT1F|xWbN{g>ENWDqgRFnB2Lq*5wISX! z?xn48WdN6>`I$r0GXSg9@t2AwdOh9slhg8BArdo3Vh1oAgRI zAwNLfyhO>jM@7@qycIjQ*0)FVFhDBJ)Zsh3|A3k9gxpk-^2f7cQ?#yu%10`3I2Pi= zfoEBKxZ9OT5R?*^@;F0JPG4U43%v5pxON1&VqRhyC(_!!G#fvETPWeOCGosjbf zB-wXKgqlNg@Cs;>PA@u7{~6dCi@!mufq)!tviY4hP#laT>fEy)S-*In zh~T{HeT39HsO|aXufh!V2)o?aWew);K*!gR9l+?`YOTIJ6xsI43Ky-~s|M?nWwpSo z4W;etnHOzvy+(9N%}}O^L4iaTpxo?vUyZ&w9f4y^TAM$-R@gpQCM>S1*fy>t#l2JX zmo->T}8NKbG<9a{_c@vsFCT%lfi{bUcfe7g6i|F!{AMhRsCHq zG}zaeF9l=_o4ipuZpgi81nMhRYc%eOm30E+B$z$Vx^!zYVLRFD@b-KgL8`T+@<#_u z2x0K>$uOw#Lpe6cmbK0poerAArQ^Oy}}fh5Ykgq=aS3J~u|jr-QkQ5|Lg}cU`B* z{&Lne*fXw;L5LHO@Dl)BfIbxMKD`7?A z8a;T477H1s>WyQu(HXOun~# z5$jN+lG5iO#nQvs3xw>klFS!yUfCywHE^UsQme-)UN%`Mww7q`9zOOsbOY2|t=9~M zBd4p09HC~InM$j+(qk)QfsZL=QG44ZGwM?K$%M&T1QyF30DqqwEL@@-Ey)p|I^C5{lTEgh9pw;RktnG_@5m zr=2nv-V1*V3qb5N2)KFdx*S5{m&9Xos6h~zmD6HMX5dp@iXM-bSI!>kv&G6AX zSLRC8)KWGq@4vRTxL;HgN%|f*)Yz$=YSXCkVL9<5S4?K>f*xY0Kt-lP_VNaY$<-=Q z51rd{K3^nTiisfrQLsV$7l%U07_Gs!pNOb)8T0HS-ic{Y)vkD;Zu#PaM>!=gr$KWg z=VicDTXkD?7-{JmLNm?H$K-Xjf*P@BA7@)<5YB8bW`&#kaBqBjes8a1d{tMNUl)6C zdp=(xGD|;KeYbw?Uv!{@hQ1f|8blQ^#21PCjz=3 zIWewO==2%-UuR|gx1*`!gZ>}N89~WY`J6!MB6nvCFvRcG+9{$)Jd#G9;<+04>+^o2 zQ1csO>${wkf#>%-Hl*k0{=F7dIM(<3Y5)HEkcQVv@O*PJ6{I;(t@rus^YL{QXSY-Hl{ELf6gsfg_|BBj}@?f z=e-}{cC0?z@^&iGpcMl2io~4Zd;$A=zAO2eKSJVhKVDEZjA)1He&w(3?_TH>r|*fb zBEFk_tNR`{H}y~Gt0}$URDt??s;l{#H6r41H98om9b?>0*g_G?eufzSX7$2KN`7t} zmNCsO>AXV#ygfDnEgX~m%LXewc#>uwx%Va*&LqLPPcEjQ0cqi*3v^0Ccb7lW>1$7TQv)e*MaD!gP!y zlID<-g;X#=-&wdlL3>Hl5Z>;LjH4ly7l(gLH|- zPJZGAD_#;6@2Ng%dSnb8At%=ek~#}^AK;>5h%a~_(R80IJ7~A+)}HGI&jBIT5+c2a zh^6LyP*;vnRaexOt!T4AER8O}=>Cdpx3p<>nn!Ewo|Os#NZ)n%v3X(cv1=>G;spc| zo(+yd@#0ebGnwbwAVst8s%aL0BdTik8XTH_$gX|GwQ6TW(wsC!;#uO&@6W`K^HL-Z z$fH~!G%N7Qq-N8-?M&<*ICj`0vc6Ea{Wpy^$LJ)+AI4FLgPBtOuMsULBW59LLdKF%@8NH~?5BjK$(Wh!=<`(?9PU3Cfh_g5LS};W2p%=?neAd^Qg=5Daj; z@h%D_VF+*&wjL;?cFtsHLO-<8OhkL-3KX*jJVM}6uGk6T%tL`9b4V7W0jm|#r?RFA zgp43uBXOmV(T7_mo^_egn-sjj(x6sf;r=uy?{yDDx{GWg*jT8pQcb= zNL-L*wo1e%VbkTvv+^|r_08ks#dWmw;iX_e zsmMCaJS@$r7l1_TlLF$Vp7z$XpfZo6`*fuAUB%t@lvw!a6%MEaACx|8-_!aH@I5)m zgYaC_Zd8`bFO`wD! zXU2H@%o5!}tL4Q0s@Crw$4kK>MEH3$RC0*%5!~yR;JIG0)y!D9>P+*9H~2%EH%!Fk z!(rIWm=25i=%+c39=EI9Ki~xl?~PWQZiGzmOH9B-TjlzLsmEQa0uCyGB8Ij34}=gd z!0ccgmYY!pmG$Z@HK4M!(>LqpraE`SC*g#`e}vwd{JtOVw|I@4ImMmPD{1hol8)A? zX{`)?I?DyXM@2m1BI9@E8Gh2ehh9*Wvb1H;jf-03;5cy4A)XZIpRFPX>T{{DY277} zQwEeg)j{PV@W}p+7_(>oPFHn2xHk8uDGJl&83gQbJv7lrIuXL3TWSii3&aU+* zjD$;jpCZpo4yzD$VBjXB;vj<=l1sY|zw=zk0krrxWH(x5H@3SDq&f;Pek=Fbb$^L!Oq>~@iX_(b;4b}UVPb{4BZVUUn17+#hO zffkcBxAA@>L!LpNd@GBzbCf2`%oFq6=&dB9`+NQdLUPA*&cG)ztdNA*_C?;Db?Oz@i|tCal&t{z<8{D6F4vBd`rtzH!&5EodFJL0O{A8_QQj4*7#HDo|D@jF!4i=NGs~Q9-R|03P zd;&C5v+Yir^^2RKq;|n|->{nGZ4u}2`+|5{x(Y+JRMiRT;m^u_x|Y#Hv~QDg6nsNy zliTXj4|Kdsh`c&4KiZ1bS?jSzh&eANi^p!BB^Va_)VD77T3QQxhwOTseig1#5UK!q zo^&t;JI9Kz)t4Gk%)`}m~UeVaRI;G;fjb-v)1>hVnei8!-s@*i`< znkmfUF4@S{cVtS~^Auw&8|Q?*G#kIj^>CCU(hDrAKR zDaz$PmkTwGc<2qFS2xV`%b@VRbI`pRhG@ZYTa$V>h`0nBB@nUuF~N+}@Vm$qocjU9 zC|mK0_E~Q*&%Syj*cankpq5wJ5f|hpFar~xFGi1tu(=ueUB^vPIO$TX(poLCS`SSp z#^ZMJbn3($x%cB!gYwOh4EgjFx#AzGl1e<*ybuq=hM(kZQ`5Cug`Cc&7g%4oW&{KO z=3#NA`m%I(JYOO3Na5{!C8hE9uc?3HIBll-@)UMFbHxvrz!AUr{i{lG(|8yE+SNaE zk;h5pm3)6Mu3erU(Xc>wIGASa)PxCH!aHs@N`GG7a$Jbh1Y%ihQ8tA-2G3IVyujIK zMD1Ny4?qI)vPcsOP6qC&!XLxpD+-_)oV?Bo@wADY^#WbPz!X0z2eP~mCFp=xNd6JM zKXtM;T5s?bEcCn^rnC-!p60#|^G9$sHbrdZ-_^^iS(NAS$o+g}C|2jWyzYW&sAORj zy`sh@S2t|qC;8(1D0He+V#&tBXj)03p`}W9YiBBrL`T?O0yN@jN1zJ?JR1_BUyoQI z93%ye;(01iM}RgDj-)~M5)%T&mN4fqci99{{2-0q5kV4I*%;#gqs%a{Jck*l50jhw zL(85i&FpS*G&)Su$&*ZzTNmG^;;;u^eGqQ_AU(Ao(uk7zI~=~Jw)@3AjkdcGzIAoG zVgcBCxZ5^*Go4vki|@g3Ilu5NI||$1^9gMLBTbJA2^*eMXq0TsL|O#t3U^8~UoDp1 z*j11UBiiJHsWl&gep5!7C!z6KiVCDWyzF4lLP<>U5qiY?^Yk;Sj;Cps={oVZ?ov&-Dr1L(* zqC5F;F%tnp=)@Vv^wj|NS@7~jS`B37qv%U5b7n2Rgv=Pi=+Y`-YfwX7$0HdKOQunk z2C#$3S=6dv{l%i#0$j4@X!iWlfsO*2g0mW{<-X~VOO<7AzHH~5p)JA$ZG+QeflRnsclra3Wy@0)68^QzB|`WYDC-YYMj z3QF${muq}7WF5dRS`Z2gxguf`$SGJGi+RHy8C(?bK1Je)cn@5pJBfEF5Eni~pWC zE4AV|J{D93;3f#HgotR(8ihMO5o1n2*##TJM7japA;Xua&#e`Xb z<=!amyz-u;jOr|}2`Q*em=BZnEi{T2s`V?V;9}rGxMtc;DOkc&^{)V727m~)0(0P~ zJ`vUGBf2;Z-HhO33YddM?`?K^+Yn@`+9`BPZh=PjN+mg>>kX#je ze$I_~^s7eF!{0 zD#0nzvh-VYF%yFksQjv>g)Znm7>&J3?O(bbjRhe(EUgKx)d*YSG2eWMj16XQTMgj> zXoaZ3=gmSta8+aBc|*XtH4hcub37U`AEWi-s%1SD^^4*k)@NI5-H$b{AvtNGXPr1n zZ0nL*dvWgm`DtHa{&RR-L&{938niAtWDnE>g`zVJk&zdB^LRCtnXHT!QNq=sM^C*} z;}ZxztU{3bTmQjrTaJY-CpAeznzthBPZ;o#JR!)MeJRH?aCk8aHEjuCR-48fo5-~3%$hmGx(y=&GC2FBbA*NS6Rr>MY*OSV*2b~$ ziO!({3<{xHYX#BZtJPzuf#jGcvg)98gZEV0ez<~pdtqVe$#=K|%W99s|Be)uFL5<; z_^Kj1a)@hQ9vN2M_YHmfLg{WUSx^}G@RG{bN?%Z&Z4j{Q2_vnCVO(wSp{zyFrZg;G z9y+XgIt=(dQm=vb)ObI5b!3FKVk?BEip|>oVp55^i-qqqs~1RBjw#V9w=R(yPk9 zAD@^@Lcx!is;G?s5T&%cMYuk18BT}p$|~r1H#nJvSHiFf-7Xz6EEa4B%G`RRmqQhX z-1?!4v|LF95TJuc6Kk=mNpWS%#ySS(%RxkXvM&QF4$}x}*FZu}Evg>!v22wSfsJ++=W$wB8df z=R}giW_20WUR~9Y+3d73?i{BUw~bbwoiA}J<0Ekw2fc;?8t)1w+o=7S2?mw^WJ?xJ zw(U%B29b9UgG8L)rtr$J33=0u0F2jerqS)rvV)Dl-^OJC>JEHHgT{`RfxnJqG1ZZG z{k3WSp#~kdZC~yPB&Da_zKACE5{zu3F1Z<; z*m{=ov3|o&YU6AzjY_j2!iA8OZ~-4*NuoQ5l~4YT99yg|jRwr@JL1-<+K%ZXc-NWiRbGO1Px<_IMN-(aa^RfE zO?B_oo6juAc>N=rT|w-Th zVp2E{C-*_9gVY5hM-V$jfAv}|-nU_!7B?VQTdi^yS$h5%g|>PZ82B!4Z4pi!GLk); z5)5+~ee`bHzTefz({;=*V2)80xBTC6-G6vn4tD1MC%F6{2yPkZ|GVOr zf&M>}TL$|7uDLyKf8fxoEftF{eEoYO(Ah3UXzL{#4Yuao6_4yhbg1fs4O2_T}`Th}W;GMcZa*+AG)%pHv>+|;bdOaz_ zN%!{yw@#7E40|Y9#2VDme1kBs~E5DJ0)@?(# z`2`u;2h6YN&VRW=NJWT`ahm=QK|#<*|2RPUKU46p(QAKkl&#TYD|j~XA?v&+PKsVn z`7u#C%`;NM2~g1itt7xALY(J4`+@n&=ccyL*0ry{&QjIh`jWwl%_dOr^=C7d6lO;UU1x)tK;*LN@5jKu zdFIE2!oXHRwV8!@t-#KiM+_ao#&~)7m3JIJeK&90PH>ppPGqV7yF2YlgE`E1<40Fr zOZI1~)vS4*|M zT!8}7Dal`NWsp8*R*Ue39?=9u$+_ve5#T zHW%I#ZF4#sMx#;dfr_h`w&7S2V=+Dok-hDqQNWL~PD6=9!OLVQu{t2qz#*=C(JcN# z`>S;j0#k!GtMy1`rphA`F93{5`+G&4u4F3A?5?It zcaGwzOU=upA<)ABA$OaaS_AC>RsFRbF|M0ATGS9<@1sF-Wpv*5MVQ^5o8GKtK8q0% zNEBF5ia2W>i(6zt(IkORHc}oB%LX~bRSO_z>@o!n8&eNMwkM|r0wHYsyu>v6a}^EJ z#0>Xsz2cPB0<6o-eJvOpaCFhW&4JU_Tdq$-@D5w#cj$L<=5659lhm+5VAB;KQ}$A| zmp{wX7fpE#hl!PrgU?ROGb$n=$Y|oyim*_#8%p7$o}`x18bR$fa!(ojyKY9}$}xGm zU^Pbd(P!e$JPN70y~n z=iue%wAt54>DP?*?;h$oqA2l*hAf0yVi^s)=Qqk8GJ;GXLF^v)20mj?_b(k7kyON5 zHV;*{=Q67;ed&Cm%ZWk(ZPaq9&`0{?1mWNryErMcRt6IUm^zhSUtB+%2u0@=_%Tk1 zDv&OYfV9pf>K;Mm>Sn_cLYNCdhN^JmFC=I2#lUt4SKRIG7-)}R zt&=@?yWA^#6e1(Kuu8BOw#cC$9g4E6)~i}H%IiSuokfYeM#%yTqEq;op*&1jM;O5{ zR*5k0%q#da9Gv~NTbprD<{{Bt^m&qZ-wCW~8;@0J;)bbXw*+PwGY~i9G*^^Zn79g4 zDh^K{;~nCyjSZ(V=z0h8tNGdUo&r&5)x0)O}dS36b0 zmEmeKJd$hw%^0qd%IGPP9T^}v(y%pCYid!EWN=U`Nhb273=zns6DcgU!|42iOI z5R!dX$#d1e5mH{WZ3U(E%a6N zjy53!z3Ke^*hQtDCuPln_7n(CMatu7#UxgK2WM(kVc>VhR@2&Oa`*#lqf{0d#WurA z0YR|BR*_4qle8-OR{27|&|_#*YiwhXy#795$09>&2Jb$$;PiLJorhfF?fuD6ZewBRqo5V=BLups$H#&L9?t5zI*>kb{cc`kvcnpU%;wrRtAT!ib!k&OijBF*Bp6v6hArl9hDfl&4 zH@cQPrwLP9@bFf3p+qt)D`-Yhh>RyRc<*q;tq;35d^?QN`?GxqR7LPEbd@96A;Tyr zk!^Le^$Om;S$6}4EFiX-3Ht`iCZeHB>!#laE~9uA?av!FV{XlN?*+5CBu#bLAi$c` zOxu!`i8#+f{g+W^U>#Hc&uC~L7pKqS*2oXf2{uj+Z^g;Au#?DHq1(7@HruS21gn@w za_bP=EDV1GR;I^KDNNuTZ~(THocB>n@fd+Hg2dpVu3|{oA*uRNtoKBG98Wh`woI|V z^4cVW8S*|U@4Oga#D$S#>Gp72{KPtjp5Ps+^?Hi9dL51-9#8~&7n{t{7JWs zNY@PB#(Exh{-FQlbFH%P@8z|2!R`S6Q083%F0u&bZ4vF8*ND(kaUiohGChL>zdorn zxv81j$-IOkbH$r=2HFzE`1@;uQkE|7^LkTqEp~k`?_M%zmyNkFI$^hpR&TMDv&A1MS z&v}Fw#TrUK=b*c$oxG0L#+3Z}tYN&=MKw61>e@v=h3u=S-$=`#c%D1~z}N)iCrpFsDlY%{a*T5za&Q~>3)@LZN7_Zn1h23Evj|6QHLPIN~5@>R1)0@NUGaiwR+ z5Zb!SS(B>);qZ_*Ov=O^JZRY@m^iU>dxxUIXlOw>3exoBd^RB#(WxukITl$u*q~kr z^X2D`#5(`I28f0G$uJgUoZy{|HRv;hcgg*+6mmGarNNPgO=mCD0|)GrVVdD>4j2Av z=pRh}V#A9Yo{M4j^gt~4{sdP4iI@z&lR}`N))X^RADywY!!>)!?4c`uaImBFlk?G5 ze|l1;LR`gQT7z>sAau;!wjhvabr`B1+uD&fgBjop5MUhOc! zCAKhYLa;?po0GPpNSMvFdVrcg6d1;snc%Xv#-htRn&9&<`oy+3ld=lL?vKVqnA24Pb+qFMN80f8zLr77+~Z+9 zj6K3jJT@v7Tbt&c9ZT`Fb`X7cbabQJt7{b8rm#UmyiOeT1=9Uirh2dSjle!!z{uW+ zu;z3P&|>AV0fCgzsLLmpjv8lJ?rAg{YKHT#VQC_}&+zGgVMT`GN2rqgoyfT;7I7E! zW64;dEcw#}#6lvpE$Nt?VqGRES21H(@ljK}Kro?X*uv7^=i393@L z?v*C*r6bhLv?N^ST%A1C5u>bI*OWb7$y~IC6f6kes+%|yzte9{A8Ji7AUUGAz6{fU zxrOTQ_J&R)GyoE9$H`(-av7lZr6Nr-8tv@F8Fu=&dr(g}jK8{^cUQ5F%WI!wsT(=m z@ZRrnO?MxLy!0N_{`H5}WkVE(oAdInz9RQ}0974PUtR zJh!eE1E)#4qugL~-XM(x4_b6dw*J4Udxz*yqJ`T!wrwXTwrwXTwr$(CZQHhO+qQFJ z{dr&Gy}xlAxBK;KRI6&#u03k6ntQH5|0k48kDNssq0NR1RC;W@3!p6>09zOaI7_D(9+=_G_?4i z3>fY9>?uiSZ(c#zh?HHbaaH5))Z^4vEqKgXm1wg71BYLj4Y`Tu2$13K8A)^i-1Tr^ z5D?ZUs+&BA^y@(SnS#?H%tw$hZ48d@HM|W|R{wW}7!P)?ZUk?tH!+WW%h5z z%tshEWIy%iaA8Ysk}H6yWu|U2r?@>f+kb7`EH*C$(;BYXk!izYG56S&?GT()=Ncs_ ze|?>918*moSGp^*wqbf4cN%N#)3L*cKVSSEQH)F^mBo6dY+HVXi2? zw{-n*z1__uu0qBjRiV>~Q39!6r`@Z^4Z^a(hgVF9u#>$ZS}HvAG{wK8s_Vd<(y$sg z1)q2q3{W9zzTQq;l!bRZ!z&mSZ%wVr^Ty9;6H>^~)qKeW2R)%}A(Ke$fO^RU!aTiE zJATh`hB6XNAmi88PC*Ol$fq3dwk7f7xUaaInmwSF2?;BE@o@R<2sNAm+P2Fb$>#ja z9gi#T=D&a6!P*B@TpEY+tVJorwW1#o-419t?2U5~7d5 z^_U%7-JrvX-M>d5Dt+}!8z`61sxt9pUe2WsJn9NL;B58<|H!`yRpy=SOlIi^~x;pmLc3QQal4rwK>+fk-#Z0@j9 z*iS02#;n~>(b_G)*Md7+BR66N+FBi>L+@JqiMjG``n%O=9pNUmCeMSx)e6mrIOutk z&O;cc<~f!@GGD>^0})37KgGkM@Mub3T+>zpN_B0ZWhhPFmSV+;xKzE>?5OY!YgsNR zQA|!mP{RkiHZTF2iQ!qdQ^CUaECgWbLgpMXM`671wZ6?f>;BkC0{sqdawmpuO9?%q z?t5HJBhLkFP$PyqLIuLP$(qSfJpR3ZKL90bbtC#4Twwq}(L#B7(9yuoZ~7nxnzt34 zcH*^rFS6-uZJ&wGNU?INmyJcn#^GsI8oqbdnZG3r{`q2Yq{9>uOL4(mdO#7%-#$=V zYDVk8F@>JkvzD_giYk3&hx=?tdaq^j4BdF$x`-J3qyMbjRVrT~58|UnHT541&!M$9 zm7;(YuzS%L9LDh2In?QLm-Sdk{O*3Qx4w(}j#|QN)Sz?Zw2h7ZUt3yhJ{#MO`)&6{ z-W3k*UXvQ7 zW*Wz2FPP%rKHcGm{eJ;m76yj@mt!;hZv=1|{%Zi2;XeSl4F45?``cbnw>;uEqT4Y$A^Cj2Zc(ycLaSE&)rE0@gV_beeCr5r+pE;VgXcp==aud5Z37Y6()eJBQ_W^*EfrX z2Q0?*Ax$vybC1>;84TLD5c<s|3}f5Hz155&sJ#!hE>_fZeX41LA&uBwOnMeU zZd5ZQ%~814X#fH@rR~K_S62cu-C~5f>V~YSNf=}Bc$o_E06c}^ypY5n7Y+pTpA`g? z=pv@@OxkyJJ2ajq3zj>qqb)kDu_3*hCcUlK3-i=m@+_)SP>*vXVz&q){fFN{J8fah zl{`p3EcL$4`G=1QsSpC`-Vv4D0y9tGdR*>3kqwR>VoJ+9X|F_8JDA>mGQT;2?RmH+ z9#MZf!=imhfR5^)C&m?w-5@@^#a7}8ZX6CUMlcGQHbZ4P^(rQUv3>Oh&z<-(-sTZc zZ!X*&v9>gLoU`Q`*LJWo%%0AQF+oO^VVp|?9KPg4J%Ow1XH?=2t7-6B;%r0KwSMa% zY9Q=1p>lW}xCglmF9nKQ8B798R`R-O333&G4L`@4?9)1s8O7h2Iz{IKg)p{?%U+X( zKPbVKpzFF83Jo2U>GHU=zovv<>>m3y!G35xvkWY#g^S=Flm_G~A^$cVf(N&L-}n93 z7ybhnk)|P11-Rc$5r2AXat=9@9`k_j)q}@B#}iRR=ELc|+F=cY$7k=xsDQGCgrrS4 zvifllkVB`#%O#8P;8)8$z1j;C`b~_8io_kt#O~@aK-VYM#{g@JSS!V{p=^Cfzkmyo z&Y8{Bii-8Z!Db1${=6c8(i+sSun(yS?WGBdsP!Ad)dAy{=C_n7TOO{QF zOC!@QD;&d;MW;Wts`C~~n7h{pG>EYoEjfLdu`l3t*{wzgQgIZBz2+yKa*zChaZZ?3 z-b^E?^ovWiGKQsT!Oew2Hzun&4(IQBcGn!&WEq}0_DbqacfAa+WayvoeDv6op3#Q< zhO5`UL|hY0(Dl^FaLXQ5qP`(mYVanq&QHuNF~8IwiCwY2+yRG85CZ2{>69LpPyh;i zgXHFGGd$u&{au^L;l4MOWIA<@kg2j67xN%o{EYWRJP_IgFheUevaB`TdB)+T6*#O$ ztGwXKjgLd!XF274>nXvv&LrkEa_6fDUa`UM?(yPrG}QUB9S?My;GK|)E5`c8HEjC6QnSFO z?s0_ORp3;>)r;a6!=MyJ!14sn1iJZ`Mjoa(t&#I%61Ogp;F1!JmavqI&{Y^c$AYc0 zidBT!yWc3mgUTod?QF)j4--=mg@@z%T#CrudRHHiS{m}5;A#dWwVqDMA;t;`R{5v9 z`{fH~U=`r(k_Z7S2SZNijG#*}erwZ7LX`|l4nn&Wy%mNpU1@kLj_qKa(C{&l;9ex% zwI}s#6k^i~*Yx}2=hvx@IjJ#@hHWUCq+l%^JBQ{y@)RY%n5u#o-F7nA$jph_0gj6L%AD=G-(Xm8xPHqjL`ERNCgQ*o04Ii^jmjKds>v%0DG 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∾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�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 diff --git a/docs/sample_chart.doc b/docs/sample_chart.doc deleted file mode 100644 index 631c0554b2..0000000000 --- a/docs/sample_chart.doc +++ /dev/null @@ -1,24 +0,0 @@ -/*! - \page somestatechart Example state diagram - - \startuml SomeState "my state diagram" - scale 600 width - - [*] -> State1 - State1 --> State2 : Succeeded - State1 --> [*] : Aborted - State2 --> State3 : Succeeded - State2 --> [*] : Aborted - state State3 { - state "Accumulate Enough Data\nLong State Name" as long1 - long1 : Just a test - [*] --> long1 - long1 --> long1 : New Data - long1 --> ProcessData : Enough Data - } - State3 --> State3 : Failed - State3 --> [*] : Succeeded / Save Result - State3 --> [*] : Aborted - - \enduml -*/ From 6580b200db70bc175526868b1b632fa19220afc1 Mon Sep 17 00:00:00 2001 From: Mayukha Vadari Date: Mon, 10 Aug 2026 13:10:18 -0400 Subject: [PATCH 06/28] refactor: Replace boost::lexical_cast with existing alternatives (#7991) --- include/xrpl/beast/unit_test/reporter.h | 3 +-- include/xrpl/beast/unit_test/suite.h | 3 +-- src/test/unit_test/multi_runner.cpp | 3 +-- .../rpc/handlers/account/AccountChannels.cpp | 16 +++++----------- src/xrpld/rpc/handlers/account/AccountLines.cpp | 16 +++++----------- src/xrpld/rpc/handlers/account/AccountOffers.cpp | 16 +++++----------- 6 files changed, 18 insertions(+), 39 deletions(-) diff --git a/include/xrpl/beast/unit_test/reporter.h b/include/xrpl/beast/unit_test/reporter.h index 0fe77a7862..cbd1c7e70d 100644 --- a/include/xrpl/beast/unit_test/reporter.h +++ b/include/xrpl/beast/unit_test/reporter.h @@ -8,7 +8,6 @@ #include #include -#include #include #include @@ -188,7 +187,7 @@ Reporter::fmtdur(clock_type::duration const& d) using namespace std::chrono; auto const ms = duration_cast(d); if (ms < seconds{1}) - return boost::lexical_cast(ms.count()) + "ms"; + return std::to_string(ms.count()) + "ms"; std::stringstream ss; ss << std::fixed << std::setprecision(1) << (ms.count() / 1000.) << "s"; return ss.str(); diff --git a/include/xrpl/beast/unit_test/suite.h b/include/xrpl/beast/unit_test/suite.h index e24904a87b..a727e3fc77 100644 --- a/include/xrpl/beast/unit_test/suite.h +++ b/include/xrpl/beast/unit_test/suite.h @@ -7,7 +7,6 @@ #include #include -#include #include #include @@ -30,7 +29,7 @@ makeReason(String const& reason, char const* file, int line) namespace fs = boost::filesystem; s.append(fs::path{file}.filename().string()); s.append("("); - s.append(boost::lexical_cast(line)); + s.append(std::to_string(line)); s.append(")"); return s; } diff --git a/src/test/unit_test/multi_runner.cpp b/src/test/unit_test/multi_runner.cpp index 71208313a4..918fc7c89f 100644 --- a/src/test/unit_test/multi_runner.cpp +++ b/src/test/unit_test/multi_runner.cpp @@ -7,7 +7,6 @@ #include #include #include -#include #include #include @@ -36,7 +35,7 @@ fmtdur(typename clock_type::duration const& d) using namespace std::chrono; auto const ms = duration_cast(d); if (ms < seconds{1}) - return boost::lexical_cast(ms.count()) + "ms"; + return std::to_string(ms.count()) + "ms"; std::stringstream ss; ss << std::fixed << std::setprecision(1) << (ms.count() / 1000.) << "s"; return ss.str(); diff --git a/src/xrpld/rpc/handlers/account/AccountChannels.cpp b/src/xrpld/rpc/handlers/account/AccountChannels.cpp index d50bf1cf07..f2da1e31ee 100644 --- a/src/xrpld/rpc/handlers/account/AccountChannels.cpp +++ b/src/xrpld/rpc/handlers/account/AccountChannels.cpp @@ -3,6 +3,7 @@ #include #include +#include #include #include #include @@ -22,9 +23,6 @@ #include #include -#include -#include - #include #include #include @@ -129,7 +127,7 @@ doAccountChannels(rpc::JsonContext& context) return rpc::expectedFieldError(jss::marker, "string"); // Marker is composed of a comma separated index and start hint. The - // former will be read as hex, and the latter using boost lexical cast. + // former will be read as hex, and the latter as a decimal integer. std::stringstream marker(params[jss::marker].asString()); std::string value; if (!std::getline(marker, value, ',')) @@ -141,14 +139,10 @@ doAccountChannels(rpc::JsonContext& context) if (!std::getline(marker, value, ',')) return rpcError(RpcInvalidParams); - try - { - startHint = boost::lexical_cast(value); - } - catch (boost::bad_lexical_cast&) - { + auto const hint = toUInt64(value); + if (!hint.has_value()) return rpcError(RpcInvalidParams); - } + startHint = *hint; // We then must check if the object pointed to by the marker is actually // owned by the account in the request. diff --git a/src/xrpld/rpc/handlers/account/AccountLines.cpp b/src/xrpld/rpc/handlers/account/AccountLines.cpp index f134c8af92..4a6d22d5d8 100644 --- a/src/xrpld/rpc/handlers/account/AccountLines.cpp +++ b/src/xrpld/rpc/handlers/account/AccountLines.cpp @@ -4,6 +4,7 @@ #include #include +#include #include #include #include @@ -22,9 +23,6 @@ #include #include -#include -#include - #include #include #include @@ -153,7 +151,7 @@ doAccountLines(rpc::JsonContext& context) return rpc::expectedFieldError(jss::marker, "string"); // Marker is composed of a comma separated index and start hint. The - // former will be read as hex, and the latter using boost lexical cast. + // former will be read as hex, and the latter as a decimal integer. std::stringstream marker(params[jss::marker].asString()); std::string value; if (!std::getline(marker, value, ',')) @@ -165,14 +163,10 @@ doAccountLines(rpc::JsonContext& context) if (!std::getline(marker, value, ',')) return rpcError(RpcInvalidParams); - try - { - startHint = boost::lexical_cast(value); - } - catch (boost::bad_lexical_cast&) - { + auto const hint = toUInt64(value); + if (!hint.has_value()) return rpcError(RpcInvalidParams); - } + startHint = *hint; // We then must check if the object pointed to by the marker is actually // owned by the account in the request. diff --git a/src/xrpld/rpc/handlers/account/AccountOffers.cpp b/src/xrpld/rpc/handlers/account/AccountOffers.cpp index 1467b14b48..a7933f65a7 100644 --- a/src/xrpld/rpc/handlers/account/AccountOffers.cpp +++ b/src/xrpld/rpc/handlers/account/AccountOffers.cpp @@ -3,6 +3,7 @@ #include #include +#include #include #include #include @@ -20,9 +21,6 @@ #include #include -#include -#include - #include #include #include @@ -97,7 +95,7 @@ doAccountOffers(rpc::JsonContext& context) return rpc::expectedFieldError(jss::marker, "string"); // Marker is composed of a comma separated index and start hint. The - // former will be read as hex, and the latter using boost lexical cast. + // former will be read as hex, and the latter as a decimal integer. std::stringstream marker(params[jss::marker].asString()); std::string value; if (!std::getline(marker, value, ',')) @@ -109,14 +107,10 @@ doAccountOffers(rpc::JsonContext& context) if (!std::getline(marker, value, ',')) return rpc::invalidFieldError(jss::marker); - try - { - startHint = boost::lexical_cast(value); - } - catch (boost::bad_lexical_cast&) - { + auto const hint = toUInt64(value); + if (!hint.has_value()) return rpc::invalidFieldError(jss::marker); - } + startHint = *hint; // We then must check if the object pointed to by the marker is actually // owned by the account in the request. From 4f8819565a8b42e536c5b6d92d299e1c21cd70af Mon Sep 17 00:00:00 2001 From: Mayukha Vadari Date: Mon, 10 Aug 2026 13:18:22 -0400 Subject: [PATCH 07/28] fix: Assorted cleanup fixes (#7988) --- include/xrpl/basics/Number.h | 2 +- include/xrpl/protocol/AMMCore.h | 2 +- include/xrpl/protocol/TxFlags.h | 3 +- include/xrpl/protocol/detail/sfields.macro | 50 ++--- include/xrpl/tx/invariants/InvariantCheck.h | 4 +- src/libxrpl/protocol/InnerObjectFormats.cpp | 6 +- src/test/app/lending/LoanBroker_test.cpp | 2 +- src/test/protocol/Hooks_test.cpp | 189 ------------------ .../rpc/handlers/account/AccountInfo.cpp | 24 +-- 9 files changed, 33 insertions(+), 249 deletions(-) delete mode 100644 src/test/protocol/Hooks_test.cpp diff --git a/include/xrpl/basics/Number.h b/include/xrpl/basics/Number.h index f90800c715..ec75724b5d 100644 --- a/include/xrpl/basics/Number.h +++ b/include/xrpl/basics/Number.h @@ -304,7 +304,7 @@ concept Integral64 = std::is_same_v || std::is_same_v // IWYU pragma: keep - -#include -#include -#include -#include -#include -#include - -#include -#include -#include - -namespace xrpl { - -class Hooks_test : public beast::unit_test::Suite -{ - /** - * This unit test was requested here: - * https://github.com/XRPLF/rippled/pull/4089#issuecomment-1050274539 - * These are tests that exercise facilities that are reserved for when Hooks - * is merged in the future. - **/ - - void - testHookFields() - { - testcase("Test Hooks fields"); - - using namespace test::jtx; - - std::vector> const fieldsToTest = { - sfHookResult, - sfHookStateChangeCount, - sfHookEmitCount, - sfHookExecutionIndex, - sfHookApiVersion, - sfHookStateCount, - sfEmitGeneration, - sfHookOn, - sfHookInstructionCount, - sfEmitBurden, - sfHookReturnCode, - sfReferenceCount, - sfEmitParentTxnID, - sfEmitNonce, - sfEmitHookHash, - sfHookStateKey, - sfHookHash, - sfHookNamespace, - sfHookSetTxnID, - sfHookStateData, - sfHookReturnString, - sfHookParameterName, - sfHookParameterValue, - sfEmitCallback, - sfHookAccount, - sfEmittedTxn, - sfHook, - sfHookDefinition, - sfHookParameter, - sfHookGrant, - sfEmitDetails, - sfHookExecutions, - sfHookExecution, - sfHookParameters, - sfHooks, - sfHookGrants}; - - for (auto const& rf : fieldsToTest) - { - SField const& f = rf.get(); - - STObject dummy{sfGeneric}; - - BEAST_EXPECT(!dummy.isFieldPresent(f)); - - switch (f.fieldType) - { - case STI_UINT8: { - dummy.setFieldU8(f, 0); - BEAST_EXPECT(dummy.getFieldU8(f) == 0); - - dummy.setFieldU8(f, 255); - BEAST_EXPECT(dummy.getFieldU8(f) == 255); - - BEAST_EXPECT(dummy.isFieldPresent(f)); - break; - } - - case STI_UINT16: { - dummy.setFieldU16(f, 0); - BEAST_EXPECT(dummy.getFieldU16(f) == 0); - - dummy.setFieldU16(f, 0xFFFFU); - BEAST_EXPECT(dummy.getFieldU16(f) == 0xFFFFU); - - BEAST_EXPECT(dummy.isFieldPresent(f)); - break; - } - - case STI_UINT32: { - dummy.setFieldU32(f, 0); - BEAST_EXPECT(dummy.getFieldU32(f) == 0); - - dummy.setFieldU32(f, 0xFFFFFFFFU); - BEAST_EXPECT(dummy.getFieldU32(f) == 0xFFFFFFFFU); - - BEAST_EXPECT(dummy.isFieldPresent(f)); - break; - } - - case STI_UINT64: { - dummy.setFieldU64(f, 0); - BEAST_EXPECT(dummy.getFieldU64(f) == 0); - - dummy.setFieldU64(f, 0xFFFFFFFFFFFFFFFFU); - BEAST_EXPECT(dummy.getFieldU64(f) == 0xFFFFFFFFFFFFFFFFU); - - BEAST_EXPECT(dummy.isFieldPresent(f)); - break; - } - - case STI_UINT256: { - uint256 const u = uint256::fromVoid( - "DEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBE" - "EFDEADBEEF"); - dummy.setFieldH256(f, u); - BEAST_EXPECT(dummy.getFieldH256(f) == u); - BEAST_EXPECT(dummy.isFieldPresent(f)); - break; - } - - case STI_VL: { - std::vector const v{1, 2, 3}; - dummy.setFieldVL(f, v); - BEAST_EXPECT(dummy.getFieldVL(f) == v); - BEAST_EXPECT(dummy.isFieldPresent(f)); - break; - } - - case STI_ACCOUNT: { - // NOLINTBEGIN(bugprone-unchecked-optional-access) - AccountID const id = - *parseBase58("rwfSjJNK2YQuN64bSWn7T2eY9FJAyAPYJT"); - // NOLINTEND(bugprone-unchecked-optional-access) - dummy.setAccountID(f, id); - BEAST_EXPECT(dummy.getAccountID(f) == id); - BEAST_EXPECT(dummy.isFieldPresent(f)); - break; - } - - case STI_OBJECT: { - dummy.emplaceBack(STObject{f}); - BEAST_EXPECT(dummy.getField(f).getFName() == f); - BEAST_EXPECT(dummy.isFieldPresent(f)); - break; - } - - case STI_ARRAY: { - STArray dummy2{f, 2}; - dummy2.pushBack(STObject{sfGeneric}); - dummy2.pushBack(STObject{sfGeneric}); - dummy.setFieldArray(f, dummy2); - BEAST_EXPECT(dummy.getFieldArray(f) == dummy2); - BEAST_EXPECT(dummy.isFieldPresent(f)); - break; - } - - default: - BEAST_EXPECT(false); - } - } - } - -public: - void - run() override - { - using namespace test::jtx; - testHookFields(); - } -}; - -BEAST_DEFINE_TESTSUITE(Hooks, protocol, xrpl); - -} // namespace xrpl diff --git a/src/xrpld/rpc/handlers/account/AccountInfo.cpp b/src/xrpld/rpc/handlers/account/AccountInfo.cpp index d4232cf451..f131af01e5 100644 --- a/src/xrpld/rpc/handlers/account/AccountInfo.cpp +++ b/src/xrpld/rpc/handlers/account/AccountInfo.cpp @@ -51,22 +51,16 @@ void injectSLE(json::Value& jv, SLE const& sle) { jv = sle.getJson(JsonOptions::Values::None); - if (sle.getType() == ltACCOUNT_ROOT) + XRPL_ASSERT(sle.getType() == ltACCOUNT_ROOT, "xrpl::injectSLE : sle is account root"); + if (sle.isFieldPresent(sfEmailHash)) { - if (sle.isFieldPresent(sfEmailHash)) - { - auto const& hash = sle.getFieldH128(sfEmailHash); - Blob const b(hash.begin(), hash.end()); - std::string md5 = strHex(makeSlice(b)); - boost::to_lower(md5); - // VFALCO TODO Give a name to this constant and move it - // to a more visible location. - jv[jss::urlgravatar] = str(boost::format("https://www.gravatar.com/avatar/%s") % md5); - } - } - else - { - jv[jss::Invalid] = true; + auto const& hash = sle.getFieldH128(sfEmailHash); + Blob const b(hash.begin(), hash.end()); + std::string md5 = strHex(makeSlice(b)); + boost::to_lower(md5); + // VFALCO TODO Give a name to this constant and move it + // to a more visible location. + jv[jss::urlgravatar] = str(boost::format("https://www.gravatar.com/avatar/%s") % md5); } } From 07aa97fda4aae2f999708a8a39f3a7d62c07221e Mon Sep 17 00:00:00 2001 From: Mayukha Vadari Date: Mon, 10 Aug 2026 13:22:40 -0400 Subject: [PATCH 08/28] test: Use std::string::starts_with/ends_with instead of Boost (#7992) --- src/test/core/SociDB_test.cpp | 3 +-- src/test/jtx/TrustedPublisherServer.h | 33 ++++++++++++--------------- src/test/rpc/NoRippleCheck_test.cpp | 10 ++++---- src/test/server/ServerStatus_test.cpp | 13 +++++------ 4 files changed, 26 insertions(+), 33 deletions(-) diff --git a/src/test/core/SociDB_test.cpp b/src/test/core/SociDB_test.cpp index 373ec66cd1..7a57641b64 100644 --- a/src/test/core/SociDB_test.cpp +++ b/src/test/core/SociDB_test.cpp @@ -6,7 +6,6 @@ #include #include -#include #include #include #include // IWYU pragma: keep @@ -108,7 +107,7 @@ public: for (auto const& i : d) { DBConfig const sc(c, i.first); - BEAST_EXPECT(boost::ends_with(sc.connectionString(), i.first + i.second)); + BEAST_EXPECT(sc.connectionString().ends_with(i.first + i.second)); } } void diff --git a/src/test/jtx/TrustedPublisherServer.h b/src/test/jtx/TrustedPublisherServer.h index f5ee8aac3a..941af374ef 100644 --- a/src/test/jtx/TrustedPublisherServer.h +++ b/src/test/jtx/TrustedPublisherServer.h @@ -16,7 +16,6 @@ #include #include -#include #include #include #include @@ -549,7 +548,7 @@ private: res.keep_alive(req.keep_alive()); bool prepare = true; - if (boost::starts_with(path, "/validators2")) + if (path.starts_with("/validators2")) { res.result(http::status::ok); res.insert("Content-Type", "application/json"); @@ -565,7 +564,7 @@ private: { int refresh = 5; static constexpr char const* kRefreshPrefix = "/validators2/refresh/"; - if (boost::starts_with(path, kRefreshPrefix)) + if (path.starts_with(kRefreshPrefix)) { refresh = boost::lexical_cast( path.substr(strlen(kRefreshPrefix))); @@ -573,7 +572,7 @@ private: res.body() = getList2_(refresh); } } - else if (boost::starts_with(path, "/validators")) + else if (path.starts_with("/validators")) { res.result(http::status::ok); res.insert("Content-Type", "application/json"); @@ -589,7 +588,7 @@ private: { int refresh = 5; static constexpr char const* kRefreshPrefix = "/validators/refresh/"; - if (boost::starts_with(path, kRefreshPrefix)) + if (path.starts_with(kRefreshPrefix)) { refresh = boost::lexical_cast( path.substr(strlen(kRefreshPrefix))); @@ -597,13 +596,13 @@ private: res.body() = getList_(refresh); } } - else if (boost::starts_with(path, "/textfile")) + else if (path.starts_with("/textfile")) { prepare = false; res.result(http::status::ok); res.insert("Content-Type", "text/example"); // if huge was requested, lie about content length - std::uint64_t const cl = boost::starts_with(path, "/textfile/huge") + std::uint64_t const cl = path.starts_with("/textfile/huge") ? std::numeric_limits::max() : 1024; res.content_length(cl); @@ -617,41 +616,39 @@ private: } } } - else if (boost::starts_with(path, "/sleep/")) + else if (path.starts_with("/sleep/")) { auto const sleepSec = boost::lexical_cast(path.substr(7)); std::this_thread::sleep_for(std::chrono::seconds(sleepSec)); } - else if (boost::starts_with(path, "/redirect")) + else if (path.starts_with("/redirect")) { - if (boost::ends_with(path, "/301")) + if (path.ends_with("/301")) { res.result(http::status::moved_permanently); } - else if (boost::ends_with(path, "/302")) + else if (path.ends_with("/302")) { res.result(http::status::found); } - else if (boost::ends_with(path, "/307")) + else if (path.ends_with("/307")) { res.result(http::status::temporary_redirect); } - else if (boost::ends_with(path, "/308")) + else if (path.ends_with("/308")) { res.result(http::status::permanent_redirect); } std::stringstream location; - if (boost::starts_with(path, "/redirect_to/")) + if (path.starts_with("/redirect_to/")) { location << path.substr(13); } - else if (!boost::starts_with(path, "/redirect_nolo")) + else if (!path.starts_with("/redirect_nolo")) { location << (ssl ? "https://" : "http://") << localEndpoint() - << (boost::starts_with(path, "/redirect_forever/") - ? path - : "/validators"); + << (path.starts_with("/redirect_forever/") ? path : "/validators"); } if (!location.str().empty()) res.insert("Location", location.str()); diff --git a/src/test/rpc/NoRippleCheck_test.cpp b/src/test/rpc/NoRippleCheck_test.cpp index 6e30f944c7..8e719e6407 100644 --- a/src/test/rpc/NoRippleCheck_test.cpp +++ b/src/test/rpc/NoRippleCheck_test.cpp @@ -27,8 +27,6 @@ #include #include -#include - #include #include @@ -203,13 +201,13 @@ class NoRippleCheck_test : public beast::unit_test::Suite if (user) { - BEAST_EXPECT(boost::starts_with(pa[0u].asString(), "You appear to have set")); - BEAST_EXPECT(boost::starts_with(pa[1u].asString(), "You should probably set")); + BEAST_EXPECT(pa[0u].asString().starts_with("You appear to have set")); + BEAST_EXPECT(pa[1u].asString().starts_with("You should probably set")); } else { - BEAST_EXPECT(boost::starts_with(pa[0u].asString(), "You should immediately set")); - BEAST_EXPECT(boost::starts_with(pa[1u].asString(), "You should clear")); + BEAST_EXPECT(pa[0u].asString().starts_with("You should immediately set")); + BEAST_EXPECT(pa[1u].asString().starts_with("You should clear")); } } else diff --git a/src/test/server/ServerStatus_test.cpp b/src/test/server/ServerStatus_test.cpp index 5adf6a08f5..f1989ed171 100644 --- a/src/test/server/ServerStatus_test.cpp +++ b/src/test/server/ServerStatus_test.cpp @@ -56,8 +56,7 @@ class ServerStatus_test : public beast::unit_test::Suite, public beast::test::En static auto makeConfig(std::string const& proto, bool admin = true, bool credentials = false) { - auto const sectionName = - boost::starts_with(proto, "h") ? Sections::kPortRpc : Sections::kPortWs; + auto const sectionName = proto.starts_with("h") ? Sections::kPortRpc : Sections::kPortWs; auto p = jtx::envconfig(); p->overwrite(sectionName, Keys::kProtocol, proto); @@ -71,9 +70,9 @@ class ServerStatus_test : public beast::unit_test::Suite, public beast::test::En } p->overwrite( - boost::starts_with(proto, "h") ? Sections::kPortWs : Sections::kPortRpc, + proto.starts_with("h") ? Sections::kPortWs : Sections::kPortRpc, Keys::kProtocol, - boost::starts_with(proto, "h") ? "ws" : "http"); + proto.starts_with("h") ? "ws" : "http"); if (proto == "https") { @@ -261,7 +260,7 @@ class ServerStatus_test : public beast::unit_test::Suite, public beast::test::En } } - if (boost::starts_with(proto, "h")) + if (proto.starts_with("h")) { auto jrc = makeJSONRPCClient(env.app().config()); jrr = jrc->invoke("ledger_accept", jp); @@ -289,7 +288,7 @@ class ServerStatus_test : public beast::unit_test::Suite, public beast::test::En Env env{*this, makeConfig(proto, admin, credentials)}; json::Value jrr; - auto const protoWs = boost::starts_with(proto, "w"); + auto const protoWs = proto.starts_with("w"); // the set of checks we do are different depending // on how the admin config options are set @@ -485,7 +484,7 @@ class ServerStatus_test : public beast::unit_test::Suite, public beast::test::En boost::beast::http::response resp; boost::system::error_code ec; - if (boost::starts_with(clientProtocol, "h")) + if (clientProtocol.starts_with("h")) { doHTTPRequest(env, yield, clientProtocol == "https", resp, ec); BEAST_EXPECT(ec); From a0e1e578a0a3977f4466d0ff274613088022cc29 Mon Sep 17 00:00:00 2001 From: Mayukha Vadari Date: Mon, 10 Aug 2026 13:23:02 -0400 Subject: [PATCH 09/28] refactor: Remove operator!= overloads that C++20 synthesizes (#7994) --- include/xrpl/basics/Buffer.h | 6 --- include/xrpl/basics/IntrusivePointer.h | 3 -- include/xrpl/basics/IntrusivePointer.ipp | 7 --- include/xrpl/basics/Number.h | 6 --- include/xrpl/basics/SHAMapHash.h | 6 --- include/xrpl/basics/Slice.h | 6 --- .../xrpl/basics/partitioned_unordered_map.h | 12 ----- .../container/detail/aged_ordered_container.h | 19 -------- .../detail/aged_unordered_container.h | 22 ---------- include/xrpl/beast/core/List.h | 7 --- include/xrpl/beast/net/IPEndpoint.h | 6 --- include/xrpl/beast/rfc2616.h | 6 --- include/xrpl/conditions/Condition.h | 6 --- include/xrpl/conditions/Fulfillment.h | 6 --- include/xrpl/json/json_value.h | 30 ------------- include/xrpl/ledger/BookDirs.h | 6 --- include/xrpl/ledger/CanonicalTXSet.h | 6 --- include/xrpl/ledger/Dir.h | 6 --- include/xrpl/ledger/detail/ReadViewFwdRange.h | 3 -- .../xrpl/ledger/detail/ReadViewFwdRange.ipp | 7 --- include/xrpl/protocol/Quality.h | 13 ------ include/xrpl/protocol/Rules.h | 3 -- include/xrpl/protocol/STAmount.h | 6 --- include/xrpl/protocol/STArray.h | 9 ---- include/xrpl/protocol/STBase.h | 2 - include/xrpl/protocol/STCurrency.h | 6 --- include/xrpl/protocol/STObject.h | 38 ---------------- include/xrpl/protocol/STPathSet.h | 9 ---- include/xrpl/protocol/SeqProxy.h | 6 --- include/xrpl/protocol/Serializer.h | 10 ----- include/xrpl/protocol/Units.h | 7 --- include/xrpl/protocol/detail/STVar.h | 6 --- include/xrpl/server/Manifest.h | 6 --- include/xrpl/shamap/SHAMap.h | 6 --- include/xrpl/shamap/SHAMapNodeID.h | 44 +++++++------------ include/xrpl/tx/paths/detail/Steps.h | 13 ------ src/libxrpl/protocol/Rules.cpp | 6 --- src/libxrpl/protocol/STBase.cpp | 6 --- src/test/jtx/amount.h | 6 --- 39 files changed, 16 insertions(+), 362 deletions(-) diff --git a/include/xrpl/basics/Buffer.h b/include/xrpl/basics/Buffer.h index 05af6c409a..705a5ef51a 100644 --- a/include/xrpl/basics/Buffer.h +++ b/include/xrpl/basics/Buffer.h @@ -226,10 +226,4 @@ operator==(Buffer const& lhs, Buffer const& rhs) noexcept return std::memcmp(lhs.data(), rhs.data(), lhs.size()) == 0; } -inline bool -operator!=(Buffer const& lhs, Buffer const& rhs) noexcept -{ - return !(lhs == rhs); -} - } // namespace xrpl diff --git a/include/xrpl/basics/IntrusivePointer.h b/include/xrpl/basics/IntrusivePointer.h index 59853ad4d0..b978016860 100644 --- a/include/xrpl/basics/IntrusivePointer.h +++ b/include/xrpl/basics/IntrusivePointer.h @@ -96,9 +96,6 @@ public: SharedIntrusive& operator=(SharedIntrusive const& rhs); - bool - operator!=(std::nullptr_t) const; - bool operator==(std::nullptr_t) const; diff --git a/include/xrpl/basics/IntrusivePointer.ipp b/include/xrpl/basics/IntrusivePointer.ipp index 67d43b05d6..6c2a71f7eb 100644 --- a/include/xrpl/basics/IntrusivePointer.ipp +++ b/include/xrpl/basics/IntrusivePointer.ipp @@ -111,13 +111,6 @@ SharedIntrusive::operator=(SharedIntrusive&& rhs) return *this; } -template -bool -SharedIntrusive::operator!=(std::nullptr_t) const -{ - return this->get() != nullptr; -} - template bool SharedIntrusive::operator==(std::nullptr_t) const diff --git a/include/xrpl/basics/Number.h b/include/xrpl/basics/Number.h index ec75724b5d..f6ce0b300d 100644 --- a/include/xrpl/basics/Number.h +++ b/include/xrpl/basics/Number.h @@ -449,12 +449,6 @@ public: x.exponent_ == y.exponent_; } - friend constexpr bool - operator!=(Number const& x, Number const& y) noexcept - { - return !(x == y); - } - friend constexpr bool operator<(Number const& l, Number const& r) noexcept { diff --git a/include/xrpl/basics/SHAMapHash.h b/include/xrpl/basics/SHAMapHash.h index 3c3d525022..1902a3b2ec 100644 --- a/include/xrpl/basics/SHAMapHash.h +++ b/include/xrpl/basics/SHAMapHash.h @@ -85,12 +85,6 @@ public: } }; -inline bool -operator!=(SHAMapHash const& x, SHAMapHash const& y) -{ - return !(x == y); -} - template <> inline std::size_t extract(SHAMapHash const& key) diff --git a/include/xrpl/basics/Slice.h b/include/xrpl/basics/Slice.h index 75c9b8c7bd..92b777ab98 100644 --- a/include/xrpl/basics/Slice.h +++ b/include/xrpl/basics/Slice.h @@ -208,12 +208,6 @@ operator==(Slice const& lhs, Slice const& rhs) noexcept return std::memcmp(lhs.data(), rhs.data(), lhs.size()) == 0; } -inline bool -operator!=(Slice const& lhs, Slice const& rhs) noexcept -{ - return !(lhs == rhs); -} - inline bool operator<(Slice const& lhs, Slice const& rhs) noexcept { diff --git a/include/xrpl/basics/partitioned_unordered_map.h b/include/xrpl/basics/partitioned_unordered_map.h index e78043e252..c6b0107b93 100644 --- a/include/xrpl/basics/partitioned_unordered_map.h +++ b/include/xrpl/basics/partitioned_unordered_map.h @@ -116,12 +116,6 @@ public: { return lhs.map == rhs.map && lhs.ait == rhs.ait && lhs.mit == rhs.mit; } - - friend bool - operator!=(Iterator const& lhs, Iterator const& rhs) - { - return !(lhs == rhs); - } }; struct ConstIterator @@ -189,12 +183,6 @@ public: { return lhs.map == rhs.map && lhs.ait == rhs.ait && lhs.mit == rhs.mit; } - - friend bool - operator!=(ConstIterator const& lhs, ConstIterator const& rhs) - { - return !(lhs == rhs); - } }; private: diff --git a/include/xrpl/beast/container/detail/aged_ordered_container.h b/include/xrpl/beast/container/detail/aged_ordered_container.h index 5b60ef7e6d..9dd83d466b 100644 --- a/include/xrpl/beast/container/detail/aged_ordered_container.h +++ b/include/xrpl/beast/container/detail/aged_ordered_container.h @@ -1038,25 +1038,6 @@ public: Compare, OtherAllocator> const& other) const; - template < - bool OtherIsMulti, - bool OtherIsMap, - class OtherT, - class OtherDuration, - class OtherAllocator> - bool - operator!=(AgedOrderedContainer< - OtherIsMulti, - OtherIsMap, - Key, - OtherT, - OtherDuration, - Compare, - OtherAllocator> const& other) const - { - return !(this->operator==(other)); - } - template < bool OtherIsMulti, bool OtherIsMap, diff --git a/include/xrpl/beast/container/detail/aged_unordered_container.h b/include/xrpl/beast/container/detail/aged_unordered_container.h index c4287b1ca1..ea271feed0 100644 --- a/include/xrpl/beast/container/detail/aged_unordered_container.h +++ b/include/xrpl/beast/container/detail/aged_unordered_container.h @@ -1340,28 +1340,6 @@ public: OtherAllocator> const& other) const requires MaybeMulti; - template < - bool OtherIsMulti, - bool OtherIsMap, - class OtherKey, - class OtherT, - class OtherDuration, - class OtherHash, - class OtherAllocator> - bool - operator!=(AgedUnorderedContainer< - OtherIsMulti, - OtherIsMap, - OtherKey, - OtherT, - OtherDuration, - OtherHash, - KeyEqual, - OtherAllocator> const& other) const - { - return !(this->operator==(other)); - } - private: bool wouldExceed(size_type additional) const diff --git a/include/xrpl/beast/core/List.h b/include/xrpl/beast/core/List.h index b9b6829d31..076ac3028b 100644 --- a/include/xrpl/beast/core/List.h +++ b/include/xrpl/beast/core/List.h @@ -82,13 +82,6 @@ public: return node_ == other.node_; } - template - bool - operator!=(ListIterator const& other) const noexcept - { - return !((*this) == other); - } - reference operator*() const noexcept { diff --git a/include/xrpl/beast/net/IPEndpoint.h b/include/xrpl/beast/net/IPEndpoint.h index d4d3b2ab12..a5fb5b4318 100644 --- a/include/xrpl/beast/net/IPEndpoint.h +++ b/include/xrpl/beast/net/IPEndpoint.h @@ -110,12 +110,6 @@ public: operator==(Endpoint const& lhs, Endpoint const& rhs); friend bool operator<(Endpoint const& lhs, Endpoint const& rhs); - - friend bool - operator!=(Endpoint const& lhs, Endpoint const& rhs) - { - return !(lhs == rhs); - } friend bool operator>(Endpoint const& lhs, Endpoint const& rhs) { diff --git a/include/xrpl/beast/rfc2616.h b/include/xrpl/beast/rfc2616.h index 1986568553..0e061845fb 100644 --- a/include/xrpl/beast/rfc2616.h +++ b/include/xrpl/beast/rfc2616.h @@ -229,12 +229,6 @@ public: return other.it_ == it_ && other.end_ == end_ && other.value_.size() == value_.size(); } - bool - operator!=(ListIterator const& other) const - { - return !(*this == other); - } - reference operator*() const { diff --git a/include/xrpl/conditions/Condition.h b/include/xrpl/conditions/Condition.h index 365a41a087..04e571a028 100644 --- a/include/xrpl/conditions/Condition.h +++ b/include/xrpl/conditions/Condition.h @@ -92,10 +92,4 @@ operator==(Condition const& lhs, Condition const& rhs) lhs.fingerprint == rhs.fingerprint; } -inline bool -operator!=(Condition const& lhs, Condition const& rhs) -{ - return !(lhs == rhs); -} - } // namespace xrpl::cryptoconditions diff --git a/include/xrpl/conditions/Fulfillment.h b/include/xrpl/conditions/Fulfillment.h index 11f3165a58..6fd75aa5a3 100644 --- a/include/xrpl/conditions/Fulfillment.h +++ b/include/xrpl/conditions/Fulfillment.h @@ -93,12 +93,6 @@ operator==(Fulfillment const& lhs, Fulfillment const& rhs) lhs.fingerprint() == rhs.fingerprint(); } -inline bool -operator!=(Fulfillment const& lhs, Fulfillment const& rhs) -{ - return !(lhs == rhs); -} - /** * Determine whether the given fulfillment and condition match */ diff --git a/include/xrpl/json/json_value.h b/include/xrpl/json/json_value.h index 260917face..be126d8b8e 100644 --- a/include/xrpl/json/json_value.h +++ b/include/xrpl/json/json_value.h @@ -72,36 +72,18 @@ operator==(StaticString x, StaticString y) return strcmp(x.cStr(), y.cStr()) == 0; } -inline bool -operator!=(StaticString x, StaticString y) -{ - return !(x == y); -} - inline bool operator==(std::string const& x, StaticString y) { return strcmp(x.c_str(), y.cStr()) == 0; } -inline bool -operator!=(std::string const& x, StaticString y) -{ - return !(x == y); -} - inline bool operator==(StaticString x, std::string const& y) { return y == x; } -inline bool -operator!=(StaticString x, std::string const& y) -{ - return !(y == x); -} - /** * @brief Represents a JSON value. * @@ -489,12 +471,6 @@ toJson(xrpl::Number const& number) bool operator==(Value const&, Value const&); -inline bool -operator!=(Value const& x, Value const& y) -{ - return !(x == y); -} - bool operator<(Value const&, Value const&); @@ -562,12 +538,6 @@ public: return isEqual(other); } - bool - operator!=(SelfType const& other) const - { - return !isEqual(other); - } - /** * Return either the index or the member name of the referenced value as a * Value. diff --git a/include/xrpl/ledger/BookDirs.h b/include/xrpl/ledger/BookDirs.h index dc4361136d..b9aa87ae52 100644 --- a/include/xrpl/ledger/BookDirs.h +++ b/include/xrpl/ledger/BookDirs.h @@ -49,12 +49,6 @@ public: bool operator==(const_iterator const& other) const; - bool - operator!=(const_iterator const& other) const - { - return !(*this == other); - } - reference operator*() const; diff --git a/include/xrpl/ledger/CanonicalTXSet.h b/include/xrpl/ledger/CanonicalTXSet.h index 11aadf4e92..3fe17d6eef 100644 --- a/include/xrpl/ledger/CanonicalTXSet.h +++ b/include/xrpl/ledger/CanonicalTXSet.h @@ -59,12 +59,6 @@ private: return lhs.txId_ == rhs.txId_; } - friend bool - operator!=(Key const& lhs, Key const& rhs) - { - return !(lhs == rhs); - } - [[nodiscard]] uint256 const& getAccount() const { diff --git a/include/xrpl/ledger/Dir.h b/include/xrpl/ledger/Dir.h index 233719cdeb..eb70b3b6a3 100644 --- a/include/xrpl/ledger/Dir.h +++ b/include/xrpl/ledger/Dir.h @@ -59,12 +59,6 @@ public: bool operator==(ConstIterator const& other) const; - bool - operator!=(ConstIterator const& other) const - { - return !(*this == other); - } - reference operator*() const; diff --git a/include/xrpl/ledger/detail/ReadViewFwdRange.h b/include/xrpl/ledger/detail/ReadViewFwdRange.h index 19ac0698c2..bfa2527bbd 100644 --- a/include/xrpl/ledger/detail/ReadViewFwdRange.h +++ b/include/xrpl/ledger/detail/ReadViewFwdRange.h @@ -85,9 +85,6 @@ public: bool operator==(Iterator const& other) const; - bool - operator!=(Iterator const& other) const; - // Can throw reference operator*() const; diff --git a/include/xrpl/ledger/detail/ReadViewFwdRange.ipp b/include/xrpl/ledger/detail/ReadViewFwdRange.ipp index c7cbc5ee61..2003280ea6 100644 --- a/include/xrpl/ledger/detail/ReadViewFwdRange.ipp +++ b/include/xrpl/ledger/detail/ReadViewFwdRange.ipp @@ -64,13 +64,6 @@ ReadViewFwdRange::Iterator::operator==(Iterator const& other) const return impl_ == other.impl_; } -template -bool -ReadViewFwdRange::Iterator::operator!=(Iterator const& other) const -{ - return !(*this == other); -} - template auto ReadViewFwdRange::Iterator::operator*() const -> reference diff --git a/include/xrpl/protocol/Quality.h b/include/xrpl/protocol/Quality.h index 3475efa977..d0d0f10cd2 100644 --- a/include/xrpl/protocol/Quality.h +++ b/include/xrpl/protocol/Quality.h @@ -75,13 +75,6 @@ operator==(TAmounts const& lhs, TAmounts const& rhs) noexcept return lhs.in == rhs.in && lhs.out == rhs.out; } -template -bool -operator!=(TAmounts const& lhs, TAmounts const& rhs) noexcept -{ - return !(lhs == rhs); -} - //------------------------------------------------------------------------------ // XRPL specific constant used for parsing qualities and other things @@ -271,12 +264,6 @@ public: return lhs.value_ == rhs.value_; } - friend bool - operator!=(Quality const& lhs, Quality const& rhs) noexcept - { - return !(lhs == rhs); - } - friend std::ostream& operator<<(std::ostream& os, Quality const& quality) { diff --git a/include/xrpl/protocol/Rules.h b/include/xrpl/protocol/Rules.h index 2c2136b6e8..d67e0d8654 100644 --- a/include/xrpl/protocol/Rules.h +++ b/include/xrpl/protocol/Rules.h @@ -98,9 +98,6 @@ public: */ bool operator==(Rules const&) const; - - bool - operator!=(Rules const& other) const; }; std::optional const& diff --git a/include/xrpl/protocol/STAmount.h b/include/xrpl/protocol/STAmount.h index cc80481582..4b2f1cc9fb 100644 --- a/include/xrpl/protocol/STAmount.h +++ b/include/xrpl/protocol/STAmount.h @@ -642,12 +642,6 @@ operator==(STAmount const& lhs, STAmount const& rhs); bool operator<(STAmount const& lhs, STAmount const& rhs); -inline bool -operator!=(STAmount const& lhs, STAmount const& rhs) -{ - return !(lhs == rhs); -} - inline bool operator>(STAmount const& lhs, STAmount const& rhs) { diff --git a/include/xrpl/protocol/STArray.h b/include/xrpl/protocol/STArray.h index 573bb6dad8..e88563fb1a 100644 --- a/include/xrpl/protocol/STArray.h +++ b/include/xrpl/protocol/STArray.h @@ -133,9 +133,6 @@ public: bool operator==(STArray const& s) const; - bool - operator!=(STArray const& s) const; - iterator erase(iterator pos); @@ -283,12 +280,6 @@ STArray::operator==(STArray const& s) const return v_ == s.v_; } -inline bool -STArray::operator!=(STArray const& s) const -{ - return v_ != s.v_; -} - inline STArray::iterator STArray::erase(iterator pos) { diff --git a/include/xrpl/protocol/STBase.h b/include/xrpl/protocol/STBase.h index acc5500a57..a8bda8f614 100644 --- a/include/xrpl/protocol/STBase.h +++ b/include/xrpl/protocol/STBase.h @@ -140,8 +140,6 @@ public: bool operator==(STBase const& t) const; - bool - operator!=(STBase const& t) const; template D& diff --git a/include/xrpl/protocol/STCurrency.h b/include/xrpl/protocol/STCurrency.h index 18642b20cf..933abaedb8 100644 --- a/include/xrpl/protocol/STCurrency.h +++ b/include/xrpl/protocol/STCurrency.h @@ -93,12 +93,6 @@ operator==(STCurrency const& lhs, STCurrency const& rhs) return lhs.currency() == rhs.currency(); } -inline bool -operator!=(STCurrency const& lhs, STCurrency const& rhs) -{ - return !operator==(lhs, rhs); -} - inline bool operator<(STCurrency const& lhs, STCurrency const& rhs) { diff --git a/include/xrpl/protocol/STObject.h b/include/xrpl/protocol/STObject.h index c7fc4fa796..dcbd08170e 100644 --- a/include/xrpl/protocol/STObject.h +++ b/include/xrpl/protocol/STObject.h @@ -432,8 +432,6 @@ public: bool operator==(STObject const& o) const; - bool - operator!=(STObject const& o) const; class FieldErr; @@ -667,36 +665,6 @@ public: return !lhs.engaged() || *lhs == *rhs; } - friend bool - operator!=(OptionalProxy const& lhs, std::nullopt_t) noexcept - { - return !(lhs == std::nullopt); - } - - friend bool - operator!=(std::nullopt_t, OptionalProxy const& rhs) noexcept - { - return !(rhs == std::nullopt); - } - - friend bool - operator!=(OptionalProxy const& lhs, optional_type const& rhs) noexcept - { - return !(lhs == rhs); - } - - friend bool - operator!=(optional_type const& lhs, OptionalProxy const& rhs) noexcept - { - return !(lhs == rhs); - } - - friend bool - operator!=(OptionalProxy const& lhs, OptionalProxy const& rhs) noexcept - { - return !(lhs == rhs); - } - // Emulate std::optional::value_or [[nodiscard]] value_type valueOr(value_type val) const; @@ -1202,12 +1170,6 @@ STObject::setFieldH160(SField const& field, BaseUInt<160, Tag> const& v) } } -inline bool -STObject::operator!=(STObject const& o) const -{ - return !(*this == o); -} - template V STObject::getFieldByValue(SField const& field) const diff --git a/include/xrpl/protocol/STPathSet.h b/include/xrpl/protocol/STPathSet.h index d527e2479f..5768721111 100644 --- a/include/xrpl/protocol/STPathSet.h +++ b/include/xrpl/protocol/STPathSet.h @@ -115,9 +115,6 @@ public: bool operator==(STPathElement const& t) const; - bool - operator!=(STPathElement const& t) const; - private: static std::size_t getHash(STPathElement const& element); @@ -432,12 +429,6 @@ STPathElement::operator==(STPathElement const& t) const accountID_ == t.accountID_ && assetID_ == t.assetID_ && issuerID_ == t.issuerID_; } -inline bool -STPathElement::operator!=(STPathElement const& t) const -{ - return !operator==(t); -} - // ------------ STPath ------------ inline STPath::STPath(std::vector p) : path_(std::move(p)) diff --git a/include/xrpl/protocol/SeqProxy.h b/include/xrpl/protocol/SeqProxy.h index fa72914591..3686d123d6 100644 --- a/include/xrpl/protocol/SeqProxy.h +++ b/include/xrpl/protocol/SeqProxy.h @@ -123,12 +123,6 @@ public: return (lhs.value() == rhs.value()); } - friend constexpr bool - operator!=(SeqProxy lhs, SeqProxy rhs) - { - return !(lhs == rhs); - } - friend constexpr bool operator<(SeqProxy lhs, SeqProxy rhs) { diff --git a/include/xrpl/protocol/Serializer.h b/include/xrpl/protocol/Serializer.h index 73bd9c8289..c1ea5c16ba 100644 --- a/include/xrpl/protocol/Serializer.h +++ b/include/xrpl/protocol/Serializer.h @@ -265,20 +265,10 @@ public: return v == data_; } bool - operator!=(Blob const& v) const - { - return v != data_; - } - bool operator==(Serializer const& v) const { return v.data_ == data_; } - bool - operator!=(Serializer const& v) const - { - return v.data_ != data_; - } static int decodeLengthLength(int b1); diff --git a/include/xrpl/protocol/Units.h b/include/xrpl/protocol/Units.h index 169ee2c543..94afd72f53 100644 --- a/include/xrpl/protocol/Units.h +++ b/include/xrpl/protocol/Units.h @@ -258,13 +258,6 @@ public: return value_ == other; } - template Other> - constexpr bool - operator!=(ValueUnit const& other) const - { - return !operator==(other); - } - constexpr bool operator<(ValueUnit const& other) const { diff --git a/include/xrpl/protocol/detail/STVar.h b/include/xrpl/protocol/detail/STVar.h index 12026f3d09..56f868b665 100644 --- a/include/xrpl/protocol/detail/STVar.h +++ b/include/xrpl/protocol/detail/STVar.h @@ -152,10 +152,4 @@ operator==(STVar const& lhs, STVar const& rhs) return lhs.get().isEquivalent(rhs.get()); } -inline bool -operator!=(STVar const& lhs, STVar const& rhs) -{ - return !(lhs == rhs); -} - } // namespace xrpl::detail diff --git a/include/xrpl/server/Manifest.h b/include/xrpl/server/Manifest.h index 786967b057..1b726f2c0c 100644 --- a/include/xrpl/server/Manifest.h +++ b/include/xrpl/server/Manifest.h @@ -306,12 +306,6 @@ operator==(Manifest const& lhs, Manifest const& rhs) lhs.serialized == rhs.serialized; } -inline bool -operator!=(Manifest const& lhs, Manifest const& rhs) -{ - return !(lhs == rhs); -} - struct ValidatorToken { std::string manifest; diff --git a/include/xrpl/shamap/SHAMap.h b/include/xrpl/shamap/SHAMap.h index e198c472fa..97ab2e9f7a 100644 --- a/include/xrpl/shamap/SHAMap.h +++ b/include/xrpl/shamap/SHAMap.h @@ -789,12 +789,6 @@ operator==(SHAMap::ConstIterator const& x, SHAMap::ConstIterator const& y) return x.item_ == y.item_; } -inline bool -operator!=(SHAMap::ConstIterator const& x, SHAMap::ConstIterator const& y) -{ - return !(x == y); -} - inline SHAMap::ConstIterator SHAMap::begin() const { diff --git a/include/xrpl/shamap/SHAMapNodeID.h b/include/xrpl/shamap/SHAMapNodeID.h index 6094892091..1189304aa7 100644 --- a/include/xrpl/shamap/SHAMapNodeID.h +++ b/include/xrpl/shamap/SHAMapNodeID.h @@ -3,6 +3,7 @@ #include #include +#include #include #include #include @@ -65,45 +66,32 @@ public: static SHAMapNodeID createID(int depth, uint256 const& key); - // FIXME-C++20: use spaceship and operator synthesis /** * Comparison operators + * + * <, >, <= and >= are synthesized from the spaceship. It is written out + * rather than defaulted because the ordering is by depth first, and the + * members are not declared in that order. */ - bool - operator<(SHAMapNodeID const& n) const + std::strong_ordering + operator<=>(SHAMapNodeID const& n) const { - return std::tie(depth_, id_) < std::tie(n.depth_, n.id_); - } - - bool - operator>(SHAMapNodeID const& n) const - { - return n < *this; - } - - bool - operator<=(SHAMapNodeID const& n) const - { - return !(n < *this); - } - - bool - operator>=(SHAMapNodeID const& n) const - { - return !(*this < n); + return std::tie(depth_, id_) <=> std::tie(n.depth_, n.id_); } + /** + * Equality, which the spaceship above does not provide. + * + * Only a *defaulted* operator<=> implicitly declares a defaulted + * operator==; the one above is user-provided, so == has to be written. + * It cannot be defaulted either, because a defaulted == would also compare + * the CountedObject base, which is not equality comparable. + */ bool operator==(SHAMapNodeID const& n) const { return (depth_ == n.depth_) && (id_ == n.id_); } - - bool - operator!=(SHAMapNodeID const& n) const - { - return !(*this == n); - } }; inline std::string diff --git a/include/xrpl/tx/paths/detail/Steps.h b/include/xrpl/tx/paths/detail/Steps.h index 8ee37c026c..1d68860adc 100644 --- a/include/xrpl/tx/paths/detail/Steps.h +++ b/include/xrpl/tx/paths/detail/Steps.h @@ -274,19 +274,6 @@ public: return lhs.equal(rhs); } - /** - * Return true if lhs != rhs. - * - * @param lhs Step to compare. - * @param rhs Step to compare. - * @return true if lhs != rhs. - */ - friend bool - operator!=(Step const& lhs, Step const& rhs) - { - return !(lhs == rhs); - } - /** * Streaming operator for a Step. */ diff --git a/src/libxrpl/protocol/Rules.cpp b/src/libxrpl/protocol/Rules.cpp index 197139027a..cb71133d8f 100644 --- a/src/libxrpl/protocol/Rules.cpp +++ b/src/libxrpl/protocol/Rules.cpp @@ -193,12 +193,6 @@ Rules::operator==(Rules const& other) const return *impl_ == *other.impl_; } -bool -Rules::operator!=(Rules const& other) const -{ - return !(*this == other); -} - bool isFeatureEnabled(uint256 const& feature, bool resultIfNoRules) { diff --git a/src/libxrpl/protocol/STBase.cpp b/src/libxrpl/protocol/STBase.cpp index f029f10e75..1e56897e30 100644 --- a/src/libxrpl/protocol/STBase.cpp +++ b/src/libxrpl/protocol/STBase.cpp @@ -38,12 +38,6 @@ STBase::operator==(STBase const& t) const return (getSType() == t.getSType()) && isEquivalent(t); } -bool -STBase::operator!=(STBase const& t) const -{ - return (getSType() != t.getSType()) || !isEquivalent(t); -} - STBase* STBase::copy(std::size_t n, void* buf) const { diff --git a/src/test/jtx/amount.h b/src/test/jtx/amount.h index 57a4502db9..94dd8aef9e 100644 --- a/src/test/jtx/amount.h +++ b/src/test/jtx/amount.h @@ -162,12 +162,6 @@ operator==(PrettyAmount const& lhs, PrettyAmount const& rhs) return lhs.value() == rhs.value(); } -inline bool -operator!=(PrettyAmount const& lhs, PrettyAmount const& rhs) -{ - return !operator==(lhs, rhs); -} - std::ostream& operator<<(std::ostream& os, PrettyAmount const& amount); From b19c3c64f24ae2e7f7e40b5ca0cbf5e94f44b803 Mon Sep 17 00:00:00 2001 From: yinyiqian1 Date: Mon, 10 Aug 2026 13:47:16 -0400 Subject: [PATCH 10/28] fix: Add zero keylet check in credential (#7971) --- .../xrpl/ledger/helpers/CredentialHelpers.h | 3 +- .../ledger/helpers/CredentialHelpers.cpp | 24 ++++++++++- .../tx/transactors/account/AccountDelete.cpp | 2 +- .../tx/transactors/escrow/EscrowFinish.cpp | 2 +- .../tx/transactors/payment/Payment.cpp | 2 +- .../payment_channel/PaymentChannelClaim.cpp | 2 +- .../transactors/token/ConfidentialMPTSend.cpp | 2 +- src/test/app/DepositAuth_test.cpp | 41 +++++++++++++++++++ 8 files changed, 71 insertions(+), 7 deletions(-) diff --git a/include/xrpl/ledger/helpers/CredentialHelpers.h b/include/xrpl/ledger/helpers/CredentialHelpers.h index 8e78a00923..8b1c819bf4 100644 --- a/include/xrpl/ledger/helpers/CredentialHelpers.h +++ b/include/xrpl/ledger/helpers/CredentialHelpers.h @@ -7,6 +7,7 @@ #include #include #include +#include #include #include #include @@ -34,7 +35,7 @@ deleteSLE(ApplyView& view, SLE::ref sleCredential, beast::Journal j); // Amendment and parameters checks for sfCredentialIDs field NotTEC -checkFields(STTx const& tx, beast::Journal j); +checkFields(STTx const& tx, Rules const& rules, beast::Journal j); // Accessing the ledger to check if provided credentials are valid. Do not use // in doApply (only in preclaim) since it does not remove expired credentials. diff --git a/src/libxrpl/ledger/helpers/CredentialHelpers.cpp b/src/libxrpl/ledger/helpers/CredentialHelpers.cpp index 226ea100e9..5ba832957d 100644 --- a/src/libxrpl/ledger/helpers/CredentialHelpers.cpp +++ b/src/libxrpl/ledger/helpers/CredentialHelpers.cpp @@ -22,6 +22,7 @@ #include #include +#include #include #include #include @@ -52,6 +53,9 @@ removeExpired(ApplyView& view, STVector256 const& arr, beast::Journal const j) for (auto const& h : arr) { // Credentials already checked in preclaim. Look only for expired here. + if (view.rules().enabled(fixCleanup3_4_0) && h.isZero()) + return std::unexpected(tecINTERNAL); // LCOV_EXCL_LINE + auto const k = keylet::credential(h); auto const sleCred = view.peek(k); @@ -124,7 +128,7 @@ deleteSLE(ApplyView& view, SLE::ref sleCredential, beast::Journal j) } NotTEC -checkFields(STTx const& tx, beast::Journal j) +checkFields(STTx const& tx, Rules const& rules, beast::Journal j) { if (!tx.isFieldPresent(sfCredentialIDs)) return tesSUCCESS; @@ -137,6 +141,13 @@ checkFields(STTx const& tx, beast::Journal j) return temMALFORMED; } + if (rules.enabled(fixCleanup3_4_0) && + std::ranges::any_of(credentials, [](uint256 const& id) { return id.isZero(); })) + { + JLOG(j.trace()) << "Malformed transaction: zero credential ID."; + return temMALFORMED; + } + std::unordered_set duplicates; for (auto const& cred : credentials) { @@ -160,6 +171,14 @@ valid(STTx const& tx, ReadView const& view, AccountID const& src, beast::Journal auto const& credIDs(tx.getFieldV256(sfCredentialIDs)); for (auto const& h : credIDs) { + if (view.rules().enabled(fixCleanup3_4_0) && h.isZero()) + { + // LCOV_EXCL_START + JLOG(j.trace()) << "Zero credential ID."; + return tecINTERNAL; + // LCOV_EXCL_STOP + } + auto const sleCred = view.read(keylet::credential(h)); if (!sleCred) { @@ -234,6 +253,9 @@ authorizedDepositPreauth(ReadView const& view, STVector256 const& credIDs, Accou lifeExtender.reserve(credIDs.size()); for (auto const& h : credIDs) { + if (view.rules().enabled(fixCleanup3_4_0) && h.isZero()) + return tefINTERNAL; // LCOV_EXCL_LINE + auto sleCred = view.read(keylet::credential(h)); if (!sleCred) // already checked in preclaim return tefINTERNAL; // LCOV_EXCL_LINE diff --git a/src/libxrpl/tx/transactors/account/AccountDelete.cpp b/src/libxrpl/tx/transactors/account/AccountDelete.cpp index ce027f4cad..0936fe26dc 100644 --- a/src/libxrpl/tx/transactors/account/AccountDelete.cpp +++ b/src/libxrpl/tx/transactors/account/AccountDelete.cpp @@ -50,7 +50,7 @@ AccountDelete::preflight(PreflightContext const& ctx) return temDST_IS_SRC; } - if (auto const err = credentials::checkFields(ctx.tx, ctx.j); !isTesSuccess(err)) + if (auto const err = credentials::checkFields(ctx.tx, ctx.rules, ctx.j); !isTesSuccess(err)) return err; return tesSUCCESS; diff --git a/src/libxrpl/tx/transactors/escrow/EscrowFinish.cpp b/src/libxrpl/tx/transactors/escrow/EscrowFinish.cpp index 5fc0aef853..32f4d9ec48 100644 --- a/src/libxrpl/tx/transactors/escrow/EscrowFinish.cpp +++ b/src/libxrpl/tx/transactors/escrow/EscrowFinish.cpp @@ -111,7 +111,7 @@ EscrowFinish::preflightSigValidated(PreflightContext const& ctx) } } - if (auto const err = credentials::checkFields(ctx.tx, ctx.j); !isTesSuccess(err)) + if (auto const err = credentials::checkFields(ctx.tx, ctx.rules, ctx.j); !isTesSuccess(err)) return err; return tesSUCCESS; diff --git a/src/libxrpl/tx/transactors/payment/Payment.cpp b/src/libxrpl/tx/transactors/payment/Payment.cpp index 17c96a1919..c8b00f0193 100644 --- a/src/libxrpl/tx/transactors/payment/Payment.cpp +++ b/src/libxrpl/tx/transactors/payment/Payment.cpp @@ -281,7 +281,7 @@ Payment::preflight(PreflightContext const& ctx) } } - if (auto const err = credentials::checkFields(ctx.tx, ctx.j); !isTesSuccess(err)) + if (auto const err = credentials::checkFields(ctx.tx, ctx.rules, ctx.j); !isTesSuccess(err)) return err; return tesSUCCESS; diff --git a/src/libxrpl/tx/transactors/payment_channel/PaymentChannelClaim.cpp b/src/libxrpl/tx/transactors/payment_channel/PaymentChannelClaim.cpp index b8118bc49f..9143a675f6 100644 --- a/src/libxrpl/tx/transactors/payment_channel/PaymentChannelClaim.cpp +++ b/src/libxrpl/tx/transactors/payment_channel/PaymentChannelClaim.cpp @@ -87,7 +87,7 @@ PaymentChannelClaim::preflight(PreflightContext const& ctx) return temBAD_SIGNATURE; } - if (auto const err = credentials::checkFields(ctx.tx, ctx.j); !isTesSuccess(err)) + if (auto const err = credentials::checkFields(ctx.tx, ctx.rules, ctx.j); !isTesSuccess(err)) return err; return tesSUCCESS; diff --git a/src/libxrpl/tx/transactors/token/ConfidentialMPTSend.cpp b/src/libxrpl/tx/transactors/token/ConfidentialMPTSend.cpp index d121ec2634..f4c7b98c41 100644 --- a/src/libxrpl/tx/transactors/token/ConfidentialMPTSend.cpp +++ b/src/libxrpl/tx/transactors/token/ConfidentialMPTSend.cpp @@ -82,7 +82,7 @@ ConfidentialMPTSend::preflight(PreflightContext const& ctx) if (hasAuditor && !isValidCiphertext(ctx.tx[sfAuditorEncryptedAmount])) return temBAD_CIPHERTEXT; - if (auto const err = credentials::checkFields(ctx.tx, ctx.j); !isTesSuccess(err)) + if (auto const err = credentials::checkFields(ctx.tx, ctx.rules, ctx.j); !isTesSuccess(err)) return err; return tesSUCCESS; diff --git a/src/test/app/DepositAuth_test.cpp b/src/test/app/DepositAuth_test.cpp index 881441e0f9..c987e603be 100644 --- a/src/test/app/DepositAuth_test.cpp +++ b/src/test/app/DepositAuth_test.cpp @@ -934,6 +934,46 @@ struct DepositPreauth_test : public beast::unit_test::Suite } } + void + testZeroCredentialID(FeatureBitset features) + { + testcase("Zero credential ID"); + + using namespace jtx; + + char const credType[] = "abcde"; + Account const issuer{"issuer"}; + Account const alice{"alice"}; + Account const bob{"bob"}; + + Env env(*this, features); + + env.fund(XRP(5000), issuer, alice, bob); + env.close(); + + env(credentials::create(alice, issuer, credType)); + env.close(); + env(credentials::accept(alice, issuer, credType)); + env.close(); + + auto const jv = credentials::ledgerEntry(env, alice, issuer, credType); + std::string const credIdx = jv[jss::result][jss::index].asString(); + + std::string const zeroIdx(64, '0'); + + // post-fixCleanup3_4_0: a zero ID is rejected by checkFields in + // preflight; pre-fixCleanup3_4_0, it will trigger assertion, so it is not testable. + env(pay(alice, bob, XRP(100)), credentials::Ids({zeroIdx}), Ter(temMALFORMED)); + env.close(); + + env(pay(alice, bob, XRP(100)), credentials::Ids({credIdx, zeroIdx}), Ter(temMALFORMED)); + env.close(); + + // A valid credential succeeds + env(pay(alice, bob, XRP(100)), credentials::Ids({credIdx})); + env.close(); + } + void testCredentialsCreation() { @@ -1446,6 +1486,7 @@ struct DepositPreauth_test : public beast::unit_test::Suite testPayment(supported - featureCredentials); testPayment(supported); testCredentialsPayment(); + testZeroCredentialID(supported); testCredentialsCreation(); testExpiredCreds(); testSortingCredentials(); From 9c292fbe4fbd62aa8780842a95ddd9b5291093f5 Mon Sep 17 00:00:00 2001 From: Ayaz Salikhov Date: Mon, 10 Aug 2026 18:49:29 +0100 Subject: [PATCH 11/28] build: Install conan configuration/profiles inside Nix devshell (#7997) --- .envrc | 4 ++++ BUILD.md | 42 +++++++++++++++++------------------------- conan/init.sh | 21 +++++++++++++++++++++ conan/profiles/default | 3 --- docs/build/nix.md | 28 +++++++++++++++++++++++----- nix/devshell.nix | 22 +++++++++++++++++++++- 6 files changed, 86 insertions(+), 34 deletions(-) create mode 100755 conan/init.sh diff --git a/.envrc b/.envrc index cecf4b4767..ec38b75f5c 100644 --- a/.envrc +++ b/.envrc @@ -1,3 +1,7 @@ watch_file nix/*.nix +# The dev shell derivation includes all of conan/ (see nix/devshell.nix), so any +# change in there has to invalidate direnv's cached environment. +watch_dir conan + use flake diff --git a/BUILD.md b/BUILD.md index ad4666b141..ae2e69bb97 100644 --- a/BUILD.md +++ b/BUILD.md @@ -53,33 +53,25 @@ releases](https://github.com/XRPLF/rippled/releases). ### Set Up Conan -Once your [development environment](./docs/build/environment.md) is ready, you -may need to set up your Conan profile. - -#### Profiles - -We recommend that you install our Conan profiles: +Once your [development environment](./docs/build/environment.md) is ready, set +Conan up for this repository: ```bash -conan config install conan/profiles/ -tf $(conan config home)/profiles/ +./conan/init.sh ``` -You can check your Conan profile by running: +That installs our [`global.conf`](./conan/global.conf), our Conan +[profiles](./conan/profiles), and the `xrplf` remote that hosts some of our +dependencies. It honours `CONAN_HOME` and never deletes an existing Conan home, +so it is safe to re-run — it only overwrites the files it manages. -```bash -conan profile show -``` +> [!TIP] +> In the [Nix development shell](./docs/build/nix.md#conan-configuration) this is +> already done for you: the script runs on entry. -If the default profile is not suitable for your environment, you can create a custom profile and pass it to Conan. -More information on customizing Conan can be found in the [Advanced Conan configuration](./docs/build/advanced_conan.md). - -#### Add xrplf remote - -Run the following command to add the `xrplf` remote, which hosts some of our dependencies: - -```bash -conan remote add --index 0 --force xrplf https://conan.xrplf.org/repository/conan/ -``` +You can inspect the resulting profile with `conan profile show`. If it is not +suitable for your environment, create a custom profile and pass it to Conan — see +[Advanced Conan configuration](./docs/build/advanced_conan.md). ### Set Up Ccache @@ -368,14 +360,14 @@ After any updates or changes to dependencies, you may need to do the following: 4. [Regenerate lockfile](./docs/build/advanced_conan.md#conan-lockfile). 5. Re-run [conan install](#build-and-test). -If you are using the Nix development shell, prebuilt Conan binaries may be -incompatible with it — see -[Building xrpld in the Nix shell](./docs/build/nix.md#building-xrpld-in-the-nix-shell). +If you are using the Nix development shell, whether prebuilt Conan binaries apply +depends on your platform — see +[Prebuilt packages](./docs/build/nix.md#prebuilt-packages). #### ERROR: Package not resolved If you're seeing an error like `ERROR: Package 'snappy/1.1.10' not resolved: Unable to find 'snappy/1.1.10#968fef506ff261592ec30c574d4a7809%1756234314.246' in remotes.`, -please [add `xrplf` remote](#add-xrplf-remote) or re-run `conan export` for [patched recipes](./docs/build/advanced_conan.md#patched-recipes). +please [set Conan up](#set-up-conan) so the `xrplf` remote is configured, or re-run `conan export` for [patched recipes](./docs/build/advanced_conan.md#patched-recipes). ### `protobuf/port_def.inc` file not found diff --git a/conan/init.sh b/conan/init.sh new file mode 100755 index 0000000000..287ee83001 --- /dev/null +++ b/conan/init.sh @@ -0,0 +1,21 @@ +#!/usr/bin/env bash +# Install our Conan configuration, profiles and the xrplf remote into CONAN_HOME. +# Safe to re-run; never deletes the Conan home. + +set -euo pipefail + +SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" +CONAN_DIR="$(conan config home)" + +echo "Installing Conan configuration into ${CONAN_DIR}" +conan config install "${SCRIPT_DIR}/global.conf" +conan config install "${SCRIPT_DIR}/profiles" -tf "${CONAN_DIR}/profiles" +# This script manages these files, so make them read-only - Conan does not +# preserve the source mode. Only the files: the directories must stay writable +# for `conan config install` to replace them. +chmod a-w "${CONAN_DIR}/global.conf" +find "${CONAN_DIR}/profiles" -type f -exec chmod a-w {} + + +echo "Adding the xrplf Conan remote" +# --index 0: our patched recipes must win over Conan Center. +conan remote add --index 0 --force xrplf https://conan.xrplf.org/repository/conan/ diff --git a/conan/profiles/default b/conan/profiles/default index f2d93213ac..1b7eaff980 100644 --- a/conan/profiles/default +++ b/conan/profiles/default @@ -1,10 +1,7 @@ {% set os = detect_api.detect_os() %} {% set arch = detect_api.detect_arch() %} {% set compiler, version, compiler_exe = detect_api.detect_default_compiler() %} -{% set compiler_version = version %} -{% if os == "Linux" %} {% set compiler_version = detect_api.default_compiler_version(compiler, version) %} -{% endif %} {% if os == "Macos" %} {# Minimum macOS the dependencies target. #} {# Without this, Conan builds each dependency against the (possibly newer) host SDK, so the #} diff --git a/docs/build/nix.md b/docs/build/nix.md index fad8bc701d..d1e40fcc89 100644 --- a/docs/build/nix.md +++ b/docs/build/nix.md @@ -124,14 +124,32 @@ nix develop -c "$SHELL" Once inside the Nix development shell, follow the standard [build instructions](../../BUILD.md#steps). The Nix shell provides all necessary tools (CMake, Ninja, Conan, etc.). -Two things differ from a system environment: - -**Prebuilt Conan packages.** There is no guarantee that binaries from the Conan cache will work when using Nix. If you encounter any errors, add `--build '*'` to the `conan install` command in [Build and Test](../../BUILD.md#build-and-test) to force Conan to compile everything from source. Keep the rest of the command as it is there, so it rebuilds the `build_type` you are actually configuring. - -**Coverage builds.** `-Dcoverage=ON` works in the `gcc` shell (and `gcc-plain` on Linux): +Coverage builds (`-Dcoverage=ON`) work in the `gcc` shell (and `gcc-plain` on Linux): each ships a `gcov` matching its compiler, since Nix's cc-wrapper does not expose one. The `clang` shells do not include `llvm-cov`, so use a `gcc` shell for coverage. +## Conan configuration + +The shell runs [`conan/init.sh`](../../conan/init.sh) on entry, so +[Set Up Conan](../../BUILD.md#set-up-conan) is already done for you. It installs +into the shell's own Conan home: `CONAN_HOME=~/.conan2-nix`. + +### Prebuilt packages + +On **Linux**, the binaries on the `xrplf` remote are built in this same Nix +environment — CI runs in Docker images that bundle the dev shell's toolchain (see +[`nix/docker`](../../nix/docker)) — so `.#gcc` and `.#clang` can reuse them. The +`-plain` shells do not match that toolchain's glibc, so binaries from the remote +are not a reliable match there. + +On **macOS**, CI builds with Apple Clang, so the remote holds nothing for the Nix +`clang` toolchain and dependencies are compiled locally. We do not publish +Nix-built macOS binaries because a Conan package ID records the compiler version +but not the nixpkgs revision. + +To compile everything from source, add `--build '*'` to the `conan install` +command. + ## Automatic Activation with direnv [direnv](https://direnv.net/) or [nix-direnv](https://github.com/nix-community/nix-direnv) can automatically activate the Nix development shell when you enter the repository directory. diff --git a/nix/devshell.nix b/nix/devshell.nix index cb4a99c76a..ac0b84e169 100644 --- a/nix/devshell.nix +++ b/nix/devshell.nix @@ -30,10 +30,29 @@ let }; customGccGcov = if pkgs.stdenv.isLinux then customCompilers.customGcov else plainGcov; + # Whole directory: init.sh locates the profiles relative to itself. + conanDir = ../conan; + + # Own Conan home, so Nix-built packages never share a cache with a system + # Conan. The stamp holds a content-addressed store path, so init.sh re-runs + # only when something in conan/ changes. + conanHook = '' + export CONAN_HOME=~/.conan2-nix + _xrpl_conan_stamp="$CONAN_HOME/.xrpld-devshell" + if [ "$(cat "$_xrpl_conan_stamp" 2>/dev/null)" != "${conanDir}" ]; then + if ${conanDir}/init.sh; then + printf '%s' "${conanDir}" >"$_xrpl_conan_stamp" + else + echo "⚠️ Conan setup failed - run ./conan/init.sh from the repository root to retry." + fi + fi + unset _xrpl_conan_stamp + ''; + # Shown when entering a *-plain shell. These exist only on Linux (see below), # where the stock toolchain diverges from CI. plainWarningHook = '' - echo "⚠️ WARNING: this is the stock nixpkgs toolchain and does not match CI's glibc. Prefer 'nix develop .#gcc' / '.#clang' unless you need to skip the custom-glibc build." + echo "⚠️ WARNING: this is the stock nixpkgs toolchain and does not match CI's glibc. Prefer 'nix develop .#gcc' / '.#clang' unless you need to skip the custom-glibc build." ''; # Tools to expose under version-suffixed names (see mkVersionedToolLinks). @@ -87,6 +106,7 @@ let shellHook = '' echo "Welcome to xrpld development shell"; ${compilerVersionHook} + ${conanHook} ${warningHook} ''; } From 4173f7e499e3dd55900bf72b3c05f68577448060 Mon Sep 17 00:00:00 2001 From: Braedon Klock Date: Mon, 10 Aug 2026 21:30:06 +0000 Subject: [PATCH 12/28] fix: Validate account_lines peer field type (#7728) Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- API-CHANGELOG.md | 1 + src/test/rpc/AccountLines_test.cpp | 47 +++++++++++++++++++ .../rpc/handlers/account/AccountLines.cpp | 5 ++ 3 files changed, 53 insertions(+) diff --git a/API-CHANGELOG.md b/API-CHANGELOG.md index 79fb8ff522..bc3672588e 100644 --- a/API-CHANGELOG.md +++ b/API-CHANGELOG.md @@ -54,6 +54,7 @@ This section contains changes targeting a future version. - `submit`: The `fail_hard` field now returns an error if the value is not a boolean. [#6529](https://github.com/XRPLF/rippled/pull/6529) - `subscribe`: The `taker` field in the `books` array now returns `actMalformed` instead of `badIssuer` if the value is not a valid account. [#6529](https://github.com/XRPLF/rippled/pull/6529) - Fixed a bug in `Forwarded` HTTP header parsing where the extracted IP address could be incorrect when no comma or semicolon delimiter follows the address. This could cause the server to misidentify a client's IP address when operating behind a reverse proxy. [#6529](https://github.com/XRPLF/rippled/pull/6529) +- `account_lines`: The `peer` field now returns an error if the value is not a string. [#7728](https://github.com/XRPLF/rippled/pull/7728) ## XRP Ledger server version 3.1.0 diff --git a/src/test/rpc/AccountLines_test.cpp b/src/test/rpc/AccountLines_test.cpp index cb20de9bf5..3de2bdefa3 100644 --- a/src/test/rpc/AccountLines_test.cpp +++ b/src/test/rpc/AccountLines_test.cpp @@ -94,6 +94,24 @@ public: LedgerHeader const ledger3Info = env.closed()->header(); BEAST_EXPECT(ledger3Info.seq == 3); + { + // test peer non-string + auto testInvalidPeerParam = [&](auto const& param) { + json::Value params; + params[jss::account] = alice.human(); + params[jss::peer] = param; + auto jrr = env.rpc("json", "account_lines", to_string(params))[jss::result]; + BEAST_EXPECT(jrr[jss::error] == "invalidParams"); + BEAST_EXPECT(jrr[jss::error_message] == "Invalid field 'peer'."); + }; + + testInvalidPeerParam(1); + testInvalidPeerParam(1.1); + testInvalidPeerParam(true); + testInvalidPeerParam(json::Value(json::ValueType::Null)); + testInvalidPeerParam(json::Value(json::ValueType::Object)); + testInvalidPeerParam(json::Value(json::ValueType::Array)); + } { // alice is funded but has no lines. An empty array is returned. json::Value params; @@ -775,6 +793,35 @@ public: LedgerHeader const ledger3Info = env.closed()->header(); BEAST_EXPECT(ledger3Info.seq == 3); + { + // test peer non-string + auto testInvalidPeerParam = [&](auto const& param) { + json::Value params; + params[jss::account] = alice.human(); + params[jss::peer] = param; + + json::Value request; + request[jss::method] = "account_lines"; + request[jss::jsonrpc] = "2.0"; + request[jss::ripplerpc] = "2.0"; + request[jss::id] = 5; + request[jss::params] = params; + + auto const lines = env.rpc("json2", to_string(request)); + BEAST_EXPECT(lines[jss::error][jss::error] == "invalidParams"); + BEAST_EXPECT(lines[jss::error][jss::message] == "Invalid field 'peer'."); + BEAST_EXPECT(lines.isMember(jss::jsonrpc) && lines[jss::jsonrpc] == "2.0"); + BEAST_EXPECT(lines.isMember(jss::ripplerpc) && lines[jss::ripplerpc] == "2.0"); + BEAST_EXPECT(lines.isMember(jss::id) && lines[jss::id] == 5); + }; + + testInvalidPeerParam(1); + testInvalidPeerParam(1.1); + testInvalidPeerParam(true); + testInvalidPeerParam(json::Value(json::ValueType::Null)); + testInvalidPeerParam(json::Value(json::ValueType::Object)); + testInvalidPeerParam(json::Value(json::ValueType::Array)); + } { // alice is funded but has no lines. An empty array is returned. json::Value params; diff --git a/src/xrpld/rpc/handlers/account/AccountLines.cpp b/src/xrpld/rpc/handlers/account/AccountLines.cpp index 4a6d22d5d8..ac98e271b6 100644 --- a/src/xrpld/rpc/handlers/account/AccountLines.cpp +++ b/src/xrpld/rpc/handlers/account/AccountLines.cpp @@ -107,7 +107,12 @@ doAccountLines(rpc::JsonContext& context) std::string strPeer; if (params.isMember(jss::peer)) + { + if (!params[jss::peer].isString()) + return rpc::invalidFieldError(jss::peer); + strPeer = params[jss::peer].asString(); + } auto const raPeerAccount = [&]() -> std::optional { return strPeer.empty() ? std::nullopt : parseBase58(strPeer); From 60291c3ed613a749f6aeced06d485b1d478d9843 Mon Sep 17 00:00:00 2001 From: Kassaking7 <96991820+Kassaking7@users.noreply.github.com> Date: Mon, 10 Aug 2026 21:34:28 +0000 Subject: [PATCH 13/28] fix: Allow OverrideFreeze to bypass individual/deep freeze on AMM trust lines (#6959) --- include/xrpl/tx/invariants/FreezeInvariant.h | 6 +- src/libxrpl/tx/invariants/FreezeInvariant.cpp | 23 +- src/test/app/AMMClawback_test.cpp | 204 ++++++++++++++++++ 3 files changed, 222 insertions(+), 11 deletions(-) diff --git a/include/xrpl/tx/invariants/FreezeInvariant.h b/include/xrpl/tx/invariants/FreezeInvariant.h index 4b3e9beec4..c66e002872 100644 --- a/include/xrpl/tx/invariants/FreezeInvariant.h +++ b/include/xrpl/tx/invariants/FreezeInvariant.h @@ -69,7 +69,8 @@ private: IssuerChanges const& changes, STTx const& tx, beast::Journal const& j, - bool enforce); + bool enforce, + bool fixOverrideFreeze); static bool validateFrozenState( @@ -78,7 +79,8 @@ private: STTx const& tx, beast::Journal const& j, bool enforce, - bool globalFreeze); + bool globalFreeze, + bool fixOverrideFreeze); }; } // namespace xrpl diff --git a/src/libxrpl/tx/invariants/FreezeInvariant.cpp b/src/libxrpl/tx/invariants/FreezeInvariant.cpp index 0a604d4c39..c4340b9aec 100644 --- a/src/libxrpl/tx/invariants/FreezeInvariant.cpp +++ b/src/libxrpl/tx/invariants/FreezeInvariant.cpp @@ -73,6 +73,7 @@ TransfersNotFrozen::finalize( * view.rules().enabled(fixFreezeExploit); */ [[maybe_unused]] bool const enforce = view.rules().enabled(featureDeepFreeze); + bool const fixOverrideFreeze = view.rules().enabled(fixCleanup3_4_0); return std::ranges::all_of(balanceChanges_, [&](auto const& entry) { auto const& [issue, changes] = entry; @@ -90,7 +91,7 @@ TransfersNotFrozen::finalize( return !enforce; } - return validateIssuerChanges(issuerSle, changes, tx, j, enforce); + return validateIssuerChanges(issuerSle, changes, tx, j, enforce, fixOverrideFreeze); }); } @@ -199,7 +200,8 @@ TransfersNotFrozen::validateIssuerChanges( IssuerChanges const& changes, STTx const& tx, beast::Journal const& j, - bool enforce) + bool enforce, + bool fixOverrideFreeze) { if (!issuer) { @@ -225,7 +227,7 @@ TransfersNotFrozen::validateIssuerChanges( { bool const high = change.line->at(sfLowLimit).getIssuer() == issuer->at(sfAccount); - if (!validateFrozenState(change, high, tx, j, enforce, globalFreeze)) + if (!validateFrozenState(change, high, tx, j, enforce, globalFreeze, fixOverrideFreeze)) { return false; } @@ -241,26 +243,29 @@ TransfersNotFrozen::validateFrozenState( STTx const& tx, beast::Journal const& j, bool enforce, - bool globalFreeze) + bool globalFreeze, + bool fixOverrideFreeze) { bool const freeze = change.balanceChangeSign < 0 && change.line->isFlag(high ? lsfLowFreeze : lsfHighFreeze); bool const deepFreeze = change.line->isFlag(high ? lsfLowDeepFreeze : lsfHighDeepFreeze); bool const frozen = globalFreeze || deepFreeze || freeze; - bool const isAMMLine = change.line->isFlag(lsfAMMNode); - if (!frozen) { return true; } - // AMMClawbacks are allowed to override some freeze rules - if ((!isAMMLine || globalFreeze) && hasPrivilege(tx, OverrideFreeze)) + // Pre-fixCleanup3_4_0: the isAMMLine check incorrectly blocked clawback on + // individually-frozen or deep-frozen AMM trust lines. + // Post-fixCleanup3_4_0: AMMClawbacks are allowed to override all freeze types. + bool const isAMMLine = change.line->isFlag(lsfAMMNode); + if ((fixOverrideFreeze || !isAMMLine || globalFreeze) && hasPrivilege(tx, OverrideFreeze)) { JLOG(j.debug()) << "Invariant check allowing funds to be moved " << (change.balanceChangeSign > 0 ? "to" : "from") - << " a frozen trustline for AMMClawback " << tx.getTransactionID(); + << " a frozen trustline for a freeze privileged transaction " + << tx.getTransactionID(); return true; } diff --git a/src/test/app/AMMClawback_test.cpp b/src/test/app/AMMClawback_test.cpp index ba416d8192..90bface1fb 100644 --- a/src/test/app/AMMClawback_test.cpp +++ b/src/test/app/AMMClawback_test.cpp @@ -2155,6 +2155,209 @@ class AMMClawback_test : public beast::unit_test::Suite } BEAST_EXPECT(env.balance(carol, eur) == eur(7750)); } + + // gw (USD issuer) individually freezes the AMM-USD trust line. + // AMMClawback must still succeed because the freeze invariant + // short-circuits before reaching the AMM line check (no receivers in + // the USD issuer's change set). Behavior is identical with or without + // fixCleanup3_4_0. + { + Env env(*this, features); + Account const gw{"gateway"}; + Account const gw2{"gateway2"}; + Account const alice{"alice"}; + env.fund(XRP(1000000), gw, gw2, alice); + env.close(); + + env(fset(gw, asfAllowTrustLineClawback)); + env.close(); + env.require(Flags(gw, asfAllowTrustLineClawback)); + + auto const usd = gw["USD"]; + env.trust(usd(100000), alice); + env(pay(gw, alice, usd(3000))); + env.close(); + + auto const eur = gw2["EUR"]; + env.trust(eur(100000), alice); + env(pay(gw2, alice, eur(3000))); + env.close(); + + AMM const amm(env, alice, eur(1000), usd(2000), Ter(tesSUCCESS)); + env.close(); + + BEAST_EXPECT( + amm.expectBalances(usd(2000), eur(1000), IOUAmount{1414213562373095, -12})); + + // gw individually freezes the AMM-USD trust line (AMM pseudo-account + // <-> gw), not alice's trust line. + env(trust(gw, STAmount{Issue{usd.currency, amm.ammAccount()}, 0}, tfSetFreeze)); + env.close(); + + env(amm::ammClawback(gw, alice, usd, eur, usd(1000)), Ter(tesSUCCESS)); + env.close(); + + env.require(Balance(alice, usd(1000))); + env.require(Balance(alice, eur(2500))); + BEAST_EXPECT(amm.expectBalances(usd(1000), eur(500), IOUAmount{7071067811865475, -13})); + BEAST_EXPECT(amm.expectLPTokens(alice, IOUAmount{7071067811865475, -13})); + } + + // gw2 (EUR issuer) individually freezes the AMM-EUR trust line. + // The EUR flow (AMM → alice) is a genuine P2P transfer checked by the + // freeze invariant. Pre-fixCleanup3_4_0 the isAMMNode guard incorrectly + // blocked AMMClawback's overrideFreeze privilege on that trust line. + { + Env env(*this, features); + Account const gw{"gateway"}; + Account const gw2{"gateway2"}; + Account const alice{"alice"}; + env.fund(XRP(1000000), gw, gw2, alice); + env.close(); + + env(fset(gw, asfAllowTrustLineClawback)); + env.close(); + env.require(Flags(gw, asfAllowTrustLineClawback)); + + auto const usd = gw["USD"]; + env.trust(usd(100000), alice); + env(pay(gw, alice, usd(3000))); + env.close(); + + auto const eur = gw2["EUR"]; + env.trust(eur(100000), alice); + env(pay(gw2, alice, eur(3000))); + env.close(); + + AMM const amm(env, alice, eur(1000), usd(2000), Ter(tesSUCCESS)); + env.close(); + + BEAST_EXPECT( + amm.expectBalances(usd(2000), eur(1000), IOUAmount{1414213562373095, -12})); + + // gw2 individually freezes the AMM-EUR trust line. + env(trust(gw2, STAmount{Issue{eur.currency, amm.ammAccount()}, 0}, tfSetFreeze)); + env.close(); + + if (features[fixCleanup3_4_0]) + { + // Post-fixCleanup3_4_0: overrideFreeze privilege applies to + // all freeze types on AMM trust lines. + env(amm::ammClawback(gw, alice, usd, eur, usd(1000)), Ter(tesSUCCESS)); + env.close(); + + env.require(Balance(alice, usd(1000))); + env.require(Balance(alice, eur(2500))); + BEAST_EXPECT( + amm.expectBalances(usd(1000), eur(500), IOUAmount{7071067811865475, -13})); + BEAST_EXPECT(amm.expectLPTokens(alice, IOUAmount{7071067811865475, -13})); + } + else + { + // Pre-fixCleanup3_4_0: the isAMMNode guard prevents the + // overrideFreeze privilege from applying to individually-frozen + // AMM trust lines, so the invariant blocks the clawback. + env(amm::ammClawback(gw, alice, usd, eur, usd(1000)), Ter(tecINVARIANT_FAILED)); + } + } + + // gw2 (EUR issuer) globally freezes its issued assets. AMMClawback + // must still be able to return EUR from the AMM to alice. + { + Env env(*this, features); + Account const gw{"gateway"}; + Account const gw2{"gateway2"}; + Account const alice{"alice"}; + env.fund(XRP(1000000), gw, gw2, alice); + env.close(); + + env(fset(gw, asfAllowTrustLineClawback)); + env.close(); + env.require(Flags(gw, asfAllowTrustLineClawback)); + + auto const usd = gw["USD"]; + env.trust(usd(100000), alice); + env(pay(gw, alice, usd(3000))); + env.close(); + + auto const eur = gw2["EUR"]; + env.trust(eur(100000), alice); + env(pay(gw2, alice, eur(3000))); + env.close(); + + AMM const amm(env, alice, eur(1000), usd(2000), Ter(tesSUCCESS)); + env.close(); + + BEAST_EXPECT( + amm.expectBalances(usd(2000), eur(1000), IOUAmount{1414213562373095, -12})); + + env(fset(gw2, asfGlobalFreeze)); + env.close(); + + env(amm::ammClawback(gw, alice, usd, eur, usd(1000)), Ter(tesSUCCESS)); + env.close(); + + env.require(Balance(alice, usd(1000))); + env.require(Balance(alice, eur(2500))); + BEAST_EXPECT(amm.expectBalances(usd(1000), eur(500), IOUAmount{7071067811865475, -13})); + BEAST_EXPECT(amm.expectLPTokens(alice, IOUAmount{7071067811865475, -13})); + } + + // Same as above but gw2 deep-freezes the AMM-EUR trust line. + if (features[featureDeepFreeze]) + { + Env env(*this, features); + Account const gw{"gateway"}; + Account const gw2{"gateway2"}; + Account const alice{"alice"}; + env.fund(XRP(1000000), gw, gw2, alice); + env.close(); + + env(fset(gw, asfAllowTrustLineClawback)); + env.close(); + env.require(Flags(gw, asfAllowTrustLineClawback)); + + auto const usd = gw["USD"]; + env.trust(usd(100000), alice); + env(pay(gw, alice, usd(3000))); + env.close(); + + auto const eur = gw2["EUR"]; + env.trust(eur(100000), alice); + env(pay(gw2, alice, eur(3000))); + env.close(); + + AMM const amm(env, alice, eur(1000), usd(2000), Ter(tesSUCCESS)); + env.close(); + + BEAST_EXPECT( + amm.expectBalances(usd(2000), eur(1000), IOUAmount{1414213562373095, -12})); + + // gw2 deep-freezes the AMM-EUR trust line. + env(trust( + gw2, + STAmount{Issue{eur.currency, amm.ammAccount()}, 0}, + tfSetFreeze | tfSetDeepFreeze)); + env.close(); + + if (features[fixCleanup3_4_0]) + { + env(amm::ammClawback(gw, alice, usd, eur, usd(1000)), Ter(tesSUCCESS)); + env.close(); + + env.require(Balance(alice, usd(1000))); + env.require(Balance(alice, eur(2500))); + BEAST_EXPECT( + amm.expectBalances(usd(1000), eur(500), IOUAmount{7071067811865475, -13})); + BEAST_EXPECT(amm.expectLPTokens(alice, IOUAmount{7071067811865475, -13})); + } + else + { + // Pre-fixCleanup3_4_0: same isAMMNode guard issue blocks the + // clawback on deep-frozen AMM trust lines. + env(amm::ammClawback(gw, alice, usd, eur, usd(1000)), Ter(tecINVARIANT_FAILED)); + } + } } void @@ -2530,6 +2733,7 @@ class AMMClawback_test : public beast::unit_test::Suite // precision loss caught in transaction layer -> tecPRECISION_LOSS all - fixAMMClawbackRounding - featureMPTokensV2, all - featureMPTokensV2, + all - fixCleanup3_4_0, all}) { testAMMClawbackSpecificAmount(features); From 6f5de9067aedad3ae5f7bb555d102ca67a67fb60 Mon Sep 17 00:00:00 2001 From: Peter Chen <34582813+PeterChen13579@users.noreply.github.com> Date: Mon, 10 Aug 2026 21:37:38 +0000 Subject: [PATCH 14/28] chore: Mark unreachable branches in Confidential Transfer with UNREACHABLE (#7903) --- src/libxrpl/protocol/ConfidentialTransfer.cpp | 91 ++++++++++++++++--- .../token/ConfidentialMPTClawback.cpp | 55 +++++++++-- .../token/ConfidentialMPTConvert.cpp | 53 +++++++++-- .../token/ConfidentialMPTConvertBack.cpp | 46 +++++++++- .../token/ConfidentialMPTMergeInbox.cpp | 35 ++++++- .../transactors/token/ConfidentialMPTSend.cpp | 59 ++++++++++-- 6 files changed, 298 insertions(+), 41 deletions(-) diff --git a/src/libxrpl/protocol/ConfidentialTransfer.cpp b/src/libxrpl/protocol/ConfidentialTransfer.cpp index fe8a08c2ef..ecd4832928 100644 --- a/src/libxrpl/protocol/ConfidentialTransfer.cpp +++ b/src/libxrpl/protocol/ConfidentialTransfer.cpp @@ -4,6 +4,7 @@ #include #include #include +#include #include #include #include @@ -124,7 +125,12 @@ std::optional makeEcPair(Slice const& buffer) { if (buffer.length() != 2 * kEcCiphertextComponentLength) - return std::nullopt; // LCOV_EXCL_LINE + { + // LCOV_EXCL_START + UNREACHABLE("xrpl::makeEcPair : callers must pre-validate ciphertext length"); + return std::nullopt; + // LCOV_EXCL_STOP + } auto parsePubKey = [](Slice const& slice, secp256k1_pubkey& out) { return secp256k1_ec_pubkey_parse(secp256k1Context(), &out, slice.data(), slice.length()); @@ -266,7 +272,13 @@ std::optional encryptCanonicalZeroAmount(Slice const& pubKeySlice, AccountID const& account, MPTID const& mptId) { if (pubKeySlice.size() != kEcPubKeyLength) - return std::nullopt; // LCOV_EXCL_LINE + { + // LCOV_EXCL_START + UNREACHABLE( + "xrpl::encryptCanonicalZeroAmount : callers must pre-validate public key length"); + return std::nullopt; + // LCOV_EXCL_STOP + } EcPair pair{}; secp256k1_pubkey pubKey; @@ -274,14 +286,24 @@ encryptCanonicalZeroAmount(Slice const& pubKeySlice, AccountID const& account, M secp256k1Context(), &pubKey, pubKeySlice.data(), kEcPubKeyLength); res != 1) { - return std::nullopt; // LCOV_EXCL_LINE + // LCOV_EXCL_START + UNREACHABLE( + "xrpl::encryptCanonicalZeroAmount : public key read from the ledger must already be " + "valid"); + return std::nullopt; + // LCOV_EXCL_STOP } if (auto res = generate_canonical_encrypted_zero( secp256k1Context(), &pair.c1, &pair.c2, &pubKey, account.data(), mptId.data()); res != 1) { - return std::nullopt; // LCOV_EXCL_LINE + // LCOV_EXCL_START + UNREACHABLE( + "xrpl::encryptCanonicalZeroAmount : canonical zero generation cannot fail for a " + "valid public key"); + return std::nullopt; + // LCOV_EXCL_STOP } return serializeEcPair(pair); @@ -301,7 +323,11 @@ verifyRevealedAmount( issuer.publicKey.size() != kEcPubKeyLength || issuer.encryptedAmount.size() != kEcGamalEncryptedTotalLength) { - return tecINTERNAL; // LCOV_EXCL_LINE + // LCOV_EXCL_START + UNREACHABLE( + "xrpl::verifyRevealedAmount : callers must pre-validate holder/issuer field lengths"); + return tecINTERNAL; + // LCOV_EXCL_STOP } auto const holderP = toParticipant(holder); @@ -313,7 +339,11 @@ verifyRevealedAmount( if (auditor->publicKey.size() != kEcPubKeyLength || auditor->encryptedAmount.size() != kEcGamalEncryptedTotalLength) { - return tecINTERNAL; // LCOV_EXCL_LINE + // LCOV_EXCL_START + UNREACHABLE( + "xrpl::verifyRevealedAmount : callers must pre-validate auditor field lengths"); + return tecINTERNAL; + // LCOV_EXCL_STOP } auditorP = toParticipant(*auditor); auditorPtr = &auditorP; @@ -337,7 +367,12 @@ checkEncryptedAmountFormat(STObject const& object) if (!object.isFieldPresent(sfHolderEncryptedAmount) || !object.isFieldPresent(sfIssuerEncryptedAmount)) { - return temMALFORMED; // LCOV_EXCL_LINE + // LCOV_EXCL_START + UNREACHABLE( + "xrpl::checkEncryptedAmountFormat : callers already enforce that these fields are " + "present"); + return temMALFORMED; + // LCOV_EXCL_STOP } if (object[sfHolderEncryptedAmount].length() != kEcGamalEncryptedTotalLength || @@ -366,7 +401,12 @@ TER verifySchnorrProof(Slice const& pubKeySlice, Slice const& proofSlice, uint256 const& contextHash) { if (proofSlice.size() != kEcSchnorrProofLength || pubKeySlice.size() != kEcPubKeyLength) - return tecINTERNAL; // LCOV_EXCL_LINE + { + // LCOV_EXCL_START + UNREACHABLE("xrpl::verifySchnorrProof : callers must pre-validate proof/public key length"); + return tecINTERNAL; + // LCOV_EXCL_STOP + } if (mpt_verify_convert_proof(proofSlice.data(), pubKeySlice.data(), contextHash.data()) != 0) return tecBAD_PROOF; @@ -385,7 +425,12 @@ verifyClawbackProof( if (ciphertext.size() != kEcGamalEncryptedTotalLength || pubKeySlice.size() != kEcPubKeyLength || proof.size() != kEcClawbackProofLength) { - return tecINTERNAL; // LCOV_EXCL_LINE + // LCOV_EXCL_START + UNREACHABLE( + "xrpl::verifyClawbackProof : callers must pre-validate ciphertext/public " + "key/proof length"); + return tecINTERNAL; + // LCOV_EXCL_STOP } if (mpt_verify_clawback_proof( @@ -420,7 +465,12 @@ verifySendProof( amountCommitment.size() != kEcPedersenCommitmentLength || balanceCommitment.size() != kEcPedersenCommitmentLength) { - return tecINTERNAL; // LCOV_EXCL_LINE + // LCOV_EXCL_START + UNREACHABLE( + "xrpl::verifySendProof : callers must pre-validate proof/participant/commitment " + "lengths"); + return tecINTERNAL; + // LCOV_EXCL_STOP } std::vector participants; @@ -433,12 +483,22 @@ verifySendProof( if (auditor->publicKey.size() != kEcPubKeyLength || auditor->encryptedAmount.size() != kEcGamalEncryptedTotalLength) { - return tecINTERNAL; // LCOV_EXCL_LINE + // LCOV_EXCL_START + UNREACHABLE("xrpl::verifySendProof : callers must pre-validate auditor field lengths"); + return tecINTERNAL; + // LCOV_EXCL_STOP } participants.push_back(toParticipant(*auditor)); } if (participants.size() != recipientCount) - return tecINTERNAL; // LCOV_EXCL_LINE + { + // LCOV_EXCL_START + UNREACHABLE( + "xrpl::verifySendProof : participant count must match the requested recipient " + "count"); + return tecINTERNAL; + // LCOV_EXCL_STOP + } if (mpt_verify_send_proof( proof.data(), @@ -468,7 +528,12 @@ verifyConvertBackProof( spendingBalance.size() != kEcGamalEncryptedTotalLength || balanceCommitment.size() != kEcPedersenCommitmentLength) { - return tecINTERNAL; // LCOV_EXCL_LINE + // LCOV_EXCL_START + UNREACHABLE( + "xrpl::verifyConvertBackProof : callers must pre-validate proof/public " + "key/balance/commitment lengths"); + return tecINTERNAL; + // LCOV_EXCL_STOP } if (mpt_verify_convert_back_proof( diff --git a/src/libxrpl/tx/transactors/token/ConfidentialMPTClawback.cpp b/src/libxrpl/tx/transactors/token/ConfidentialMPTClawback.cpp index 6366e99105..19ec99702a 100644 --- a/src/libxrpl/tx/transactors/token/ConfidentialMPTClawback.cpp +++ b/src/libxrpl/tx/transactors/token/ConfidentialMPTClawback.cpp @@ -1,6 +1,7 @@ #include #include +#include #include #include #include @@ -70,7 +71,14 @@ ConfidentialMPTClawback::preclaim(PreclaimContext const& ctx) // Sanity check: account must be the same as issuer if (sleIssuance->getAccountID(sfIssuer) != account) - return tefINTERNAL; // LCOV_EXCL_LINE + { + // LCOV_EXCL_START + UNREACHABLE( + "xrpl::ConfidentialMPTClawback::preclaim : preflight already validated the " + "submitter is the issuer"); + return tefINTERNAL; + // LCOV_EXCL_STOP + } // Check if issuance has issuer ElGamal public key if (!sleIssuance->isFieldPresent(sfIssuerEncryptionKey)) @@ -127,7 +135,14 @@ ConfidentialMPTClawback::doApply() auto sleHolderMPToken = view().peek(keylet::mptoken(mptIssuanceID, holder)); if (!sleIssuance || !sleHolderMPToken) - return tecINTERNAL; // LCOV_EXCL_LINE + { + // LCOV_EXCL_START + UNREACHABLE( + "xrpl::ConfidentialMPTClawback::doApply : preclaim already validated these " + "objects exist"); + return tecINTERNAL; + // LCOV_EXCL_STOP + } auto const clawAmount = ctx_.tx[sfMPTAmount]; @@ -137,11 +152,25 @@ ConfidentialMPTClawback::doApply() // After clawback, the balance should be encrypted zero. auto const encZeroForHolder = encryptCanonicalZeroAmount(holderPubKey, holder, mptIssuanceID); if (!encZeroForHolder) - return tecINTERNAL; // LCOV_EXCL_LINE + { + // LCOV_EXCL_START + UNREACHABLE( + "xrpl::ConfidentialMPTClawback::doApply : canonical zero encryption cannot fail " + "for an already-valid holder public key"); + return tecINTERNAL; + // LCOV_EXCL_STOP + } auto encZeroForIssuer = encryptCanonicalZeroAmount(issuerPubKey, holder, mptIssuanceID); if (!encZeroForIssuer) - return tecINTERNAL; // LCOV_EXCL_LINE + { + // LCOV_EXCL_START + UNREACHABLE( + "xrpl::ConfidentialMPTClawback::doApply : canonical zero encryption cannot fail " + "for an already-valid issuer public key"); + return tecINTERNAL; + // LCOV_EXCL_STOP + } // Set holder's confidential balances to encrypted zero (*sleHolderMPToken)[sfConfidentialBalanceInbox] = *encZeroForHolder; @@ -154,14 +183,28 @@ ConfidentialMPTClawback::doApply() // Sanity check: the issuance must have an auditor public key if // auditing is enabled. if (!sleIssuance->isFieldPresent(sfAuditorEncryptionKey)) - return tecINTERNAL; // LCOV_EXCL_LINE + { + // LCOV_EXCL_START + UNREACHABLE( + "xrpl::ConfidentialMPTClawback::doApply : the holder's auditor balance implies " + "the issuance has an auditor public key"); + return tecINTERNAL; + // LCOV_EXCL_STOP + } auto const auditorPubKey = (*sleIssuance)[sfAuditorEncryptionKey]; auto encZeroForAuditor = encryptCanonicalZeroAmount(auditorPubKey, holder, mptIssuanceID); if (!encZeroForAuditor) - return tecINTERNAL; // LCOV_EXCL_LINE + { + // LCOV_EXCL_START + UNREACHABLE( + "xrpl::ConfidentialMPTClawback::doApply : canonical zero encryption cannot " + "fail for an already-valid auditor public key"); + return tecINTERNAL; + // LCOV_EXCL_STOP + } (*sleHolderMPToken)[sfAuditorEncryptedBalance] = std::move(*encZeroForAuditor); } diff --git a/src/libxrpl/tx/transactors/token/ConfidentialMPTConvert.cpp b/src/libxrpl/tx/transactors/token/ConfidentialMPTConvert.cpp index 454eb39ead..5be3892151 100644 --- a/src/libxrpl/tx/transactors/token/ConfidentialMPTConvert.cpp +++ b/src/libxrpl/tx/transactors/token/ConfidentialMPTConvert.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #include #include #include @@ -89,7 +90,14 @@ ConfidentialMPTConvert::preclaim(PreclaimContext const& ctx) // already checked in preflight, but should also check that issuer on the // issuance isn't the account either if (sleIssuance->getAccountID(sfIssuer) == account) - return tefINTERNAL; // LCOV_EXCL_LINE + { + // LCOV_EXCL_START + UNREACHABLE( + "xrpl::ConfidentialMPTConvert::preclaim : issuer derived from the MPT ID must " + "match the ledger's stored issuer"); + return tefINTERNAL; + // LCOV_EXCL_STOP + } bool const hasAuditor = ctx.tx.isFieldPresent(sfAuditorEncryptedAmount); bool const requiresAuditor = sleIssuance->isFieldPresent(sfAuditorEncryptionKey); @@ -207,11 +215,25 @@ ConfidentialMPTConvert::doApply() auto sleMptoken = view().peek(keylet::mptoken(mptIssuanceID, accountID_)); if (!sleMptoken) - return tecINTERNAL; // LCOV_EXCL_LINE + { + // LCOV_EXCL_START + UNREACHABLE( + "xrpl::ConfidentialMPTConvert::doApply : preclaim already validated the MPToken " + "exists"); + return tecINTERNAL; + // LCOV_EXCL_STOP + } auto sleIssuance = view().peek(keylet::mptokenIssuance(mptIssuanceID)); if (!sleIssuance) - return tecINTERNAL; // LCOV_EXCL_LINE + { + // LCOV_EXCL_START + UNREACHABLE( + "xrpl::ConfidentialMPTConvert::doApply : preclaim already validated the issuance " + "exists"); + return tecINTERNAL; + // LCOV_EXCL_STOP + } auto const amtToConvert = ctx_.tx[sfMPTAmount]; auto const amt = (*sleMptoken)[~sfMPTAmount].valueOr(0); @@ -273,7 +295,14 @@ ConfidentialMPTConvert::doApply() if (auditorEc) { if (!sleMptoken->isFieldPresent(sfAuditorEncryptedBalance)) - return tecINTERNAL; // LCOV_EXCL_LINE + { + // LCOV_EXCL_START + UNREACHABLE( + "xrpl::ConfidentialMPTConvert::doApply : issuance-level auditing implies " + "the MPToken already carries an auditor balance"); + return tecINTERNAL; + // LCOV_EXCL_STOP + } auto sum = homomorphicAdd(*auditorEc, (*sleMptoken)[sfAuditorEncryptedBalance]); if (!sum) @@ -308,7 +337,14 @@ ConfidentialMPTConvert::doApply() (*sleMptoken)[sfHolderEncryptionKey], accountID_, mptIssuanceID); if (!zeroBalance) - return tecINTERNAL; // LCOV_EXCL_LINE + { + // LCOV_EXCL_START + UNREACHABLE( + "xrpl::ConfidentialMPTConvert::doApply : canonical zero encryption cannot fail " + "for an already-valid holder public key"); + return tecINTERNAL; + // LCOV_EXCL_STOP + } (*sleMptoken)[sfConfidentialBalanceSpending] = std::move(*zeroBalance); } @@ -316,7 +352,12 @@ ConfidentialMPTConvert::doApply() { // both sfIssuerEncryptedBalance and sfConfidentialBalanceInbox should // exist together - return tecINTERNAL; // LCOV_EXCL_LINE + // LCOV_EXCL_START + UNREACHABLE( + "xrpl::ConfidentialMPTConvert::doApply : confidential balance fields must be all " + "present or all absent"); + return tecINTERNAL; + // LCOV_EXCL_STOP } view().update(sleIssuance); diff --git a/src/libxrpl/tx/transactors/token/ConfidentialMPTConvertBack.cpp b/src/libxrpl/tx/transactors/token/ConfidentialMPTConvertBack.cpp index 87f9e476d6..1e3617ffbd 100644 --- a/src/libxrpl/tx/transactors/token/ConfidentialMPTConvertBack.cpp +++ b/src/libxrpl/tx/transactors/token/ConfidentialMPTConvertBack.cpp @@ -2,6 +2,7 @@ #include #include +#include #include #include #include @@ -72,7 +73,14 @@ verifyProofs( std::shared_ptr const& mptoken) { if (!mptoken->isFieldPresent(sfHolderEncryptionKey)) - return tecINTERNAL; // LCOV_EXCL_LINE + { + // LCOV_EXCL_START + UNREACHABLE( + "xrpl::verifyProofs : preclaim already validated the holder encryption key is " + "present"); + return tecINTERNAL; + // LCOV_EXCL_STOP + } auto const mptIssuanceID = tx[sfMPTokenIssuanceID]; auto const account = tx[sfAccount]; @@ -169,7 +177,14 @@ ConfidentialMPTConvertBack::preclaim(PreclaimContext const& ctx) // already checked in preflight, but should also check that issuer on // the issuance isn't the account either if (sleIssuance->getAccountID(sfIssuer) == account) - return tefINTERNAL; // LCOV_EXCL_LINE + { + // LCOV_EXCL_START + UNREACHABLE( + "xrpl::ConfidentialMPTConvertBack::preclaim : issuer derived from the MPT ID must " + "match the ledger's stored issuer"); + return tefINTERNAL; + // LCOV_EXCL_STOP + } auto const sleMptoken = ctx.view.read(keylet::mptoken(mptIssuanceID, account)); if (!sleMptoken) @@ -185,7 +200,14 @@ ConfidentialMPTConvertBack::preclaim(PreclaimContext const& ctx) // Sanity check: holder's MPToken must have auditor balance field if auditing // is enabled if (requiresAuditor && !sleMptoken->isFieldPresent(sfAuditorEncryptedBalance)) - return tefINTERNAL; // LCOV_EXCL_LINE + { + // LCOV_EXCL_START + UNREACHABLE( + "xrpl::ConfidentialMPTConvertBack::preclaim : issuance-level auditing implies the " + "MPToken already carries an auditor balance"); + return tefINTERNAL; + // LCOV_EXCL_STOP + } // if the total circulating confidential balance is smaller than what the // holder is trying to convert back, we know for sure this txn should @@ -215,11 +237,25 @@ ConfidentialMPTConvertBack::doApply() auto sleMptoken = view().peek(keylet::mptoken(mptIssuanceID, accountID_)); if (!sleMptoken) - return tecINTERNAL; // LCOV_EXCL_LINE + { + // LCOV_EXCL_START + UNREACHABLE( + "xrpl::ConfidentialMPTConvertBack::doApply : preclaim already validated the " + "MPToken exists"); + return tecINTERNAL; + // LCOV_EXCL_STOP + } auto sleIssuance = view().peek(keylet::mptokenIssuance(mptIssuanceID)); if (!sleIssuance) - return tecINTERNAL; // LCOV_EXCL_LINE + { + // LCOV_EXCL_START + UNREACHABLE( + "xrpl::ConfidentialMPTConvertBack::doApply : preclaim already validated the " + "issuance exists"); + return tecINTERNAL; + // LCOV_EXCL_STOP + } auto const amtToConvertBack = ctx_.tx[sfMPTAmount]; auto const amt = (*sleMptoken)[~sfMPTAmount].valueOr(0); diff --git a/src/libxrpl/tx/transactors/token/ConfidentialMPTMergeInbox.cpp b/src/libxrpl/tx/transactors/token/ConfidentialMPTMergeInbox.cpp index 0b98382a61..6485578cb4 100644 --- a/src/libxrpl/tx/transactors/token/ConfidentialMPTMergeInbox.cpp +++ b/src/libxrpl/tx/transactors/token/ConfidentialMPTMergeInbox.cpp @@ -2,6 +2,7 @@ #include #include +#include #include #include #include @@ -49,7 +50,14 @@ ConfidentialMPTMergeInbox::preclaim(PreclaimContext const& ctx) // already checked in preflight, but should also check that issuer on the // issuance isn't the account either if (sleIssuance->getAccountID(sfIssuer) == ctx.tx[sfAccount]) - return tefINTERNAL; // LCOV_EXCL_LINE + { + // LCOV_EXCL_START + UNREACHABLE( + "xrpl::ConfidentialMPTMergeInbox::preclaim : issuer derived from the MPT ID must " + "match the ledger's stored issuer"); + return tefINTERNAL; + // LCOV_EXCL_STOP + } auto const sleMptoken = ctx.view.read(keylet::mptoken(ctx.tx[sfMPTokenIssuanceID], ctx.tx[sfAccount])); @@ -82,14 +90,26 @@ ConfidentialMPTMergeInbox::doApply() auto const mptIssuanceID = ctx_.tx[sfMPTokenIssuanceID]; auto sleMptoken = view().peek(keylet::mptoken(mptIssuanceID, accountID_)); if (!sleMptoken) - return tecINTERNAL; // LCOV_EXCL_LINE + { + // LCOV_EXCL_START + UNREACHABLE( + "xrpl::ConfidentialMPTMergeInbox::doApply : preclaim already validated the " + "MPToken exists"); + return tecINTERNAL; + // LCOV_EXCL_STOP + } // sanity check if (!sleMptoken->isFieldPresent(sfConfidentialBalanceSpending) || !sleMptoken->isFieldPresent(sfConfidentialBalanceInbox) || !sleMptoken->isFieldPresent(sfHolderEncryptionKey)) { - return tecINTERNAL; // LCOV_EXCL_LINE + // LCOV_EXCL_START + UNREACHABLE( + "xrpl::ConfidentialMPTMergeInbox::doApply : preclaim already validated these " + "fields are present"); + return tecINTERNAL; + // LCOV_EXCL_STOP } // Merge inbox into spending: spending = spending + inbox @@ -114,7 +134,14 @@ ConfidentialMPTMergeInbox::doApply() encryptCanonicalZeroAmount((*sleMptoken)[sfHolderEncryptionKey], accountID_, mptIssuanceID); if (!zeroEncryption) - return tecINTERNAL; // LCOV_EXCL_LINE + { + // LCOV_EXCL_START + UNREACHABLE( + "xrpl::ConfidentialMPTMergeInbox::doApply : canonical zero encryption cannot fail " + "for an already-valid holder public key"); + return tecINTERNAL; + // LCOV_EXCL_STOP + } (*sleMptoken)[sfConfidentialBalanceInbox] = std::move(*zeroEncryption); diff --git a/src/libxrpl/tx/transactors/token/ConfidentialMPTSend.cpp b/src/libxrpl/tx/transactors/token/ConfidentialMPTSend.cpp index f4c7b98c41..e713ae5029 100644 --- a/src/libxrpl/tx/transactors/token/ConfidentialMPTSend.cpp +++ b/src/libxrpl/tx/transactors/token/ConfidentialMPTSend.cpp @@ -2,6 +2,7 @@ #include #include +#include #include #include #include @@ -105,7 +106,14 @@ verifySendProofs( { // Sanity check if (!sleSenderMPToken || !sleDestinationMPToken || !sleIssuance) - return tecINTERNAL; // LCOV_EXCL_LINE + { + // LCOV_EXCL_START + UNREACHABLE( + "xrpl::detail::verifySendProofs : caller must pre-validate sender/destination/" + "issuance existence"); + return tecINTERNAL; + // LCOV_EXCL_STOP + } auto const hasAuditor = ctx.tx.isFieldPresent(sfAuditorEncryptedAmount); @@ -204,7 +212,14 @@ ConfidentialMPTSend::preclaim(PreclaimContext const& ctx) // Sanity check: issuer isn't the sender if (sleIssuance->getAccountID(sfIssuer) == ctx.tx[sfAccount]) - return tefINTERNAL; // LCOV_EXCL_LINE + { + // LCOV_EXCL_START + UNREACHABLE( + "xrpl::ConfidentialMPTSend::preclaim : issuer derived from the MPT ID must match " + "the ledger's stored issuer"); + return tefINTERNAL; + // LCOV_EXCL_STOP + } // Check sender's MPToken existence auto const sleSenderMPToken = ctx.view.read(keylet::mptoken(mptIssuanceID, account)); @@ -238,7 +253,12 @@ ConfidentialMPTSend::preclaim(PreclaimContext const& ctx) (!sleSenderMPToken->isFieldPresent(sfAuditorEncryptedBalance) || !sleDestinationMPToken->isFieldPresent(sfAuditorEncryptedBalance))) { - return tefINTERNAL; // LCOV_EXCL_LINE + // LCOV_EXCL_START + UNREACHABLE( + "xrpl::ConfidentialMPTSend::preclaim : issuance-level auditing implies both " + "MPTokens already carry an auditor balance"); + return tefINTERNAL; + // LCOV_EXCL_STOP } // Check lock @@ -283,7 +303,14 @@ ConfidentialMPTSend::doApply() auto const sleDestAcct = view().read(keylet::account(destination)); if (!sleSenderMPToken || !sleDestinationMPToken || !sleIssuance || !sleDestAcct) - return tecINTERNAL; // LCOV_EXCL_LINE + { + // LCOV_EXCL_START + UNREACHABLE( + "xrpl::ConfidentialMPTSend::doApply : preclaim already validated these objects " + "exist"); + return tecINTERNAL; + // LCOV_EXCL_STOP + } // Deposit preauth authorization was already verified in preclaim. // Remove any expired credentials. @@ -353,7 +380,13 @@ ConfidentialMPTSend::doApply() auto rerandomizedDestEc = rerandomizeCiphertext( destEc, (*sleDestinationMPToken)[sfHolderEncryptionKey], sendChallenge); if (!rerandomizedDestEc) - return tecINTERNAL; // LCOV_EXCL_LINE + { + // LCOV_EXCL_START + JLOG(ctx_.journal.error()) + << "ConfidentialMPTSend failed to rerandomize destination inbox ciphertext."; + return tecINTERNAL; + // LCOV_EXCL_STOP + } auto const curInbox = (*sleDestinationMPToken)[sfConfidentialBalanceInbox]; auto newInbox = homomorphicAdd(curInbox, *rerandomizedDestEc); @@ -374,7 +407,13 @@ ConfidentialMPTSend::doApply() auto rerandomizedIssuerEc = rerandomizeCiphertext(issuerEc, (*sleIssuance)[sfIssuerEncryptionKey], sendChallenge); if (!rerandomizedIssuerEc) - return tecINTERNAL; // LCOV_EXCL_LINE + { + // LCOV_EXCL_START + JLOG(ctx_.journal.error()) + << "ConfidentialMPTSend failed to rerandomize destination issuer ciphertext."; + return tecINTERNAL; + // LCOV_EXCL_STOP + } auto const curIssuerEnc = (*sleDestinationMPToken)[sfIssuerEncryptedBalance]; auto newIssuerEnc = homomorphicAdd(curIssuerEnc, *rerandomizedIssuerEc); @@ -396,7 +435,13 @@ ConfidentialMPTSend::doApply() auto rerandomizedAuditorEc = rerandomizeCiphertext( *auditorEc, (*sleIssuance)[sfAuditorEncryptionKey], sendChallenge); if (!rerandomizedAuditorEc) - return tecINTERNAL; // LCOV_EXCL_LINE + { + // LCOV_EXCL_START + JLOG(ctx_.journal.error()) + << "ConfidentialMPTSend failed to rerandomize destination auditor ciphertext."; + return tecINTERNAL; + // LCOV_EXCL_STOP + } auto const curAuditorEnc = (*sleDestinationMPToken)[sfAuditorEncryptedBalance]; auto newAuditorEnc = homomorphicAdd(curAuditorEnc, *rerandomizedAuditorEc); From 909cc5bba90879b6187595d46af743fa38df7c51 Mon Sep 17 00:00:00 2001 From: Bryan Date: Mon, 10 Aug 2026 21:37:53 +0000 Subject: [PATCH 15/28] fix: Prevent silent zero AMM clawbacks due to integer MPT rounding (#7704) Co-authored-by: Bart --- .../tx/transactors/dex/AMMClawback.cpp | 9 +- src/test/app/AMMClawbackMPT_test.cpp | 155 +++++++++++++++++- 2 files changed, 156 insertions(+), 8 deletions(-) diff --git a/src/libxrpl/tx/transactors/dex/AMMClawback.cpp b/src/libxrpl/tx/transactors/dex/AMMClawback.cpp index c1ef9f875e..455b2ad5c5 100644 --- a/src/libxrpl/tx/transactors/dex/AMMClawback.cpp +++ b/src/libxrpl/tx/transactors/dex/AMMClawback.cpp @@ -256,7 +256,7 @@ AMMClawback::applyGuts(Sandbox& sb) } if (!isTesSuccess(result)) - return result; // LCOV_EXCL_LINE + return result; if (sb.rules().enabled(fixCleanup3_3_0) && sb.rules().enabled(fixAMMv1_3)) { @@ -353,6 +353,13 @@ AMMClawback::equalWithdrawMatchingOneAmount( auto amountRounded = getRoundedAsset(rules, amountBalance, frac, IsDeposit::No); + // The requested clawback amount is likely too small and results in + // one-sided pool withdrawal due to round off. Fail so the issuer can + // clawback a larger amount. + if (rules.enabled(fixCleanup3_4_0) && + (amountRounded == beast::kZero || amount2Rounded == beast::kZero)) + return {tecAMM_FAILED, STAmount{}, STAmount{}, STAmount{}}; + return AMMWithdraw::withdraw( sb, ammSle, diff --git a/src/test/app/AMMClawbackMPT_test.cpp b/src/test/app/AMMClawbackMPT_test.cpp index 6facafde4a..6c7aa99156 100644 --- a/src/test/app/AMMClawbackMPT_test.cpp +++ b/src/test/app/AMMClawbackMPT_test.cpp @@ -137,7 +137,6 @@ class AMMClawbackMPT_test : public beast::unit_test::Suite AMM amm(env, gw, btc(100), usd(100)); env.close(); amm.deposit(alice, 1'000); - env.close(); // can not clawback when tfMPTCanClawback is not enabled env(amm::ammClawback(gw, alice, btc, usd, std::nullopt), Ter(tecNO_PERMISSION)); @@ -503,6 +502,150 @@ class AMMClawbackMPT_test : public beast::unit_test::Suite } } + void + testAMMClawbackAmountRoundsToZero(FeatureBitset features) + { + // Ensure a clawback that rounds down to zero MPT fails with + // tecAMM_FAILED instead of silently burning the holder's LP. + testcase("test AMMClawback amount that rounds down to zero"); + using namespace jtx; + + Env env(*this, features); + Account const gw{"gateway"}; + Account const alice{"alice"}; + Account const bob{"bob"}; + env.fund(XRP(10'000'000), gw, alice, bob); + env.close(); + + env(fset(gw, asfAllowTrustLineClawback)); + env.close(); + + // The clawed asset (amountRounded) rounds to zero while its XRP + // counterpart is always large. + { + MPTTester const mptBtc( + {.env = env, + .issuer = gw, + .holders = {alice, bob}, + .pay = 1'000, + .flags = tfMPTCanClawback | kMptDexFlags}); + MPT const btc = mptBtc; + + AMM amm(env, alice, btc(3), XRP(333'000)); + amm.deposit(bob, btc(3), XRP(333'000)); + + [[maybe_unused]] auto const [poolBtcBefore, poolXrpBefore, lptBefore] = amm.balances(); + BEAST_EXPECT(poolBtcBefore == btc(6)); + + auto const issuerOABefore = mptBtc.getBalance(gw); + auto const aliceLpBefore = amm.getLPTokensBalance(alice.id()); + auto const bobLpBefore = amm.getLPTokensBalance(bob.id()); + + // Attempt to clawback 1/6th of the BTC pool. When the zero-rounding + // guard is active (gated by fixCleanup3_4_0) the rounded amount + // drops to 0 and should trigger tecAMM_FAILED. + env(amm::ammClawback(gw, alice, btc, XRP, btc(1)), + Ter(features[fixCleanup3_4_0] ? TER{tecAMM_FAILED} : TER{tesSUCCESS})); + env.close(); + + [[maybe_unused]] auto const [poolBtcAfter, poolXrpAfter, lptAfter] = amm.balances(); + auto const issuerOAAfter = mptBtc.getBalance(gw); + auto const aliceLpAfter = amm.getLPTokensBalance(alice.id()); + auto const bobLpAfter = amm.getLPTokensBalance(bob.id()); + + if (features[fixCleanup3_4_0]) + { + // Post-fixCleanup3_4_0: Clawback fails because the BTC balance + // would round to zero. All balances must remain untouched. + BEAST_EXPECT(poolBtcAfter == poolBtcBefore); + BEAST_EXPECT(poolXrpAfter == poolXrpBefore); + BEAST_EXPECT(issuerOAAfter == issuerOABefore); + BEAST_EXPECT(aliceLpAfter == aliceLpBefore); + BEAST_EXPECT(bobLpAfter == bobLpBefore); + } + else + { + // Pre-fixCleanup3_4_0: BTC rounds to zero and the clawback + // silently burns alice's LP without clawing back any BTC. + BEAST_EXPECT(poolBtcAfter == poolBtcBefore); + BEAST_EXPECT(poolXrpAfter < poolXrpBefore); + BEAST_EXPECT(issuerOAAfter == issuerOABefore); + BEAST_EXPECT(aliceLpAfter < aliceLpBefore); + BEAST_EXPECT(bobLpAfter == bobLpBefore); + } + } + + // The pool above only ever rounds the clawed asset (amountRounded) to + // zero; its XRP counterpart is always large. Exercise the other operand + // of the guard (amount2Rounded == 0) with an MPT/MPT pool where the + // *paired* asset is the tiny integer that floors to zero while the + // clawed asset still rounds non-zero. + { + Account const carol{"carol"}; + Account const dan{"dan"}; + env.fund(XRP(10'000'000), carol, dan); + env.close(); + + MPTTester const mptBtc( + {.env = env, + .issuer = gw, + .holders = {carol, dan}, + .pay = 100'000, + .flags = tfMPTCanClawback | kMptDexFlags}); + MPT const btc = mptBtc; + + MPTTester const mptEth( + {.env = env, + .issuer = gw, + .holders = {carol, dan}, + .pay = 1'000, + .flags = tfMPTCanClawback | kMptDexFlags}); + MPT const eth = mptEth; + + // btc pool dwarfs the eth pool, so a ~1/12th claw withdraws a + // non-zero btc amount while the eth counterpart rounds to zero. + AMM amm(env, carol, btc(3'000), eth(3)); + amm.deposit(dan, btc(3'000), eth(3)); + + [[maybe_unused]] auto const [poolBtcBefore, poolEthBefore, lptBefore] = amm.balances(); + BEAST_EXPECT(poolBtcBefore == btc(6'000)); + BEAST_EXPECT(poolEthBefore == eth(6)); + + auto const carolLpBefore = amm.getLPTokensBalance(carol.id()); + auto const danLpBefore = amm.getLPTokensBalance(dan.id()); + + env(amm::ammClawback(gw, carol, btc, eth, btc(500)), + Ter(features[fixCleanup3_4_0] ? TER{tecAMM_FAILED} : TER{tesSUCCESS})); + env.close(); + + [[maybe_unused]] auto const [poolBtcAfter, poolEthAfter, lptAfter] = amm.balances(); + auto const carolLpAfter = amm.getLPTokensBalance(carol.id()); + auto const danLpAfter = amm.getLPTokensBalance(dan.id()); + + if (features[fixCleanup3_4_0]) + { + // Post-fixCleanup3_4_0: clawback fails because the ETH (Asset2) + // balance would round to zero (guard fires via + // amount2Rounded == 0). All balances must remain untouched. + BEAST_EXPECT(poolBtcAfter == poolBtcBefore); + BEAST_EXPECT(poolEthAfter == poolEthBefore); + BEAST_EXPECT(carolLpAfter == carolLpBefore); + BEAST_EXPECT(danLpAfter == danLpBefore); + } + else + { + // Pre-fixCleanup3_4_0: the asymmetric round-off goes through. + // btc is clawed (non-zero) but eth rounds to zero, so the eth + // pool is untouched while carol's LP is burned. This asymmetry + // proves amount2Rounded == 0 is the trigger. + BEAST_EXPECT(poolBtcAfter < poolBtcBefore); + BEAST_EXPECT(poolEthAfter == poolEthBefore); + BEAST_EXPECT(carolLpAfter < carolLpBefore); + BEAST_EXPECT(danLpAfter == danLpBefore); + } + } + } + void testAMMClawbackAll(FeatureBitset features) { @@ -543,7 +686,6 @@ class AMMClawbackMPT_test : public beast::unit_test::Suite // gw clawback all BTC from alice amm.deposit(bob, btc(1'000'000000), usd(2000)); - env.close(); BEAST_EXPECT(amm.expectBalances(btc(3'000'000000), usd(3000), IOUAmount(3000000))); auto aliceBTC = env.balance(alice, btc); @@ -921,7 +1063,6 @@ class AMMClawbackMPT_test : public beast::unit_test::Suite BEAST_EXPECT(amm.expectBalances(btc(2'000'000000), usd(8'000), IOUAmount(4'000'000))); amm.deposit(bob, btc(1'000'000000), usd(4'000)); - env.close(); BEAST_EXPECT(amm.expectBalances(btc(3'000'000000), usd(12'000), IOUAmount(6'000'000))); auto aliceBTC = env.balance(alice, btc); @@ -1361,7 +1502,6 @@ class AMMClawbackMPT_test : public beast::unit_test::Suite env.close(); BEAST_EXPECT(amm.expectBalances(XRP(100), btc(400), IOUAmount(200000))); amm.deposit(alice, btc(400)); - env.close(); BEAST_EXPECT(amm.expectBalances(XRP(100), btc(800), IOUAmount{282842'712474619, -9})); auto aliceBTC = env.balance(alice, MPT(btc)); @@ -1407,7 +1547,6 @@ class AMMClawbackMPT_test : public beast::unit_test::Suite env.close(); BEAST_EXPECT(amm.expectBalances(usd(100), btc(400), IOUAmount(200))); amm.deposit(alice, btc(400)); - env.close(); BEAST_EXPECT(amm.expectBalances(usd(100), btc(800), IOUAmount{282'842712474619, -12})); auto aliceBTC = env.balance(alice, MPT(btc)); @@ -1462,7 +1601,6 @@ class AMMClawbackMPT_test : public beast::unit_test::Suite env.close(); BEAST_EXPECT(amm.expectBalances(usd(100), btc(400), IOUAmount(200))); amm.deposit(alice, btc(400)); - env.close(); BEAST_EXPECT(amm.expectBalances(usd(100), btc(800), IOUAmount{282'842712474619, -12})); auto aliceBTC = env.balance(alice, MPT(btc)); @@ -1669,7 +1807,7 @@ class AMMClawbackMPT_test : public beast::unit_test::Suite env(amm::ammClawback(gw, alice, btc, usd, std::nullopt), Ter(tecNO_PERMISSION)); // Although USD is clawable with asfAllowTrustLineClawback. - // When tfClawTwoAssets is set, we will claw Asser2 as well. + // When tfClawTwoAssets is set, we will claw Asset2 as well. // But Asset2 is not clawable. tfMPTCanClawback was not set for BTC. env(amm::ammClawback(gw, alice, usd, btc, std::nullopt), Txflags(tfClawTwoAssets), @@ -1819,6 +1957,9 @@ class AMMClawbackMPT_test : public beast::unit_test::Suite testInvalidRequest(all); testFeatureDisabled(all); testAMMClawbackAmount(all); + testAMMClawbackAmount(all - fixCleanup3_4_0); + testAMMClawbackAmountRoundsToZero(all); + testAMMClawbackAmountRoundsToZero(all - fixCleanup3_4_0); testAMMClawbackAll(all); testAMMClawbackAmountSameIssuer(all); testAMMClawbackAllSameIssuer(all); From 639943123cced8fe0aff2b6477527558d31b5f6b Mon Sep 17 00:00:00 2001 From: Chenna Keshava B S <21219765+ckeshava@users.noreply.github.com> Date: Tue, 11 Aug 2026 00:49:02 +0000 Subject: [PATCH 16/28] fix: Validate buy/sell flag in nft RPC input (#7725) --- src/test/app/NFToken_test.cpp | 82 +++++++++++++++++++ .../rpc/handlers/orderbook/NFTOffersHelpers.h | 11 +++ 2 files changed, 93 insertions(+) diff --git a/src/test/app/NFToken_test.cpp b/src/test/app/NFToken_test.cpp index a7437eea7f..7fcd34640b 100644 --- a/src/test/app/NFToken_test.cpp +++ b/src/test/app/NFToken_test.cpp @@ -4790,6 +4790,87 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite checkOffers("nft_buy_offers", 501, 2, __LINE__); } + void + testNftXxxOffersMarkerWrongSide(FeatureBitset features) + { + // A pagination marker passed to nft_buy_offers / nft_sell_offers must + // reference an offer on the same side (buy vs. sell) as the directory + // being enumerated. A wrong-side marker is rejected with invalidParams. + // + // Note: the pre-fix code also returned invalidParams for a wrong-side + // marker, but only after scanning the entire target directory (an + // O(directory size) walk usable to burn CPU). The fix short-circuits + // that scan. The scan-avoidance is not observable from the RPC + // response, so this test locks the rejection contract (wrong-side -> + // error, same-side -> success) rather than the performance property. + testcase("nft_buy_offers and nft_sell_offers wrong-side marker"); + + using namespace test::jtx; + + Env env{*this, features}; + + Account const issuer{"issuer"}; + Account const buyer{"buyer"}; + + env.fund(XRP(10000), issuer, buyer); + env.close(); + + // Mint a transferable NFT. + uint256 const nftID{token::getNextID(env, issuer, 0u, tfTransferable)}; + env(token::mint(issuer, 0), Txflags(tfTransferable)); + env.close(); + + // Create one sell offer (from the issuer, who owns the NFT) and one + // buy offer (from the buyer) for the same NFT. + env(token::createOffer(issuer, nftID, XRP(100)), Txflags(tfSellNFToken)); + env(token::createOffer(buyer, nftID, XRP(50)), token::Owner(issuer)); + env.close(); + + // Grab the index of the single offer on each side from the RPC + // response so we can use it as a marker. + auto firstOfferIndex = [this, &env, &nftID](char const* request) { + json::Value params; + params[jss::nft_id] = to_string(nftID); + json::Value const result = env.rpc("json", request, to_string(params))[jss::result]; + BEAST_EXPECT(result.isMember(jss::offers) && result[jss::offers].size() == 1); + return result[jss::offers][0u][jss::nft_offer_index].asString(); + }; + + std::string const sellOfferIndex = firstOfferIndex("nft_sell_offers"); + std::string const buyOfferIndex = firstOfferIndex("nft_buy_offers"); + + auto queryWithMarker = [&env, &nftID](char const* request, std::string const& marker) { + json::Value params; + params[jss::nft_id] = to_string(nftID); + params[jss::marker] = marker; + return env.rpc("json", request, to_string(params))[jss::result]; + }; + + // A marker referencing an offer on the wrong side is rejected with + // invalidParams. + { + // Sell-side marker passed to nft_buy_offers. + json::Value const result = queryWithMarker("nft_buy_offers", sellOfferIndex); + BEAST_EXPECT(result[jss::error].asString() == "invalidParams"); + } + { + // Buy-side marker passed to nft_sell_offers. + json::Value const result = queryWithMarker("nft_sell_offers", buyOfferIndex); + BEAST_EXPECT(result[jss::error].asString() == "invalidParams"); + } + + // A same-side marker is still accepted. With a single offer on each + // side, resuming after it simply yields no further offers. + { + json::Value const result = queryWithMarker("nft_buy_offers", buyOfferIndex); + BEAST_EXPECT(!result.isMember(jss::error)); + } + { + json::Value const result = queryWithMarker("nft_sell_offers", sellOfferIndex); + BEAST_EXPECT(!result.isMember(jss::error)); + } + } + void testNFTokenNegOffer(FeatureBitset features) { @@ -7305,6 +7386,7 @@ protected: testNFTokenWithTickets(features); testNFTokenDeleteAccount(features); testNftXxxOffers(features); + testNftXxxOffersMarkerWrongSide(features); testNFTokenNegOffer(features); testIOUWithTransferFee(features); testBrokeredSaleToSelf(features); diff --git a/src/xrpld/rpc/handlers/orderbook/NFTOffersHelpers.h b/src/xrpld/rpc/handlers/orderbook/NFTOffersHelpers.h index e03830ae0d..21bf3f8be8 100644 --- a/src/xrpld/rpc/handlers/orderbook/NFTOffersHelpers.h +++ b/src/xrpld/rpc/handlers/orderbook/NFTOffersHelpers.h @@ -93,6 +93,17 @@ enumerateNFTOffers(rpc::JsonContext& context, uint256 const& nftId, Keylet const if (!sle || nftId != sle->getFieldH256(sfNFTokenID)) return rpcError(RpcInvalidParams); + // Reject a marker that references an offer on the opposite side + // (buy vs. sell) of the directory being enumerated. Without this + // check the marker's node hint points into the other directory, so + // forEachItemAfter never finds `startAfter` and instead scans every + // page of `directory` before returning invalidParams -- turning an + // O(1) rejection into an O(directory size) walk. + auto const offerDir = + sle->isFlag(lsfSellNFToken) ? keylet::nftSells(nftId) : keylet::nftBuys(nftId); + if (directory.key != offerDir.key) + return rpcError(RpcInvalidParams); + startHint = sle->getFieldU64(sfNFTokenOfferNode); appendNftOfferJson(context.app, sle, jsonOffers); offers.reserve(reserve); From 0a572833eae96c28a30e7f5dcc143fb26cfa33bd Mon Sep 17 00:00:00 2001 From: Alex Kremer Date: Tue, 11 Aug 2026 12:38:40 +0000 Subject: [PATCH 17/28] chore: Gtest migration followups second pass (#7888) --- .cspell.config.yaml | 1 + cmake/XrplCov.cmake | 1 + include/xrpl/basics/Buffer.h | 14 + src/benchmarks/libxrpl/nodestore/Backend.cpp | 19 +- .../libxrpl/nodestore/NodeStoreBench.h | 5 +- src/tests/libxrpl/basics/Buffer.cpp | 517 +++++++++++------- src/tests/libxrpl/basics/IntrusiveShared.cpp | 11 +- src/tests/libxrpl/basics/base_uint.cpp | 227 ++++---- src/tests/libxrpl/shamap/SHAMap.cpp | 3 +- 9 files changed, 446 insertions(+), 352 deletions(-) diff --git a/.cspell.config.yaml b/.cspell.config.yaml index 21b0145f43..bb763e9935 100644 --- a/.cspell.config.yaml +++ b/.cspell.config.yaml @@ -365,6 +365,7 @@ words: - xchain - ximinez - XMACRO + - xored - xrpkuwait - xrpl - xrpld diff --git a/cmake/XrplCov.cmake b/cmake/XrplCov.cmake index 86ba534a88..05d9ed3806 100644 --- a/cmake/XrplCov.cmake +++ b/cmake/XrplCov.cmake @@ -44,6 +44,7 @@ setup_target_for_coverage_gcovr( EXCLUDE "src/test" "src/tests" + "src/benchmarks" "include/xrpl/beast/test" "include/xrpl/beast/unit_test" "${CMAKE_BINARY_DIR}/pb-xrpl.libpb" diff --git a/include/xrpl/basics/Buffer.h b/include/xrpl/basics/Buffer.h index 705a5ef51a..00a6b7ecf9 100644 --- a/include/xrpl/basics/Buffer.h +++ b/include/xrpl/basics/Buffer.h @@ -3,6 +3,7 @@ #include #include +#include #include #include #include @@ -156,6 +157,19 @@ public: } /** @} */ + /** + * Set every byte in the buffer to the given value. + * + * The size is unchanged, and this is a no-op on an empty buffer. + * + * @param value the byte to write to every position. + */ + void + fill(std::uint8_t value) noexcept + { + std::fill_n(p_.get(), size_, value); + } + /** * Reset the buffer. * All memory is deallocated. The resulting size is 0. diff --git a/src/benchmarks/libxrpl/nodestore/Backend.cpp b/src/benchmarks/libxrpl/nodestore/Backend.cpp index cd3e15bd65..9d5937f869 100644 --- a/src/benchmarks/libxrpl/nodestore/Backend.cpp +++ b/src/benchmarks/libxrpl/nodestore/Backend.cpp @@ -41,10 +41,11 @@ struct RunState release() { harness.reset(); - Batch{}.swap(present); - Batch{}.swap(recent); - std::vector{}.swap(missing); - std::vector{}.swap(shuffle); + present = Batch{}; + recent = Batch{}; + missing = std::vector{}; + shuffle = std::vector{}; + avgPayload = 0; } }; @@ -239,9 +240,13 @@ registerWorkload(BackendConfig const& bc, Workload const& w) if (!w.pinToPool) { auto rs = std::make_shared(); - auto* b = benchmark::RegisterBenchmark(name, makeRunner(w, cfg, rs)); - b->RangeMultiplier(10)->Range(kPoolSizes.front(), kPoolSizes.back()); - b->Threads(1)->Threads(4)->Threads(8)->UseRealTime(); + benchmark::RegisterBenchmark(name, makeRunner(w, cfg, rs)) + ->RangeMultiplier(10) + ->Range(kPoolSizes.front(), kPoolSizes.back()) + ->Threads(1) + ->Threads(4) + ->Threads(8) + ->UseRealTime(); return; } diff --git a/src/benchmarks/libxrpl/nodestore/NodeStoreBench.h b/src/benchmarks/libxrpl/nodestore/NodeStoreBench.h index debdc5d47a..6122dd2535 100644 --- a/src/benchmarks/libxrpl/nodestore/NodeStoreBench.h +++ b/src/benchmarks/libxrpl/nodestore/NodeStoreBench.h @@ -297,12 +297,11 @@ struct BackendConfig inline std::vector const& backendConfigs() { + // Use factory settings for each DB static std::vector const kConfigs = { {.name = "nudb", .config = "type=nudb"}, #if XRPL_ROCKSDB_AVAILABLE - {.name = "rocksdb", - .config = "type=rocksdb,open_files=2000,filter_bits=12,cache_mb=256," - "file_size_mb=8,file_size_mult=2"}, + {.name = "rocksdb", .config = "type=rocksdb"}, #endif }; return kConfigs; diff --git a/src/tests/libxrpl/basics/Buffer.cpp b/src/tests/libxrpl/basics/Buffer.cpp index 9cdf610282..a3f78e8bcf 100644 --- a/src/tests/libxrpl/basics/Buffer.cpp +++ b/src/tests/libxrpl/basics/Buffer.cpp @@ -4,6 +4,7 @@ #include +#include #include #include #include @@ -12,8 +13,18 @@ namespace xrpl::test { +static_assert(std::is_nothrow_move_constructible_v); +static_assert(std::is_nothrow_move_assignable_v); + struct BufferTest : public ::testing::Test { + static constexpr auto kRandomData = std::to_array( + {0xa8, 0xa1, 0x38, 0x45, 0x23, 0xec, 0xe4, 0x23, 0x71, 0x6d, 0x2a, + 0x18, 0xb4, 0x70, 0xcb, 0xf5, 0xac, 0x2d, 0x89, 0x4d, 0x19, 0x9c, + 0xf0, 0x2c, 0x15, 0xd1, 0xf9, 0x9b, 0x66, 0xd2, 0x30, 0xd3}); + + static constexpr std::size_t kHalf = kRandomData.size() / 2; + static bool sane(Buffer const& b) { @@ -22,239 +33,321 @@ struct BufferTest : public ::testing::Test return b.data() != nullptr; } + + /** + * Check the state Buffer documents for a moved-from buffer: "the other buffer is reset", i.e. + * empty and sane. + * + * Zeroing the size is not incidental tidiness. Moving the member unique_ptr nulls the data + * pointer whether Buffer wants it or not, so a moved-from buffer that kept its old size would + * lie about itself everywhere: alloc() would take its `n == size_` early-out and hand back a + * null pointer while still reporting the old size, fill() would run std::fill_n over a null + * pointer, and the Slice conversion would publish {nullptr, oldSize} to callers. A moved-from + * Buffer has to be a usable empty Buffer rather than a landmine, which is why the tests below + * assert this state instead of treating a moved-from buffer as untouchable. + */ + static void + checkEmptyAfterMove(Buffer const& buf) + { + EXPECT_TRUE(sane(buf)); + EXPECT_TRUE(buf.empty()); + } + + Buffer const emptyBuffer; + Buffer const firstHalf{kRandomData.data(), kHalf}; + Buffer const secondHalf{kRandomData.data() + kHalf, kHalf}; + Buffer const whole{kRandomData.data(), kRandomData.size()}; }; -TEST_F(BufferTest, buffer) +TEST_F(BufferTest, default_constructed_is_empty) { - std::uint8_t const data[] = {0xa8, 0xa1, 0x38, 0x45, 0x23, 0xec, 0xe4, 0x23, 0x71, 0x6d, 0x2a, - 0x18, 0xb4, 0x70, 0xcb, 0xf5, 0xac, 0x2d, 0x89, 0x4d, 0x19, 0x9c, - 0xf0, 0x2c, 0x15, 0xd1, 0xf9, 0x9b, 0x66, 0xd2, 0x30, 0xd3}; + Buffer const b; - Buffer const b0; - EXPECT_TRUE(sane(b0)); - EXPECT_TRUE(b0.empty()); + EXPECT_TRUE(sane(b)); + EXPECT_TRUE(b.empty()); + EXPECT_EQ(b.data(), nullptr); +} - Buffer b1{0}; - EXPECT_TRUE(sane(b1)); - EXPECT_TRUE(b1.empty()); - std::memcpy(b1.alloc(16), data, 16); - EXPECT_TRUE(sane(b1)); - EXPECT_FALSE(b1.empty()); - EXPECT_EQ(b1.size(), 16); +TEST_F(BufferTest, zero_sized_construction_is_empty) +{ + Buffer const b{0}; - Buffer b2{b1.size()}; - EXPECT_TRUE(sane(b2)); - EXPECT_FALSE(b2.empty()); - EXPECT_EQ(b2.size(), b1.size()); - std::memcpy(b2.data(), data + 16, 16); + EXPECT_TRUE(sane(b)); + EXPECT_TRUE(b.empty()); +} - Buffer b3{data, sizeof(data)}; - EXPECT_TRUE(sane(b3)); - EXPECT_FALSE(b3.empty()); - EXPECT_EQ(b3.size(), sizeof(data)); - EXPECT_EQ(std::memcmp(b3.data(), data, b3.size()), 0); +TEST_F(BufferTest, alloc_grows_an_empty_buffer) +{ + Buffer b{0}; + std::memcpy(b.alloc(kHalf), kRandomData.data(), kHalf); - // Check equality and inequality comparisons. - // For code readability, we want to use general - // EXPECT_TRUE instead of specific EXPECT_EQ etc. - EXPECT_TRUE(b0 == b0); - EXPECT_TRUE(b0 != b1); - EXPECT_TRUE(b1 == b1); - EXPECT_TRUE(b1 != b2); - EXPECT_TRUE(b2 != b3); + EXPECT_TRUE(sane(b)); + EXPECT_FALSE(b.empty()); + EXPECT_EQ(b.size(), kHalf); + EXPECT_EQ(b, firstHalf); +} - // Check copy constructors and copy assignments: - { - Buffer x{b0}; - EXPECT_EQ(x, b0); - EXPECT_TRUE(sane(x)); - Buffer y{b1}; - EXPECT_EQ(y, b1); - EXPECT_TRUE(sane(y)); - x = b2; - EXPECT_EQ(x, b2); - EXPECT_TRUE(sane(x)); - x = y; - EXPECT_EQ(x, y); - EXPECT_TRUE(sane(x)); - y = b3; - EXPECT_EQ(y, b3); - EXPECT_TRUE(sane(y)); - x = b0; - EXPECT_EQ(x, b0); - EXPECT_TRUE(sane(x)); +TEST_F(BufferTest, sized_construction_reserves_without_filling) +{ + Buffer b{kHalf}; + + EXPECT_TRUE(sane(b)); + EXPECT_FALSE(b.empty()); + EXPECT_EQ(b.size(), kHalf); + + std::memcpy(b.data(), kRandomData.data() + kHalf, kHalf); + EXPECT_EQ(b, secondHalf); +} + +TEST_F(BufferTest, construction_copies_raw_memory) +{ + Buffer const b{kRandomData.data(), kRandomData.size()}; + + EXPECT_TRUE(sane(b)); + EXPECT_FALSE(b.empty()); + EXPECT_EQ(b.size(), kRandomData.size()); + EXPECT_EQ(std::memcmp(b.data(), kRandomData.data(), b.size()), 0); +} + +TEST_F(BufferTest, equality_compares_contents) +{ + // Uses EXPECT_TRUE rather than EXPECT_EQ/EXPECT_NE because the operators are what is under test + // here. + EXPECT_TRUE(emptyBuffer == emptyBuffer); + EXPECT_TRUE(firstHalf == firstHalf); + + EXPECT_TRUE(emptyBuffer != firstHalf); + EXPECT_TRUE(firstHalf != secondHalf); + EXPECT_TRUE(secondHalf != whole); +} + +TEST_F(BufferTest, copy_construction) +{ + Buffer const fromEmpty{emptyBuffer}; + EXPECT_TRUE(sane(fromEmpty)); + EXPECT_EQ(fromEmpty, emptyBuffer); + + Buffer const fromNonEmpty{firstHalf}; + EXPECT_TRUE(sane(fromNonEmpty)); + EXPECT_EQ(fromNonEmpty, firstHalf); +} + +TEST_F(BufferTest, copy_assignment) +{ + Buffer b{emptyBuffer}; + + // empty <- non-empty + b = secondHalf; + EXPECT_TRUE(sane(b)); + EXPECT_EQ(b, secondHalf); + + // non-empty <- non-empty of a different size + b = whole; + EXPECT_TRUE(sane(b)); + EXPECT_EQ(b, whole); + + // non-empty <- empty + b = emptyBuffer; + EXPECT_TRUE(sane(b)); + EXPECT_EQ(b, emptyBuffer); +} + +TEST_F(BufferTest, self_assignment_preserves_contents) +{ #ifdef __clang__ #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wself-assign-overloaded" #endif - x = x; - EXPECT_EQ(x, b0); - EXPECT_TRUE(sane(x)); - y = y; - EXPECT_EQ(y, b3); - EXPECT_TRUE(sane(y)); + Buffer emptyCopy{emptyBuffer}; + emptyCopy = emptyCopy; + EXPECT_TRUE(sane(emptyCopy)); + EXPECT_EQ(emptyCopy, emptyBuffer); + + Buffer wholeCopy{whole}; + wholeCopy = wholeCopy; + EXPECT_TRUE(sane(wholeCopy)); + EXPECT_EQ(wholeCopy, whole); #ifdef __clang__ #pragma clang diagnostic pop #endif - } +} - // Check move constructor & move assignments: +TEST_F(BufferTest, move_construct_from_empty) +{ + Buffer source; + Buffer const moved{std::move(source)}; + + checkEmptyAfterMove(source); // NOLINT(bugprone-use-after-move) + EXPECT_TRUE(sane(moved)); + EXPECT_TRUE(moved.empty()); +} + +TEST_F(BufferTest, move_construct_from_non_empty) +{ + Buffer source{firstHalf}; + Buffer const moved{std::move(source)}; + + checkEmptyAfterMove(source); // NOLINT(bugprone-use-after-move) + EXPECT_TRUE(sane(moved)); + EXPECT_EQ(moved, firstHalf); +} + +TEST_F(BufferTest, move_assign_empty_to_empty) +{ + Buffer target; + Buffer source; + + target = std::move(source); + + EXPECT_TRUE(sane(target)); + EXPECT_TRUE(target.empty()); + checkEmptyAfterMove(source); // NOLINT(bugprone-use-after-move) +} + +TEST_F(BufferTest, move_assign_non_empty_to_empty) +{ + Buffer target; + Buffer source{firstHalf}; + + target = std::move(source); + + EXPECT_TRUE(sane(target)); + EXPECT_EQ(target, firstHalf); + checkEmptyAfterMove(source); // NOLINT(bugprone-use-after-move) +} + +TEST_F(BufferTest, move_assign_empty_to_non_empty) +{ + Buffer target{firstHalf}; + Buffer source; + + target = std::move(source); + + EXPECT_TRUE(sane(target)); + EXPECT_TRUE(target.empty()); + checkEmptyAfterMove(source); // NOLINT(bugprone-use-after-move) +} + +TEST_F(BufferTest, move_assign_non_empty_to_non_empty) +{ + Buffer target{firstHalf}; + Buffer sameSize{secondHalf}; + Buffer largerSize{whole}; + + target = std::move(sameSize); + EXPECT_TRUE(sane(target)); + EXPECT_EQ(target, secondHalf); + checkEmptyAfterMove(sameSize); // NOLINT(bugprone-use-after-move) + + target = std::move(largerSize); + EXPECT_TRUE(sane(target)); + EXPECT_EQ(target, whole); + checkEmptyAfterMove(largerSize); // NOLINT(bugprone-use-after-move) +} + +TEST_F(BufferTest, construction_from_slice) +{ + Buffer const fromEmpty{static_cast(emptyBuffer)}; + EXPECT_TRUE(sane(fromEmpty)); + EXPECT_EQ(fromEmpty, emptyBuffer); + + Buffer const fromNonEmpty{static_cast(whole)}; + EXPECT_TRUE(sane(fromNonEmpty)); + EXPECT_EQ(fromNonEmpty, whole); +} + +TEST_F(BufferTest, assignment_from_slice) +{ + Buffer b; + + // empty <- empty slice + b = static_cast(emptyBuffer); + EXPECT_TRUE(sane(b)); + EXPECT_EQ(b, emptyBuffer); + + // empty <- non-empty slice + b = static_cast(firstHalf); + EXPECT_TRUE(sane(b)); + EXPECT_EQ(b, firstHalf); + + // non-empty <- non-empty slice + b = static_cast(secondHalf); + EXPECT_TRUE(sane(b)); + EXPECT_EQ(b, secondHalf); + + // non-empty <- empty slice + b = static_cast(emptyBuffer); + EXPECT_TRUE(sane(b)); + EXPECT_EQ(b, emptyBuffer); +} + +TEST_F(BufferTest, resize_allocates_and_clear_releases) +{ + auto check = [](Buffer const& original, std::size_t size) { + SCOPED_TRACE(::testing::Message() << "size: " << size); + + Buffer b{original}; + + // Resizing to zero is equivalent to clearing. + b(size); + EXPECT_TRUE(sane(b)); + EXPECT_EQ(b.size(), size); + EXPECT_EQ(b.data() == nullptr, size == 0); + + b(size + 1); + EXPECT_TRUE(sane(b)); + EXPECT_EQ(b.size(), size + 1); + EXPECT_NE(b.data(), nullptr); + + b.clear(); + EXPECT_TRUE(sane(b)); + EXPECT_TRUE(b.empty()); + EXPECT_EQ(b.data(), nullptr); + + // clear() is idempotent. + b.clear(); + EXPECT_TRUE(sane(b)); + EXPECT_TRUE(b.empty()); + EXPECT_EQ(b.data(), nullptr); + }; + + for (auto size = 0uz; size < kHalf; ++size) { - static_assert(std::is_nothrow_move_constructible_v); - static_assert(std::is_nothrow_move_assignable_v); - - { // Move-construct from empty buf - Buffer x; - Buffer const y{std::move(x)}; - EXPECT_TRUE(sane(x)); // NOLINT(bugprone-use-after-move) - EXPECT_TRUE(x.empty()); // NOLINT(bugprone-use-after-move) - EXPECT_TRUE(sane(y)); - EXPECT_TRUE(y.empty()); - EXPECT_EQ(x, y); // NOLINT(bugprone-use-after-move) - } - - { // Move-construct from non-empty buf - Buffer x{b1}; - Buffer const y{std::move(x)}; - EXPECT_TRUE(sane(x)); // NOLINT(bugprone-use-after-move) - EXPECT_TRUE(x.empty()); // NOLINT(bugprone-use-after-move) - EXPECT_TRUE(sane(y)); - EXPECT_EQ(y, b1); - } - - { // Move assign empty buf to empty buf - Buffer x; - Buffer y; - - x = std::move(y); - EXPECT_TRUE(sane(x)); - EXPECT_TRUE(x.empty()); - EXPECT_TRUE(sane(y)); // NOLINT(bugprone-use-after-move) - EXPECT_TRUE(y.empty()); // NOLINT(bugprone-use-after-move) - } - - { // Move assign non-empty buf to empty buf - Buffer x; - Buffer y{b1}; - - x = std::move(y); - EXPECT_TRUE(sane(x)); - EXPECT_EQ(x, b1); - EXPECT_TRUE(sane(y)); // NOLINT(bugprone-use-after-move) - EXPECT_TRUE(y.empty()); // NOLINT(bugprone-use-after-move) - } - - { // Move assign empty buf to non-empty buf - Buffer x{b1}; - Buffer y; - - x = std::move(y); - EXPECT_TRUE(sane(x)); - EXPECT_TRUE(x.empty()); - EXPECT_TRUE(sane(y)); // NOLINT(bugprone-use-after-move) - EXPECT_TRUE(y.empty()); // NOLINT(bugprone-use-after-move) - } - - { // Move assign non-empty buf to non-empty buf - Buffer x{b1}; - Buffer y{b2}; - Buffer z{b3}; - - x = std::move(y); - EXPECT_TRUE(sane(x)); - EXPECT_FALSE(x.empty()); - EXPECT_TRUE(sane(y)); // NOLINT(bugprone-use-after-move) - EXPECT_TRUE(y.empty()); // NOLINT(bugprone-use-after-move) - - x = std::move(z); - EXPECT_TRUE(sane(x)); - EXPECT_FALSE(x.empty()); - EXPECT_TRUE(sane(z)); // NOLINT(bugprone-use-after-move) - EXPECT_TRUE(z.empty()); // NOLINT(bugprone-use-after-move) - } - } - - { - Buffer w{static_cast(b0)}; - EXPECT_TRUE(sane(w)); - EXPECT_EQ(w, b0); - - Buffer x{static_cast(b1)}; - EXPECT_TRUE(sane(x)); - EXPECT_EQ(x, b1); - - Buffer y{static_cast(b2)}; - EXPECT_TRUE(sane(y)); - EXPECT_EQ(y, b2); - - Buffer z{static_cast(b3)}; - EXPECT_TRUE(sane(z)); - EXPECT_EQ(z, b3); - - // Assign empty slice to empty buffer - w = static_cast(b0); - EXPECT_TRUE(sane(w)); - EXPECT_EQ(w, b0); - - // Assign non-empty slice to empty buffer - w = static_cast(b1); - EXPECT_TRUE(sane(w)); - EXPECT_EQ(w, b1); - - // Assign non-empty slice to non-empty buffer - x = static_cast(b2); - EXPECT_TRUE(sane(x)); - EXPECT_EQ(x, b2); - - // Assign non-empty slice to non-empty buffer - y = static_cast(z); - EXPECT_TRUE(sane(y)); - EXPECT_EQ(y, z); - - // Assign empty slice to non-empty buffer: - z = static_cast(b0); - EXPECT_TRUE(sane(z)); - EXPECT_EQ(z, b0); - } - - { - auto test = [](Buffer const& b, std::size_t i) { - Buffer x{b}; - - // Try to allocate some number of bytes, possibly - // zero (which means clear) and sanity check - x(i); - EXPECT_TRUE(sane(x)); - EXPECT_EQ(x.size(), i); - EXPECT_EQ((x.data() == nullptr), (i == 0)); - - // Try to allocate some more data (always non-zero) - x(i + 1); - EXPECT_TRUE(sane(x)); - EXPECT_EQ(x.size(), i + 1); - EXPECT_NE(x.data(), nullptr); - - // Try to clear: - x.clear(); - EXPECT_TRUE(sane(x)); - EXPECT_TRUE(x.empty()); - EXPECT_EQ(x.data(), nullptr); - - // Try to clear again: - x.clear(); - EXPECT_TRUE(sane(x)); - EXPECT_TRUE(x.empty()); - EXPECT_EQ(x.data(), nullptr); - }; - - for (std::size_t i = 0; i < 16; ++i) - { - test(b0, i); - test(b1, i); - } + check(emptyBuffer, size); + check(firstHalf, size); } } +TEST_F(BufferTest, fill_sets_every_byte) +{ + Buffer b{4}; + b.fill(0xab); + + EXPECT_EQ(b.size(), 4); + for (auto const byte : Slice{b}) + EXPECT_EQ(byte, 0xab); +} + +TEST_F(BufferTest, fill_overwrites_and_keeps_size) +{ + Buffer b{4}; + b.fill(0xab); + b.fill(0x00); + + EXPECT_EQ(b.size(), 4); + for (auto const byte : Slice{b}) + EXPECT_EQ(byte, 0x00); +} + +TEST_F(BufferTest, fill_on_empty_buffer_is_a_noop) +{ + Buffer empty; + empty.fill(0xff); + + EXPECT_TRUE(empty.empty()); + EXPECT_EQ(empty.data(), nullptr); +} + } // namespace xrpl::test diff --git a/src/tests/libxrpl/basics/IntrusiveShared.cpp b/src/tests/libxrpl/basics/IntrusiveShared.cpp index b9f8930b7b..c6c9fcfef0 100644 --- a/src/tests/libxrpl/basics/IntrusiveShared.cpp +++ b/src/tests/libxrpl/basics/IntrusiveShared.cpp @@ -92,6 +92,7 @@ public: static constexpr std::size_t kMaxStates = 128; static std::array, kMaxStates> state; static std::atomic nextId; + static TrackedState getState(std::size_t id) { @@ -100,13 +101,12 @@ public: return state[id].load(std::memory_order_acquire); } + static void resetStates(bool resetCallback) { for (std::size_t i = 0; i < kMaxStates; ++i) - { state[i].store(TrackedState::Uninitialized, std::memory_order_release); - } nextId.store(0, std::memory_order_release); if (resetCallback) TIBase::tracingCallback = [](TrackedState, std::optional) {}; @@ -120,6 +120,7 @@ public: { TIBase::resetStates(resetCallback); } + ~ResetStatesGuard() { TIBase::resetStates(resetCallback); @@ -130,6 +131,7 @@ public: { state[id].store(TrackedState::Alive, std::memory_order_relaxed); } + ~TIBase() override { using enum TrackedState; @@ -218,9 +220,7 @@ TEST(IntrusiveSharedTest, basics) EXPECT_EQ(TIBase::getState(id), Alive); EXPECT_EQ(b->useCount(), 1); for (auto i = 0uz; i < 10; ++i) - { strong.push_back(b); - } b.reset(); EXPECT_EQ(TIBase::getState(id), Alive); strong.resize(strong.size() - 1); @@ -244,8 +244,7 @@ TEST(IntrusiveSharedTest, basics) EXPECT_EQ(TIBase::getState(id), PartiallyDeleted); while (!weak.empty()) { - weak.resize(weak.size() - 1); - if (!weak.empty()) + if (weak.resize(weak.size() - 1); !weak.empty()) { EXPECT_EQ(TIBase::getState(id), PartiallyDeleted); } diff --git a/src/tests/libxrpl/basics/base_uint.cpp b/src/tests/libxrpl/basics/base_uint.cpp index 10795f4563..969705b5b7 100644 --- a/src/tests/libxrpl/basics/base_uint.cpp +++ b/src/tests/libxrpl/basics/base_uint.cpp @@ -6,6 +6,7 @@ #include +#include #include #include @@ -205,125 +206,119 @@ TEST_F(BaseUintTest, base_uint) Blob const raw{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12}; EXPECT_EQ(BaseUInt96::kBytes, raw.size()); - BaseUInt96 u = BaseUInt96::fromRaw(raw); - uset.insert(u); - EXPECT_EQ(raw.size(), u.size()); - EXPECT_EQ(to_string(u), "0102030405060708090A0B0C"); - EXPECT_EQ(toShortString(u), "01020304..."); - EXPECT_EQ(*u.data(), 1); - EXPECT_EQ(u.signum(), 1); - EXPECT_FALSE(!u); - EXPECT_FALSE(u.isZero()); - EXPECT_TRUE(u.isNonZero()); - unsigned char t = 0; - for (auto& d : u) - { - EXPECT_EQ(d, ++t); - } + BaseUInt96 ascending = BaseUInt96::fromRaw(raw); + uset.insert(ascending); + EXPECT_EQ(raw.size(), ascending.size()); + EXPECT_EQ(to_string(ascending), "0102030405060708090A0B0C"); + EXPECT_EQ(toShortString(ascending), "01020304..."); + EXPECT_EQ(*ascending.data(), 1); + EXPECT_EQ(ascending.signum(), 1); + EXPECT_FALSE(!ascending); + EXPECT_FALSE(ascending.isZero()); + EXPECT_TRUE(ascending.isNonZero()); + unsigned char expectedByte = 0; + for (auto& byte : ascending) + EXPECT_EQ(byte, ++expectedByte); - // Test hash_append by "hashing" with a no-op hasher (h) + // Test hash_append by "hashing" with a no-op hasher (hasher) // and then extracting the bytes that were written during hashing - // back into another base_uint (w) for comparison with the original - Nonhash<96> h{}; - hash_append(h, u); - BaseUInt96 const w = - BaseUInt96::fromRaw(std::vector(h.data.begin(), h.data.end())); - EXPECT_EQ(w, u); + // back into another base_uint (rehashed) for comparison with the original + Nonhash<96> hasher{}; + hash_append(hasher, ascending); + BaseUInt96 const rehashed = + BaseUInt96::fromRaw(std::vector(hasher.data.begin(), hasher.data.end())); + EXPECT_EQ(rehashed, ascending); - BaseUInt96 v{~u}; - uset.insert(v); - EXPECT_EQ(to_string(v), "FEFDFCFBFAF9F8F7F6F5F4F3"); - EXPECT_EQ(toShortString(v), "FEFDFCFB..."); - EXPECT_EQ(*v.data(), 0xfe); - EXPECT_EQ(v.signum(), 1); - EXPECT_FALSE(!v); - EXPECT_FALSE(v.isZero()); - EXPECT_TRUE(v.isNonZero()); + BaseUInt96 complement{~ascending}; + uset.insert(complement); + EXPECT_EQ(to_string(complement), "FEFDFCFBFAF9F8F7F6F5F4F3"); + EXPECT_EQ(toShortString(complement), "FEFDFCFB..."); + EXPECT_EQ(*complement.data(), 0xfe); + EXPECT_EQ(complement.signum(), 1); + EXPECT_FALSE(!complement); + EXPECT_FALSE(complement.isZero()); + EXPECT_TRUE(complement.isNonZero()); - t = 0xff; - for (auto& d : v) - { - EXPECT_EQ(d, --t); - } + expectedByte = 0xff; + for (auto& byte : complement) + EXPECT_EQ(byte, --expectedByte); - EXPECT_LT(u, v); - EXPECT_GT(v, u); + EXPECT_LT(ascending, complement); + EXPECT_GT(complement, ascending); - v = u; - EXPECT_EQ(v, u); + complement = ascending; + EXPECT_EQ(complement, ascending); - BaseUInt96 z{beast::kZero}; - uset.insert(z); - EXPECT_EQ(to_string(z), "000000000000000000000000"); - EXPECT_EQ(toShortString(z), "00000000..."); - EXPECT_EQ(*z.data(), 0); - EXPECT_EQ(*z.begin(), 0); - EXPECT_EQ(*std::prev(z.end(), 1), 0); - EXPECT_EQ(z.signum(), 0); - EXPECT_TRUE(!z); - EXPECT_TRUE(z.isZero()); - EXPECT_FALSE(z.isNonZero()); - for (auto& d : z) - { - EXPECT_EQ(d, 0); - } + BaseUInt96 zero{beast::kZero}; + uset.insert(zero); + EXPECT_EQ(to_string(zero), "000000000000000000000000"); + EXPECT_EQ(toShortString(zero), "00000000..."); + EXPECT_EQ(*zero.data(), 0); + EXPECT_EQ(*zero.begin(), 0); + EXPECT_EQ(*std::prev(zero.end(), 1), 0); + EXPECT_EQ(zero.signum(), 0); + EXPECT_TRUE(!zero); + EXPECT_TRUE(zero.isZero()); + EXPECT_FALSE(zero.isNonZero()); + for (auto& byte : zero) + EXPECT_EQ(byte, 0); { // There are several ways to create a zero. beast::kZero is tested above. Test some // others. - BaseUInt96 const z1; - EXPECT_EQ(z1, z) << to_string(z1); + BaseUInt96 const defaultZero; + EXPECT_EQ(defaultZero, zero) << to_string(defaultZero); - BaseUInt96 const z2{}; - EXPECT_EQ(z2, z) << to_string(z2); + BaseUInt96 const bracedZero{}; + EXPECT_EQ(bracedZero, zero) << to_string(bracedZero); - BaseUInt96 const z3{0u}; - EXPECT_EQ(z3, z) << to_string(z3); + BaseUInt96 const zeroFromUInt{0u}; + EXPECT_EQ(zeroFromUInt, zero) << to_string(zeroFromUInt); } - BaseUInt96 n{z}; - n++; - EXPECT_EQ(n, BaseUInt96(1)); - n--; - EXPECT_EQ(n, beast::kZero); - EXPECT_EQ(n, z); - n--; - EXPECT_EQ(to_string(n), "FFFFFFFFFFFFFFFFFFFFFFFF"); - EXPECT_EQ(toShortString(n), "FFFFFFFF..."); - n = beast::kZero; - EXPECT_EQ(n, z); + BaseUInt96 counter{zero}; + counter++; + EXPECT_EQ(counter, BaseUInt96(1)); + counter--; + EXPECT_EQ(counter, beast::kZero); + EXPECT_EQ(counter, zero); + counter--; + EXPECT_EQ(to_string(counter), "FFFFFFFFFFFFFFFFFFFFFFFF"); + EXPECT_EQ(toShortString(counter), "FFFFFFFF..."); + counter = beast::kZero; + EXPECT_EQ(counter, zero); - BaseUInt96 zp1{z}; - zp1++; - BaseUInt96 zm1{z}; - zm1--; - BaseUInt96 const x{zm1 ^ zp1}; - uset.insert(x); - EXPECT_EQ(to_string(x), "FFFFFFFFFFFFFFFFFFFFFFFE") << to_string(x); - EXPECT_EQ(toShortString(x), "FFFFFFFF...") << toShortString(x); + BaseUInt96 zeroPlusOne{zero}; + zeroPlusOne++; + BaseUInt96 zeroMinusOne{zero}; + zeroMinusOne--; + BaseUInt96 const xored{zeroMinusOne ^ zeroPlusOne}; + uset.insert(xored); + EXPECT_EQ(to_string(xored), "FFFFFFFFFFFFFFFFFFFFFFFE") << to_string(xored); + EXPECT_EQ(toShortString(xored), "FFFFFFFF...") << toShortString(xored); EXPECT_EQ(uset.size(), 4); - BaseUInt96 tmp; - EXPECT_TRUE(tmp.parseHex(to_string(u))); - EXPECT_EQ(tmp, u); - tmp = z; + BaseUInt96 parsed; + EXPECT_TRUE(parsed.parseHex(to_string(ascending))); + EXPECT_EQ(parsed, ascending); + parsed = zero; // fails with extra char - EXPECT_FALSE(tmp.parseHex("A" + to_string(u))); - tmp = z; + EXPECT_FALSE(parsed.parseHex("A" + to_string(ascending))); + parsed = zero; // fails with extra char at end - EXPECT_FALSE(tmp.parseHex(to_string(u) + "A")); + EXPECT_FALSE(parsed.parseHex(to_string(ascending) + "A")); // fails with a non-hex character at some point in the string: - tmp = z; + parsed = zero; for (std::size_t i = 0; i != 24; ++i) { - std::string x = to_string(z); - x[i] = ('G' + (i % 10)); - EXPECT_FALSE(tmp.parseHex(x)); + std::string xored = to_string(zero); + xored[i] = ('G' + (i % 10)); + EXPECT_FALSE(parsed.parseHex(xored)); } // Walking 1s: @@ -332,8 +327,8 @@ TEST_F(BaseUintTest, base_uint) std::string s1 = "000000000000000000000000"; s1[i] = '1'; - EXPECT_TRUE(tmp.parseHex(s1)); - EXPECT_EQ(to_string(tmp), s1); + EXPECT_TRUE(parsed.parseHex(s1)); + EXPECT_EQ(to_string(parsed), s1); } // Walking 0s: @@ -342,8 +337,8 @@ TEST_F(BaseUintTest, base_uint) std::string s1 = "111111111111111111111111"; s1[i] = '0'; - EXPECT_TRUE(tmp.parseHex(s1)); - EXPECT_EQ(to_string(tmp), s1); + EXPECT_TRUE(parsed.parseHex(s1)); + EXPECT_EQ(to_string(parsed), s1); } // Constexpr constructors @@ -357,39 +352,27 @@ TEST_F(BaseUintTest, base_uint) // Using the constexpr constructor in a non-constexpr context // with an error in the parsing throws an exception. { - // Invalid length for string. - bool caught = false; - try - { - // Try to prevent constant evaluation. - std::vector str(23, '7'); + // Invalid length for string. The vector keeps this out of a constant + // expression, so the constructor throws instead of failing to compile. + auto tooShort = [] { + std::vector const str(23, '7'); std::string_view const sView(str.data(), str.size()); [[maybe_unused]] BaseUInt96 const t96(sView); - } - catch (std::invalid_argument const& e) - { - EXPECT_EQ(e.what(), std::string("invalid length for hex string")); - caught = true; - } - EXPECT_TRUE(caught); + }; + EXPECT_THAT( + tooShort, + ::testing::ThrowsMessage("invalid length for hex string")); } { // Invalid character in string. - bool caught = false; - try - { - // Try to prevent constant evaluation. + auto badCharacter = [] { std::vector str(23, '7'); str.push_back('G'); std::string_view const sView(str.data(), str.size()); [[maybe_unused]] BaseUInt96 const t96(sView); - } - catch (std::range_error const& e) - { - EXPECT_EQ(e.what(), std::string("invalid hex character")); - caught = true; - } - EXPECT_TRUE(caught); + }; + EXPECT_THAT( + badCharacter, ::testing::ThrowsMessage("invalid hex character")); } // Verify that constexpr base_uints interpret a string the same @@ -412,11 +395,11 @@ TEST_F(BaseUintTest, base_uint) "fFfFfFfFfFfFfFfFfFfFfFfF", }); - for (StrBaseUInt const& t : kTestCases) + for (StrBaseUInt const& expectedByte : kTestCases) { BaseUInt96 t96; - EXPECT_TRUE(t96.parseHex(t.str)); - EXPECT_EQ(t96, t.tst); + EXPECT_TRUE(t96.parseHex(expectedByte.str)); + EXPECT_EQ(t96, expectedByte.tst); } } } diff --git a/src/tests/libxrpl/shamap/SHAMap.cpp b/src/tests/libxrpl/shamap/SHAMap.cpp index e662e16be4..c84cdf504f 100644 --- a/src/tests/libxrpl/shamap/SHAMap.cpp +++ b/src/tests/libxrpl/shamap/SHAMap.cpp @@ -16,7 +16,6 @@ #include #include -#include #include #include #include @@ -113,7 +112,7 @@ protected: intToVuc(std::uint8_t v) { Buffer vuc{32}; - std::fill_n(vuc.data(), vuc.size(), v); + vuc.fill(v); return vuc; } }; From c74724a7197da3db16cc1c7274543c7ba4ce2dbc Mon Sep 17 00:00:00 2001 From: Ayaz Salikhov Date: Tue, 11 Aug 2026 12:44:01 +0000 Subject: [PATCH 18/28] build: Reimagine linker warnings in different scenarios (#7974) --- cmake/XrplCompiler.cmake | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/cmake/XrplCompiler.cmake b/cmake/XrplCompiler.cmake index e262acf1c9..21566add01 100644 --- a/cmake/XrplCompiler.cmake +++ b/cmake/XrplCompiler.cmake @@ -188,6 +188,32 @@ else() endif() endif() +# Linker warnings are errors where we control the toolchain and the dependencies: CI and the Nix dev shell. +# On non-Nix macOS we suppress the deployment target warning: an old Conan profile may not pin os.version. +if(is_macos OR is_linux) + if(is_ci OR is_nix_compiler) + if(is_macos) + set(fatal_warnings_flag "-Wl,-fatal_warnings") + else() + set(fatal_warnings_flag "-Wl,--fatal-warnings") + endif() + message( + STATUS + "Treating all linker warnings as errors (${fatal_warnings_flag})" + ) + target_link_options(common INTERFACE "${fatal_warnings_flag}") + unset(fatal_warnings_flag) + elseif(is_macos) + set(silence_flag "-Wl,-deployment_target_mismatches,suppress") + message( + STATUS + "Silencing macOS deployment target mismatch warnings (${silence_flag})" + ) + target_link_options(common INTERFACE "${silence_flag}") + unset(silence_flag) + endif() +endif() + # Antithesis instrumentation will only be built and deployed using machines running Linux. if(voidstar) if(NOT CMAKE_BUILD_TYPE STREQUAL "Debug") From d43e5acaa70f8d45a1691ab522a9c84c4fc95006 Mon Sep 17 00:00:00 2001 From: luisfernandomendozav <109832400+luisfernandomendozav@users.noreply.github.com> Date: Tue, 11 Aug 2026 13:23:07 +0000 Subject: [PATCH 19/28] fix: Validate account/ident type in gateway_balances (#7655) --- API-CHANGELOG.md | 1 + src/test/rpc/GatewayBalances_test.cpp | 40 +++++++++++++++++++ .../rpc/handlers/account/GatewayBalances.cpp | 6 +++ 3 files changed, 47 insertions(+) diff --git a/API-CHANGELOG.md b/API-CHANGELOG.md index bc3672588e..c853cfb07c 100644 --- a/API-CHANGELOG.md +++ b/API-CHANGELOG.md @@ -54,6 +54,7 @@ This section contains changes targeting a future version. - `submit`: The `fail_hard` field now returns an error if the value is not a boolean. [#6529](https://github.com/XRPLF/rippled/pull/6529) - `subscribe`: The `taker` field in the `books` array now returns `actMalformed` instead of `badIssuer` if the value is not a valid account. [#6529](https://github.com/XRPLF/rippled/pull/6529) - Fixed a bug in `Forwarded` HTTP header parsing where the extracted IP address could be incorrect when no comma or semicolon delimiter follows the address. This could cause the server to misidentify a client's IP address when operating behind a reverse proxy. [#6529](https://github.com/XRPLF/rippled/pull/6529) +- `gateway_balances`: The `account` and `ident` fields now return an `invalidParams` error if the value is not a string, instead of an `internal` error. [#7655](https://github.com/XRPLF/rippled/pull/7655) - `account_lines`: The `peer` field now returns an error if the value is not a string. [#7728](https://github.com/XRPLF/rippled/pull/7728) ## XRP Ledger server version 3.1.0 diff --git a/src/test/rpc/GatewayBalances_test.cpp b/src/test/rpc/GatewayBalances_test.cpp index 106b9b5f1a..91d9126f61 100644 --- a/src/test/rpc/GatewayBalances_test.cpp +++ b/src/test/rpc/GatewayBalances_test.cpp @@ -176,6 +176,45 @@ public: }); } + void + testGWBInvalidAccount(FeatureBitset features) + { + testcase("Gateway Balances with non-string account/ident"); + using namespace std::chrono_literals; + using namespace jtx; + Env env(*this, features); + + Account const alice{"alice"}; + env.fund(XRP(10000), alice); + env.close(); + + auto wsc = makeWSClient(env.app().config()); + + // A non-string "account" must be rejected cleanly with invalidParams + // rather than throwing a Json::LogicError that surfaces as internal. + json::Value qry; + qry[jss::account] = 42; + qry[jss::hotwallet] = alice.human(); + + forAllApiVersions([&, this](unsigned apiVersion) { + qry[jss::api_version] = apiVersion; + auto jv = wsc->invoke("gateway_balances", qry); + expect(jv[jss::status] == "error"); + BEAST_EXPECT(jv[jss::result][jss::error] == "invalidParams"); + }); + + // The same applies to a non-string "ident". + json::Value qry2; + qry2[jss::ident] = 42; + + forAllApiVersions([&, this](unsigned apiVersion) { + qry2[jss::api_version] = apiVersion; + auto jv = wsc->invoke("gateway_balances", qry2); + expect(jv[jss::status] == "error"); + BEAST_EXPECT(jv[jss::result][jss::error] == "invalidParams"); + }); + } + void testGWBOverflow() { @@ -280,6 +319,7 @@ public: { testGWB(feature); testGWBApiVersions(feature); + testGWBInvalidAccount(feature); } testGWBWithMPT(); testGWBOverflow(); diff --git a/src/xrpld/rpc/handlers/account/GatewayBalances.cpp b/src/xrpld/rpc/handlers/account/GatewayBalances.cpp index ff19d1d1e5..041e878a3f 100644 --- a/src/xrpld/rpc/handlers/account/GatewayBalances.cpp +++ b/src/xrpld/rpc/handlers/account/GatewayBalances.cpp @@ -63,6 +63,12 @@ doGatewayBalances(rpc::JsonContext& context) if (!(params.isMember(jss::account) || params.isMember(jss::ident))) return rpc::missingFieldError(jss::account); + if (params.isMember(jss::account) && !params[jss::account].isString()) + return rpc::invalidFieldError(jss::account); + + if (params.isMember(jss::ident) && !params[jss::ident].isString()) + return rpc::invalidFieldError(jss::ident); + std::string const strIdent( params.isMember(jss::account) ? params[jss::account].asString() : params[jss::ident].asString()); From a3147740f2f610f0810a6e4eb56aa7cbe4c5cc12 Mon Sep 17 00:00:00 2001 From: klemenfn <102049210+klemenfn@users.noreply.github.com> Date: Tue, 11 Aug 2026 13:24:56 +0000 Subject: [PATCH 20/28] build: Fix GCC 14 compilation (#7981) Co-authored-by: Ayaz Salikhov --- include/xrpl/json/json_value.h | 2 ++ src/xrpld/rpc/detail/RPCLedgerHelpers.cpp | 7 +++++++ 2 files changed, 9 insertions(+) diff --git a/include/xrpl/json/json_value.h b/include/xrpl/json/json_value.h index be126d8b8e..57936a774f 100644 --- a/include/xrpl/json/json_value.h +++ b/include/xrpl/json/json_value.h @@ -4,6 +4,7 @@ #include #include +#include #include #include #include @@ -524,6 +525,7 @@ public: class ValueIteratorBase { public: + using iterator_category = std::bidirectional_iterator_tag; using size_t = unsigned int; using difference_type = int; using SelfType = ValueIteratorBase; diff --git a/src/xrpld/rpc/detail/RPCLedgerHelpers.cpp b/src/xrpld/rpc/detail/RPCLedgerHelpers.cpp index 52e68e87f1..19fe294924 100644 --- a/src/xrpld/rpc/detail/RPCLedgerHelpers.cpp +++ b/src/xrpld/rpc/detail/RPCLedgerHelpers.cpp @@ -331,6 +331,13 @@ getLedger<>(std::shared_ptr&, LedgerShortcut shortcut, Context c template Status getLedger<>(std::shared_ptr&, uint256 const&, Context const&); +// explicit instantiation of ledgerFromSpecifier +template Status +ledgerFromSpecifier<>( + std::shared_ptr&, + org::xrpl::rpc::v1::LedgerSpecifier const&, + Context const&); + // The previous version of the lookupLedger command would accept the // "ledger_index" argument as a string and silently treat it as a request to // return the current ledger which, while not strictly wrong, could cause a lot From 6ca2fb84d4da09d01af6d1f233147374599aa440 Mon Sep 17 00:00:00 2001 From: Mayukha Vadari Date: Tue, 11 Aug 2026 18:15:35 +0000 Subject: [PATCH 21/28] refactor: Replace Boost trim and to_lower with libxrpl helpers (#7995) --- include/xrpl/basics/StringUtilities.h | 22 ++++++++++ src/libxrpl/basics/StringUtilities.cpp | 40 +++++++++++++++++-- src/libxrpl/crypto/RFC1751.cpp | 4 +- src/libxrpl/server/Manifest.cpp | 6 +-- src/libxrpl/server/Port.cpp | 4 +- src/tests/libxrpl/basics/StringUtilities.cpp | 40 +++++++++++++++++++ src/xrpld/app/main/GRPCServer.cpp | 4 +- src/xrpld/app/main/Main.cpp | 6 +-- src/xrpld/core/detail/Config.cpp | 3 +- src/xrpld/rpc/detail/ServerHandler.cpp | 4 +- .../rpc/handlers/account/AccountInfo.cpp | 4 +- .../rpc/handlers/admin/data/CanDelete.cpp | 5 +-- .../server_info/ServerDefinitions.cpp | 4 +- 13 files changed, 118 insertions(+), 28 deletions(-) diff --git a/include/xrpl/basics/StringUtilities.h b/include/xrpl/basics/StringUtilities.h index 2b360d2fda..d606613c65 100644 --- a/include/xrpl/basics/StringUtilities.h +++ b/include/xrpl/basics/StringUtilities.h @@ -125,9 +125,31 @@ struct ParsedUrl bool parseUrl(ParsedUrl& pUrl, std::string const& strUrl); +/** + * Remove leading and trailing ASCII whitespace. + * + * Whitespace is the fixed set " \t\n\v\f\r"; the current locale is not + * consulted, so the result depends only on the input. + * + * @param str The string to trim. + * @return @p str without leading or trailing whitespace. + */ std::string trimWhitespace(std::string str); +/** + * Fold ASCII upper case letters to lower case. + * + * Only 'A' through 'Z' are remapped; every other byte is left alone and the + * current locale is not consulted, so the result depends only on the input. + * + * @param str The string to fold. + * @return @p str with each ASCII upper case letter replaced by its lower case + * equivalent. + */ +std::string +toLower(std::string str); + std::optional toUInt64(std::string const& s); diff --git a/src/libxrpl/basics/StringUtilities.cpp b/src/libxrpl/basics/StringUtilities.cpp index 2b7deecb8e..9eb1bff995 100644 --- a/src/libxrpl/basics/StringUtilities.cpp +++ b/src/libxrpl/basics/StringUtilities.cpp @@ -5,15 +5,15 @@ #include #include -#include -#include #include #include #include +#include #include #include #include +#include #include #include @@ -67,7 +67,7 @@ parseUrl(ParsedUrl& pUrl, std::string const& strUrl) } pUrl.scheme = smMatch[1]; - boost::algorithm::to_lower(pUrl.scheme); + pUrl.scheme = toLower(pUrl.scheme); pUrl.username = smMatch[2]; pUrl.password = smMatch[3]; std::string const domain = smMatch[4]; @@ -93,10 +93,42 @@ parseUrl(ParsedUrl& pUrl, std::string const& strUrl) return true; } +namespace { + +// Deliberately not std::isspace / std::tolower: those consult the current C +// locale, so the same input could trim or fold differently depending on +// process-wide state set by something else entirely. Everything these helpers +// are used on (config keys and values, URL schemes, hex digests) is ASCII, and +// the callers want a fixed answer, so spell the ASCII rules out. + +constexpr bool +isAsciiSpace(char c) +{ + return c == ' ' || c == '\t' || c == '\n' || c == '\v' || c == '\f' || c == '\r'; +} + +constexpr char +toAsciiLower(char c) +{ + return (c >= 'A' && c <= 'Z') ? static_cast(c - 'A' + 'a') : c; +} + +} // namespace + std::string trimWhitespace(std::string str) { - boost::trim(str); + auto const end = std::ranges::find_if_not(str | std::views::reverse, isAsciiSpace).base(); + str.erase(end, str.end()); + str.erase(str.begin(), std::ranges::find_if_not(str, isAsciiSpace)); + + return str; +} + +std::string +toLower(std::string str) +{ + std::ranges::transform(str, str.begin(), toAsciiLower); return str; } diff --git a/src/libxrpl/crypto/RFC1751.cpp b/src/libxrpl/crypto/RFC1751.cpp index 4b17e1443c..f6342928ab 100644 --- a/src/libxrpl/crypto/RFC1751.cpp +++ b/src/libxrpl/crypto/RFC1751.cpp @@ -1,11 +1,11 @@ #include +#include #include #include #include #include -#include #include #include @@ -397,7 +397,7 @@ RFC1751::getKeyFromEnglish(std::string& strKey, std::string const& strHuman) std::string strTrimmed(strHuman); - boost::algorithm::trim(strTrimmed); + strTrimmed = trimWhitespace(strTrimmed); boost::algorithm::split( vWords, strTrimmed, boost::algorithm::is_space(), boost::algorithm::token_compress_on); diff --git a/src/libxrpl/server/Manifest.cpp b/src/libxrpl/server/Manifest.cpp index 0760196a3b..c85c8445f0 100644 --- a/src/libxrpl/server/Manifest.cpp +++ b/src/libxrpl/server/Manifest.cpp @@ -23,8 +23,6 @@ #include #include -#include - #include #include #include @@ -277,7 +275,7 @@ loadValidatorToken(std::vector const& blob, beast::Journal journal) [](std::size_t init, std::string const& s) { return init + s.size(); })); for (auto const& line : blob) - tokenStr += boost::algorithm::trim_copy(line); + tokenStr += trimWhitespace(line); tokenStr = base64Decode(tokenStr); @@ -653,7 +651,7 @@ ManifestCache::load( [](std::size_t init, std::string const& s) { return init + s.size(); })); for (auto const& line : configRevocation) - revocationStr += boost::algorithm::trim_copy(line); + revocationStr += trimWhitespace(line); auto mo = deserializeManifest(base64Decode(revocationStr)); diff --git a/src/libxrpl/server/Port.cpp b/src/libxrpl/server/Port.cpp index 694d4448d5..a7892bc0e8 100644 --- a/src/libxrpl/server/Port.cpp +++ b/src/libxrpl/server/Port.cpp @@ -1,5 +1,6 @@ #include +#include #include #include #include @@ -9,7 +10,6 @@ #include #include -#include #include #include #include @@ -98,7 +98,7 @@ populate( while (std::getline(ss, ip, ',')) { - boost::algorithm::trim(ip); + ip = trimWhitespace(ip); bool v4 = false; boost::asio::ip::network_v4 v4Net; boost::asio::ip::network_v6 v6Net; diff --git a/src/tests/libxrpl/basics/StringUtilities.cpp b/src/tests/libxrpl/basics/StringUtilities.cpp index a10711abdb..0180e25db0 100644 --- a/src/tests/libxrpl/basics/StringUtilities.cpp +++ b/src/tests/libxrpl/basics/StringUtilities.cpp @@ -290,4 +290,44 @@ TEST_F(StringUtilitiesTest, to_string) EXPECT_EQ(result, "hello"); } +TEST_F(StringUtilitiesTest, trimWhitespace) +{ + EXPECT_EQ(trimWhitespace(""), ""); + EXPECT_EQ(trimWhitespace(" "), ""); + EXPECT_EQ(trimWhitespace("abc"), "abc"); + EXPECT_EQ(trimWhitespace(" abc"), "abc"); + EXPECT_EQ(trimWhitespace("abc "), "abc"); + EXPECT_EQ(trimWhitespace(" \t\n\v\f\r abc \t\n\v\f\r "), "abc"); + + // Interior whitespace is preserved. + EXPECT_EQ(trimWhitespace(" a b\tc "), "a b\tc"); +} + +TEST_F(StringUtilitiesTest, toLower) +{ + EXPECT_EQ(toLower(""), ""); + EXPECT_EQ(toLower("ABC"), "abc"); + EXPECT_EQ(toLower("AbC123"), "abc123"); + EXPECT_EQ(toLower("already lower"), "already lower"); + + // Only 'A'-'Z' are remapped. Neighbouring punctuation and digits, which a + // buggy range check could catch, must survive untouched. + EXPECT_EQ(toLower("@[`{_^"), "@[`{_^"); +} + +// Both helpers are documented as depending only on their input. Guard that by +// checking the bytes just outside ASCII, which a locale-aware isspace/tolower +// could classify differently. +TEST_F(StringUtilitiesTest, trimAndLowerIgnoreLocale) +{ + // 0xA0 is NO-BREAK SPACE in Latin-1 and is whitespace to some locales. + std::string const nbsp("\xA0", 1); + EXPECT_EQ(trimWhitespace(nbsp), nbsp); + EXPECT_EQ(trimWhitespace(" " + nbsp + " "), nbsp); + + // 0xC0 is LATIN CAPITAL LETTER A WITH GRAVE in Latin-1. + std::string const agrave("\xC0", 1); + EXPECT_EQ(toLower(agrave), agrave); +} + } // namespace xrpl diff --git a/src/xrpld/app/main/GRPCServer.cpp b/src/xrpld/app/main/GRPCServer.cpp index 1b20ff1d49..fc4a9794bd 100644 --- a/src/xrpld/app/main/GRPCServer.cpp +++ b/src/xrpld/app/main/GRPCServer.cpp @@ -9,6 +9,7 @@ #include #include +#include #include #include #include @@ -24,7 +25,6 @@ #include #include -#include #include #include #include @@ -371,7 +371,7 @@ GRPCServerImpl::GRPCServerImpl(Application& app) std::string ip; while (std::getline(ss, ip, ',')) { - boost::algorithm::trim(ip); + ip = trimWhitespace(ip); auto const addr = boost::asio::ip::make_address(ip); if (addr.is_unspecified()) diff --git a/src/xrpld/app/main/Main.cpp b/src/xrpld/app/main/Main.cpp index a23b84f2e8..ba6520db5f 100644 --- a/src/xrpld/app/main/Main.cpp +++ b/src/xrpld/app/main/Main.cpp @@ -6,6 +6,7 @@ #include #include +#include #include #include #include @@ -21,7 +22,6 @@ #include #include -#include #include #include // IWYU pragma: keep #include @@ -211,7 +211,7 @@ public: boost::split(v, patterns, boost::algorithm::is_any_of(",")); selectors_.reserve(v.size()); std::ranges::for_each(v, [this](std::string s) { - boost::trim(s); + s = trimWhitespace(s); if (selectors_.empty() || !s.empty()) selectors_.emplace_back(beast::unit_test::Selector::ModeT::Automatch, s); }); @@ -614,7 +614,7 @@ run(int argc, char** argv) std::vector result; for (auto& s : strVec) { - boost::trim(s); + s = trimWhitespace(s); if (!s.empty()) result.push_back(std::stoi(s)); } diff --git a/src/xrpld/core/detail/Config.cpp b/src/xrpld/core/detail/Config.cpp index e93ccec56e..f263fb49ab 100644 --- a/src/xrpld/core/detail/Config.cpp +++ b/src/xrpld/core/detail/Config.cpp @@ -20,7 +20,6 @@ #include #include #include -#include #include #include #include @@ -185,7 +184,7 @@ parseIniFile(std::string const& strInput, bool const bTrim) for (auto& strValue : vLines) { if (bTrim) - boost::algorithm::trim(strValue); + strValue = trimWhitespace(strValue); if (strValue.empty() || strValue[0] == '#') { diff --git a/src/xrpld/rpc/detail/ServerHandler.cpp b/src/xrpld/rpc/detail/ServerHandler.cpp index 0181d5b10f..827d8705fd 100644 --- a/src/xrpld/rpc/detail/ServerHandler.cpp +++ b/src/xrpld/rpc/detail/ServerHandler.cpp @@ -8,6 +8,7 @@ #include #include +#include #include #include #include @@ -44,7 +45,6 @@ #include #include -#include #include #include #include @@ -113,7 +113,7 @@ authorized(Port const& port, std::map const& h) if ((it == h.end()) || (!it->second.starts_with("Basic "))) return false; std::string strUserPass64 = it->second.substr(6); - boost::trim(strUserPass64); + strUserPass64 = trimWhitespace(strUserPass64); std::string const strUserPass = base64Decode(strUserPass64); std::string::size_type const nColon = strUserPass.find(':'); if (nColon == std::string::npos) diff --git a/src/xrpld/rpc/handlers/account/AccountInfo.cpp b/src/xrpld/rpc/handlers/account/AccountInfo.cpp index f131af01e5..eed4e4cfe3 100644 --- a/src/xrpld/rpc/handlers/account/AccountInfo.cpp +++ b/src/xrpld/rpc/handlers/account/AccountInfo.cpp @@ -5,6 +5,7 @@ #include #include +#include #include #include #include @@ -22,7 +23,6 @@ #include #include -#include #include #include @@ -57,7 +57,7 @@ injectSLE(json::Value& jv, SLE const& sle) auto const& hash = sle.getFieldH128(sfEmailHash); Blob const b(hash.begin(), hash.end()); std::string md5 = strHex(makeSlice(b)); - boost::to_lower(md5); + md5 = toLower(md5); // VFALCO TODO Give a name to this constant and move it // to a more visible location. jv[jss::urlgravatar] = str(boost::format("https://www.gravatar.com/avatar/%s") % md5); diff --git a/src/xrpld/rpc/handlers/admin/data/CanDelete.cpp b/src/xrpld/rpc/handlers/admin/data/CanDelete.cpp index 91db16bb4f..5c96bfb215 100644 --- a/src/xrpld/rpc/handlers/admin/data/CanDelete.cpp +++ b/src/xrpld/rpc/handlers/admin/data/CanDelete.cpp @@ -3,14 +3,13 @@ #include #include +#include #include #include #include #include #include -#include - #include #include #include @@ -38,7 +37,7 @@ doCanDelete(rpc::JsonContext& context) else { std::string canDeleteStr = canDelete.asString(); - boost::to_lower(canDeleteStr); + canDeleteStr = toLower(canDeleteStr); if (canDeleteStr.find_first_not_of("0123456789") == std::string::npos) { diff --git a/src/xrpld/rpc/handlers/server_info/ServerDefinitions.cpp b/src/xrpld/rpc/handlers/server_info/ServerDefinitions.cpp index 32a084a833..c297c2482d 100644 --- a/src/xrpld/rpc/handlers/server_info/ServerDefinitions.cpp +++ b/src/xrpld/rpc/handlers/server_info/ServerDefinitions.cpp @@ -2,6 +2,7 @@ #include +#include #include #include #include @@ -14,7 +15,6 @@ #include #include -#include #include #include @@ -106,7 +106,7 @@ ServerDefinitions::translate(std::string const& inp) std::string token = inpToProcess.substr(0, pos); if (token.size() > 1) { - boost::algorithm::to_lower(token); + token = toLower(token); token[0] -= ('a' - 'A'); out += token; } From 26cc683ec143e8a5fcc6dd09c2c1fe25ac08b94c Mon Sep 17 00:00:00 2001 From: Gregory Tsipenyuk Date: Tue, 11 Aug 2026 18:15:51 +0000 Subject: [PATCH 22/28] fix: Assorted MPT/DEX fixes (#7299) Co-authored-by: Valentin Balaschenko <13349202+vlntb@users.noreply.github.com> --- include/xrpl/ledger/helpers/AMMHelpers.h | 28 +- include/xrpl/ledger/helpers/MPTokenHelpers.h | 8 + include/xrpl/protocol/AmountConversions.h | 2 +- include/xrpl/protocol/QualityFunction.h | 9 + include/xrpl/tx/paths/detail/StrandFlow.h | 25 +- include/xrpl/tx/transactors/dex/AMMWithdraw.h | 7 + src/libxrpl/ledger/helpers/MPTokenHelpers.cpp | 13 +- src/libxrpl/protocol/QualityFunction.cpp | 17 +- src/libxrpl/protocol/STAmount.cpp | 90 ++- src/libxrpl/tx/invariants/MPTInvariant.cpp | 7 +- src/libxrpl/tx/paths/BookStep.cpp | 246 ++++-- src/libxrpl/tx/paths/MPTEndpointStep.cpp | 3 +- src/libxrpl/tx/paths/OfferStream.cpp | 63 +- .../tx/transactors/check/CheckCash.cpp | 2 +- .../tx/transactors/dex/AMMClawback.cpp | 12 + .../tx/transactors/dex/AMMWithdraw.cpp | 47 +- src/test/app/AMMClawbackMPT_test.cpp | 252 +++++++ src/test/app/AMMExtendedMPT_test.cpp | 133 +++- src/test/app/AMMExtended_test.cpp | 48 +- src/test/app/AMMMPT_test.cpp | 157 +++- src/test/app/AMM_test.cpp | 71 +- src/test/app/EscrowToken_test.cpp | 181 +++++ src/test/app/FlowMPT_test.cpp | 160 ++++ src/test/app/OfferMPT_test.cpp | 711 ++++++++++++++++++ src/test/protocol/STAmount_test.cpp | 85 +++ 25 files changed, 2198 insertions(+), 179 deletions(-) diff --git a/include/xrpl/ledger/helpers/AMMHelpers.h b/include/xrpl/ledger/helpers/AMMHelpers.h index 7d41bfce81..a68171c426 100644 --- a/include/xrpl/ledger/helpers/AMMHelpers.h +++ b/include/xrpl/ledger/helpers/AMMHelpers.h @@ -226,7 +226,7 @@ getAMMOfferStartWithTakerGets( auto getAmounts = [&pool, &tfee](Number const& nTakerGetsProposed) { // Round downward to minimize the offer and to maximize the quality. - // This has the most impact when takerGets is XRP. + // This has the most impact when takerGets is integral. auto const takerGets = toAmount(getAsset(pool.out), nTakerGetsProposed, Number::RoundingMode::Downward); return TAmounts{swapAssetOut(pool, takerGets, tfee), takerGets}; @@ -294,7 +294,7 @@ getAMMOfferStartWithTakerPays( auto getAmounts = [&pool, &tfee](Number const& nTakerPaysProposed) { // Round downward to minimize the offer and to maximize the quality. - // This has the most impact when takerPays is XRP. + // This has the most impact when takerPays is integral. auto const takerPays = toAmount(getAsset(pool.in), nTakerPaysProposed, Number::RoundingMode::Downward); return TAmounts{takerPays, swapAssetIn(pool, takerPays, tfee)}; @@ -313,11 +313,11 @@ getAMMOfferStartWithTakerPays( * is equal to LOB quality (in this case AMM offer quality is * better than LOB quality) or AMM offer is equal to LOB quality * (in this case SPQ is better than LOB quality). - * Pre-amendment code calculates takerPays first. If takerGets is XRP, - * it is rounded down, which results in worse offer quality than - * LOB quality, and the offer might fail to generate. - * Post-amendment code calculates the XRP offer side first. The result - * is rounded down, which makes the offer quality better. + * Pre-amendment code calculates takerPays first. If takerGets is the + * economically coarser integral side, it is rounded down, which results in + * worse offer quality than LOB quality, and the offer might fail to generate. + * Post-amendment code calculates the economically coarser integral offer side + * first. The result is rounded down, which makes the offer quality better. * It might not be possible to match either SPQ or AMM offer to LOB * quality. This generally happens at higher fees. * @param pool AMM pool balances @@ -396,10 +396,18 @@ changeSpotPriceQuality( return std::nullopt; } - // Generate the offer starting with XRP side. Return seated offer amounts - // if the offer can be generated, otherwise nullopt. auto amounts = [&]() { - if (isXRP(getAsset(pool.out))) + bool const inIntegral = getAsset(pool.in).integral(); + bool const outIntegral = getAsset(pool.out).integral(); + + // Preserve historical behavior for fractional pairs and XRP/IOU-style + // one-integral-side pairs. For two integral assets, pick the side whose + // minimum unit is economically coarser at this quality. + // + // Quality::rate() is input units per output unit, so one output unit is + // coarser when it costs at least one input unit. Ties use takerGets, + // matching the historical XRP-output behavior. + if (outIntegral && (!inIntegral || Number(quality.rate()) >= 1)) return getAMMOfferStartWithTakerGets(pool, quality, tfee); return getAMMOfferStartWithTakerPays(pool, quality, tfee); }(); diff --git a/include/xrpl/ledger/helpers/MPTokenHelpers.h b/include/xrpl/ledger/helpers/MPTokenHelpers.h index 5418e5b26a..7babefd196 100644 --- a/include/xrpl/ledger/helpers/MPTokenHelpers.h +++ b/include/xrpl/ledger/helpers/MPTokenHelpers.h @@ -261,6 +261,14 @@ checkCreateMPT( xrpl::MPTIssue const& mptIssue, xrpl::AccountID const& holder, SLE::ref sponsorSle, + std::uint32_t flags, + beast::Journal j); + +TER +checkCreateMPT( + xrpl::ApplyView& view, + xrpl::MPTIssue const& mptIssue, + xrpl::AccountID const& holder, beast::Journal j); //------------------------------------------------------------------------------ diff --git a/include/xrpl/protocol/AmountConversions.h b/include/xrpl/protocol/AmountConversions.h index 3bcd80e827..ed68be62fe 100644 --- a/include/xrpl/protocol/AmountConversions.h +++ b/include/xrpl/protocol/AmountConversions.h @@ -154,7 +154,7 @@ T toAmount(Asset const& asset, Number const& n, Number::RoundingMode mode = Number::getround()) { SaveNumberRoundMode const rm(Number::getround()); - if (isXRP(asset)) + if (asset.integral()) Number::setround(mode); if constexpr (std::is_same_v) diff --git a/include/xrpl/protocol/QualityFunction.h b/include/xrpl/protocol/QualityFunction.h index 128b37ce12..4fcc730c42 100644 --- a/include/xrpl/protocol/QualityFunction.h +++ b/include/xrpl/protocol/QualityFunction.h @@ -60,6 +60,15 @@ public: std::optional outFromAvgQ(Quality const& quality); + /** + * Return whether `out` produces at least the requested + * average quality. + * @param quality requested average quality (quality limit) + * @param out output amount to test + */ + [[nodiscard]] bool + satisfiesAvgQ(Quality const& quality, Number const& out) const; + /** * Return true if the quality function is constant */ diff --git a/include/xrpl/tx/paths/detail/StrandFlow.h b/include/xrpl/tx/paths/detail/StrandFlow.h index c932c49cca..fcca97ecfc 100644 --- a/include/xrpl/tx/paths/detail/StrandFlow.h +++ b/include/xrpl/tx/paths/detail/StrandFlow.h @@ -1,6 +1,7 @@ #pragma once #include +#include #include #include #include @@ -373,7 +374,7 @@ qualityUpperBound(ReadView const& v, Strand const& strand) * increases quality of AMM steps, increasing the strand's composite * quality as the result. */ -template +template inline TOutAmt limitOut( ReadView const& v, @@ -411,21 +412,29 @@ limitOut( auto const out = qf->outFromAvgQ(limitQuality); if (!out) return remainingOut; - if constexpr (std::is_same_v) + if constexpr (std::is_same_v || std::is_same_v) { - return XRPAmount{*out}; + auto const roundedOut = TOutAmt{*out}; + // Integral outputs that round above the continuous target can + // realize worse average quality than the requested limit. Keep the + // default rounded value when it still satisfies the limit, since it + // is the largest matching offer; otherwise round down. + if (v.rules().enabled(featureMPTokensV2) && roundedOut > *out && + !qf->satisfiesAvgQ(limitQuality, roundedOut)) + { + NumberRoundModeGuard const g(Number::RoundingMode::Downward); + return TOutAmt{*out}; + } + return roundedOut; } else if constexpr (std::is_same_v) { return IOUAmount{*out}; } - else if constexpr (std::is_same_v) - { - return MPTAmount{*out}; - } else { - return STAmount{remainingOut.asset(), out->mantissa(), out->exponent()}; + static constexpr bool kAlwaysFalse = !std::is_same_v; + static_assert(kAlwaysFalse, "Unhandled StepAmount type"); } }(); // A tiny difference could be due to the round off diff --git a/include/xrpl/tx/transactors/dex/AMMWithdraw.h b/include/xrpl/tx/transactors/dex/AMMWithdraw.h index 7004dd57c1..6861fa7bc4 100644 --- a/include/xrpl/tx/transactors/dex/AMMWithdraw.h +++ b/include/xrpl/tx/transactors/dex/AMMWithdraw.h @@ -118,6 +118,7 @@ public: Sandbox& view, SLE const& ammSle, AccountID const account, + std::optional const& clawbackIssuer, AccountID const& ammAccount, STAmount const& amountBalance, STAmount const& amount2Balance, @@ -138,6 +139,11 @@ public: * @param view * @param ammSle AMM ledger entry * @param ammAccount AMM account + * @param clawbackIssuer when set (AMMClawback path), the issuer performing + * the clawback. A recreated MPToken is only auto-authorized when the + * asset's issuer matches this account, so a clawback cannot grant + * authorization on behalf of a different (paired-asset) issuer. + * @param account LP account * @param amountBalance current LP asset1 balance * @param amountWithdraw asset1 withdraw amount * @param amount2Withdraw asset2 withdraw amount @@ -153,6 +159,7 @@ public: Sandbox& view, SLE const& ammSle, AccountID const& ammAccount, + std::optional const& clawbackIssuer, AccountID const& account, STAmount const& amountBalance, STAmount const& amountWithdraw, diff --git a/src/libxrpl/ledger/helpers/MPTokenHelpers.cpp b/src/libxrpl/ledger/helpers/MPTokenHelpers.cpp index 6fe7328fa7..b239d0d3d1 100644 --- a/src/libxrpl/ledger/helpers/MPTokenHelpers.cpp +++ b/src/libxrpl/ledger/helpers/MPTokenHelpers.cpp @@ -952,6 +952,7 @@ checkCreateMPT( xrpl::MPTIssue const& mptIssue, xrpl::AccountID const& holder, SLE::ref sponsorSle, + std::uint32_t flags, beast::Journal j) { if (mptIssue.getIssuer() == holder) @@ -961,7 +962,7 @@ checkCreateMPT( auto const mptokenID = keylet::mptoken(mptIssuanceID.key, holder); if (!view.exists(mptokenID)) { - if (auto const err = createMPToken(view, mptIssue.getMptID(), holder, sponsorSle, 0); + if (auto const err = createMPToken(view, mptIssue.getMptID(), holder, sponsorSle, flags); !isTesSuccess(err)) { return err; @@ -977,6 +978,16 @@ checkCreateMPT( return tesSUCCESS; } +TER +checkCreateMPT( + xrpl::ApplyView& view, + xrpl::MPTIssue const& mptIssue, + xrpl::AccountID const& holder, + beast::Journal j) +{ + return checkCreateMPT(view, mptIssue, holder, {}, 0, j); +} + std::int64_t maxMPTAmount(SLE const& sleIssuance) { diff --git a/src/libxrpl/protocol/QualityFunction.cpp b/src/libxrpl/protocol/QualityFunction.cpp index e862770406..ffe583b7e1 100644 --- a/src/libxrpl/protocol/QualityFunction.cpp +++ b/src/libxrpl/protocol/QualityFunction.cpp @@ -38,7 +38,22 @@ QualityFunction::outFromAvgQ(Quality const& quality) return std::nullopt; return out; } - return std::nullopt; + // The sole caller (StrandFlow::limitOut) only invokes this on a non-const + // quality function, so m_ != 0 here, and a real payment/offer never yields + // a zero-rate limit quality (it would divide by zero above). This fallback + // is therefore unreachable in practice. + return std::nullopt; // LCOV_EXCL_LINE +} + +bool +QualityFunction::satisfiesAvgQ(Quality const& quality, Number const& out) const +{ + // satisfiesAvgQ is only reached from StrandFlow::limitOut *after* + // outFromAvgQ returned a value, which requires a non-zero rate. So a + // zero-rate quality never reaches here; this guard is defensive. + if (quality.rate() == beast::kZero) + return false; // LCOV_EXCL_LINE + return m_ * out + b_ >= 1 / quality.rate(); } } // namespace xrpl diff --git a/src/libxrpl/protocol/STAmount.cpp b/src/libxrpl/protocol/STAmount.cpp index 212c34322b..83b2983756 100644 --- a/src/libxrpl/protocol/STAmount.cpp +++ b/src/libxrpl/protocol/STAmount.cpp @@ -1445,6 +1445,59 @@ public: operator=(DontAffectNumberRoundMode const&) = delete; }; +Number::RoundingMode +roundMode(bool const resultNegative, bool const roundUp) +{ + using enum Number::RoundingMode; + // STAmount roundUp means "away from zero". The legacy scaled-mantissa + // multiply and divide paths reach that result with slightly different + // mechanics, including a final TowardsZero materialization in multiply. + // + // The MPT/V2 Number path already performs the operation under the directed + // mode below. Use the same mode again when converting back to STAmount so a + // fractional integral result stays consistently rounded after Number + // arithmetic, independent of whether the operation was multiply or divide. + return roundUp ^ resultNegative ? Upward : Downward; +} + +STAmount +roundNumberResult( + Asset const& asset, + bool const resultNegative, + bool const roundUp, + Number const& number) +{ + // MPT/V2 Number arithmetic uses directed rounding both for the operation + // and for materializing the final integral amount. + NumberRoundModeGuard const finalRound(roundMode(resultNegative, roundUp)); + auto result = STAmount{asset, number}; + [[maybe_unused]] bool const nonzeroPositiveRoundUp = + roundUp && !resultNegative && number != beast::kZero; + ALWAYS( + !nonzeroPositiveRoundUp || result != beast::kZero, + "xrpl::roundNumberResult : positive rounded-up MPT result is representable"); + + if (roundUp && !resultNegative && !result) + { + // Intended to preserve existing mulRound/divRound behavior for a + // positive result too small to represent in the target asset. + // + // Unreachable in practice: when roundUp is set, roundMode() above + // selects Upward, and materializing a Number into an STAmount honors + // that mode (Number::operator rep()), so any positive value rounds up + // to at least the smallest representable unit. Hence, a positive result + // is never !result here; the only zero case is a zero operand, which + // the mulRound/divRound callers handle before reaching this function. + // LCOV_EXCL_START + if (asset.integral()) + return STAmount{asset, 1}; + return STAmount{asset, STAmount::kMinValue, STAmount::kMinOffset, false}; + // LCOV_EXCL_STOP + } + + return result; +} + } // anonymous namespace // Pass the canonicalizeRound function pointer as a template parameter. @@ -1486,6 +1539,22 @@ mulRoundImpl(STAmount const& v1, STAmount const& v2, Asset const& asset, bool ro return STAmount(asset, minV * maxV); } + bool const resultNegative = v1.negative() != v2.negative(); + + if (asset.holds() && isFeatureEnabled(featureMPTokensV2, false)) + { + // MPT DEX can combine 63-bit MPT amounts with IOU-shaped transfer + // rates. Use Number arithmetic under MPTokensV2 so the rounded + // operation is not limited by the legacy uint64_t scaled mantissa. + Number result; + { + NumberRoundModeGuard const operationRound(roundMode(resultNegative, roundUp)); + result = Number{v1} * Number{v2}; + } + + return roundNumberResult(asset, resultNegative, roundUp, result); + } + std::uint64_t value1 = v1.mantissa(), value2 = v2.mantissa(); int offset1 = v1.exponent(), offset2 = v2.exponent(); @@ -1506,9 +1575,6 @@ mulRoundImpl(STAmount const& v1, STAmount const& v2, Asset const& asset, bool ro --offset2; } } - - bool const resultNegative = v1.negative() != v2.negative(); - // We multiply the two mantissas (each is between 10^15 // and 10^16), so their product is in the 10^30 to 10^32 // range. Dividing their product by 10^14 maintains the @@ -1575,6 +1641,22 @@ divRoundImpl(STAmount const& num, STAmount const& den, Asset const& asset, bool if (num == beast::kZero) return {asset}; + bool const resultNegative = (num.negative() != den.negative()); + + if (asset.holds() && isFeatureEnabled(featureMPTokensV2, false)) + { + // Match the multiply path above: Number performs the rounded + // operation, then STAmount materializes the final MPT amount using the + // same final rounding mode as the legacy path below. + Number result; + { + NumberRoundModeGuard const operationRound(roundMode(resultNegative, roundUp)); + result = Number{num} / Number{den}; + } + + return roundNumberResult(asset, resultNegative, roundUp, result); + } + std::uint64_t numVal = num.mantissa(), denVal = den.mantissa(); int numOffset = num.exponent(), denOffset = den.exponent(); @@ -1596,8 +1678,6 @@ divRoundImpl(STAmount const& num, STAmount const& den, Asset const& asset, bool } } - bool const resultNegative = (num.negative() != den.negative()); - // We divide the two mantissas (each is between 10^15 // and 10^16). To maintain precision, we multiply the // numerator by 10^17 (the product is in the range of diff --git a/src/libxrpl/tx/invariants/MPTInvariant.cpp b/src/libxrpl/tx/invariants/MPTInvariant.cpp index 77c5ad781e..12ec078c82 100644 --- a/src/libxrpl/tx/invariants/MPTInvariant.cpp +++ b/src/libxrpl/tx/invariants/MPTInvariant.cpp @@ -282,12 +282,13 @@ ValidMPTIssuance::finalize( "but created bad number of mptokens"; return false; } - // At most one MPToken may be created on withdraw/clawback since: + // At most two MPToken may be created on withdraw/clawback since: // - Liquidity Provider must have at least one token in order - // participate in AMM pool liquidity. + // participate in AMM pool liquidity or have LPTokens only. // - At most two MPTokens may be deleted if AMM pool, which has exactly // two tokens, is empty after withdraw/clawback. - if (mptokensCreated_ > 1 || mptokensDeleted_ > 2) + SOMETIMES(mptokensCreated_ == 2, "AMM withdraw/clawback recreated two MPTokens"); + if (mptokensCreated_ > 2 || mptokensDeleted_ > 2) { JLOG(j.fatal()) << "Invariant failed: MPT authorize succeeded " "but created/deleted bad number of mptokens"; diff --git a/src/libxrpl/tx/paths/BookStep.cpp b/src/libxrpl/tx/paths/BookStep.cpp index e7c2e9ee29..2823627108 100644 --- a/src/libxrpl/tx/paths/BookStep.cpp +++ b/src/libxrpl/tx/paths/BookStep.cpp @@ -44,7 +44,9 @@ #include #include #include +#include #include +#include #include #include #include @@ -653,7 +655,15 @@ limitStepIn( // under an amendment. ofrAmt = offer.limitIn(ofrAmt, inLmt, /* roundUp */ false); stpAmt.out = ofrAmt.out; - ownerGives = mulRatio(ofrAmt.out, transferRateOut, QUALITY_ONE, /*roundUp*/ false); + // Round up for MPT output so the offer owner pays the full + // ceil(amount × rate) fee, matching direct Payment semantics. IOU uses + // floating-point arithmetic so the floor/ceil distinction is sub-epsilon + // there; preserve the historical false to avoid changing IOU behavior. + ownerGives = mulRatio( + ofrAmt.out, + transferRateOut, + QUALITY_ONE, + /*roundUp*/ std::is_same_v); } } @@ -672,7 +682,11 @@ limitStepOut( if (limit < stpAmt.out) { stpAmt.out = limit; - ownerGives = mulRatio(stpAmt.out, transferRateOut, QUALITY_ONE, /*roundUp*/ false); + ownerGives = mulRatio( + stpAmt.out, + transferRateOut, + QUALITY_ONE, + /*roundUp*/ std::is_same_v); ofrAmt = offer.limitOut( ofrAmt, stpAmt.out, @@ -727,17 +741,20 @@ BookStep::forEachOffer( bool const isAssetInMPT = assetIn.holds(); auto const& owner = offer.owner(); - if (isAssetInMPT) - { - // Create MPToken for the offer's owner. No need to check - // for the reserve since the offer is removed if it is consumed. - // Therefore, the owner count remains the same. - if (auto const err = checkCreateMPT(sb, assetIn.get(), owner, {}, j_); - !isTesSuccess(err)) + auto removeOffer = [&](std::string_view logMessage = {}) { + auto const key = offer.key(); + if (!logMessage.empty()) { - return true; + JLOG(j_.trace()) << logMessage << (key ? " " + to_string(*key) : ""); } - } + if (key) + offers.permRmOffer(*key); + if (!offerAttempted) + { + // Change quality only if no previous offers were tried. + ofrQ = std::nullopt; + } + }; // It shouldn't matter from auth point of view whether it's sb // or afView. Amendment guard this change just in case. @@ -745,17 +762,15 @@ BookStep::forEachOffer( // Make sure offer owner has authorization to own Assets from issuer // and MPT assets can be traded/transferred. // An account can always own XRP or their own Assets. - if (!isTesSuccess(requireAuth(applyView, assetIn, owner)) || !checkMPTDEX(sb, owner)) + // Missing MPTokens are allowed during offer discovery; they are + // created later if the offer is actually consumed. + auto const authType = isAssetInMPT ? AuthType::WeakAuth : AuthType::Legacy; + if (!isTesSuccess(requireAuth(applyView, assetIn, owner, authType)) || + !checkMPTDEX(sb, owner)) { // Offer owner not authorized to hold IOU/MPT from issuer. // Remove this offer even if no crossing occurs. - if (auto const key = offer.key()) - offers.permRmOffer(*key); - if (!offerAttempted) - { - // Change quality only if no previous offers were tried. - ofrQ = std::nullopt; - } + removeOffer(); // Returning true causes offers.step() to delete the offer. return true; } @@ -768,52 +783,88 @@ BookStep::forEachOffer( static_cast(this)->getOfrOutRate(prevStep_, owner, strandDst_, trOut)); auto ofrAmt = offer.amount(); - TAmounts stpAmt{mulRatio(ofrAmt.in, ofrInRate, QUALITY_ONE, /*roundUp*/ true), ofrAmt.out}; - - // owner pays the transfer fee. - auto ownerGives = mulRatio(ofrAmt.out, ofrOutRate, QUALITY_ONE, /*roundUp*/ false); - - auto const funds = offer.isFunded() - ? ownerGives // Offer owner is issuer; they have unlimited funds - : offers.ownerFunds(); - - // Only if CLOB offer - if (funds < ownerGives) + TAmounts stpAmt{ofrAmt.in, ofrAmt.out}; + auto ownerGives = ofrAmt.out; + try { - // We already know offer.owner()!=offer.issueOut().account - ownerGives = funds; - stpAmt.out = mulRatio(ownerGives, QUALITY_ONE, ofrOutRate, /*roundUp*/ false); - - // 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. - ofrAmt = offer.limitOut(ofrAmt, stpAmt.out, /*roundUp*/ false); - + // All arithmetic in this block runs before the offer is consumed. + // A crafted MPTokensV2 offer can overflow while transfer rates or + // crossing limits are applied; remove that unusable offer instead + // of letting it persist as a tecINTERNAL source. stpAmt.in = mulRatio(ofrAmt.in, ofrInRate, QUALITY_ONE, /*roundUp*/ true); - } - // Limit offer's input if MPT, BookStep is the first step (an issuer - // is making a cross-currency payment), and this offer is not owned - // by the issuer. Otherwise, OutstandingAmount may overflow. - auto const& issuer = assetIn.getIssuer(); - if (isAssetInMPT && !prevStep_ && offer.owner() != issuer) - { - // Funds available to issue - auto const available = toAmount(accountFunds( - sb, - issuer, - assetIn, // STAmount{0}, but the default is not used - FreezeHandling::IgnoreFreeze, - AuthHandling::IgnoreAuth, - j_)); - if (stpAmt.in > available) + // owner pays the transfer fee. + ownerGives = mulRatio( + ofrAmt.out, + ofrOutRate, + QUALITY_ONE, + /*roundUp*/ std::is_same_v); + + auto const funds = offer.isFunded() + ? ownerGives // Offer owner is issuer; they have unlimited funds + : offers.ownerFunds(); + + // Only if CLOB offer + if (funds < ownerGives) { - limitStepIn(offer, ofrAmt, stpAmt, ownerGives, ofrInRate, ofrOutRate, available); - } - } + // We already know offer.owner()!=offer.issueOut().account + ownerGives = funds; + stpAmt.out = mulRatio(ownerGives, QUALITY_ONE, ofrOutRate, /*roundUp*/ false); - offerAttempted = true; - return callback(offer, ofrAmt, stpAmt, ownerGives, ofrInRate, ofrOutRate); + // 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. + ofrAmt = offer.limitOut(ofrAmt, stpAmt.out, /*roundUp*/ false); + + stpAmt.in = mulRatio(ofrAmt.in, ofrInRate, QUALITY_ONE, /*roundUp*/ true); + } + + // Limit offer's input if MPT, BookStep is the first step (an issuer + // is making a cross-currency payment), and this offer is not owned + // by the issuer. Otherwise, OutstandingAmount may overflow. + auto const& issuer = assetIn.getIssuer(); + if (isAssetInMPT && !prevStep_ && offer.owner() != issuer) + { + // Funds available to issue + auto const available = toAmount(accountFunds( + sb, + issuer, + assetIn, // STAmount{0}, but the default is not used + FreezeHandling::IgnoreFreeze, + AuthHandling::IgnoreAuth, + j_)); + if (stpAmt.in > available) + { + limitStepIn( + offer, ofrAmt, stpAmt, ownerGives, ofrInRate, ofrOutRate, available); + } + } + + offerAttempted = true; + return callback(offer, ofrAmt, stpAmt, ownerGives, ofrInRate, ofrOutRate); + } + catch (std::overflow_error const&) + { + if (sb.rules().enabled(featureMPTokensV2)) + { + SOMETIMES( + true, + "BookStep::forEachOffer removed MPT offer after " + "overflow during crossing"); + removeOffer("Removing offer with overflowing amount calculation"); + return true; + } + // An overflow can only be produced by a crafted MPT offer, and MPT + // offers require featureMPTokensV2 (enforced at OfferCreate + // preflight). So the amendment is always enabled when we get here + // and this legacy re-throw is unreachable in practice. + // LCOV_EXCL_START + XRPL_ASSERT( + sb.rules().enabled(featureMPTokensV2), + "xrpl::BookStep::forEachOffer : overflow implies MPTokensV2"); + throw; + // LCOV_EXCL_STOP + } }; // At any payment engine iteration, AMM offer can only be consumed once. @@ -873,6 +924,22 @@ BookStep::consumeOffer( // The offer owner gets the ofrAmt. The difference between ofrAmt and // stepAmt is a transfer fee that goes to book_.in.account { + if constexpr (std::is_same_v) + { + // If the offer's TakerPays asset is an MPT, the offer owner must + // hold an MPToken to receive it. Create one here if it doesn't + // already exist. + if (auto const err = checkCreateMPT(sb, book_.in.get(), offer.owner(), j_); + !isTesSuccess(err)) + { + // checkCreateMPT only fails on tecDIR_FULL (its source line is + // itself LCOV-excluded) or a missing offer-owner account, which + // cannot happen since that account owns the offer being + // consumed. Defensive and unreachable in practice. + Throw(err); // LCOV_EXCL_LINE + } + } + auto const dr = offer.send( sb, book_.in.getIssuer(), offer.owner(), toSTAmount(ofrAmt.in, book_.in), j_); if (!isTesSuccess(dr)) @@ -1043,6 +1110,13 @@ BookStep::revImp( auto ofrAdjAmt = ofrAmt; auto stpAdjAmt = stpAmt; auto ownerGivesAdj = ownerGives; + // This reduction can overflow via the transfer-rate mulRatio() on a + // 63-bit MPT amount (IOU rescales instead of throwing, and XRP stays + // under the int64 limit, so only MPT reaches it today), but + // savedIns/savedOuts are not updated until after it succeeds. The outer + // execOffer() catch can therefore remove the offer under + // featureMPTokensV2 (legacy propagate-the-exception behavior otherwise) + // without rolling back local state. limitStepOut( offer, ofrAdjAmt, @@ -1144,12 +1218,25 @@ BookStep::fwdImp( auto stpAdjAmt = stpAmt; auto ownerGivesAdj = ownerGives; + // limitStepIn()/limitStepOut() can throw std::overflow_error from the + // transfer-rate mulRatio() on a 63-bit MPT amount. (IOUAmount::mulRatio + // rescales rather than throwing, and XRP amounts/rates stay under the + // int64 limit, so in practice only MPT reaches this today.) execOffer() + // catches it: under featureMPTokensV2 the offending offer is removed; + // otherwise the legacy behavior (propagate the exception) is preserved. + // Keep candidate accumulator changes local until those calls succeed so + // the catch path does not observe partially updated state. Re-sum the + // staged sets to preserve historical flat_multiset summing behavior. + auto savedInsAdj = savedIns; + auto savedOutsAdj = savedOuts; + auto resultAdj = result; typename boost::container::flat_multiset::const_iterator lastOut; + if (stpAmt.in <= remainingIn) { - savedIns.insert(stpAmt.in); - lastOut = savedOuts.insert(stpAmt.out); - result = TAmounts(sum(savedIns), sum(savedOuts)); + savedInsAdj.insert(stpAmt.in); + lastOut = savedOutsAdj.insert(stpAmt.out); + resultAdj = TAmounts(sum(savedInsAdj), sum(savedOutsAdj)); // consume the offer even if stepAmt.in == remainingIn processMore = true; } @@ -1163,15 +1250,15 @@ BookStep::fwdImp( transferRateIn, transferRateOut, remainingIn); - savedIns.insert(remainingIn); - lastOut = savedOuts.insert(stpAdjAmt.out); - result.out = sum(savedOuts); - result.in = in; + savedInsAdj.insert(remainingIn); + lastOut = savedOutsAdj.insert(stpAdjAmt.out); + resultAdj.out = sum(savedOutsAdj); + resultAdj.in = in; processMore = false; } - if (result.out > cache_->out && result.in <= cache_->in) + if (resultAdj.out > cache_->out && resultAdj.in <= cache_->in) { // The step produced more output in the forward pass than the // reverse pass while consuming the same input (or less). If we @@ -1181,8 +1268,8 @@ BookStep::fwdImp( // input provided in the forward step and produce the output // requested from the reverse step. auto const lastOutAmt = *lastOut; - savedOuts.erase(lastOut); - auto const remainingOut = cache_->out - sum(savedOuts); + savedOutsAdj.erase(lastOut); + auto const remainingOut = cache_->out - sum(savedOutsAdj); auto ofrAdjAmtRev = ofrAmt; auto stpAdjAmtRev = stpAmt; auto ownerGivesAdjRev = ownerGives; @@ -1197,13 +1284,13 @@ BookStep::fwdImp( if (stpAdjAmtRev.in == remainingIn) { - result.in = in; - result.out = cache_->out; + resultAdj.in = in; + resultAdj.out = cache_->out; - savedIns.clear(); - savedIns.insert(result.in); - savedOuts.clear(); - savedOuts.insert(result.out); + savedInsAdj.clear(); + savedInsAdj.insert(resultAdj.in); + savedOutsAdj.clear(); + savedOutsAdj.insert(resultAdj.out); ofrAdjAmt = ofrAdjAmtRev; stpAdjAmt.in = remainingIn; @@ -1214,10 +1301,15 @@ BookStep::fwdImp( { // This is (likely) a problem case, and will be caught // with later checks - savedOuts.insert(lastOutAmt); + savedOutsAdj.insert(lastOutAmt); } } + // Commit the staged accounting only after limitStepIn()/limitStepOut() + // have succeeded. + savedIns = std::move(savedInsAdj); + savedOuts = std::move(savedOutsAdj); + result = resultAdj; remainingIn = in - result.in; this->consumeOffer(sb, offer, ofrAdjAmt, stpAdjAmt, ownerGivesAdj); diff --git a/src/libxrpl/tx/paths/MPTEndpointStep.cpp b/src/libxrpl/tx/paths/MPTEndpointStep.cpp index 0a0f6a9f27..a47cfa15a5 100644 --- a/src/libxrpl/tx/paths/MPTEndpointStep.cpp +++ b/src/libxrpl/tx/paths/MPTEndpointStep.cpp @@ -410,8 +410,7 @@ MPTEndpointOfferCrossingStep::checkCreateMPT(ApplyView& view, xrpl::DebtDirectio // for the reserve since the offer doesn't go on the books // if crossed. Insufficient reserve is allowed if the offer // crossed. See CreateOffer::applyGuts() for reserve check. - if (auto const err = xrpl::checkCreateMPT(view, mptIssue_, dst_, {}, j_); - !isTesSuccess(err)) + if (auto const err = xrpl::checkCreateMPT(view, mptIssue_, dst_, j_); !isTesSuccess(err)) { JLOG(j_.trace()) << "MPTEndpointStep::checkCreateMPT: failed create MPT"; resetCache(srcDebtDir); diff --git a/src/libxrpl/tx/paths/OfferStream.cpp b/src/libxrpl/tx/paths/OfferStream.cpp index ecc8416a2b..2f2fef49f0 100644 --- a/src/libxrpl/tx/paths/OfferStream.cpp +++ b/src/libxrpl/tx/paths/OfferStream.cpp @@ -29,6 +29,8 @@ #include #include +#include +#include namespace xrpl { @@ -136,17 +138,17 @@ template TOfferStreamBase::shouldRmSmallIncreasedQOffer() const { // Consider removing the offer if: - // o `TakerPays` is XRP (because of XRP drops granularity) or + // o `TakerPays` is integral (because XRP/MPT have indivisible units) or // o `TakerPays` and `TakerGets` are both IOU and `TakerPays`<`TakerGets` - static constexpr bool kInIsXrp = std::is_same_v; - static constexpr bool kOutIsXrp = std::is_same_v; + constexpr bool const kInIsIntegral = !std::is_same_v; + constexpr bool const kOutIsIntegral = !std::is_same_v; - if constexpr (kOutIsXrp) + if constexpr (!kInIsIntegral && kOutIsIntegral) { - // If `TakerGets` is XRP, the worst this offer's quality can change is - // to about 10^-81 `TakerPays` and 1 drop `TakerGets`. This will be - // remarkably good quality for any realistic asset, so these offers - // don't need this extra check. + // If only `TakerGets` is integral, the worst this offer's quality can + // change is to about 10^-81 `TakerPays` and 1 unit `TakerGets`. This + // will be perfect quality for any realistic asset, so these + // offers don't need this extra check. return false; } @@ -156,7 +158,7 @@ TOfferStreamBase::shouldRmSmallIncreasedQOffer() const TAmounts const ofrAmts{ toAmount(offer_.amount().in), toAmount(offer_.amount().out)}; - if constexpr (!kInIsXrp && !kOutIsXrp) + if constexpr (!kInIsIntegral && !kOutIsIntegral) { if (Number(ofrAmts.in) >= Number(ofrAmts.out)) return false; @@ -165,7 +167,12 @@ TOfferStreamBase::shouldRmSmallIncreasedQOffer() const TTakerGets const ownerFunds = toAmount(*ownerFunds_); auto const effectiveAmounts = [&] { - if (offer_.owner() != offer_.assetOut().getIssuer() && ownerFunds < ofrAmts.out) + // Issuer-owned IOU offers are self-funded without a limit. MPT issuer + // offers are bounded by remaining issuance capacity, so they still need + // to be clipped by ownerFunds. + bool const issuerHasUnlimitedFunds = offer_.owner() == offer_.assetOut().getIssuer() && + offer_.assetOut().template holds(); + if (!issuerHasUnlimitedFunds && ownerFunds < ofrAmts.out) { // adjust the amounts by owner funds. // @@ -305,7 +312,41 @@ TOfferStreamBase::step() continue; } - if (shouldRmSmallIncreasedQOffer()) + // Partially funded offers can be reduced before BookStep sees them. + // If that strict reduction overflows under MPTokensV2, remove the + // unusable offer instead of leaving it at the book tip. + bool shouldRemoveSmallIncreasedQOffer = false; + try + { + shouldRemoveSmallIncreasedQOffer = shouldRmSmallIncreasedQOffer(); + } + catch (std::overflow_error const&) + { + if (view_.rules().enabled(featureMPTokensV2)) + { + SOMETIMES( + true, + "OfferStream::step removed MPT offer with overflowing " + "reduced quality"); + permRmOffer(entry->key()); + JLOG(j_.warn()) << "Removing offer with overflowing reduced quality " + << entry->key(); + offer_ = TOffer{}; + continue; + } + // The strict reduction only overflows for a crafted MPT offer, and + // MPT offers require featureMPTokensV2 (enforced at OfferCreate + // preflight). So the amendment is always enabled here and this + // legacy re-throw is unreachable in practice. + // LCOV_EXCL_START + XRPL_ASSERT( + view_.rules().enabled(featureMPTokensV2), + "xrpl::TOfferStreamBase::step : overflow implies MPTokensV2"); + throw; + // LCOV_EXCL_STOP + } + + if (shouldRemoveSmallIncreasedQOffer) { auto const originalFunds = accountFundsHelper( cancelView_, diff --git a/src/libxrpl/tx/transactors/check/CheckCash.cpp b/src/libxrpl/tx/transactors/check/CheckCash.cpp index e4d8f192c0..857f759752 100644 --- a/src/libxrpl/tx/transactors/check/CheckCash.cpp +++ b/src/libxrpl/tx/transactors/check/CheckCash.cpp @@ -528,7 +528,7 @@ CheckCash::doApply() return tecINSUFFICIENT_RESERVE; if (auto const err = - checkCreateMPT(psb, mptID, accountID_, *sponsorSle, j_); + checkCreateMPT(psb, mptID, accountID_, *sponsorSle, 0, j_); !isTesSuccess(err)) { return err; diff --git a/src/libxrpl/tx/transactors/dex/AMMClawback.cpp b/src/libxrpl/tx/transactors/dex/AMMClawback.cpp index 455b2ad5c5..e690cd7693 100644 --- a/src/libxrpl/tx/transactors/dex/AMMClawback.cpp +++ b/src/libxrpl/tx/transactors/dex/AMMClawback.cpp @@ -227,6 +227,7 @@ AMMClawback::applyGuts(Sandbox& sb) sb, *ammSle, holder, + issuer, ammAccount, amountBalance, amount2Balance, @@ -311,6 +312,14 @@ AMMClawback::equalWithdrawMatchingOneAmount( STAmount const& holdLPtokens, STAmount const& amount) { + // The clawback issuer signs for its own asset only. Threaded into the + // withdrawal so a recreated MPToken is auto-authorized only for the + // clawback issuer's asset, never for a paired asset from another issuer. + // preflight guarantees sfAccount is the clawed asset's issuer (it rejects + // the tx as temMALFORMED when sfAsset's issuer != sfAccount), so this is + // the issuer, not just any signer. + AccountID const issuer = ctx_.tx[sfAccount]; + auto frac = Number{amount} / amountBalance; auto amount2Withdraw = amount2Balance * frac; @@ -324,6 +333,7 @@ AMMClawback::equalWithdrawMatchingOneAmount( sb, ammSle, holder, + issuer, ammAccount, amountBalance, amount2Balance, @@ -364,6 +374,7 @@ AMMClawback::equalWithdrawMatchingOneAmount( sb, ammSle, ammAccount, + issuer, holder, amountBalance, amountRounded, @@ -384,6 +395,7 @@ AMMClawback::equalWithdrawMatchingOneAmount( sb, ammSle, ammAccount, + issuer, holder, amountBalance, amount, diff --git a/src/libxrpl/tx/transactors/dex/AMMWithdraw.cpp b/src/libxrpl/tx/transactors/dex/AMMWithdraw.cpp index 5294dd0c7f..edd2cc2037 100644 --- a/src/libxrpl/tx/transactors/dex/AMMWithdraw.cpp +++ b/src/libxrpl/tx/transactors/dex/AMMWithdraw.cpp @@ -19,6 +19,7 @@ #include #include #include +#include #include #include #include @@ -516,6 +517,7 @@ AMMWithdraw::withdraw( view, ammSle, ammAccount, + std::nullopt, accountID_, amountBalance, amountWithdraw, @@ -536,6 +538,7 @@ AMMWithdraw::withdraw( Sandbox& view, SLE const& ammSle, AccountID const& ammAccount, + std::optional const& clawbackIssuer, AccountID const& account, STAmount const& amountBalance, STAmount const& amountWithdraw, @@ -703,14 +706,48 @@ AMMWithdraw::withdraw( if (mptokenKey && account != asset.getIssuer()) { auto const& mptIssue = asset.get(); + std::uint32_t createFlags = 0; if (auto const err = requireAuth(view, mptIssue, account, AuthType::WeakAuth); !isTesSuccess(err)) - return err; + { + if (authHandling != AuthHandling::IgnoreAuth || err != tecNO_AUTH) + { + // Unreachable in practice. Normal withdraws (authHandling + // != IgnoreAuth) are rejected for unauthorized holders in + // preclaim, so they never get here. Under clawback + // (IgnoreAuth) requireAuth returns a non-tecNO_AUTH error + // (e.g. tecEXPIRED) only for a domain-authorized MPT, but no + // such MPT can be in an AMM pool: a directly domain-gated + // RequireAuth MPT fails AMMCreate/deposit with tecNO_AUTH, + // and vault shares (whose recursive auth could yield + // tecEXPIRED) are rejected by AMMCreate with tecWRONG_ASSET. + return err; // LCOV_EXCL_LINE + } - if (auto const err = checkCreateMPT(view, mptIssue, account, {}, journal); + // AMMClawback ignores authorization so the issuer can recover + // MPT locked in the pool even if the holder deleted their + // MPToken. Only auto-authorize the recreated MPToken for the + // clawback issuer's own asset: authorization is granted by an + // asset's issuer, and the clawback transaction is signed by + // that issuer only for its own asset. For a paired asset issued + // by a different account, recreate the MPToken *unauthorized* so + // the clawback does not grant authorization on behalf of that + // issuer (which would bypass its lsfMPTRequireAuth). The holder + // still receives the paired asset (accountSend only requires the + // MPToken to exist, not to be authorized); the balance remains + // gated by its issuer until that issuer authorizes it. + if (clawbackIssuer && asset.getIssuer() == *clawbackIssuer) + createFlags = lsfMPTAuthorized; + } + + if (auto const err = checkCreateMPT(view, mptIssue, account, {}, createFlags, journal); !isTesSuccess(err)) { - return err; + // checkCreateMPT only fails on tecDIR_FULL (its source line is + // itself LCOV-excluded) or a missing account, which cannot + // happen since `account` is the withdrawing LP. Defensive and + // unreachable in practice. + return err; // LCOV_EXCL_LINE } } return tesSUCCESS; @@ -804,6 +841,7 @@ AMMWithdraw::equalWithdrawTokens( view, ammSle, accountID_, + std::nullopt, ammAccount, amountBalance, amount2Balance, @@ -856,6 +894,7 @@ AMMWithdraw::equalWithdrawTokens( Sandbox& view, SLE const& ammSle, AccountID const account, + std::optional const& clawbackIssuer, AccountID const& ammAccount, STAmount const& amountBalance, STAmount const& amount2Balance, @@ -878,6 +917,7 @@ AMMWithdraw::equalWithdrawTokens( view, ammSle, ammAccount, + clawbackIssuer, account, amountBalance, amountBalance, @@ -913,6 +953,7 @@ AMMWithdraw::equalWithdrawTokens( view, ammSle, ammAccount, + clawbackIssuer, account, amountBalance, amountWithdraw, diff --git a/src/test/app/AMMClawbackMPT_test.cpp b/src/test/app/AMMClawbackMPT_test.cpp index 6c7aa99156..1d75c4db22 100644 --- a/src/test/app/AMMClawbackMPT_test.cpp +++ b/src/test/app/AMMClawbackMPT_test.cpp @@ -16,6 +16,8 @@ #include #include #include +#include +#include #include #include #include @@ -1476,6 +1478,60 @@ class AMMClawbackMPT_test : public beast::unit_test::Suite } } + void + testClawbackCreatesMissingMPToken(FeatureBitset features) + { + testcase("test AMMClawback creates missing MPToken"); + using namespace jtx; + + auto test = [&](std::optional const clawAmount) { + Env env{*this, features}; + Account const gw{"gateway"}; + Account const alice{"alice"}; + env.fund(XRP(1'000'000), gw, alice); + env.close(); + + MPTTester token( + {.env = env, + .issuer = gw, + .holders = {alice}, + .pay = 1'000, + .flags = tfMPTCanClawback | tfMPTRequireAuth | kMptDexFlags, + .authHolder = true}); + + AMM ammAlice(env, alice, token(1'000), XRP(1'000)); + env.close(); + BEAST_EXPECT(env.balance(alice, token) == token(0)); + + // The holder can delete the zero-balance MPToken while still + // holding LP tokens. A regular AMMWithdraw remains subject to + // RequireAuth and cannot recreate the missing token. + token.authorize({.account = alice, .flags = tfMPTUnauthorize}); + env.close(); + BEAST_EXPECT(!env.le(keylet::mptoken(token.issuanceID(), alice.id()))); + ammAlice.withdrawAll(alice, std::nullopt, Ter(tecNO_AUTH)); + env.close(); + BEAST_EXPECT(!env.le(keylet::mptoken(token.issuanceID(), alice.id()))); + + // AMMClawback ignores authorization and must be able to recreate + // the holder MPToken so the issuer can recover MPT from the pool. + std::optional amount; + if (clawAmount) + amount = token(*clawAmount); + env(amm::ammClawback(gw, alice, token, XRP, amount)); + env.close(); + + auto const sleMpt = env.le(keylet::mptoken(token.issuanceID(), alice.id())); + BEAST_EXPECT(sleMpt && sleMpt->isFlag(lsfMPTAuthorized)); + env.require(Balance(alice, token(0))); + + BEAST_EXPECT(clawAmount ? ammAlice.ammExists() : !ammAlice.ammExists()); + }; + + test(std::nullopt); + test(400); + } + void testSingleDepositAndClawback(FeatureBitset features) { @@ -1949,6 +2005,199 @@ class AMMClawbackMPT_test : public beast::unit_test::Suite } } + // Test that AMMClawback succeeds when the LP has previously deleted both + // zero-balance MPToken objects in an MPT/MPT pool. The fix changes the + // ValidMPTIssuance invariant threshold from > 1 to > 2 so that the two + // MPToken creations triggered by the internal AMMWithdraw are permitted. + void + testClawbackAfterDeletingMPTokens(FeatureBitset features) + { + testcase("test AMMClawback after holder deletes zero-balance MPTokens"); + using namespace jtx; + + // Partial clawback (one asset): verify both MPTokens are recreated and + // the non-claw asset is returned to alice. + { + Env env(*this, features); + Account const gw{"gateway"}; + Account const alice{"alice"}; + env.fund(XRP(100'000), gw, alice); + env.close(); + + MPTTester btc( + {.env = env, + .issuer = gw, + .holders = {alice}, + .pay = 10'000, + .flags = tfMPTCanClawback | kMptDexFlags}); + + MPTTester eth( + {.env = env, + .issuer = gw, + .holders = {alice}, + .pay = 10'000, + .flags = tfMPTCanClawback | kMptDexFlags}); + + // Alice deposits everything into the MPT/MPT pool; her MPT + // balances drop to zero. + AMM const amm(env, alice, btc(10'000), eth(10'000)); + env.close(); + BEAST_EXPECT(amm.expectBalances(btc(10'000), eth(10'000), IOUAmount{10'000})); + + auto aliceBTC = env.balance(alice, btc); + auto aliceETH = env.balance(alice, eth); + BEAST_EXPECT(aliceBTC == btc(0)); + BEAST_EXPECT(aliceETH == eth(0)); + + // Alice deletes both zero-balance MPTokens to reclaim reserves. + btc.authorize({.account = alice, .flags = tfMPTUnauthorize}); + eth.authorize({.account = alice, .flags = tfMPTUnauthorize}); + BEAST_EXPECT(!env.le(keylet::mptoken(btc.issuanceID(), alice.id()))); + BEAST_EXPECT(!env.le(keylet::mptoken(eth.issuanceID(), alice.id()))); + + // gw claws back some BTC from alice's share in the pool. + // AMMWithdraw internally creates both missing MPTokens + // (mptokensCreated_ == 2); the invariant (> 2) allows this. + env(amm::ammClawback(gw, alice, btc, eth, btc(1'000))); + env.close(); + + // Both MPToken objects must have been recreated. + BEAST_EXPECT(env.le(keylet::mptoken(btc.issuanceID(), alice.id()))); + BEAST_EXPECT(env.le(keylet::mptoken(eth.issuanceID(), alice.id()))); + + // The non-claw asset (eth) was returned to alice. + BEAST_EXPECT(env.balance(alice, eth) > aliceETH); + // The claw asset (btc) was burned; alice's btc balance stays 0. + env.require(Balance(alice, aliceBTC)); + BEAST_EXPECT(amm.ammExists()); + } + + // Full clawback (two assets, tfClawTwoAssets): verify both MPTokens + // are recreated and the AMM is deleted when fully drained. + { + Env env(*this, features); + Account const gw{"gateway"}; + Account const alice{"alice"}; + env.fund(XRP(100'000), gw, alice); + env.close(); + + MPTTester btc( + {.env = env, + .issuer = gw, + .holders = {alice}, + .pay = 10'000, + .flags = tfMPTCanClawback | kMptDexFlags}); + + MPTTester eth( + {.env = env, + .issuer = gw, + .holders = {alice}, + .pay = 10'000, + .flags = tfMPTCanClawback | kMptDexFlags}); + + AMM const amm(env, alice, btc(10'000), eth(10'000)); + env.close(); + + auto aliceBTC = env.balance(alice, btc); + auto aliceETH = env.balance(alice, eth); + + btc.authorize({.account = alice, .flags = tfMPTUnauthorize}); + eth.authorize({.account = alice, .flags = tfMPTUnauthorize}); + BEAST_EXPECT(!env.le(keylet::mptoken(btc.issuanceID(), alice.id()))); + BEAST_EXPECT(!env.le(keylet::mptoken(eth.issuanceID(), alice.id()))); + + // Full two-asset clawback: both assets are clawed and alice + // receives nothing back. The AMM should be empty and deleted. + env(amm::ammClawback(gw, alice, btc, eth, std::nullopt), Txflags(tfClawTwoAssets)); + env.close(); + + BEAST_EXPECT(!amm.ammExists()); + // Both assets were clawed; alice's balances remain at zero. + env.require(Balance(alice, aliceBTC)); + env.require(Balance(alice, aliceETH)); + } + } + + void + testClawbackCrossIssuerPairedAssetAuth(FeatureBitset features) + { + testcase("test AMMClawback recreates paired-issuer MPToken unauthorized"); + using namespace jtx; + + // Cross-issuer MPT/MPT pool: btc is issued by gw, eth by gw2, and both + // require authorization. Alice deposits her entire balance of both and + // deletes the resulting zero-balance MPTokens. When gw claws back its + // own asset (btc), the two-asset withdrawal must recreate both of + // Alice's MPTokens so the pool can pay her the paired asset. The + // recreated MPToken may only be auto-authorized for the clawback + // issuer's own asset (btc); the paired asset's issuer (gw2) never + // consented, so eth must be recreated *unauthorized*, leaving gw2 in + // control of its own token and preserving its RequireAuth guarantee. + Env env(*this, features); + Account const gw{"gateway"}; + Account const gw2{"gateway2"}; + Account const alice{"alice"}; + env.fund(XRP(100'000), gw, gw2, alice); + env.close(); + + MPTTester btc( + {.env = env, + .issuer = gw, + .holders = {alice}, + .pay = 10'000, + .flags = tfMPTCanClawback | tfMPTRequireAuth | kMptDexFlags, + .authHolder = true}); + + MPTTester eth( + {.env = env, + .issuer = gw2, + .holders = {alice}, + .pay = 10'000, + .flags = tfMPTCanClawback | tfMPTRequireAuth | kMptDexFlags, + .authHolder = true}); + + // Alice deposits everything into the pool; her MPT balances drop to 0. + AMM const amm(env, alice, btc(10'000), eth(10'000)); + env.close(); + BEAST_EXPECT(amm.expectBalances(btc(10'000), eth(10'000), IOUAmount{10'000})); + BEAST_EXPECT(env.balance(alice, btc) == btc(0)); + BEAST_EXPECT(env.balance(alice, eth) == eth(0)); + + // Alice deletes both zero-balance MPTokens to reclaim reserves. + btc.authorize({.account = alice, .flags = tfMPTUnauthorize}); + eth.authorize({.account = alice, .flags = tfMPTUnauthorize}); + BEAST_EXPECT(!env.le(keylet::mptoken(btc.issuanceID(), alice.id()))); + BEAST_EXPECT(!env.le(keylet::mptoken(eth.issuanceID(), alice.id()))); + + // gw (issuer of btc) claws back part of Alice's btc. This is a + // cross-issuer pool, so tfClawTwoAssets is not permitted: only btc is + // clawed back, while the paired eth is returned to Alice. + env(amm::ammClawback(gw, alice, btc, eth, btc(1'000))); + env.close(); + + // Both MPTokens were recreated so the withdrawal could pay Alice. + auto const sleBtc = env.le(keylet::mptoken(btc.issuanceID(), alice.id())); + auto const sleEth = env.le(keylet::mptoken(eth.issuanceID(), alice.id())); + BEAST_EXPECT(sleBtc); + BEAST_EXPECT(sleEth); + + // The clawback issuer's own asset (btc) may be recreated authorized: + // gw has authority over its own token. + BEAST_EXPECT(sleBtc && sleBtc->isFlag(lsfMPTAuthorized)); + + // The paired asset (eth) is issued by gw2, who did not sign this + // transaction. It must be recreated *unauthorized* so gw2's RequireAuth + // is not bypassed. This is the core assertion for the cross-issuer fix. + BEAST_EXPECT(sleEth && !sleEth->isFlag(lsfMPTAuthorized)); + + // The clawback still completed: btc was clawed back (Alice keeps a zero + // btc balance) and the paired eth was delivered into Alice's now + // unauthorized, gw2-gated MPToken (non-zero raw balance). + BEAST_EXPECT(sleBtc && sleBtc->getFieldU64(sfMPTAmount) == 0); + BEAST_EXPECT(sleEth && sleEth->getFieldU64(sfMPTAmount) > 0); + BEAST_EXPECT(amm.ammExists()); + } + void run() override { @@ -1965,6 +2214,9 @@ class AMMClawbackMPT_test : public beast::unit_test::Suite testAMMClawbackAllSameIssuer(all); testAMMClawbackIssuesEachOther(all); testAssetFrozenOrLocked(all); + testClawbackCreatesMissingMPToken(all); + testClawbackAfterDeletingMPTokens(all); + testClawbackCrossIssuerPairedAssetAuth(all); testSingleDepositAndClawback(all); testLastHolderLPTokenBalance(all); testLastHolderLPTokenBalance(all - fixAMMv1_3 - fixAMMClawbackRounding); diff --git a/src/test/app/AMMExtendedMPT_test.cpp b/src/test/app/AMMExtendedMPT_test.cpp index f04ea39f2b..5059128d4b 100644 --- a/src/test/app/AMMExtendedMPT_test.cpp +++ b/src/test/app/AMMExtendedMPT_test.cpp @@ -188,20 +188,28 @@ private: {features}); // tfPassive -- place the offer without crossing it. - testAMM( - [&](AMM& ammAlice, Env& env) { - // Carol creates a passive offer that could cross AMM. - // Carol's offer should stay in the ledger. - auto const& btc = MPT(ammAlice[1]); - env(offer(carol_, XRP(100), btc(100), tfPassive)); - env.close(); - BEAST_EXPECT(ammAlice.expectBalances(XRP(10'100), btc(10'000), ammAlice.tokens())); - BEAST_EXPECT(expectOffers(env, carol_, 1, {{{XRP(100), btc(100)}}})); - }, - {{XRP(10'100), gAmmmpt(10'000)}}, - 0, - std::nullopt, - {features}); + { + Env env{*this, features}; + fund(env, gw_, {alice_, carol_}, XRP(30'000'000)); + + MPTTester const btc( + {.env = env, + .issuer = gw_, + .holders = {alice_, carol_}, + .pay = 30'000'000, + .flags = kMptDexFlags}); + + AMM const ammAlice(env, alice_, XRP(10'100'000), btc(10'000'000)); + + // Scale the exact-quality fixture up so the visual relationship + // stays clear: the passive CLOB offer has the same 1:1 quality as + // the generated AMM offer, so it should not cross. + env(offer(carol_, XRP(100'000), btc(100'000), tfPassive)); + env.close(); + BEAST_EXPECT( + ammAlice.expectBalances(XRP(10'100'000), btc(10'000'000), ammAlice.tokens())); + BEAST_EXPECT(expectOffers(env, carol_, 1, {{{XRP(100'000), btc(100'000)}}})); + } // tfPassive -- cross only offers of better quality. testAMM( @@ -1084,9 +1092,9 @@ private: // AMM is consumed up to the first cam Offer quality BEAST_EXPECT(ammCarol.expectBalances( - aBux(3'093'541'659'651'604), bBux(3'200'215'509'984'418), ammCarol.tokens())); + aBux(3'093'541'659'651'603), bBux(3'200'215'509'984'419), ammCarol.tokens())); BEAST_EXPECT(expectOffers( - env, cam, 1, {{Amounts{bBux(200'215'509'984'418), aBux(200'215'509'984'419)}}})); + env, cam, 1, {{Amounts{bBux(200'215'509'984'419), aBux(200'215'509'984'419)}}})); } void @@ -1241,7 +1249,7 @@ private: BEAST_EXPECT(sa == XRP(100'000'000)); // Bob gets ~99.99e12ETH. This is the amount Bob // can get out of AMM for 100,000,000XRP. - BEAST_EXPECT(equal(da, eth(99'999'900'000'100))); + BEAST_EXPECT(equal(da, eth(99'999'900'000'099))); } // carol holds ETH, sells ETH for XRP @@ -1505,6 +1513,96 @@ private: } } + void + pathFindMPTAMMExecutableSourceAmount() + { + testcase("Path Find: MPT AMM source amount is executable"); + using namespace jtx; + + auto const checkQuote = [&](std::int64_t usdPool, + std::int64_t eurPool, + std::int64_t deliverAmount, + std::int64_t expectedSourceAmount) { + Env env = pathTestEnv(); + env.fund(XRP(30'000), gw_, alice_, bob_, carol_); + env.close(); + + MPTTester const usd( + {.env = env, + .issuer = gw_, + .holders = {alice_, bob_, carol_}, + .pay = usdPool, + .flags = kMptDexFlags}); + + MPTTester const eur( + {.env = env, + .issuer = gw_, + .holders = {alice_, bob_, carol_}, + .pay = eurPool, + .flags = kMptDexFlags}); + + AMM const ammCarol(env, carol_, usd(usdPool), eur(eurPool)); + env.close(); + + STPathSet st; + STAmount sa, da; + auto const deliver = eur(deliverAmount); + std::tie(st, sa, da) = findPaths( + env, + alice_, + bob_, + deliver, + std::nullopt, + usd.issuanceID(), + std::nullopt, + std::nullopt); + + // Each quote must execute when used as an exact-output SendMax. + BEAST_EXPECT(equal(da, deliver)); + BEAST_EXPECT(equal(sa, usd(expectedSourceAmount))); + BEAST_EXPECT(!st.empty()); + + auto const before = eur.getBalance(bob_); + env(pay(alice_, bob_, deliver), + Json(jss::Paths, st.getJson(JsonOptions::Values::None)), + Sendmax(sa), + Txflags(tfNoRippleDirect)); + BEAST_EXPECT(eur.getBalance(bob_) == before + deliverAmount); + }; + + struct TestCase + { + std::int64_t usdPool; + std::int64_t eurPool; + std::int64_t deliverAmount; + std::int64_t expectedSourceAmount; + }; + + // Cover the original 2:1 pool and the same pool scaled down by 1000. + // clang-format off + TestCase const testCases[] = { + {.usdPool = 2'000'000, .eurPool = 1'000'000, .deliverAmount = 1, .expectedSourceAmount = 3}, + {.usdPool = 2'000'000, .eurPool = 1'000'000, .deliverAmount = 2, .expectedSourceAmount = 5}, + {.usdPool = 2'000'000, .eurPool = 1'000'000, .deliverAmount = 10, .expectedSourceAmount = 21}, + {.usdPool = 2'000'000, .eurPool = 1'000'000, .deliverAmount = 100, .expectedSourceAmount = 201}, + {.usdPool = 2'000'000, .eurPool = 1'000'000, .deliverAmount = 1'000, .expectedSourceAmount = 2'003}, + {.usdPool = 2'000, .eurPool = 1'000, .deliverAmount = 1, .expectedSourceAmount = 3}, + {.usdPool = 2'000, .eurPool = 1'000, .deliverAmount = 2, .expectedSourceAmount = 5}, + {.usdPool = 2'000, .eurPool = 1'000, .deliverAmount = 10, .expectedSourceAmount = 21}, + {.usdPool = 2'000, .eurPool = 1'000, .deliverAmount = 100, .expectedSourceAmount = 223}, + }; + // clang-format on + + for (auto const& testCase : testCases) + { + checkQuote( + testCase.usdPool, + testCase.eurPool, + testCase.deliverAmount, + testCase.expectedSourceAmount); + } + } + void testFalseDry(FeatureBitset features) { @@ -3583,6 +3681,7 @@ private: pathFind01(); pathFind02(); pathFind06(); + pathFindMPTAMMExecutableSourceAmount(); } void diff --git a/src/test/app/AMMExtended_test.cpp b/src/test/app/AMMExtended_test.cpp index bb532b361a..83c848b7c4 100644 --- a/src/test/app/AMMExtended_test.cpp +++ b/src/test/app/AMMExtended_test.cpp @@ -267,20 +267,39 @@ private: {features}); // tfPassive -- place the offer without crossing it. - testAMM( - [&](AMM& ammAlice, Env& env) { - // Carol creates a passive offer that could cross AMM. - // Carol's offer should stay in the ledger. - env(offer(carol_, XRP(100), USD(100), tfPassive)); - env.close(); - BEAST_EXPECT( - ammAlice.expectBalances(XRP(10'100), STAmount{USD, 10'000}, ammAlice.tokens())); - BEAST_EXPECT(expectOffers(env, carol_, 1, {{{XRP(100), STAmount{USD, 100}}}})); - }, - {{XRP(10'100), USD(10'000)}}, - 0, - std::nullopt, - {features}); + if (features[featureMPTokensV2]) + { + Env env{*this, features}; + fund(env, gw_, {alice_, carol_}, XRP(30'000'000), {USD(30'000'000)}); + + AMM const ammAlice(env, alice_, XRP(10'100'000), USD(10'000'000)); + + // Scale the exact-quality fixture up so the visual relationship + // stays clear: the passive CLOB offer has the same 1:1 quality as + // the generated AMM offer, so it should not cross. + env(offer(carol_, XRP(100'000), USD(100'000), tfPassive)); + env.close(); + BEAST_EXPECT( + ammAlice.expectBalances(XRP(10'100'000), USD(10'000'000), ammAlice.tokens())); + BEAST_EXPECT(expectOffers(env, carol_, 1, {{{XRP(100'000), USD(100'000)}}})); + } + else + { + testAMM( + [&](AMM& ammAlice, Env& env) { + // Carol creates a passive offer that could cross AMM. + // Carol's offer should stay in the ledger. + env(offer(carol_, XRP(100), USD(100), tfPassive)); + env.close(); + BEAST_EXPECT(ammAlice.expectBalances( + XRP(10'100), STAmount{USD, 10'000}, ammAlice.tokens())); + BEAST_EXPECT(expectOffers(env, carol_, 1, {{{XRP(100), STAmount{USD, 100}}}})); + }, + {{XRP(10'100), USD(10'000)}}, + 0, + std::nullopt, + {features}); + } // tfPassive -- cross only offers of better quality. testAMM( @@ -1359,6 +1378,7 @@ private: testRmFundedOffer(all_ - fixAMMv1_1 - fixAMMv1_3); testEnforceNoRipple(all_); testFillModes(all_); + testFillModes(all_ - featureMPTokensV2); testOfferCrossWithXRP(all_); testOfferCrossWithLimitOverride(all_); testCurrencyConversionEntire(all_); diff --git a/src/test/app/AMMMPT_test.cpp b/src/test/app/AMMMPT_test.cpp index 7078ea6769..90a267f56f 100644 --- a/src/test/app/AMMMPT_test.cpp +++ b/src/test/app/AMMMPT_test.cpp @@ -32,14 +32,17 @@ #include #include #include +#include #include #include #include #include #include +#include #include #include #include +#include #include #include #include @@ -3269,6 +3272,48 @@ private: ammAlice.expectBalances(MPT(ammAlice[1])(1), XRP(10'000), IOUAmount{100000})); }, {{XRP(10'000), gAmmmpt(10'000)}}); + + // MPT/MPT equal withdrawal after LP deletes both zero-balance MPTokens. + // AMMWithdraw must recreate both missing MPTokens; the invariant allows + // up to two MPToken creations per AMMWithdraw/AMMClawback (threshold > 2). + { + Env env{*this}; + env.fund(XRP(30'000), gw_, alice_); + env.close(); + MPTTester btc( + {.env = env, + .issuer = gw_, + .holders = {alice_}, + .pay = 10'000, + .flags = kMptDexFlags}); + MPTTester eth( + {.env = env, + .issuer = gw_, + .holders = {alice_}, + .pay = 10'000, + .flags = kMptDexFlags}); + + // Alice deposits everything into the MPT/MPT pool; her MPT + // balances drop to zero. + AMM ammAlice(env, alice_, btc(10'000), eth(10'000)); + BEAST_EXPECT(expectMPT(env, alice_, btc(0))); + BEAST_EXPECT(expectMPT(env, alice_, eth(0))); + + // Alice deletes both zero-balance MPTokens to reclaim reserve. + btc.authorize({.account = alice_, .flags = tfMPTUnauthorize}); + eth.authorize({.account = alice_, .flags = tfMPTUnauthorize}); + BEAST_EXPECT(!env.le(keylet::mptoken(btc.issuanceID(), alice_.id()))); + BEAST_EXPECT(!env.le(keylet::mptoken(eth.issuanceID(), alice_.id()))); + + // Equal withdrawal succeeds: both missing MPTokens are recreated + // (mptokensCreated_ == 2, which satisfies the > 2 invariant check). + ammAlice.withdrawAll(alice_); + BEAST_EXPECT(env.le(keylet::mptoken(btc.issuanceID(), alice_.id()))); + BEAST_EXPECT(env.le(keylet::mptoken(eth.issuanceID(), alice_.id()))); + BEAST_EXPECT(expectMPT(env, alice_, btc(10'000))); + BEAST_EXPECT(expectMPT(env, alice_, eth(10'000))); + BEAST_EXPECT(!ammAlice.ammExists()); + } } void @@ -4041,9 +4086,9 @@ private: { auto jtx = env.jt(tx, Seq(1), Fee(10)); env.app().config().features.erase(featureMPTokensV2); - PreflightContext const pfctx( + PreflightContext const ctx( env.app(), *jtx.stx, env.current()->rules(), TapNone, env.journal); - auto pf = AMMBid::checkExtraFeatures(pfctx); + auto pf = AMMBid::checkExtraFeatures(ctx); BEAST_EXPECT(pf == false); env.app().config().features.insert(featureMPTokensV2); } @@ -4053,9 +4098,9 @@ private: jtx.jv["Asset2"]["currency"] = "XRP"; jtx.jv["Asset2"].removeMember("mpt_issuance_id"); jtx.stx = env.ust(jtx); - PreflightContext const pfctx( + PreflightContext const ctx( env.app(), *jtx.stx, env.current()->rules(), TapNone, env.journal); - auto pf = AMMBid::preflight(pfctx); + auto pf = AMMBid::preflight(ctx); BEAST_EXPECT(pf == temBAD_AMM_TOKENS); } } @@ -4901,7 +4946,7 @@ private: XRP(10'100), MPT(ammAlice[1])(10'000'000000000001), ammAlice.tokens())); env.require(Balance(carol_, MPT(ammAlice[1])(30'199'999999999999))); - // Initial 30,000 - 10000(AMM pool LP) - 100(AMMoffer) - + // Initial 30,000 - 10000(AMM pool LP) - 100(AMM offer) - // - 100(offer) - 10(tx fee) - 10(tx fee of MPTTester init as // holder) - one reserve BEAST_EXPECT(expectLedgerEntryRoot( @@ -5010,12 +5055,12 @@ private: env.close(); BEAST_EXPECT( - amm.expectBalances(XRPAmount(909'090'909), btc(550'000000055001), amm.tokens())); - // Offer ~91XRP/49.99e12BTC + amm.expectBalances(XRPAmount(909'090'910), btc(549'999999450001), amm.tokens())); + // Offer ~91XRP/50e12BTC BEAST_EXPECT(expectOffers( - env, carol_, 1, {{Amounts{XRPAmount{9'090'909}, btc(4'999999950000)}}})); - // Carol pays 0.1% fee on 50'000000055000BTC = 50'000000055BTC - env.require(Balance(carol_, btc(29'949'949'999'944'943))); + env, carol_, 1, {{Amounts{XRPAmount{9'090'910}, btc(5'000000500000)}}})); + // Carol pays 0.1% fee on 49'999999450001BTC. + env.require(Balance(carol_, btc(29'949'950'000'550'548))); } { @@ -5065,15 +5110,15 @@ private: env.close(); BEAST_EXPECT(ammAlice.expectBalances( - btc(1'060'6848287928033), eth(1'037'0658372213574), ammAlice.tokens())); + btc(1'060'6848287928025), eth(1'037'0658372213582), ammAlice.tokens())); // Consumed offer ~72.93e13ETH/72.93e13BTC BEAST_EXPECT(expectOffers( - env, carol_, 1, {Amounts{eth(27'0658372213574), btc(27'0658372213575)}})); + env, carol_, 1, {Amounts{eth(27'0658372213582), btc(27'0658372213582)}})); BEAST_EXPECT(expectOffers(env, bob_, 0)); BEAST_EXPECT(expectOffers(env, ed, 0)); - env.require(Balance(carol_, btc(19'116'439'640'089'955))); - env.require(Balance(carol_, eth(20'729'341'627'786'426))); + env.require(Balance(carol_, btc(19'116'439'640'089'965))); + env.require(Balance(carol_, eth(20'729'341'627'786'418))); env.require(Balance(bob_, btc(20'100'000'000'000'000))); env.require(Balance(ed, eth(19'875'000'000'000'000))); } @@ -5672,6 +5717,87 @@ private: }); } + void + testAMMOfferGenerationPolicy(FeatureBitset features) + { + testcase("AMM payment offer generation picks economically coarser integral side"); + + using namespace jtx; + + enum class GeneratedFirst { TakerPays, TakerGets }; + + auto const check = [&](std::uint64_t mptUnitsPerXRP, GeneratedFirst generatedFirst) { + TAmounts const pool{ + XRPAmount{1'000'000}, MPTAmount{1'000'000'125}}; + TAmounts const clobOffer{ + kDropsPerXrp, MPTAmount{static_cast(mptUnitsPerXRP)}}; + Quality const clobQuality{clobOffer}; + + auto const expectedAmounts = generatedFirst == GeneratedFirst::TakerGets + ? getAMMOfferStartWithTakerGets(pool, clobQuality, 0) + : getAMMOfferStartWithTakerPays(pool, clobQuality, 0); + auto const otherAmounts = generatedFirst == GeneratedFirst::TakerGets + ? getAMMOfferStartWithTakerPays(pool, clobQuality, 0) + : getAMMOfferStartWithTakerGets(pool, clobQuality, 0); + BEAST_EXPECT(expectedAmounts); + BEAST_EXPECT(otherAmounts); + if (!expectedAmounts || !otherAmounts) + return; + + // Make the tested branch observable: these cases are chosen so the + // payment consumes different AMM amounts depending on which side + // is generated first. + BEAST_EXPECT(*expectedAmounts != *otherAmounts); + + Env env(*this, features); + auto const gw = Account("gw"); + auto const lp = Account("lp"); + auto const maker = Account("maker"); + auto const taker = Account("taker"); + auto const dst = Account("dst"); + + env.fund(XRP(10'000), gw, lp, maker, taker, dst); + env.close(); + + MPTTester const token( + {.env = env, .issuer = gw, .holders = {lp, maker, dst}, .flags = kMptDexFlags}); + env(pay(gw, lp, token(pool.out.value()))); + env(pay(gw, maker, token(10'000'000))); + env.close(); + + AMM const amm(env, lp, drops(pool.in), token(pool.out.value())); + auto const makerOfferSeq = env.seq(maker); + env(offer(maker, XRP(1), token(mptUnitsPerXRP)), Txflags(tfPassive)); + env.close(); + + env(pay(taker, dst, token(expectedAmounts->out.value())), + Sendmax(drops(expectedAmounts->in))); + env.close(); + + BEAST_EXPECT(amm.expectBalances( + drops(pool.in + expectedAmounts->in), + token((pool.out - expectedAmounts->out).value()), + amm.tokens())); + env.require(Balance(dst, token(expectedAmounts->out.value()))); + BEAST_EXPECT(env.le(keylet::offer(maker.id(), SeqProxy::rawSequence(makerOfferSeq)))); + }; + + // CLOB price: 10'000'000 MPT per 1 XRP, so one raw MPT unit is worth + // 0.1 drops. One drop is the economically coarser unit and the AMM + // offer is generated from takerPays. + check(10 * kDropsPerXrp.drops(), GeneratedFirst::TakerPays); + + // CLOB price: 1'000'000 MPT per 1 XRP, so one raw MPT unit is worth + // one drop. Ties use takerGets to preserve the historical XRP-output + // behavior. + check(kDropsPerXrp.drops(), GeneratedFirst::TakerGets); + + // CLOB price: 100'000 MPT per 1 XRP, so one raw MPT unit is worth + // 10 drops. MPT is the economically coarser unit and the AMM offer is + // generated from takerGets. + check(kDropsPerXrp.drops() / 10, GeneratedFirst::TakerGets); + } + void testTradingFee(FeatureBitset features) { @@ -7242,7 +7368,7 @@ private: // overflow. Deposit has no such bound, which is why only the deposit // path was exposed. // - // These mirror the deposit repros: the same oversized two-asset + // These mirror the deposit tests: the same oversized two-asset // request is rejected cleanly. If the preclaim bound is ever weakened, // equalWithdrawLimit would be reached with a huge frac and // Number::operator rep() would escape as tefEXCEPTION, failing this. @@ -7318,6 +7444,7 @@ private: testAMMTokens(); testAmendment(); testAMMAndCLOB(all); + testAMMOfferGenerationPolicy(all); testTradingFee(all); testTradingFee(all - fixAMMv1_3); testAdjustedTokens(all); diff --git a/src/test/app/AMM_test.cpp b/src/test/app/AMM_test.cpp index 8f8079c34a..e1732aaf0e 100644 --- a/src/test/app/AMM_test.cpp +++ b/src/test/app/AMM_test.cpp @@ -3778,6 +3778,21 @@ private: BEAST_EXPECT(amm.expectBalances(XRP(1'000), USD(500), amm.tokens())); BEAST_EXPECT(expectOffers(env, carol_, 1, {{Amounts{XRP(100), USD(55)}}})); } + else if (!features[featureMPTokensV2]) + { + BEAST_EXPECT(amm.expectBalances( + XRPAmount(909'090'909), + STAmount{USD, UINT64_C(550'000000055), -9}, + amm.tokens())); + BEAST_EXPECT(expectOffers( + env, + carol_, + 1, + {{Amounts{XRPAmount{9'090'909}, STAmount{USD, 4'99999995, -8}}}})); + BEAST_EXPECT( + env.balance(carol_, USD) == + STAmount(USD, UINT64_C(29'949'94999999494), -11)); + } else { // Post-amendment the transfer fee is taken into account @@ -3788,19 +3803,19 @@ private: // quality. // AMM offer ~50USD/91XRP BEAST_EXPECT(amm.expectBalances( - XRPAmount(909'090'909), - STAmount{USD, UINT64_C(550'000000055), -9}, + XRPAmount(909'090'910), + STAmount{USD, UINT64_C(549'99999945), -8}, amm.tokens())); - // Offer ~91XRP/49.99USD + // Offer ~91XRP/50USD BEAST_EXPECT(expectOffers( env, carol_, 1, - {{Amounts{XRPAmount{9'090'909}, STAmount{USD, 4'99999995, -8}}}})); + {{Amounts{XRPAmount{9'090'910}, STAmount{USD, 5'0000005, -7}}}})); // Carol pays 0.1% fee on ~50USD =~ 0.05USD BEAST_EXPECT( env.balance(carol_, USD) == - STAmount(USD, UINT64_C(29'949'94999999494), -11)); + STAmount(USD, UINT64_C(29'949'95000060055), -11)); } }, {{XRP(1'000), USD(500)}}, @@ -6451,7 +6466,7 @@ private: BEAST_EXPECT(expectOffers(env, bob_, 1, {{Amounts{USD(1), XRPAmount(500)}}})); BEAST_EXPECT(expectOffers(env, carol_, 1, {{Amounts{XRP(100), USD(55)}}})); } - else + else if (!features[featureMPTokensV2]) { BEAST_EXPECT(amm.expectBalances( XRPAmount(909'090'909), @@ -6464,6 +6479,19 @@ private: {{Amounts{XRPAmount{9'090'909}, STAmount{USD, 4'99999995, -8}}}})); BEAST_EXPECT(expectOffers(env, bob_, 1, {{Amounts{USD(1), XRPAmount(500)}}})); } + else + { + BEAST_EXPECT(amm.expectBalances( + XRPAmount(909'090'910), + STAmount{USD, UINT64_C(549'99999945), -8}, + amm.tokens())); + BEAST_EXPECT(expectOffers( + env, + carol_, + 1, + {{Amounts{XRPAmount{9'090'910}, STAmount{USD, 5'0000005, -7}}}})); + BEAST_EXPECT(expectOffers(env, bob_, 1, {{Amounts{USD(1), XRPAmount(500)}}})); + } } // There is no blocking offer, the same AMM liquidity is consumed @@ -6475,10 +6503,30 @@ private: AMM const amm(env, alice_, XRP(1'000), USD(500)); env(offer(carol_, XRP(100), USD(55))); env.close(); - BEAST_EXPECT(amm.expectBalances( - XRPAmount(909'090'909), STAmount{USD, UINT64_C(550'000000055), -9}, amm.tokens())); - BEAST_EXPECT(expectOffers( - env, carol_, 1, {{Amounts{XRPAmount{9'090'909}, STAmount{USD, 4'99999995, -8}}}})); + if (!features[featureMPTokensV2]) + { + BEAST_EXPECT(amm.expectBalances( + XRPAmount(909'090'909), + STAmount{USD, UINT64_C(550'000000055), -9}, + amm.tokens())); + BEAST_EXPECT(expectOffers( + env, + carol_, + 1, + {{Amounts{XRPAmount{9'090'909}, STAmount{USD, 4'99999995, -8}}}})); + } + else + { + BEAST_EXPECT(amm.expectBalances( + XRPAmount(909'090'910), + STAmount{USD, UINT64_C(549'99999945), -8}, + amm.tokens())); + BEAST_EXPECT(expectOffers( + env, + carol_, + 1, + {{Amounts{XRPAmount{9'090'910}, STAmount{USD, 5'0000005, -7}}}})); + } } } @@ -7400,6 +7448,7 @@ private: testFlags(); testRippling(); testAMMAndCLOB(all); + testAMMAndCLOB(all - featureMPTokensV2); testAMMAndCLOB(all - fixAMMv1_1 - fixAMMv1_3); testTradingFee(all); testTradingFee(all - fixAMMv1_3); @@ -7419,8 +7468,10 @@ private: testOverflowOffer(all - fixAMMv1_1 - fixAMMv1_3); testSwapRounding(); testFixChangeSpotPriceQuality(all); + testFixChangeSpotPriceQuality(all - featureMPTokensV2); testFixChangeSpotPriceQuality(all - fixAMMv1_1 - fixAMMv1_3); testFixAMMOfferBlockedByLOB(all); + testFixAMMOfferBlockedByLOB(all - featureMPTokensV2); testFixAMMOfferBlockedByLOB(all - fixAMMv1_1 - fixAMMv1_3); testLPTokenBalance(all); testLPTokenBalance(all - fixAMMv1_3); diff --git a/src/test/app/EscrowToken_test.cpp b/src/test/app/EscrowToken_test.cpp index 7e7509c3b7..72db63bd3f 100644 --- a/src/test/app/EscrowToken_test.cpp +++ b/src/test/app/EscrowToken_test.cpp @@ -3749,6 +3749,186 @@ struct EscrowToken_test : public beast::unit_test::Suite BEAST_EXPECT(issuerMPTEscrowed(env, mpt) == 0); } + void + testMPTLargeLockedRate(FeatureBitset features) + { + testcase("MPT large locked rate"); + using namespace test::jtx; + using namespace std::literals; + + auto constexpr escrowAmount = 200'000'000'000'000'000LL; + auto constexpr noOverflowEscrowAmount = 186'000'000'000'000'000LL; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account("gw"); + + for (auto const testFeatures : + {features - featureMPTokensV2 - fixCleanup3_4_0, + features - featureMPTokensV2, + (features | featureMPTokensV2) - fixCleanup3_4_0, + features | featureMPTokensV2}) + { + bool const mptV2 = testFeatures[featureMPTokensV2]; + bool const tokenEscrowV1 = testFeatures[fixTokenEscrowV1]; + // The transfer-fee split in EscrowFinish only overflows on the + // legacy divideRound(amount, lockedRate, ...) path, which runs when + // fixCleanup3_4_0 is disabled. With fixCleanup3_4_0 the split uses + // mulRatio (128-bit intermediate), which cannot overflow. Without + // it, this large amount overflows unless the MPTokensV2 Number path + // is active. So the finish succeeds when either amendment is enabled. + bool const cleanup340 = testFeatures[fixCleanup3_4_0]; + bool const noOverflow = cleanup340 || mptV2; + auto const expectedErr = noOverflow ? Ter(tesSUCCESS) : Ter(tefEXCEPTION); + + // Finish with a large MPT amount and non-zero transfer fee. When the + // computation overflows (legacy divideRound path, no MPTokensV2) the + // finish fails with tefEXCEPTION and the escrow is untouched; + // otherwise it unlocks the escrow. + { + Env env{*this, testFeatures}; + env.fund(XRP(1'000), alice, bob, gw); + auto const baseFee = env.current()->fees().base; + + MPTTester const mpt( + {.env = env, + .issuer = gw, + .holders = {alice, bob}, + .transferFee = 1'000, + .flags = tfMPTCanEscrow | tfMPTCanTransfer}); + env(pay(gw, alice, mpt(escrowAmount))); + env.close(); + + auto const preAlice = env.balance(alice, mpt); + auto const preBob = env.balance(bob, mpt); + auto const seq = env.seq(alice); + env(escrow::create(alice, bob, mpt(escrowAmount)), + escrow::kCondition(escrow::kCb1), + escrow::kFinishTime(env.now() + 1s), + escrow::kCancelTime(env.now() + 500s), + Fee(baseFee * 150)); + env.close(); + + BEAST_EXPECT(mptEscrowed(env, alice, mpt) == escrowAmount); + BEAST_EXPECT(issuerMPTEscrowed(env, mpt) == escrowAmount); + + env(escrow::finish(bob, alice, seq), + escrow::kCondition(escrow::kCb1), + escrow::kFulfillment(escrow::kFb1), + Fee(baseFee * 150), + expectedErr); + env.close(); + + if (noOverflow) + { + BEAST_EXPECT(!env.le(keylet::escrow(alice.id(), SeqProxy::rawSequence(seq)))); + BEAST_EXPECT(env.balance(alice, mpt) == preAlice - mpt(escrowAmount)); + auto const postBob = env.balance(bob, mpt); + BEAST_EXPECT(postBob.value() > preBob.value()); + BEAST_EXPECT(postBob.value() < (preBob + mpt(escrowAmount)).value()); + auto const xferFee = escrowAmount - (postBob.value() - preBob.value()); + auto const expectedEscrow = tokenEscrowV1 ? 0 : xferFee; + BEAST_EXPECT(mptEscrowed(env, alice, mpt) == expectedEscrow); + BEAST_EXPECT(issuerMPTEscrowed(env, mpt) == expectedEscrow); + } + else + { + BEAST_EXPECT(env.le(keylet::escrow(alice.id(), SeqProxy::rawSequence(seq)))); + BEAST_EXPECT(env.balance(alice, mpt) == preAlice - mpt(escrowAmount)); + BEAST_EXPECT(env.balance(bob, mpt) == preBob); + BEAST_EXPECT(mptEscrowed(env, alice, mpt) == escrowAmount); + BEAST_EXPECT(issuerMPTEscrowed(env, mpt) == escrowAmount); + } + } + + // Control: a still-large amount below the legacy overflow boundary + // finishes successfully in both feature modes. + { + Env env{*this, testFeatures}; + env.fund(XRP(1'000), alice, bob, gw); + auto const baseFee = env.current()->fees().base; + + MPTTester const mpt( + {.env = env, + .issuer = gw, + .holders = {alice, bob}, + .transferFee = 1'000, + .flags = tfMPTCanEscrow | tfMPTCanTransfer}); + env(pay(gw, alice, mpt(noOverflowEscrowAmount))); + env.close(); + + auto const preAlice = env.balance(alice, mpt); + auto const preBob = env.balance(bob, mpt); + auto const seq = env.seq(alice); + env(escrow::create(alice, bob, mpt(noOverflowEscrowAmount)), + escrow::kCondition(escrow::kCb1), + escrow::kFinishTime(env.now() + 1s), + escrow::kCancelTime(env.now() + 500s), + Fee(baseFee * 150)); + env.close(); + + BEAST_EXPECT(mptEscrowed(env, alice, mpt) == noOverflowEscrowAmount); + BEAST_EXPECT(issuerMPTEscrowed(env, mpt) == noOverflowEscrowAmount); + + env(escrow::finish(bob, alice, seq), + escrow::kCondition(escrow::kCb1), + escrow::kFulfillment(escrow::kFb1), + Fee(baseFee * 150), + Ter(tesSUCCESS)); + env.close(); + + BEAST_EXPECT(!env.le(keylet::escrow(alice.id(), SeqProxy::rawSequence(seq)))); + BEAST_EXPECT(env.balance(alice, mpt) == preAlice - mpt(noOverflowEscrowAmount)); + auto const postBob = env.balance(bob, mpt); + BEAST_EXPECT(postBob.value() > preBob.value()); + BEAST_EXPECT(postBob.value() < (preBob + mpt(noOverflowEscrowAmount)).value()); + auto const xferFee = noOverflowEscrowAmount - (postBob.value() - preBob.value()); + auto const expectedEscrow = tokenEscrowV1 ? 0 : xferFee; + BEAST_EXPECT(mptEscrowed(env, alice, mpt) == expectedEscrow); + BEAST_EXPECT(issuerMPTEscrowed(env, mpt) == expectedEscrow); + } + + // Cancel returns the escrow to the owner using parity rate, so it + // does not hit the transfer-rate division in either feature mode. + { + Env env{*this, testFeatures}; + env.fund(XRP(1'000), alice, bob, gw); + auto const baseFee = env.current()->fees().base; + + MPTTester const mpt( + {.env = env, + .issuer = gw, + .holders = {alice, bob}, + .transferFee = 1'000, + .flags = tfMPTCanEscrow | tfMPTCanTransfer}); + env(pay(gw, alice, mpt(escrowAmount))); + env.close(); + + auto const preAlice = env.balance(alice, mpt); + auto const preBob = env.balance(bob, mpt); + auto const seq = env.seq(alice); + env(escrow::create(alice, bob, mpt(escrowAmount)), + escrow::kCondition(escrow::kCb1), + escrow::kFinishTime(env.now() + 1s), + escrow::kCancelTime(env.now() + 3s), + Fee(baseFee * 150)); + env.close(); + + BEAST_EXPECT(mptEscrowed(env, alice, mpt) == escrowAmount); + BEAST_EXPECT(issuerMPTEscrowed(env, mpt) == escrowAmount); + + env(escrow::cancel(alice, alice, seq), Fee(baseFee), Ter(tesSUCCESS)); + env.close(); + + BEAST_EXPECT(!env.le(keylet::escrow(alice.id(), SeqProxy::rawSequence(seq)))); + BEAST_EXPECT(env.balance(alice, mpt) == preAlice); + BEAST_EXPECT(env.balance(bob, mpt) == preBob); + BEAST_EXPECT(env.balance(gw, mpt) == -mpt(escrowAmount)); + BEAST_EXPECT(mptEscrowed(env, alice, mpt) == 0); + BEAST_EXPECT(issuerMPTEscrowed(env, mpt) == 0); + } + } + } + void testMPTRequireAuth(FeatureBitset features) { @@ -4047,6 +4227,7 @@ struct EscrowToken_test : public beast::unit_test::Suite testMPTMetaAndOwnership(features); testMPTGateway(features); testMPTLockedRate(features); + testMPTLargeLockedRate(features); testMPTRequireAuth(features); testMPTLock(features); testMPTCanTransfer(features); diff --git a/src/test/app/FlowMPT_test.cpp b/src/test/app/FlowMPT_test.cpp index a94834eb28..49e3f9be94 100644 --- a/src/test/app/FlowMPT_test.cpp +++ b/src/test/app/FlowMPT_test.cpp @@ -26,6 +26,7 @@ #include #include #include +#include #include #include #include @@ -742,6 +743,164 @@ struct FlowMPT_test : public beast::unit_test::Suite return result; } + void + testOfferOwnerMPTCreation(FeatureBitset features) + { + using namespace jtx; + Account const alice("alice"); + Account const bob("bob"); + Account const carol("carol"); + Account const gw("gw"); + + { + testcase("Reserve-edge offer owner cannot create another object"); + + Env env(*this, features); + + auto const baseFee = env.current()->fees().base; + auto const ownerIncrement = reserve(env, 1) - reserve(env, 0); + auto const xrpOffer = ownerIncrement - drops(1); + auto const bobStart = reserve(env, 2) - drops(1) + baseFee; + + env.fund(XRP(10'000), alice, gw); + env.fund(bobStart, bob); + env.close(); + + MPTTester const usd({.env = env, .issuer = gw, .maxAmt = 10}); + + env(offer(bob, usd(1), xrpOffer)); + env.close(); + + env.require(Balance(bob, reserve(env, 2) - drops(1)), Owners(bob, 1)); + + // This mirrors the full-crossing setup below. Bob has enough XRP + // for the resting offer, but not enough to pay a fee and add + // another owner-count object while the offer remains on ledger. + env(check::create(bob, alice, drops(1)), Ter(tecINSUFFICIENT_RESERVE)); + env.close(); + + env.require(Owners(bob, 1)); + BEAST_EXPECT(offersOnAccount(env, bob).size() == 1); + } + + { + testcase("Reserve-edge offer owner creates MPToken during consume"); + + Env env(*this, features); + + auto const baseFee = env.current()->fees().base; + auto const ownerIncrement = reserve(env, 1) - reserve(env, 0); + auto const xrpOffer = ownerIncrement - drops(1); + auto const bobStart = reserve(env, 2) - drops(1) + baseFee; + + env.fund(XRP(10'000), alice, carol, gw); + env.fund(bobStart, bob); + env.close(); + + MPTTester const usd({.env = env, .issuer = gw, .holders = {alice}, .maxAmt = 10}); + + env(pay(gw, alice, usd(1))); + env(offer(bob, usd(1), xrpOffer)); + env.close(); + + env.require(Balance(bob, reserve(env, 2) - drops(1)), Owners(bob, 1)); + BEAST_EXPECT(!env.le(keylet::mptoken(usd.issuanceID(), bob.id()))); + auto const carolXRP = env.balance(carol); + + // Bob has enough XRP for the resting offer but is close to + // reserve. The payment should not create Bob's USD MPToken until + // the offer is actually consumed, otherwise the temporary owner + // count increase can make the offer look underfunded during path + // execution. + env(pay(alice, carol, xrpOffer), + Path(~XRP), + Sendmax(usd(1)), + Txflags(tfNoRippleDirect)); + env.close(); + + env.require(Balance(carol, carolXRP + xrpOffer)); + env.require(Balance(bob, usd(1))); + env.require(Balance(bob, reserve(env, 1)), Owners(bob, 1)); + BEAST_EXPECT(env.le(keylet::mptoken(usd.issuanceID(), bob.id()))); + BEAST_EXPECT(offersOnAccount(env, bob).empty()); + } + + { + testcase("Partial offer owner creates MPToken during consume"); + + Env env(*this, features); + + auto const baseFee = env.current()->fees().base; + auto const ownerIncrement = reserve(env, 1) - reserve(env, 0); + auto const bobStart = reserve(env, 3) + baseFee; + + env.fund(XRP(10'000), alice, carol, gw); + env.fund(bobStart, bob); + env.close(); + + MPTTester const usd({.env = env, .issuer = gw, .holders = {alice}, .maxAmt = 10}); + + env(pay(gw, alice, usd(1))); + env(offer(bob, usd(2), drops(2 * ownerIncrement))); + env.close(); + + env.require(Balance(bob, reserve(env, 3)), Owners(bob, 1)); + BEAST_EXPECT(!env.le(keylet::mptoken(usd.issuanceID(), bob.id()))); + auto const carolXRP = env.balance(carol); + + // Partial consumption leaves Bob's offer on the ledger, so he ends + // up owning both the remaining offer and a newly created MPToken. + // The MPToken is created regardless of reserve; this setup simply + // funds Bob enough that he still meets reserve(2) afterward (the + // under-reserved case is covered in OfferMPT_test's no-reserve-check + // testcase). + env(pay(alice, carol, drops(ownerIncrement)), + Path(~XRP), + Sendmax(usd(1)), + Txflags(tfNoRippleDirect)); + env.close(); + + env.require(Balance(carol, carolXRP + drops(ownerIncrement))); + env.require(Balance(bob, usd(1))); + env.require(Balance(bob, reserve(env, 2)), Owners(bob, 2)); + BEAST_EXPECT(env.le(keylet::mptoken(usd.issuanceID(), bob.id()))); + BEAST_EXPECT(offersOnAccount(env, bob).size() == 1); + BEAST_EXPECT(isOffer(env, bob, usd(1), drops(ownerIncrement))); + } + + { + testcase("Issuer-owned offer does not create issuer MPToken"); + + Env env(*this, features); + + env.fund(XRP(10'000), alice, carol, gw); + env.close(); + + MPTTester const usd({.env = env, .issuer = gw, .holders = {alice}, .maxAmt = 10}); + + env(pay(gw, alice, usd(1))); + env(offer(gw, usd(1), drops(1'000))); + env.close(); + + BEAST_EXPECT(!env.le(keylet::mptoken(usd.issuanceID(), gw.id()))); + auto const carolXRP = env.balance(carol); + + // The issuer can own an offer that receives its own MPT without an + // MPToken. Consuming that offer should keep the issuer side + // tokenless. + env(pay(alice, carol, drops(1'000)), + Path(~XRP), + Sendmax(usd(1)), + Txflags(tfNoRippleDirect)); + env.close(); + + env.require(Balance(alice, usd(0))); + env.require(Balance(carol, carolXRP + drops(1'000))); + BEAST_EXPECT(!env.le(keylet::mptoken(usd.issuanceID(), gw.id()))); + BEAST_EXPECT(offersOnAccount(env, gw).empty()); + } + } + void testSelfPayment1(FeatureBitset features) { @@ -2121,6 +2280,7 @@ struct FlowMPT_test : public beast::unit_test::Suite testFalseDry(features); testDirectStep(features); testBookStep(features); + testOfferOwnerMPTCreation(features); testTransferRate(features); testSelfPayment1(features); testSelfPayment2(features); diff --git a/src/test/app/OfferMPT_test.cpp b/src/test/app/OfferMPT_test.cpp index d03b1b8e93..e262954fdf 100644 --- a/src/test/app/OfferMPT_test.cpp +++ b/src/test/app/OfferMPT_test.cpp @@ -1,3 +1,5 @@ +#include +#include #include #include #include @@ -5,6 +7,7 @@ #include #include #include +#include #include #include #include @@ -22,6 +25,7 @@ #include #include +#include #include #include #include @@ -35,6 +39,7 @@ #include #include #include +#include #include #include #include @@ -46,6 +51,7 @@ #include #include #include +#include #include #include #include @@ -609,6 +615,267 @@ public: testHelper2TokensMix(test); } + void + testMPTIssuerOfferUsesRemainingCapacity(FeatureBitset features) + { + testcase("MPT issuer offer dust removal uses remaining issuance capacity"); + + using namespace jtx; + + Account const issuer{"issuer"}; + Account const carol{"carol"}; + Account const bob{"bob"}; + + Env env{*this, features}; + env.fund(XRP(10'000), issuer, carol, bob); + env.close(); + + MPTTester const musd( + {.env = env, .issuer = issuer, .holders = {carol, bob}, .maxAmt = 101}); + + // The issuer offer is fully fundable when placed. Later issuance leaves + // only one MPT of remaining capacity, so this issuer-owned MPT offer + // must be clipped by owner funds just like a holder-funded offer. + auto const issuerOfferSeq = env.seq(issuer); + env(offer(issuer, drops(1), musd(100))); + env.close(); + + env(pay(issuer, carol, musd(100))); + env.close(); + BEAST_EXPECT(env.balance(issuer, musd) == musd(-100)); + BEAST_EXPECT(env.balance(carol, musd) == musd(100)); + + // Carol's same-quality offer provides the legitimately funded side of + // the crossing. Without the issuer-cap dust-removal check, Bob would + // receive Carol's 100 MPT plus one free self-issued MPT from issuer's + // stale offer while paying only Carol's one drop. + auto const carolOfferSeq = env.seq(carol); + env(offer(carol, drops(1), musd(100))); + env.close(); + + auto const issuerOffer = keylet::offer(issuer.id(), SeqProxy::rawSequence(issuerOfferSeq)); + auto const carolOffer = keylet::offer(carol.id(), SeqProxy::rawSequence(carolOfferSeq)); + BEAST_EXPECT(env.le(issuerOffer) != nullptr); + BEAST_EXPECT(env.le(carolOffer) != nullptr); + + env(offer(bob, musd(101), drops(2), tfImmediateOrCancel)); + env.close(); + + BEAST_EXPECT(env.le(issuerOffer) == nullptr); + BEAST_EXPECT(env.le(carolOffer) == nullptr); + env.require(offers(issuer, 0), offers(carol, 0), offers(bob, 0)); + BEAST_EXPECT(env.balance(issuer, musd) == musd(-100)); + BEAST_EXPECT(env.balance(carol, musd) == musd(0)); + BEAST_EXPECT(env.balance(bob, musd) == musd(100)); + } + + void + testPartiallyFundedMPTInputOfferZeroInput(FeatureBitset features) + { + using namespace jtx; + auto const alice = Account{"alice"}; + auto const bob = Account{"bob"}; + + { + testcase("Partially funded MPT/XRP input offer cannot be consumed for free"); + + Env env{*this, features}; + auto const gw = Account{"gw"}; + + env.fund(XRP(10'000), gw, alice, bob); + env.close(); + + MPTTester const usd({.env = env, .issuer = gw, .holders = {alice}}); + + auto const aliceOfferSeq = env.seq(alice); + env(offer(alice, usd(1), drops(1'000'000))); + env.close(); + + auto const targetBalance = reserve(env, 2) + drops(999'999); + auto const drain = env.balance(alice).value().xrp() - targetBalance.value().xrp() - + env.current()->fees().base; + env(pay(alice, gw, drops(drain))); + env.close(); + + auto const aliceXRPBefore = env.balance(alice); + auto const bobXRPBefore = env.balance(bob); + + env(pay(gw, bob, drops(1'000'000)), + Sendmax(usd(1)), + Path(~XRP), + Txflags(tfNoRippleDirect | tfPartialPayment), + Ter(tecPATH_DRY)); + env.close(); + + // alice's offer sells 1,000,000 drops for usd(1) but she can fund + // only 999,999. Filling the clipped remainder would require a + // fractional usd (MPT) input that rounds down to zero, so without + // the fix the taker could take the funded drops for free. + // shouldRmSmallIncreasedQOffer() now treats the MPT input as + // integral (like XRP) and removes the degraded offer, so the + // payment goes dry. The removal happens only inside the crossing: + // tecPATH_DRY discards everything but the fee, so the offer itself + // stays in the ledger, unconsumed. + BEAST_EXPECT( + env.le(keylet::offer(alice.id(), SeqProxy::rawSequence(aliceOfferSeq))) != nullptr); + BEAST_EXPECT(env.balance(alice) == aliceXRPBefore); + BEAST_EXPECT(env.balance(bob) == bobXRPBefore); + } + + { + testcase("Partially funded MPT/IOU input offer cannot be consumed for free"); + + Env env{*this, features}; + auto const mptIssuer = Account{"mptIssuer"}; + auto const iouIssuer = Account{"iouIssuer"}; + + env.fund(XRP(10'000), mptIssuer, iouIssuer, alice, bob); + env.close(); + + auto const eur = iouIssuer["EUR"]; + env.trust(eur(100), alice, bob); + env(pay(iouIssuer, alice, eur(0.5))); + env.close(); + + MPTTester const usd({.env = env, .issuer = mptIssuer, .holders = {alice}}); + + auto const aliceOfferSeq = env.seq(alice); + env(offer(alice, usd(1), eur(1))); + env.close(); + + auto const aliceEURBefore = env.balance(alice, eur); + auto const bobEURBefore = env.balance(bob, eur); + + env(pay(mptIssuer, bob, eur(1)), + Sendmax(usd(1)), + Path(~eur), + Txflags(tfNoRippleDirect | tfPartialPayment), + Ter(tecPATH_DRY)); + env.close(); + + // Same zero-input regression as the MPT/XRP case above, but with + // an IOU (eur) output leg: the fractional usd (MPT) input rounds + // to zero. The degraded offer is removed during crossing, the + // payment goes dry, and tecPATH_DRY leaves the offer in the ledger. + BEAST_EXPECT( + env.le(keylet::offer(alice.id(), SeqProxy::rawSequence(aliceOfferSeq))) != nullptr); + BEAST_EXPECT(env.balance(alice, eur) == aliceEURBefore); + BEAST_EXPECT(env.balance(bob, eur) == bobEURBefore); + } + + { + testcase("Partially funded MPT/MPT input offer cannot be consumed for free"); + + Env env{*this, features}; + auto const issuerA = Account{"issuerA"}; + auto const issuerB = Account{"issuerB"}; + + env.fund(XRP(10'000), issuerA, issuerB, alice, bob); + env.close(); + + MPTTester const usd({.env = env, .issuer = issuerA, .holders = {alice}}); + MPTTester const eur({.env = env, .issuer = issuerB, .holders = {alice, bob}}); + + env(pay(issuerB, alice, eur(999'999))); + env.close(); + + auto const aliceOfferSeq = env.seq(alice); + env(offer(alice, usd(1), eur(1'000'000))); + env.close(); + + auto const aliceEURBefore = eur.getBalance(alice); + auto const bobEURBefore = eur.getBalance(bob); + + env(pay(issuerA, bob, eur(1'000'000)), + Sendmax(usd(1)), + Path(~eur), + Txflags(tfNoRippleDirect | tfPartialPayment), + Ter(tecPATH_DRY)); + env.close(); + + // Same zero-input regression as above, but with both legs MPT: the + // fractional usd (MPT) input rounds to zero. The degraded offer is + // removed during crossing, the payment goes dry, and tecPATH_DRY + // leaves the offer in the ledger. + BEAST_EXPECT( + env.le(keylet::offer(alice.id(), SeqProxy::rawSequence(aliceOfferSeq))) != nullptr); + BEAST_EXPECT(env.balance(alice, eur) == eur(aliceEURBefore)); + BEAST_EXPECT(env.balance(bob, eur) == eur(bobEURBefore)); + } + + { + // The dry cases above never observe the degraded offer actually + // being removed, because tecPATH_DRY rolls the removal back. Here a + // second, fully funded offer lets the crossing succeed, so the + // removal persists: alice's degraded offer is deleted from the + // book (not taken for free) while carol's good offer fills. + testcase( + "Partially funded MPT input offer is removed, not consumed, " + "when a funded offer crosses"); + + Env env{*this, features}; + auto const gw = Account{"gw"}; + auto const carol = Account{"carol"}; + + env.fund(XRP(10'000), gw, alice, carol, bob); + env.close(); + + MPTTester const usd({.env = env, .issuer = gw, .holders = {alice, carol, bob}}); + + // alice's offer sells 1,000,000 drops for usd(1) but, as in the + // dry cases above, she can fund only 999,999 drops, so filling the + // clipped remainder would require a fractional usd (MPT) input that + // rounds down to zero. + auto const aliceOfferSeq = env.seq(alice); + env(offer(alice, usd(1), drops(1'000'000))); + env.close(); + + auto const targetBalance = reserve(env, 2) + drops(999'999); + auto const drain = env.balance(alice).value().xrp() - targetBalance.value().xrp() - + env.current()->fees().base; + env(pay(alice, gw, drops(drain))); + env.close(); + + // carol's same-quality offer is fully funded and provides the + // legitimate side of the crossing. + auto const carolOfferSeq = env.seq(carol); + env(offer(carol, usd(1), drops(1'000'000))); + env.close(); + + // bob needs usd to buy drops. + env(pay(gw, bob, usd(2))); + env.close(); + + auto const aliceOffer = keylet::offer(alice.id(), SeqProxy::rawSequence(aliceOfferSeq)); + auto const carolOffer = keylet::offer(carol.id(), SeqProxy::rawSequence(carolOfferSeq)); + BEAST_EXPECT(env.le(aliceOffer) != nullptr); + BEAST_EXPECT(env.le(carolOffer) != nullptr); + + auto const aliceXRPBefore = env.balance(alice); + auto const bobXRPBefore = env.balance(bob); + + // bob buys drops with usd, wanting more than carol alone supplies so + // the crossing also reaches alice's offer. carol's offer fills; + // alice's degraded offer is removed rather than taken for free, so + // bob receives only carol's 1,000,000 drops and pays only usd(1). + env(offer(bob, drops(2'000'000), usd(2), tfImmediateOrCancel)); + env.close(); + + BEAST_EXPECT(env.le(aliceOffer) == nullptr); + BEAST_EXPECT(env.le(carolOffer) == nullptr); + env.require(offers(alice, 0), offers(carol, 0), offers(bob, 0)); + + // alice's offer was removed, not consumed: her balances are + // unchanged and none of her funded 999'999 drops leaked to bob. + BEAST_EXPECT(env.balance(alice) == aliceXRPBefore); + BEAST_EXPECT(env.balance(alice, usd) == usd(0)); + BEAST_EXPECT(env.balance(carol, usd) == usd(1)); + BEAST_EXPECT(env.balance(bob, usd) == usd(1)); + BEAST_EXPECT( + env.balance(bob) == bobXRPBefore + drops(1'000'000) - env.current()->fees().base); + } + } + void testInsufficientReserve(FeatureBitset features) { @@ -947,6 +1214,161 @@ public: } } + void + testMPTAMMLimitQualityRounding(FeatureBitset features) + { + testcase("MPT AMM limitQuality checks rounded integral output"); + + using namespace jtx; + + Account const gw{"gateway"}; + Account const alice{"alice"}; + Account const bob{"bob"}; + + // IOC used to reject the AMM strand with tecKILLED. The continuous + // limitQuality target is about 32.88 MPT; rounding to nearest requested + // 33 MPT and made the realized AMM quality miss Bob's limit. The + // discrete fallback takes the largest satisfying integer output: 32. + { + Env env{*this, features}; + + env.fund(XRP(10'000), gw, alice, bob); + env.close(); + + MPTTester const btc( + {.env = env, + .issuer = gw, + .holders = {alice, bob}, + .pay = 100'000, + .flags = kMptDexFlags}); + AMM const amm(env, alice, XRP(100), btc(1'000)); + + auto const bobBTCBefore = btc.getBalance(bob); + auto const [xrpBefore, btcBefore, lpBefore] = amm.balances(); + + env(offer(bob, btc(100), drops(10'340'000)), Txflags(tfImmediateOrCancel)); + env.close(); + + auto const [xrpAfter, btcAfter, lpAfter] = amm.balances(); + BEAST_EXPECT(btc.getBalance(bob) == bobBTCBefore + 32); + BEAST_EXPECT(xrpAfter > xrpBefore); + BEAST_EXPECT(btcAfter < btcBefore); + BEAST_EXPECT(lpAfter == lpBefore); + BEAST_EXPECT(expectOffers(env, bob, 0)); + } + + // A standard OfferCreate at the same limit used to bypass the AMM and + // rest unchanged on the book. It should now take the largest + // satisfying 32-MPT AMM fill first, then leave only the remainder on + // the book. + { + Env env{*this, features}; + + env.fund(XRP(10'000), gw, alice, bob); + env.close(); + + MPTTester const btc( + {.env = env, + .issuer = gw, + .holders = {alice, bob}, + .pay = 100'000, + .flags = kMptDexFlags}); + AMM const amm(env, alice, XRP(100), btc(1'000)); + + auto const bobBTCBefore = btc.getBalance(bob); + auto const [xrpBefore, btcBefore, lpBefore] = amm.balances(); + + env(offer(bob, btc(100), drops(10'340'000))); + env.close(); + + auto const [xrpAfter, btcAfter, lpAfter] = amm.balances(); + BEAST_EXPECT(btc.getBalance(bob) == bobBTCBefore + 32); + BEAST_EXPECT(xrpAfter > xrpBefore); + BEAST_EXPECT(btcAfter < btcBefore); + BEAST_EXPECT(lpAfter == lpBefore); + BEAST_EXPECT(expectOffers(env, bob, 1)); + + auto const bobOffers = offersOnAccount(env, bob); + if (BEAST_EXPECT(bobOffers.size() == 1)) + { + BEAST_EXPECT((*bobOffers[0])[sfTakerPays] != btc(100)); + BEAST_EXPECT((*bobOffers[0])[sfTakerGets] != drops(10'340'000)); + } + } + + // Mirror the IOC case with the integral output flipped from MPT units + // to XRP drops. The same continuous target (~32.88) used to round up + // to 33 drops and miss limitQuality; the discrete fallback allows the + // largest satisfying 32-drop AMM fill. + { + Env env{*this, features}; + + env.fund(XRP(10'000), gw, alice, bob); + env.close(); + + MPTTester const btc( + {.env = env, + .issuer = gw, + .holders = {alice, bob}, + .pay = 200'000'000, + .flags = kMptDexFlags}); + AMM const amm(env, alice, drops(1'000), btc(100'000'000)); + + auto const bobXRPBefore = env.balance(bob, XRP); + auto const baseFee = env.current()->fees().base; + auto const [xrpBefore, btcBefore, lpBefore] = amm.balances(); + + env(offer(bob, drops(100), btc(10'340'000)), Txflags(tfImmediateOrCancel)); + env.close(); + + auto const [xrpAfter, btcAfter, lpAfter] = amm.balances(); + env.require(Balance(bob, bobXRPBefore + drops(32) - baseFee)); + BEAST_EXPECT(xrpAfter < xrpBefore); + BEAST_EXPECT(btcAfter > btcBefore); + BEAST_EXPECT(lpAfter == lpBefore); + BEAST_EXPECT(expectOffers(env, bob, 0)); + } + + // Mirror the standard OfferCreate case as well. It should consume the + // largest satisfying 32-drop AMM fill before leaving only the remainder + // on the book. + { + Env env{*this, features}; + + env.fund(XRP(10'000), gw, alice, bob); + env.close(); + + MPTTester const btc( + {.env = env, + .issuer = gw, + .holders = {alice, bob}, + .pay = 200'000'000, + .flags = kMptDexFlags}); + AMM const amm(env, alice, drops(1'000), btc(100'000'000)); + + auto const bobXRPBefore = env.balance(bob, XRP); + auto const baseFee = env.current()->fees().base; + auto const [xrpBefore, btcBefore, lpBefore] = amm.balances(); + + env(offer(bob, drops(100), btc(10'340'000))); + env.close(); + + auto const [xrpAfter, btcAfter, lpAfter] = amm.balances(); + env.require(Balance(bob, bobXRPBefore + drops(32) - baseFee)); + BEAST_EXPECT(xrpAfter < xrpBefore); + BEAST_EXPECT(btcAfter > btcBefore); + BEAST_EXPECT(lpAfter == lpBefore); + BEAST_EXPECT(expectOffers(env, bob, 1)); + + auto const bobOffers = offersOnAccount(env, bob); + if (BEAST_EXPECT(bobOffers.size() == 1)) + { + BEAST_EXPECT((*bobOffers[0])[sfTakerPays] != drops(100)); + BEAST_EXPECT((*bobOffers[0])[sfTakerGets] != btc(10'340'000)); + } + } + } + void testMalformed(FeatureBitset features) { @@ -2727,6 +3149,50 @@ public: using namespace jtx; auto const gw1 = Account("gateway1"); + { + auto const issuer = Account("issuer"); + auto const sender = Account("sender"); + auto const receiver = Account("receiver"); + auto const seller = Account("seller"); + auto const buyer = Account("buyer"); + + Env env{*this, features}; + env.fund(XRP(10'000), issuer, sender, receiver, seller, buyer); + env.close(); + + MPTTester mpt{ + {.env = env, + .issuer = issuer, + .holders = {sender, receiver, seller, buyer}, + .transferFee = 100}}; + MPT const token = mpt; + + mpt.pay(issuer, sender, 2'000); + mpt.pay(issuer, seller, 2'000); + + // A direct holder-to-holder payment of 999 MPT at a 0.1% fee + // requires 1000 from the sender and burns one MPT. + env(pay(sender, receiver, token(999)), Ter(tecPATH_PARTIAL)); + env.close(); + env(pay(sender, receiver, token(999)), Sendmax(token(1'000))); + env.close(); + + BEAST_EXPECT(mpt.getBalance(sender) == 1'000); + BEAST_EXPECT(mpt.getBalance(receiver) == 999); + BEAST_EXPECT(mpt.getBalance(issuer) == 3'999); + + // CLOB crossing should apply the same fee quantum. The offer + // owner pays ceil(999 * 1.001) = 1000, not floor(...) = 999. + env(offer(seller, XRP(999), token(999))); + env.close(); + env(offer(buyer, token(999), XRP(999))); + env.close(); + + BEAST_EXPECT(mpt.getBalance(seller) == 1'000); + BEAST_EXPECT(mpt.getBalance(buyer) == 999); + BEAST_EXPECT(mpt.getBalance(issuer) == 3'998); + } + auto test = [&](auto&& issue1, auto&& issue2) { Env env{*this, features}; @@ -3102,6 +3568,247 @@ public: } } + void + testTransferRateOverflowOffer(FeatureBitset features) + { + testcase("Transfer Rate Overflow Offer"); + + using namespace jtx; + + auto const issuer = Account("issuer"); + auto const taker = Account("taker"); + + { + Env env{*this, features}; + env.fund(XRP(10'000), issuer, taker); + env.close(); + + auto constexpr takerFunds = 2'000'000'000'000'000'000LL; + MPTTester const token{ + {.env = env, + .issuer = issuer, + .holders = {taker}, + .transferFee = 50'000, + .pay = takerFunds, + .maxAmt = kMaxMpTokenAmount}}; + + // Covers OfferCreate::flowCross() sendMax calculation. A large + // non-issuer MPT offer with a transfer fee used to overflow in + // multiplyRound() before the offer could be placed. + auto constexpr offerAmount = 1'230'000'000'000'000'000LL; + auto const takerSeq = env.seq(taker); + env(offer(taker, XRP(1), token(offerAmount))); + env.close(); + + BEAST_EXPECT( + env.le(keylet::offer(taker.id(), SeqProxy::rawSequence(takerSeq))) != nullptr); + BEAST_EXPECT(env.balance(taker, token) == token(takerFunds)); + } + + // Each scenario below targets a BookStep/OfferStream overflow path. + // The expected behavior is the same in all cases: remove the unusable + // book tip offer and let the taker's crossing offer remain rather than + // returning tecINTERNAL with the poison offer still on-ledger. + { + Env env{*this, features}; + env.fund(XRP(10'000), issuer, taker); + env.close(); + + MPTTester const token{ + {.env = env, .issuer = issuer, .holders = {taker}, .transferFee = 10'000}}; + + // Covers BookStep::forEachOffer() offer preparation, where + // ownerGives = mulRatio(ofrAmt.out, transferRateOut) overflowed + // for an oversized MPT output with a transfer fee. + std::int64_t const poisonAmount = 8'500'000'000'000'000'000LL; + auto const poisonSeq = env.seq(issuer); + env(offer(issuer, XRP(1), token(poisonAmount))); + env.close(); + + auto const poisonKeylet = keylet::offer(issuer.id(), SeqProxy::rawSequence(poisonSeq)); + BEAST_EXPECT(env.le(poisonKeylet) != nullptr); + + auto const takerSeq = env.seq(taker); + env(offer(taker, token(100), XRP(100))); + env.close(); + + BEAST_EXPECT(env.le(poisonKeylet) == nullptr); + BEAST_EXPECT( + env.le(keylet::offer(taker.id(), SeqProxy::rawSequence(takerSeq))) != nullptr); + } + + { + auto const gwA = Account("gatewayA"); + auto const gwB = Account("gatewayB"); + auto const alice = Account("alice"); + auto const mallory = Account("mallory"); + + Env env{*this, features}; + env.fund(XRP(10'000), gwA, gwB, alice, mallory); + env.close(); + + MPTTester const tokenA{ + {.env = env, .issuer = gwA, .holders = {alice, mallory}, .transferFee = 50'000}}; + + MPTTester const tokenB{{.env = env, .issuer = gwB, .holders = {alice, mallory}}}; + + env(pay(gwA, alice, tokenA(1'000))); + + // Covers BookStep::forEachOffer() offer preparation, where + // stpAmt.in = mulRatio(ofrAmt.in, transferRateIn) overflowed. + // The MPT/MPT amounts keep the offer quality reachable while + // applying tokenA's transfer rate overflows the input side. + std::int64_t const poisonPays = 6'148'914'691'236'517'205LL; + std::int64_t const poisonGets = 34'000'000'000'000'000LL; + env(pay(gwB, mallory, tokenB(poisonGets))); + + auto const poisonSeq = env.seq(mallory); + env(offer(mallory, tokenA(poisonPays), tokenB(poisonGets))); + env.close(); + + auto const poisonKeylet = keylet::offer(mallory.id(), SeqProxy::rawSequence(poisonSeq)); + BEAST_EXPECT(env.le(poisonKeylet) != nullptr); + + auto const aliceSeq = env.seq(alice); + env(offer(alice, tokenB(1), tokenA(100))); + env.close(); + + BEAST_EXPECT(env.le(poisonKeylet) == nullptr); + BEAST_EXPECT( + env.le(keylet::offer(alice.id(), SeqProxy::rawSequence(aliceSeq))) != nullptr); + } + + { + Env env{*this, features}; + env.fund(XRP(10'000), issuer, taker); + env.close(); + + MPTTester const token{ + {.env = env, .issuer = issuer, .holders = {taker}, .maxAmt = kMaxMpTokenAmount}}; + + // Give the taker exactly one MPT. If the old rounding overflow + // collapsed the required input to the minimum positive amount, the + // taker could afford the bad fill and the balance checks below + // would catch the economic gain. + env(pay(issuer, taker, token(1))); + env.close(); + + // Covers BookStep::revImp() output reduction. The issuer's offer + // is fully funded and has no transfer fee, so offer preparation + // succeeds. The taker asks for slightly less output, forcing + // limitStepOut() to reduce the offer; that strict reduction used + // to overflow and leave the poison offer on the book. + auto const funded = 1'844'674'407'370'955'162LL; + auto const offerOut = funded + 1; + + auto const poisonSeq = env.seq(issuer); + env(offer(issuer, XRP(1), token(offerOut))); + env.close(); + + auto const poisonKeylet = keylet::offer(issuer.id(), SeqProxy::rawSequence(poisonSeq)); + BEAST_EXPECT(env.le(poisonKeylet) != nullptr); + + auto const issuerXRPBefore = env.balance(issuer, XRP); + auto const takerXRPBefore = env.balance(taker, XRP); + auto const takerMPTBefore = env.balance(taker, token); + auto const fee = env.current()->fees().base; + + auto const takerSeq = env.seq(taker); + env(offer(taker, token(funded), XRP(1))); + env.close(); + + // The former overflow point must not turn into a near-free fill: + // the unusable offer is removed, the taker's offer remains, and no + // value changes hands beyond the taker's transaction fee. + BEAST_EXPECT(env.le(poisonKeylet) == nullptr); + BEAST_EXPECT( + env.le(keylet::offer(taker.id(), SeqProxy::rawSequence(takerSeq))) != nullptr); + BEAST_EXPECT(env.balance(issuer, XRP) == issuerXRPBefore); + BEAST_EXPECT(env.balance(taker, XRP) == takerXRPBefore - fee); + BEAST_EXPECT(env.balance(taker, token) == takerMPTBefore); + } + + { + auto const poisonMaker = Account("poisonMaker"); + + Env env{*this, features}; + env.fund(XRP(10'000), issuer, poisonMaker, taker); + env.close(); + + MPTTester const token{ + {.env = env, + .issuer = issuer, + .holders = {poisonMaker, taker}, + .maxAmt = kMaxMpTokenAmount}}; + + // Covers OfferStream::step() filtering. The offer is mostly + // funded, but reducing it to the actual owner funds inside + // shouldRmSmallIncreasedQOffer() used to overflow before BookStep + // saw the offer. + auto const funded = 1'844'674'407'370'955'162LL; + auto const offerOut = funded + 1; + env(pay(issuer, poisonMaker, token(funded))); + + auto const poisonSeq = env.seq(poisonMaker); + env(offer(poisonMaker, XRP(1), token(offerOut))); + env.close(); + + auto const poisonKeylet = + keylet::offer(poisonMaker.id(), SeqProxy::rawSequence(poisonSeq)); + BEAST_EXPECT(env.le(poisonKeylet) != nullptr); + + auto const takerSeq = env.seq(taker); + env(offer(taker, token(1), XRP(1))); + env.close(); + + BEAST_EXPECT(env.le(poisonKeylet) == nullptr); + BEAST_EXPECT( + env.le(keylet::offer(taker.id(), SeqProxy::rawSequence(takerSeq))) != nullptr); + BEAST_EXPECT(env.balance(poisonMaker, token) == token(funded)); + BEAST_EXPECT(env.balance(taker, token) == token(0)); + } + + { + // Same overflow scenario as the ownerGives case above, but run with + // trace-level logging so BookStep::forEachOffer's removeOffer() + // emits its "Removing offer with overflowing amount calculation" + // trace line. This exercises the JLOG body inside removeOffer, + // which is skipped when logging is above trace severity. + std::string logs; + { + Env env{ + *this, + envconfig(), + features, + std::make_unique(&logs), + beast::Severity::Trace}; + env.fund(XRP(10'000), issuer, taker); + env.close(); + + MPTTester const token{ + {.env = env, .issuer = issuer, .holders = {taker}, .transferFee = 10'000}}; + + std::int64_t const poisonAmount = 8'500'000'000'000'000'000LL; + auto const poisonSeq = env.seq(issuer); + env(offer(issuer, XRP(1), token(poisonAmount))); + env.close(); + + auto const poisonKeylet = + keylet::offer(issuer.id(), SeqProxy::rawSequence(poisonSeq)); + BEAST_EXPECT(env.le(poisonKeylet) != nullptr); + + auto const takerSeq = env.seq(taker); + env(offer(taker, token(100), XRP(100))); + env.close(); + + BEAST_EXPECT(env.le(poisonKeylet) == nullptr); + BEAST_EXPECT( + env.le(keylet::offer(taker.id(), SeqProxy::rawSequence(takerSeq))) != nullptr); + } + BEAST_EXPECT(logs.contains("Removing offer with overflowing amount calculation")); + } + } + void testSelfCrossOffer1(FeatureBitset features) { @@ -4920,6 +5627,7 @@ public: testSellOffer(features); testSellWithFillOrKill(features); testTransferRateOffer(features); + testTransferRateOverflowOffer(features); testSelfCrossOffer(features); testSelfIssueOffer(features); testDirectToDirectPath(features); @@ -4934,8 +5642,11 @@ public: testDeletedOfferIssuer(features); testTicketOffer(features); testTicketCancelOffer(features); + testMPTAMMLimitQualityRounding(features); testRmSmallIncreasedQOffersXRP(features); testRmSmallIncreasedQOffersMPT(features); + testMPTIssuerOfferUsesRemainingCapacity(features); + testPartiallyFundedMPTInputOfferZeroInput(features); testFillOrKill(features); testTickSize(features); testAutoCreateReserve(features); diff --git a/src/test/protocol/STAmount_test.cpp b/src/test/protocol/STAmount_test.cpp index f6c5a94752..c3a681cf01 100644 --- a/src/test/protocol/STAmount_test.cpp +++ b/src/test/protocol/STAmount_test.cpp @@ -1,16 +1,21 @@ #include +#include #include +#include #include #include #include #include #include +#include #include #include #include #include #include +#include +#include #include #include #include @@ -24,6 +29,7 @@ #include #include #include +#include namespace xrpl { @@ -990,6 +996,84 @@ public: } } + void + testMPTRateRounding() + { + testcase("MPT transfer rate rounding uses Number arithmetic"); + + MPTIssue const asset{makeMptID(1, AccountID(0x4985601))}; + Rate const transferRate{1'500'000'000}; + STAmount const largeAmount{asset, UINT64_C(1'230'000'000'000'000'000)}; + STAmount const scaledAmount{asset, UINT64_C(1'845'000'000'000'000'000)}; + + auto rules = [](bool const mptV2) { + // Rules keeps a reference to the presets set, so use static + // storage here rather than a local temporary. + static std::unordered_set> const kNoFeatures; + static std::unordered_set> const kMptV2Features{ + featureMPTokensV2}; + return Rules{mptV2 ? kMptV2Features : kNoFeatures}; + }; + + auto throwsOverflow = [&](auto&& f, bool expected = true) { + bool threw = false; + try + { + f(); + } + catch (std::overflow_error const&) + { + threw = true; + } + BEAST_EXPECT(threw == expected); + }; + + { + CurrentTransactionRulesGuard const rg(rules(false)); + + throwsOverflow([&] { (void)multiplyRound(largeAmount, transferRate, asset, true); }); + throwsOverflow([&] { (void)divideRound(scaledAmount, transferRate, asset, true); }); + } + + { + CurrentTransactionRulesGuard const rg(rules(true)); + + throwsOverflow( + [&] { (void)multiplyRound(largeAmount, transferRate, asset, true); }, false); + throwsOverflow( + [&] { (void)divideRound(scaledAmount, transferRate, asset, true); }, false); + } + + { + CurrentTransactionRulesGuard const rg(rules(true)); + STAmount const one{asset, 1}; + STAmount const two{asset, 2}; + + BEAST_EXPECT(multiplyRound(one, transferRate, asset, true) == two); + BEAST_EXPECT(multiplyRound(one, transferRate, asset, false) == one); + BEAST_EXPECT(divideRound(two, transferRate, asset, true) == two); + BEAST_EXPECT(divideRound(two, transferRate, asset, false) == one); + + BEAST_EXPECT(multiplyRound(largeAmount, transferRate, asset, true) == scaledAmount); + BEAST_EXPECT(divideRound(scaledAmount, transferRate, asset, true) == largeAmount); + } + + { + // mulRound with an integral (XRP) operand whose mantissa is below + // kMinValue exercises the legacy value-scaling loop that normalizes + // the mantissa before multiply. The MPTokensV2 Number path is + // not taken here because the target asset is an IOU. + Issue const usd{Currency(0x5553440000000000), AccountID(0x4985601)}; + STAmount const iouVal{usd, 5}; + STAmount const xrpVal{XRPAmount{7}}; // integral, mantissa < kMinValue + + auto const up = mulRound(iouVal, xrpVal, usd, /*roundUp*/ true); + auto const down = mulRound(iouVal, xrpVal, usd, /*roundUp*/ false); + BEAST_EXPECT(down.signum() > 0); + BEAST_EXPECT(up >= down); + } + } + void testCanSubtractXRP() { @@ -1267,6 +1351,7 @@ public: testCanAddXRP(); testCanAddIOU(); testCanAddMPT(); + testMPTRateRounding(); testCanSubtractXRP(); testCanSubtractIOU(); testCanSubtractMPT(); From 153b7839a758f7c7d79b08378161b2a04af73432 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Wed, 12 Aug 2026 13:40:39 +0000 Subject: [PATCH 23/28] refactor: Replace `boost::filesystem` with `std::filesystem` across the codebase (#7012) Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: mvadari <8029314+mvadari@users.noreply.github.com> Co-authored-by: Mayukha Vadari Co-authored-by: Mayukha Vadari Co-authored-by: Ayaz Salikhov Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: mathbunnyru <12270691+mathbunnyru@users.noreply.github.com> --- include/xrpl/basics/Archive.h | 4 +- include/xrpl/basics/FileUtilities.h | 69 +++++++++++-- include/xrpl/basics/Log.h | 8 +- include/xrpl/beast/unit_test/suite.h | 4 +- include/xrpl/beast/utility/temp_dir.h | 71 -------------- include/xrpl/core/PerfLog.h | 7 +- include/xrpl/rdb/DatabaseCon.h | 11 +-- include/xrpl/rdb/RelationalDatabase.h | 1 - include/xrpl/server/State.h | 2 - .../libxrpl/nodestore/NodeStoreBench.h | 6 +- src/libxrpl/basics/Archive.cpp | 8 +- src/libxrpl/basics/FileUtilities.cpp | 96 +++++++++++++++---- src/libxrpl/basics/Log.cpp | 6 +- src/libxrpl/nodestore/backend/NuDBFactory.cpp | 17 ++-- .../nodestore/backend/RocksDBFactory.cpp | 8 +- src/libxrpl/rdb/SociDB.cpp | 8 +- src/libxrpl/server/Vacuum.cpp | 9 +- src/test/app/GRPCServerTLS_test.cpp | 10 +- src/test/app/LedgerLoad_test.cpp | 16 ++-- src/test/app/Manifest_test.cpp | 18 ++-- src/test/app/SHAMapStore_test.cpp | 5 +- src/test/app/ValidatorSite_test.cpp | 5 +- src/test/basics/PerfLog_test.cpp | 44 ++++----- src/test/core/Config_test.cpp | 24 ++--- src/test/core/SociDB_test.cpp | 23 +++-- src/test/unit_test/FileDirGuard.h | 13 ++- src/tests/libxrpl/basics/FileUtilities.cpp | 44 ++++----- src/tests/libxrpl/nodestore/Backend.cpp | 4 +- src/tests/libxrpl/nodestore/Database.cpp | 10 +- src/tests/libxrpl/nodestore/NuDBFactory.cpp | 28 +++--- src/xrpld/app/main/GRPCServer.cpp | 3 +- src/xrpld/app/misc/SHAMapStoreImp.cpp | 36 ++++--- src/xrpld/app/misc/ValidatorList.h | 5 +- src/xrpld/app/misc/detail/ValidatorList.cpp | 21 ++-- src/xrpld/app/rdb/backend/detail/Node.cpp | 13 +-- src/xrpld/core/Config.h | 11 +-- src/xrpld/core/detail/Config.cpp | 68 ++++++------- src/xrpld/perflog/detail/PerfLogImp.cpp | 17 ++-- 38 files changed, 383 insertions(+), 370 deletions(-) delete mode 100644 include/xrpl/beast/utility/temp_dir.h diff --git a/include/xrpl/basics/Archive.h b/include/xrpl/basics/Archive.h index 66d6a019af..67261352e9 100644 --- a/include/xrpl/basics/Archive.h +++ b/include/xrpl/basics/Archive.h @@ -1,6 +1,6 @@ #pragma once -#include +#include namespace xrpl { @@ -13,6 +13,6 @@ namespace xrpl { * @throws runtime_error */ void -extractTarLz4(boost::filesystem::path const& src, boost::filesystem::path const& dst); +extractTarLz4(std::filesystem::path const& src, std::filesystem::path const& dst); } // namespace xrpl diff --git a/include/xrpl/basics/FileUtilities.h b/include/xrpl/basics/FileUtilities.h index c7a427b8a9..ca3435be03 100644 --- a/include/xrpl/basics/FileUtilities.h +++ b/include/xrpl/basics/FileUtilities.h @@ -1,24 +1,79 @@ #pragma once -#include -#include - #include +#include #include #include +#include namespace xrpl { std::string getFileContents( - boost::system::error_code& ec, - boost::filesystem::path const& sourcePath, + std::error_code& ec, + std::filesystem::path const& sourcePath, std::optional maxSize = std::nullopt); void writeFileContents( - boost::system::error_code& ec, - boost::filesystem::path const& destPath, + std::error_code& ec, + std::filesystem::path const& destPath, std::string const& contents); +/** + * Generate a unique, non-existing path under @p base whose filename starts with + * @p prefix and ends with a random hex suffix. + * + * Attempts up to @p maxAttempts paths. Throws `std::runtime_error` if a unique + * path cannot be found or if the filesystem returns an error while checking for + * existence. + */ +std::filesystem::path +uniqueRandomPath( + std::filesystem::path const& base, + std::string const& prefix = "", + std::size_t maxAttempts = 100); + +/** + * RAII temporary directory. + * + * The directory and all its contents are deleted when + * the instance of `TempDir` is destroyed. + */ +class TempDir +{ + std::filesystem::path path_; + +public: +#if !GENERATING_DOCS + TempDir(TempDir const&) = delete; + TempDir& + operator=(TempDir const&) = delete; +#endif + + /** + * Construct a temporary directory. + */ + TempDir(); + + /** + * Destroy a temporary directory. + */ + ~TempDir(); + + /** + * Get the native path for the temporary directory. + */ + [[nodiscard]] std::string + path() const; + + /** + * Get the native path for a file. + * + * The file does not need to exist. + */ + [[nodiscard]] std::string + file(std::string const& name) const; +}; + } // namespace xrpl diff --git a/include/xrpl/basics/Log.h b/include/xrpl/basics/Log.h index 945dc1b4ec..3aceac5f4a 100644 --- a/include/xrpl/basics/Log.h +++ b/include/xrpl/basics/Log.h @@ -3,8 +3,8 @@ #include #include -#include +#include #include #include #include @@ -84,7 +84,7 @@ private: * @return `true` if the file was opened. */ bool - open(boost::filesystem::path const& path); + open(std::filesystem::path const& path); /** * Close and re-open the system file associated with the log @@ -133,7 +133,7 @@ private: private: std::unique_ptr stream_; - boost::filesystem::path path_; + std::filesystem::path path_; }; std::mutex mutable mutex_; @@ -152,7 +152,7 @@ public: virtual ~Logs() = default; bool - open(boost::filesystem::path const& pathToLogFile); + open(std::filesystem::path const& pathToLogFile); beast::Journal::Sink& get(std::string const& name); diff --git a/include/xrpl/beast/unit_test/suite.h b/include/xrpl/beast/unit_test/suite.h index a727e3fc77..2b06fb4e05 100644 --- a/include/xrpl/beast/unit_test/suite.h +++ b/include/xrpl/beast/unit_test/suite.h @@ -6,10 +6,10 @@ #include -#include #include #include +#include #include #include #include @@ -26,7 +26,7 @@ makeReason(String const& reason, char const* file, int line) std::string s(reason); if (!s.empty()) s.append(": "); - namespace fs = boost::filesystem; + namespace fs = std::filesystem; s.append(fs::path{file}.filename().string()); s.append("("); s.append(std::to_string(line)); diff --git a/include/xrpl/beast/utility/temp_dir.h b/include/xrpl/beast/utility/temp_dir.h deleted file mode 100644 index a0ff1e6940..0000000000 --- a/include/xrpl/beast/utility/temp_dir.h +++ /dev/null @@ -1,71 +0,0 @@ -#pragma once - -#include - -#include - -namespace beast { - -/** - * RAII temporary directory. - * - * The directory and all its contents are deleted when - * the instance of `temp_dir` is destroyed. - */ -class TempDir -{ - boost::filesystem::path path_; - -public: -#if !GENERATING_DOCS - TempDir(TempDir const&) = delete; - TempDir& - operator=(TempDir const&) = delete; -#endif - - /** - * Construct a temporary directory. - */ - TempDir() - { - auto const dir = boost::filesystem::temp_directory_path(); - do - { - path_ = dir / boost::filesystem::unique_path(); - } while (boost::filesystem::exists(path_)); - boost::filesystem::create_directory(path_); - } - - /** - * Destroy a temporary directory. - */ - ~TempDir() - { - // use non-throwing calls in the destructor - boost::system::error_code ec; - boost::filesystem::remove_all(path_, ec); - // TODO: warn/notify if ec set ? - } - - /** - * Get the native path for the temporary directory - */ - [[nodiscard]] std::string - path() const - { - return path_.string(); - } - - /** - * Get the native path for the a file. - * - * The file does not need to exist. - */ - [[nodiscard]] std::string - file(std::string const& name) const - { - return (path_ / name).string(); - } -}; - -} // namespace beast diff --git a/include/xrpl/core/PerfLog.h b/include/xrpl/core/PerfLog.h index f09665e291..dd78a8f9a6 100644 --- a/include/xrpl/core/PerfLog.h +++ b/include/xrpl/core/PerfLog.h @@ -4,10 +4,9 @@ #include #include -#include - #include #include +#include #include #include #include @@ -44,7 +43,7 @@ public: */ struct Setup { - boost::filesystem::path perfLog; + std::filesystem::path perfLog; // log_interval is in milliseconds to support faster testing. milliseconds logInterval{seconds(1)}; }; @@ -149,7 +148,7 @@ public: }; PerfLog::Setup -setupPerfLog(Section const& section, boost::filesystem::path const& configDir); +setupPerfLog(Section const& section, std::filesystem::path const& configDir); std::unique_ptr makePerfLog( diff --git a/include/xrpl/rdb/DatabaseCon.h b/include/xrpl/rdb/DatabaseCon.h index 90aed04337..5c20f65784 100644 --- a/include/xrpl/rdb/DatabaseCon.h +++ b/include/xrpl/rdb/DatabaseCon.h @@ -6,13 +6,12 @@ #include #include -#include - #include #include #include #include +#include #include #include #include @@ -80,7 +79,7 @@ public: StartUpType startUp = StartUpType::Normal; bool standAlone = false; - boost::filesystem::path dataDir; + std::filesystem::path dataDir; // Indicates whether or not to return the `globalPragma` // from commonPragma() bool useGlobalPragma = false; @@ -143,7 +142,7 @@ public: template DatabaseCon( - boost::filesystem::path const& dataDir, + std::filesystem::path const& dataDir, std::string const& dbName, std::array const& pragma, std::array const& initSQL, @@ -155,7 +154,7 @@ public: // Use this constructor to setup checkpointing template DatabaseCon( - boost::filesystem::path const& dataDir, + std::filesystem::path const& dataDir, std::string const& dbName, std::array const& pragma, std::array const& initSQL, @@ -190,7 +189,7 @@ private: template DatabaseCon( - boost::filesystem::path const& pPath, + std::filesystem::path const& pPath, std::vector const* commonPragma, std::array const& pragma, std::array const& initSQL, diff --git a/include/xrpl/rdb/RelationalDatabase.h b/include/xrpl/rdb/RelationalDatabase.h index e5784c7418..e858f578f8 100644 --- a/include/xrpl/rdb/RelationalDatabase.h +++ b/include/xrpl/rdb/RelationalDatabase.h @@ -14,7 +14,6 @@ #include #include -#include #include #include diff --git a/include/xrpl/server/State.h b/include/xrpl/server/State.h index 8590f6e18f..b79253c12c 100644 --- a/include/xrpl/server/State.h +++ b/include/xrpl/server/State.h @@ -4,8 +4,6 @@ #include #include -#include - #include namespace xrpl { diff --git a/src/benchmarks/libxrpl/nodestore/NodeStoreBench.h b/src/benchmarks/libxrpl/nodestore/NodeStoreBench.h index 6122dd2535..a90207f26a 100644 --- a/src/benchmarks/libxrpl/nodestore/NodeStoreBench.h +++ b/src/benchmarks/libxrpl/nodestore/NodeStoreBench.h @@ -2,10 +2,10 @@ #include #include +#include #include #include #include -#include #include #include #include @@ -227,7 +227,7 @@ sliceFixedBatches(Batch const& pool, std::size_t batchSize) */ struct BackendHarness { - beast::TempDir tempDir; ///< Declared first so it is destroyed last + TempDir tempDir; ///< Declared first so it is destroyed last DummyScheduler scheduler; beast::Journal journal{beast::Journal::getNullSink()}; std::unique_ptr backend; @@ -257,7 +257,7 @@ struct BackendHarness */ struct DatabaseHarness { - beast::TempDir tempDir; + TempDir tempDir; DummyScheduler scheduler; beast::Journal journal{beast::Journal::getNullSink()}; std::unique_ptr db; diff --git a/src/libxrpl/basics/Archive.cpp b/src/libxrpl/basics/Archive.cpp index bba144ed04..5ab0d88c1d 100644 --- a/src/libxrpl/basics/Archive.cpp +++ b/src/libxrpl/basics/Archive.cpp @@ -2,22 +2,20 @@ #include -#include -#include - #include #include #include +#include #include #include namespace xrpl { void -extractTarLz4(boost::filesystem::path const& src, boost::filesystem::path const& dst) +extractTarLz4(std::filesystem::path const& src, std::filesystem::path const& dst) { - if (!is_regular_file(src)) + if (!std::filesystem::is_regular_file(src)) Throw("Invalid source file"); using archive_ptr = std::unique_ptr; diff --git a/src/libxrpl/basics/FileUtilities.cpp b/src/libxrpl/basics/FileUtilities.cpp index 1a6e604724..bed2b756ac 100644 --- a/src/libxrpl/basics/FileUtilities.cpp +++ b/src/libxrpl/basics/FileUtilities.cpp @@ -1,29 +1,31 @@ #include -#include -#include -#include -#include -#include +#include #include #include +#include #include +#include #include +#include #include #include +#include +#include +#include #include +#include namespace xrpl { std::string getFileContents( - boost::system::error_code& ec, - boost::filesystem::path const& sourcePath, + std::error_code& ec, + std::filesystem::path const& sourcePath, std::optional maxSize) { - using namespace boost::filesystem; - using namespace boost::system::errc; + using namespace std::filesystem; path const fullPath{canonical(sourcePath, ec)}; if (ec) @@ -32,15 +34,15 @@ getFileContents( if (maxSize && (file_size(fullPath, ec) > *maxSize || ec)) { if (!ec) - ec = make_error_code(file_too_large); + ec = make_error_code(std::errc::file_too_large); return {}; } - std::ifstream fileStream(fullPath.string(), std::ios::in); + std::ifstream fileStream(fullPath, std::ios::in); if (!fileStream) { - ec = make_error_code(static_cast(errno)); + ec.assign(errno, std::generic_category()); return {}; } @@ -49,7 +51,7 @@ getFileContents( if (fileStream.bad()) { - ec = make_error_code(static_cast(errno)); + ec.assign(errno, std::generic_category()); return {}; } @@ -58,18 +60,15 @@ getFileContents( void writeFileContents( - boost::system::error_code& ec, - boost::filesystem::path const& destPath, + std::error_code& ec, + std::filesystem::path const& destPath, std::string const& contents) { - using namespace boost::filesystem; - using namespace boost::system::errc; - - std::ofstream fileStream(destPath.string(), std::ios::out | std::ios::trunc); + std::ofstream fileStream(destPath, std::ios::out | std::ios::trunc); if (!fileStream) { - ec = make_error_code(static_cast(errno)); + ec.assign(errno, std::generic_category()); return; } @@ -77,9 +76,64 @@ writeFileContents( if (fileStream.bad()) { - ec = make_error_code(static_cast(errno)); + ec.assign(errno, std::generic_category()); return; } } +std::filesystem::path +uniqueRandomPath( + std::filesystem::path const& base, + std::string const& prefix, + std::size_t maxAttempts) +{ + std::random_device rd; + for (std::size_t attempt = 0; attempt < maxAttempts; ++attempt) + { + std::ostringstream oss; + oss << prefix << std::hex << std::setfill('0') << std::setw(8) << rd() << std::setw(8) + << rd(); + auto candidate = base / oss.str(); + std::error_code ec; + bool const exists = std::filesystem::exists(candidate, ec); + if (ec) + { + Throw( + "Unable to check path '" + candidate.string() + "': " + ec.message()); + } + if (!exists) + return candidate; + } + Throw("Unable to generate a unique path under '" + base.string() + "'"); +} + +TempDir::TempDir() : path_(uniqueRandomPath(std::filesystem::temp_directory_path())) +{ + std::filesystem::create_directory(path_); +} + +TempDir::~TempDir() +{ + // use non-throwing calls in the destructor + std::error_code ec; + std::filesystem::remove_all(path_, ec); + if (ec) + { + std::cerr << "Unable to remove temporary directory '" << path_.string() + << "': " << ec.message() << '\n'; + } +} + +std::string +TempDir::path() const +{ + return path_.string(); +} + +std::string +TempDir::file(std::string const& name) const +{ + return (path_ / name).string(); +} + } // namespace xrpl diff --git a/src/libxrpl/basics/Log.cpp b/src/libxrpl/basics/Log.cpp index d1e54a515f..68525f5a65 100644 --- a/src/libxrpl/basics/Log.cpp +++ b/src/libxrpl/basics/Log.cpp @@ -5,10 +5,10 @@ #include #include -#include #include #include +#include #include #include #include @@ -54,7 +54,7 @@ Logs::File::isOpen() const noexcept } bool -Logs::File::open(boost::filesystem::path const& path) +Logs::File::open(std::filesystem::path const& path) { close(); @@ -114,7 +114,7 @@ Logs::Logs(beast::Severity thresh) : thresh_(thresh) // default severity } bool -Logs::open(boost::filesystem::path const& pathToLogFile) +Logs::open(std::filesystem::path const& pathToLogFile) { return file_.open(pathToLogFile); } diff --git a/src/libxrpl/nodestore/backend/NuDBFactory.cpp b/src/libxrpl/nodestore/backend/NuDBFactory.cpp index bbf37f3edf..98173858e8 100644 --- a/src/libxrpl/nodestore/backend/NuDBFactory.cpp +++ b/src/libxrpl/nodestore/backend/NuDBFactory.cpp @@ -16,8 +16,6 @@ #include #include -#include -#include #include #include @@ -36,12 +34,14 @@ #include #include #include +#include #include #include #include #include #include #include +#include #include namespace xrpl::node_store { @@ -131,7 +131,7 @@ public: void open(bool createIfMissing, uint64_t appType, uint64_t uid, uint64_t salt) override { - using namespace boost::filesystem; + using namespace std::filesystem; if (db.is_open()) { // LCOV_EXCL_START @@ -194,11 +194,12 @@ public: if (deletePath) { - boost::filesystem::remove_all(name, ec); - if (ec) + std::error_code fsec; + std::filesystem::remove_all(name, fsec); + if (fsec) { - JLOG(j.fatal()) - << "Filesystem remove_all of " << name << " failed with: " << ec.message(); + JLOG(j.fatal()) << "Filesystem remove_all of " << name + << " failed with: " << fsec.message(); } } } @@ -352,7 +353,7 @@ private: static std::size_t parseBlockSize(std::string const& name, Section const& keyValues, beast::Journal journal) { - using namespace boost::filesystem; + using namespace std::filesystem; auto const folder = path(name); auto const kp = (folder / "nudb.key").string(); diff --git a/src/libxrpl/nodestore/backend/RocksDBFactory.cpp b/src/libxrpl/nodestore/backend/RocksDBFactory.cpp index 4b7a1171fe..6f00b762b2 100644 --- a/src/libxrpl/nodestore/backend/RocksDBFactory.cpp +++ b/src/libxrpl/nodestore/backend/RocksDBFactory.cpp @@ -19,9 +19,6 @@ #include #include -#include -#include - #include #include #include @@ -37,6 +34,7 @@ #include #include +#include #include #include #include @@ -262,8 +260,8 @@ public: db.reset(); if (deletePath_) { - boost::filesystem::path const dir = name; - boost::filesystem::remove_all(dir); + std::filesystem::path const dir = name; + std::filesystem::remove_all(dir); } } } diff --git a/src/libxrpl/rdb/SociDB.cpp b/src/libxrpl/rdb/SociDB.cpp index 2c3fb1bde1..84006acbe7 100644 --- a/src/libxrpl/rdb/SociDB.cpp +++ b/src/libxrpl/rdb/SociDB.cpp @@ -5,13 +5,11 @@ #include #include -#include -#include - #include #include #include +#include #include #include #include @@ -45,8 +43,8 @@ getSociSqliteInit(std::string const& name, std::string const& dir, std::string c Throw( "Sqlite databases must specify a dir and a name. Name: " + name + " Dir: " + dir); } - boost::filesystem::path file(dir); - if (is_directory(file)) + std::filesystem::path file(dir); + if (std::filesystem::is_directory(file)) file /= name + ext; return file.string(); } diff --git a/src/libxrpl/server/Vacuum.cpp b/src/libxrpl/server/Vacuum.cpp index 63d40af156..df768d509a 100644 --- a/src/libxrpl/server/Vacuum.cpp +++ b/src/libxrpl/server/Vacuum.cpp @@ -5,13 +5,12 @@ #include #include -#include -#include #include // IWYU pragma: keep #include #include +#include #include #include @@ -20,12 +19,12 @@ namespace xrpl { bool doVacuumDB(DatabaseCon::Setup const& setup, beast::Journal j) { - boost::filesystem::path const dbPath = setup.dataDir / kTxDbName; + std::filesystem::path const dbPath = setup.dataDir / kTxDbName; - uintmax_t const dbSize = file_size(dbPath); + uintmax_t const dbSize = std::filesystem::file_size(dbPath); XRPL_ASSERT(dbSize != static_cast(-1), "xrpl::doVacuumDB : file_size succeeded"); - if (auto available = space(dbPath.parent_path()).available; available < dbSize) + if (auto available = std::filesystem::space(dbPath.parent_path()).available; available < dbSize) { std::cerr << "The database filesystem must have at least as " "much free space as the size of " diff --git a/src/test/app/GRPCServerTLS_test.cpp b/src/test/app/GRPCServerTLS_test.cpp index a48986d004..58ccf33959 100644 --- a/src/test/app/GRPCServerTLS_test.cpp +++ b/src/test/app/GRPCServerTLS_test.cpp @@ -1,13 +1,12 @@ #include #include +#include #include #include #include #include -#include - #include #include #include @@ -17,6 +16,7 @@ #include #include #include +#include #include #include #include @@ -254,10 +254,8 @@ public: TemporaryTLSCertificates() { - auto tmpDir = std::filesystem::temp_directory_path(); - auto uniqueDirName = - boost::filesystem::unique_path(std::string(kCertsDirPrefix) + "%%%%%%%%"); - tempDir_ = tmpDir / uniqueDirName.string(); + tempDir_ = xrpl::uniqueRandomPath( + std::filesystem::temp_directory_path(), std::string(kCertsDirPrefix)); std::filesystem::create_directories(tempDir_); writeFile(tempDir_ / kCaCertFilename, kCaCertContent); diff --git a/src/test/app/LedgerLoad_test.cpp b/src/test/app/LedgerLoad_test.cpp index ee3bfe5192..8fb10c1088 100644 --- a/src/test/app/LedgerLoad_test.cpp +++ b/src/test/app/LedgerLoad_test.cpp @@ -7,10 +7,10 @@ #include +#include #include #include #include -#include #include #include #include @@ -18,16 +18,16 @@ #include #include -#include -#include #include +#include #include #include #include #include #include #include +#include namespace xrpl { @@ -61,7 +61,7 @@ class LedgerLoad_test : public beast::unit_test::Suite }; SetupData - setupLedger(beast::TempDir const& td) + setupLedger(TempDir const& td) { using namespace test::jtx; SetupData retval = {.dbPath = td.path()}; @@ -139,7 +139,7 @@ class LedgerLoad_test : public beast::unit_test::Suite { testcase("Load ledger: Bad Files"); using namespace test::jtx; - using namespace boost::filesystem; + using namespace std::filesystem; // empty path except([&] { @@ -161,8 +161,8 @@ class LedgerLoad_test : public beast::unit_test::Suite }); // make a corrupted version of the ledger file (last 10 bytes removed). - boost::system::error_code ec; - auto ledgerFileCorrupt = boost::filesystem::path{sd.dbPath} / "ledgerdata_bad.json"; + std::error_code ec; + auto ledgerFileCorrupt = std::filesystem::path{sd.dbPath} / "ledgerdata_bad.json"; copy_file(sd.ledgerFile, ledgerFileCorrupt, copy_options::overwrite_existing, ec); if (!BEAST_EXPECTS(!ec, ec.message())) return; @@ -330,7 +330,7 @@ public: void run() override { - beast::TempDir const td; + TempDir const td; auto sd = setupLedger(td); // test cases diff --git a/src/test/app/Manifest_test.cpp b/src/test/app/Manifest_test.cpp index ef2043a22c..14d176b45f 100644 --- a/src/test/app/Manifest_test.cpp +++ b/src/test/app/Manifest_test.cpp @@ -22,14 +22,12 @@ #include #include -#include -#include - #include #include #include #include #include +#include #include #include #include @@ -56,18 +54,18 @@ private: } static void - cleanupDatabaseDir(boost::filesystem::path const& dbPath) + cleanupDatabaseDir(std::filesystem::path const& dbPath) { - using namespace boost::filesystem; + using namespace std::filesystem; if (!exists(dbPath) || !is_directory(dbPath) || !is_empty(dbPath)) return; remove(dbPath); } static void - setupDatabaseDir(boost::filesystem::path const& dbPath) + setupDatabaseDir(std::filesystem::path const& dbPath) { - using namespace boost::filesystem; + using namespace std::filesystem; if (!exists(dbPath)) { create_directory(dbPath); @@ -80,10 +78,10 @@ private: Throw("Cannot create directory: " + dbPath.string()); } } - static boost::filesystem::path + static std::filesystem::path getDatabasePath() { - return boost::filesystem::current_path() / "manifest_test_databases"; + return std::filesystem::current_path() / "manifest_test_databases"; } public: @@ -351,7 +349,7 @@ public: BEAST_EXPECT(loaded.revoked(pk)); } } - boost::filesystem::remove(getDatabasePath() / boost::filesystem::path(dbName)); + std::filesystem::remove(getDatabasePath() / std::filesystem::path(dbName)); } void diff --git a/src/test/app/SHAMapStore_test.cpp b/src/test/app/SHAMapStore_test.cpp index 6ee7442d23..82019affba 100644 --- a/src/test/app/SHAMapStore_test.cpp +++ b/src/test/app/SHAMapStore_test.cpp @@ -23,10 +23,9 @@ #include #include -#include - #include #include +#include #include #include #include @@ -493,7 +492,7 @@ public: makeBackendRotating(jtx::Env& env, NodeStoreScheduler& scheduler, std::string path) { Section section{env.app().config().section(Sections::kNodeDatabase)}; - boost::filesystem::path newPath; + std::filesystem::path newPath; if (!BEAST_EXPECT(path.size())) return {}; diff --git a/src/test/app/ValidatorSite_test.cpp b/src/test/app/ValidatorSite_test.cpp index 8400f2d794..8373efe85b 100644 --- a/src/test/app/ValidatorSite_test.cpp +++ b/src/test/app/ValidatorSite_test.cpp @@ -15,13 +15,12 @@ #include #include -#include -#include #include #include #include +#include #include #include #include @@ -704,7 +703,7 @@ public: .effectiveOverlap = detail::kDefaultEffectiveOverlap, .expectedRefreshMin = 60 * 24}}); // max of 24 hours } - using namespace boost::filesystem; + using namespace std::filesystem; for (auto const& file : directory_iterator(good.subdir())) { remove_all(file); diff --git a/src/test/basics/PerfLog_test.cpp b/src/test/basics/PerfLog_test.cpp index 24ea971515..f7679dc488 100644 --- a/src/test/basics/PerfLog_test.cpp +++ b/src/test/basics/PerfLog_test.cpp @@ -15,14 +15,10 @@ #include #include -#include -#include -#include -#include - #include #include #include +#include #include #include #include @@ -31,6 +27,7 @@ #include #include #include +#include #include #include #include @@ -43,7 +40,7 @@ class PerfLog_test : public beast::unit_test::Suite { enum class WithFile : bool { No = false, Yes = true }; - using path = boost::filesystem::path; + using path = std::filesystem::path; // We're only using Env for its Journal. That Journal gives better // coverage in unit tests. @@ -66,14 +63,14 @@ class PerfLog_test : public beast::unit_test::Suite // The error code is intentionally ignored: if the path doesn't // exist (the common case on a clean runner) remove_all returns // an error, and that's fine — there's nothing to clean up. - using namespace boost::filesystem; - boost::system::error_code ec; + using namespace std::filesystem; + std::error_code ec; remove_all(logDir(), ec); } ~Fixture() { - using namespace boost::filesystem; + using namespace std::filesystem; auto const dir{logDir()}; auto const file{logFile()}; @@ -96,7 +93,7 @@ class PerfLog_test : public beast::unit_test::Suite static path logDir() { - using namespace boost::filesystem; + using namespace std::filesystem; return temp_directory_path() / "perf_log_test_dir"; } @@ -129,7 +126,7 @@ class PerfLog_test : public beast::unit_test::Suite static void wait() { - using namespace boost::filesystem; + using namespace std::filesystem; auto const path = logFile(); if (!exists(path)) @@ -201,7 +198,7 @@ public: void testFileCreation() { - using namespace boost::filesystem; + using namespace std::filesystem; { // Verify a PerfLog creates its file when constructed. @@ -250,28 +247,30 @@ public: // Put a write protected file where PerfLog wants to write its // file. Make sure that PerfLog tries to shutdown the server // since it can't open its file. + using std::filesystem::perms; + Fixture fixture{env_.app(), j_}; if (!BEAST_EXPECT(!exists(fixture.logDir()))) return; // Construct and write protect a file to prevent PerfLog // from creating its file. - boost::system::error_code ec; - boost::filesystem::create_directories(fixture.logDir(), ec); + std::error_code ec; + std::filesystem::create_directories(fixture.logDir(), ec); if (!BEAST_EXPECT(!ec)) return; - auto fileWriteable = [](boost::filesystem::path const& p) -> bool { - return std::ofstream{p.c_str(), std::ios::out | std::ios::app}.is_open(); + auto fileWriteable = [](std::filesystem::path const& p) -> bool { + return std::ofstream{p, std::ios::out | std::ios::app}.is_open(); }; if (!BEAST_EXPECT(fileWriteable(fixture.logFile()))) return; - boost::filesystem::permissions( + std::filesystem::permissions( fixture.logFile(), - perms::remove_perms | perms::owner_write | perms::others_write | - perms::group_write); + perms::owner_write | perms::others_write | perms::group_write, + std::filesystem::perm_options::remove); // If the test is running as root, then the write protect may have // no effect. Make sure write protect worked before proceeding. @@ -295,9 +294,10 @@ public: perfLog->stop(); // Fix file permissions so the file can be cleaned up. - boost::filesystem::permissions( + std::filesystem::permissions( fixture.logFile(), - perms::add_perms | perms::owner_write | perms::others_write | perms::group_write); + perms::owner_write | perms::others_write | perms::group_write, + std::filesystem::perm_options::add); } } @@ -962,7 +962,7 @@ public: // We can't fully test rotate because unit tests must run on Windows, // and Windows doesn't (may not?) support rotate. But at least call // the interface and see that it doesn't crash. - using namespace boost::filesystem; + using namespace std::filesystem; Fixture fixture{env_.app(), j_}; BEAST_EXPECT(!exists(fixture.logDir())); diff --git a/src/test/core/Config_test.cpp b/src/test/core/Config_test.cpp index ac5471fd3c..dec6393010 100644 --- a/src/test/core/Config_test.cpp +++ b/src/test/core/Config_test.cpp @@ -3,14 +3,13 @@ #include +#include #include -#include #include #include #include // IWYU pragma: keep #include -#include #include // IWYU pragma: keep #include #include @@ -20,6 +19,7 @@ #include #include #include +#include #include #include #include @@ -179,7 +179,7 @@ public: [[nodiscard]] bool dataDirExists() const { - return boost::filesystem::is_directory(dataDir_); + return std::filesystem::is_directory(dataDir_); } [[nodiscard]] bool @@ -192,7 +192,7 @@ public: { try { - using namespace boost::filesystem; + using namespace std::filesystem; if (rmDataDir_) rmDir(dataDir_); } @@ -273,7 +273,7 @@ public: class Config_test final : public TestSuite { private: - using path = boost::filesystem::path; + using path = std::filesystem::path; public: void @@ -309,7 +309,7 @@ port_wss_admin { testcase("config_file"); - using namespace boost::filesystem; + using namespace std::filesystem; auto const cwd = current_path(); // Test both config file names. @@ -319,7 +319,7 @@ port_wss_admin for (auto const& configFile : configFiles) { // Use a temporary directory for testing. - beast::TempDir const td; + TempDir const td; current_path(td.path()); path const f = td.file(std::string{configFile}); std::ofstream o(f.string()); @@ -341,13 +341,13 @@ port_wss_admin { // Point the current working directory to a temporary directory, so // we don't pick up an actual config file from the repository root. - beast::TempDir const td; + TempDir const td; current_path(td.path()); // The XDG config directory is set: the config file must be in a // subdirectory named after the system. { - beast::TempDir const tc; + TempDir const tc; // Set the HOME and XDG_CONFIG_HOME environment variables. The // HOME variable is not used when XDG_CONFIG_HOME is set, but @@ -381,7 +381,7 @@ port_wss_admin // The XDG config directory is not set: the config file must be in a // subdirectory named .config followed by the system name. { - beast::TempDir const tc; + TempDir const tc; // Set only the HOME environment variable. char const* h = getenv("HOME"); @@ -425,7 +425,7 @@ port_wss_admin { testcase("database_path"); - using namespace boost::filesystem; + using namespace std::filesystem; { boost::format cc("[database_path]\n%1%\n"); @@ -601,7 +601,7 @@ main { testcase("validators_file"); - using namespace boost::filesystem; + using namespace std::filesystem; { // load should throw for missing specified validators file boost::format cc("[validators_file]\n%1%\n"); diff --git a/src/test/core/SociDB_test.cpp b/src/test/core/SociDB_test.cpp index 7a57641b64..a7bb8e71bc 100644 --- a/src/test/core/SociDB_test.cpp +++ b/src/test/core/SociDB_test.cpp @@ -6,8 +6,6 @@ #include #include -#include -#include #include // IWYU pragma: keep #include // IWYU pragma: keep @@ -19,6 +17,7 @@ #include #include #include +#include #include #include #include @@ -31,7 +30,7 @@ class SociDB_test final : public TestSuite { private: static void - setupSQLiteConfig(BasicConfig& config, boost::filesystem::path const& dbPath) + setupSQLiteConfig(BasicConfig& config, std::filesystem::path const& dbPath) { config.overwrite(Sections::kSqdb, Keys::kBackend, "sqlite"); auto value = dbPath.string(); @@ -40,18 +39,18 @@ private: } static void - cleanupDatabaseDir(boost::filesystem::path const& dbPath) + cleanupDatabaseDir(std::filesystem::path const& dbPath) { - using namespace boost::filesystem; + using namespace std::filesystem; if (!exists(dbPath) || !is_directory(dbPath) || !is_empty(dbPath)) return; remove(dbPath); } static void - setupDatabaseDir(boost::filesystem::path const& dbPath) + setupDatabaseDir(std::filesystem::path const& dbPath) { - using namespace boost::filesystem; + using namespace std::filesystem; if (!exists(dbPath)) { create_directory(dbPath); @@ -64,10 +63,10 @@ private: Throw("Cannot create directory: " + dbPath.string()); } } - static boost::filesystem::path + static std::filesystem::path getDatabasePath() { - return boost::filesystem::current_path() / "socidb_test_databases"; + return std::filesystem::current_path() / "socidb_test_databases"; } public: @@ -157,7 +156,7 @@ public: checkValues(s); } { - namespace bfs = boost::filesystem; + namespace bfs = std::filesystem; // Remove the database bfs::path const dbPath(sc.connectionString()); if (bfs::is_regular_file(dbPath)) @@ -231,7 +230,7 @@ public: // boost::tuple. DO NOT USE soci row! } { - namespace bfs = boost::filesystem; + namespace bfs = std::filesystem; // Remove the database bfs::path const dbPath(sc.connectionString()); if (bfs::is_regular_file(dbPath)) @@ -283,7 +282,7 @@ public: s << "SELECT LedgerSeq FROM Ledgers;", soci::into(ledgersLS); BEAST_EXPECT(ledgersLS.size() == numRows); } - namespace bfs = boost::filesystem; + namespace bfs = std::filesystem; // Remove the database bfs::path const dbPath(sc.connectionString()); if (bfs::is_regular_file(dbPath)) diff --git a/src/test/unit_test/FileDirGuard.h b/src/test/unit_test/FileDirGuard.h index b583f821a4..2e6b3fd179 100644 --- a/src/test/unit_test/FileDirGuard.h +++ b/src/test/unit_test/FileDirGuard.h @@ -3,9 +3,8 @@ #include #include -#include - #include +#include #include #include #include @@ -20,7 +19,7 @@ namespace xrpl::detail { class DirGuard { protected: - using path = boost::filesystem::path; + using path = std::filesystem::path; private: path subDir_; @@ -47,7 +46,7 @@ public: DirGuard(beast::unit_test::Suite& test, path subDir, bool useCounter = true) : subDir_(std::move(subDir)), test_(test) { - using namespace boost::filesystem; + using namespace std::filesystem; static auto kSubDirCounter = 0; if (useCounter) @@ -73,7 +72,7 @@ public: { try { - using namespace boost::filesystem; + using namespace std::filesystem; if (rmSubDir_) rmDir(subDir_); @@ -130,7 +129,7 @@ public: { try { - using namespace boost::filesystem; + using namespace std::filesystem; if (exists(file_)) { remove(file_); @@ -160,7 +159,7 @@ public: [[nodiscard]] bool fileExists() const { - return boost::filesystem::exists(file_); + return std::filesystem::exists(file_); } }; diff --git a/src/tests/libxrpl/basics/FileUtilities.cpp b/src/tests/libxrpl/basics/FileUtilities.cpp index cd24abd696..5cf2b72709 100644 --- a/src/tests/libxrpl/basics/FileUtilities.cpp +++ b/src/tests/libxrpl/basics/FileUtilities.cpp @@ -2,16 +2,14 @@ #include -#include -#include -#include -#include - #include +#include #include +#include #include #include +#include namespace xrpl { @@ -20,15 +18,14 @@ namespace { class TempFile { public: - explicit TempFile(boost::filesystem::path file, std::string const& contents) - : dir_( - boost::filesystem::temp_directory_path() / - boost::filesystem::unique_path("xrpl-file-utilities-%%%%-%%%%-%%%%")) - , file_(dir_ / file) + explicit TempFile(std::string const& file, std::string const& contents) + : file_( + uniqueRandomPath(std::filesystem::temp_directory_path(), "xrpl-file-utilities-") / + file) { - boost::filesystem::create_directory(dir_); + std::filesystem::create_directory(file_.parent_path()); - std::ofstream output(file_.string()); + std::ofstream output(file_); if (!output) throw std::runtime_error("Unable to create temporary test file"); @@ -37,33 +34,36 @@ public: ~TempFile() { - boost::system::error_code ec; - boost::filesystem::remove(file_, ec); - boost::filesystem::remove(dir_, ec); + // use non-throwing calls in the destructor + std::error_code ec; + auto const dir = file_.parent_path(); + std::filesystem::remove_all(dir, ec); + if (ec) + { + std::cerr << "Unable to remove temporary directory '" << dir.string() + << "': " << ec.message() << '\n'; + } } - [[nodiscard]] boost::filesystem::path const& + [[nodiscard]] std::filesystem::path const& file() const { return file_; } private: - boost::filesystem::path dir_; - boost::filesystem::path file_; + std::filesystem::path file_; }; } // namespace TEST(FileUtilitiesTest, get_file_contents) { - using namespace boost::system; - constexpr char const* kExpectedContents = "This file is very short. That's all we need."; TempFile const file("test_file", "This is temporary text that should get overwritten"); - error_code ec; + std::error_code ec; auto const path = file.file(); writeFileContents(ec, path, kExpectedContents); @@ -86,7 +86,7 @@ TEST(FileUtilitiesTest, get_file_contents) { // Test with small max auto const bad = getFileContents(ec, path, 16); - EXPECT_TRUE(ec && ec.value() == boost::system::errc::file_too_large); + EXPECT_TRUE(ec && ec.value() == static_cast(std::errc::file_too_large)); EXPECT_TRUE(bad.empty()); } } diff --git a/src/tests/libxrpl/nodestore/Backend.cpp b/src/tests/libxrpl/nodestore/Backend.cpp index eb78851429..3bd36ced8d 100644 --- a/src/tests/libxrpl/nodestore/Backend.cpp +++ b/src/tests/libxrpl/nodestore/Backend.cpp @@ -1,8 +1,8 @@ #include #include +#include #include -#include #include #include #include @@ -84,7 +84,7 @@ protected: } DummyScheduler scheduler_; - beast::TempDir const tempDir_; + TempDir const tempDir_; beast::Journal const journal_{TestSink::instance()}; Section params_; Batch batch_; diff --git a/src/tests/libxrpl/nodestore/Database.cpp b/src/tests/libxrpl/nodestore/Database.cpp index 82012ed347..a3f7340f62 100644 --- a/src/tests/libxrpl/nodestore/Database.cpp +++ b/src/tests/libxrpl/nodestore/Database.cpp @@ -1,8 +1,8 @@ #include #include +#include #include -#include #include #include #include @@ -81,7 +81,7 @@ protected: } DummyScheduler scheduler_; - beast::TempDir const nodeDb_; + TempDir const nodeDb_; beast::Journal const journal_{TestSink::instance()}; Section nodeParams_; Batch batch_; @@ -157,7 +157,7 @@ INSTANTIATE_TEST_SUITE_P( TEST(NodeStoreDatabase, memory_earliest_seq) { DummyScheduler scheduler; - beast::TempDir const nodeDb; + TempDir const nodeDb; Section nodeParams; nodeParams.set("type", "memory"); nodeParams.set("path", nodeDb.path()); @@ -204,7 +204,7 @@ TEST_P(DatabaseImportTest, same_backend) DummyScheduler scheduler; beast::Journal const journal(TestSink::instance()); - beast::TempDir const srcDir; + TempDir const srcDir; Section srcParams; srcParams.set("type", type); srcParams.set("path", srcDir.path()); @@ -222,7 +222,7 @@ TEST_P(DatabaseImportTest, same_backend) // re-open source and import into a fresh destination auto src = Manager::instance().makeDatabase(megabytes(4), scheduler, 2, srcParams, journal); - beast::TempDir const destDir; + TempDir const destDir; Section destParams; destParams.set("type", type); destParams.set("path", destDir.path()); diff --git a/src/tests/libxrpl/nodestore/NuDBFactory.cpp b/src/tests/libxrpl/nodestore/NuDBFactory.cpp index c126984630..7240f08256 100644 --- a/src/tests/libxrpl/nodestore/NuDBFactory.cpp +++ b/src/tests/libxrpl/nodestore/NuDBFactory.cpp @@ -1,6 +1,6 @@ #include +#include #include -#include #include #include #include @@ -58,7 +58,7 @@ runRoundTrip(Section const& params, std::size_t expectedBlocksize) TEST(NuDBFactory, default_block_size) { - beast::TempDir const tempDir; + TempDir const tempDir; auto const params = makeSection(tempDir.path()); ASSERT_NO_FATAL_FAILURE(runRoundTrip(params, 4096)); } @@ -69,14 +69,14 @@ TEST(NuDBFactory, valid_block_sizes) for (auto const size : kValidSizes) { SCOPED_TRACE("size=" + std::to_string(size)); - beast::TempDir const tempDir; + TempDir const tempDir; auto const params = makeSection(tempDir.path(), std::to_string(size)); ASSERT_NO_FATAL_FAILURE(runRoundTrip(params, size)); } // empty value is ignored by config parser; default (4096) is used { - beast::TempDir const tempDir; + TempDir const tempDir; auto const params = makeSection(tempDir.path(), ""); ASSERT_NO_FATAL_FAILURE(runRoundTrip(params, 4096)); } @@ -101,7 +101,7 @@ TEST(NuDBFactory, invalid_block_sizes) for (auto const& size : kInvalidSizes) { SCOPED_TRACE("size='" + size + "'"); - beast::TempDir const tempDir; + TempDir const tempDir; auto const params = makeSection(tempDir.path(), size); EXPECT_THROW(runRoundTrip(params, 4096), std::exception); } @@ -111,7 +111,7 @@ TEST(NuDBFactory, invalid_block_sizes) for (auto const& size : kWhitespaceSizes) { SCOPED_TRACE("size='" + size + "'"); - beast::TempDir const tempDir; + TempDir const tempDir; auto const params = makeSection(tempDir.path(), size); EXPECT_THROW(runRoundTrip(params, 4096), std::exception); } @@ -121,7 +121,7 @@ TEST(NuDBFactory, log_messages) { // valid custom block size emits info log { - beast::TempDir const tempDir; + TempDir const tempDir; auto const params = makeSection(tempDir.path(), "8192"); test::CaptureSink sink(beast::Severity::Info); beast::Journal const journal(sink); @@ -135,7 +135,7 @@ TEST(NuDBFactory, log_messages) // invalid block size throws with informative message { - beast::TempDir const tempDir; + TempDir const tempDir; auto const params = makeSection(tempDir.path(), "5000"); test::CaptureSink sink(beast::Severity::Warning); beast::Journal const journal(sink); @@ -156,7 +156,7 @@ TEST(NuDBFactory, log_messages) // non-numeric value throws { - beast::TempDir const tempDir; + TempDir const tempDir; auto const params = makeSection(tempDir.path(), "invalid"); test::CaptureSink sink(beast::Severity::Warning); beast::Journal const journal(sink); @@ -191,7 +191,7 @@ TEST(NuDBFactory, power_of_two_validation) for (auto const& [size, shouldWork] : kCASES) { SCOPED_TRACE("size=" + size + " shouldWork=" + (shouldWork ? "true" : "false")); - beast::TempDir const tempDir; + TempDir const tempDir; auto const params = makeSection(tempDir.path(), size); test::CaptureSink sink(beast::Severity::Warning); beast::Journal const journal(sink); @@ -216,7 +216,7 @@ TEST(NuDBFactory, power_of_two_validation) TEST(NuDBFactory, both_constructor_variants) { - beast::TempDir const tempDir; + TempDir const tempDir; auto const params = makeSection(tempDir.path(), "16384"); DummyScheduler scheduler; beast::Journal const journal(TestSink::instance()); @@ -235,7 +235,7 @@ TEST(NuDBFactory, configuration_parsing) { // basic valid format emits success log { - beast::TempDir const tempDir; + TempDir const tempDir; auto const params = makeSection(tempDir.path(), "8192"); test::CaptureSink sink(beast::Severity::Info); beast::Journal const journal(sink); @@ -250,7 +250,7 @@ TEST(NuDBFactory, configuration_parsing) for (auto const& format : kWhitespaceFormats) { SCOPED_TRACE("format='" + format + "'"); - beast::TempDir const tempDir; + TempDir const tempDir; auto const params = makeSection(tempDir.path(), format); test::CaptureSink sink(beast::Severity::Debug); beast::Journal const journal(sink); @@ -265,7 +265,7 @@ TEST(NuDBFactory, data_persistence) for (auto const& size : kBlockSizes) { SCOPED_TRACE("size=" + size); - beast::TempDir const tempDir; + TempDir const tempDir; auto const params = makeSection(tempDir.path(), size); DummyScheduler scheduler; beast::Journal const journal(TestSink::instance()); diff --git a/src/xrpld/app/main/GRPCServer.cpp b/src/xrpld/app/main/GRPCServer.cpp index fc4a9794bd..c1ea5e874b 100644 --- a/src/xrpld/app/main/GRPCServer.cpp +++ b/src/xrpld/app/main/GRPCServer.cpp @@ -49,6 +49,7 @@ #include #include #include +#include #include #include @@ -615,7 +616,7 @@ GRPCServerImpl::createServerCredentials() try { - boost::system::error_code ec; + std::error_code ec; grpc::SslServerCredentialsOptions sslOpts; grpc::SslServerCredentialsOptions::PemKeyCertPair keyCertPair; diff --git a/src/xrpld/app/misc/SHAMapStoreImp.cpp b/src/xrpld/app/misc/SHAMapStoreImp.cpp index e41837d206..9e3f1ac52b 100644 --- a/src/xrpld/app/misc/SHAMapStoreImp.cpp +++ b/src/xrpld/app/misc/SHAMapStoreImp.cpp @@ -6,6 +6,7 @@ #include #include +#include #include #include #include @@ -27,12 +28,10 @@ #include #include -#include -#include -#include #include #include +#include #include #include #include @@ -426,10 +425,10 @@ SHAMapStoreImp::dbPaths() if (boost::iequals(get(section, Keys::kType), "memory")) return; - boost::filesystem::path dbPath = get(section, Keys::kPath); - if (boost::filesystem::exists(dbPath)) + std::filesystem::path dbPath = get(section, Keys::kPath); + if (std::filesystem::exists(dbPath)) { - if (!boost::filesystem::is_directory(dbPath)) + if (!std::filesystem::is_directory(dbPath)) { journal_.error() << "node db path must be a directory. " << dbPath.string(); Throw("node db path must be a directory."); @@ -437,7 +436,7 @@ SHAMapStoreImp::dbPaths() } else { - boost::filesystem::create_directories(dbPath); + std::filesystem::create_directories(dbPath); } SavedState state = stateDb_.getState(); @@ -448,8 +447,8 @@ SHAMapStoreImp::dbPaths() return false; // Check if configured "path" matches stored directory path - using namespace boost::filesystem; - auto const stored{path(sPath)}; + using namespace std::filesystem; + auto const stored{std::filesystem::path(sPath)}; if (stored.parent_path() == dbPath) return false; @@ -467,9 +466,9 @@ SHAMapStoreImp::dbPaths() bool writableDbExists = false; bool archiveDbExists = false; - std::vector pathsToDelete; - for (boost::filesystem::directory_iterator it(dbPath); - it != boost::filesystem::directory_iterator(); + std::vector pathsToDelete; + for (std::filesystem::directory_iterator it(dbPath); + it != std::filesystem::directory_iterator(); ++it) { if (state.writableDb == it->path().string()) @@ -490,7 +489,7 @@ SHAMapStoreImp::dbPaths() (!archiveDbExists && !state.archiveDb.empty()) || (writableDbExists != archiveDbExists) || state.writableDb.empty() != state.archiveDb.empty()) { - boost::filesystem::path stateDbPathName = app_.config().legacy(Sections::kDatabasePath); + std::filesystem::path stateDbPathName = app_.config().legacy(Sections::kDatabasePath); stateDbPathName /= dbName_; stateDbPathName += "*"; @@ -512,15 +511,15 @@ SHAMapStoreImp::dbPaths() } // The necessary directories exist. Now, remove any others. - for (boost::filesystem::path const& p : pathsToDelete) - boost::filesystem::remove_all(p); + for (std::filesystem::path const& p : pathsToDelete) + std::filesystem::remove_all(p); } std::unique_ptr SHAMapStoreImp::makeBackendRotating(std::string path) { Section section{app_.config().section(Sections::kNodeDatabase)}; - boost::filesystem::path newPath; + std::filesystem::path newPath; if (!path.empty()) { @@ -528,10 +527,7 @@ SHAMapStoreImp::makeBackendRotating(std::string path) } else { - boost::filesystem::path p = get(section, Keys::kPath); - p /= dbPrefix_; - p += ".%%%%"; - newPath = boost::filesystem::unique_path(p); + newPath = uniqueRandomPath(get(section, Keys::kPath), dbPrefix_ + "."); } section.set(Keys::kPath, newPath.string()); diff --git a/src/xrpld/app/misc/ValidatorList.h b/src/xrpld/app/misc/ValidatorList.h index 3f9039eab8..abec6cf4e0 100644 --- a/src/xrpld/app/misc/ValidatorList.h +++ b/src/xrpld/app/misc/ValidatorList.h @@ -17,6 +17,7 @@ #include #include #include +#include #include #include #include @@ -238,7 +239,7 @@ class ValidatorList ManifestCache& validatorManifests_; ManifestCache& publisherManifests_; TimeKeeper& timeKeeper_; - boost::filesystem::path const dataPath_; + std::filesystem::path const dataPath_; beast::Journal const j_; std::shared_mutex mutable mutex_; using scoped_lock = std::scoped_lock; @@ -866,7 +867,7 @@ private: /** * Get the filename used for caching UNLs */ - boost::filesystem::path + std::filesystem::path getCacheFileName(scoped_lock const&, PublicKey const& pubKey) const; /** diff --git a/src/xrpld/app/misc/detail/ValidatorList.cpp b/src/xrpld/app/misc/detail/ValidatorList.cpp index e355cfacab..0ada8ed55f 100644 --- a/src/xrpld/app/misc/detail/ValidatorList.cpp +++ b/src/xrpld/app/misc/detail/ValidatorList.cpp @@ -29,12 +29,8 @@ #include #include -#include #include #include -#include -#include -#include #include @@ -43,6 +39,7 @@ #include #include #include +#include #include #include #include @@ -54,6 +51,7 @@ #include #include #include +#include #include #include @@ -288,7 +286,7 @@ ValidatorList::load( return true; } -boost::filesystem::path +std::filesystem::path ValidatorList::getCacheFileName(ValidatorList::scoped_lock const&, PublicKey const& pubKey) const { return dataPath_ / (kFilePrefix + strHex(pubKey)); @@ -372,9 +370,9 @@ ValidatorList::cacheValidatorFile(ValidatorList::scoped_lock const& lock, Public if (dataPath_.empty()) return; - boost::filesystem::path const filename = getCacheFileName(lock, pubKey); + std::filesystem::path const filename = getCacheFileName(lock, pubKey); - boost::system::error_code ec; + std::error_code ec; json::Value value = buildFileData(strHex(pubKey), publisherLists_.at(pubKey), j_); // xrpld should be the only process writing to this file, so @@ -1295,8 +1293,7 @@ std::vector ValidatorList::loadLists() { using namespace std::string_literals; - using namespace boost::filesystem; - using namespace boost::system::errc; + using namespace std::filesystem; std::scoped_lock const lock{mutex_}; @@ -1304,12 +1301,12 @@ ValidatorList::loadLists() sites.reserve(publisherLists_.size()); for (auto const& [pubKey, publisherCollection] : publisherLists_) { - boost::system::error_code ec; + std::error_code ec; if (publisherCollection.status == PublisherStatus::Available) continue; - boost::filesystem::path const filename = getCacheFileName(lock, pubKey); + std::filesystem::path const filename = getCacheFileName(lock, pubKey); auto const fullPath{canonical(filename, ec)}; if (ec) @@ -1320,7 +1317,7 @@ ValidatorList::loadLists() { // Treat an empty file as a missing file, because // nobody else is going to write it. - ec = make_error_code(no_such_file_or_directory); + ec = make_error_code(std::errc::no_such_file_or_directory); } if (ec) continue; diff --git a/src/xrpld/app/rdb/backend/detail/Node.cpp b/src/xrpld/app/rdb/backend/detail/Node.cpp index b2f14c71ea..ff57087ec5 100644 --- a/src/xrpld/app/rdb/backend/detail/Node.cpp +++ b/src/xrpld/app/rdb/backend/detail/Node.cpp @@ -40,7 +40,6 @@ #include #include -#include #include #include // IWYU pragma: keep #include @@ -58,6 +57,7 @@ #include #include #include +#include #include #include #include @@ -66,6 +66,7 @@ #include #include #include +#include #include #include #include @@ -1393,8 +1394,8 @@ getTransaction( bool dbHasSpace(soci::session& session, Config const& config, beast::Journal j) { - boost::filesystem::space_info const space = - boost::filesystem::space(config.legacy(Sections::kDatabasePath)); + std::filesystem::space_info const space = + std::filesystem::space(config.legacy(Sections::kDatabasePath)); if (space.available < megabytes(512)) { @@ -1405,9 +1406,9 @@ dbHasSpace(soci::session& session, Config const& config, beast::Journal j) if (config.useTxTables()) { DatabaseCon::Setup const dbSetup = setupDatabaseCon(config); - boost::filesystem::path const dbPath = dbSetup.dataDir / kTxDbName; - boost::system::error_code ec; - std::optional dbSize = boost::filesystem::file_size(dbPath, ec); + std::filesystem::path const dbPath = dbSetup.dataDir / kTxDbName; + std::error_code ec; + std::optional dbSize = std::filesystem::file_size(dbPath, ec); if (ec) { JLOG(j.error()) << "Error checking transaction db file size: " << ec.message(); diff --git a/src/xrpld/core/Config.h b/src/xrpld/core/Config.h index ac28b6e224..2dea8f3597 100644 --- a/src/xrpld/core/Config.h +++ b/src/xrpld/core/Config.h @@ -11,11 +11,10 @@ #include #include -#include // VFALCO FIX: This include should not be here - #include #include #include +#include #include #include #include @@ -97,17 +96,17 @@ public: /** * Returns the full path and filename of the debug log file. */ - [[nodiscard]] boost::filesystem::path + [[nodiscard]] std::filesystem::path getDebugLogFile() const; private: - boost::filesystem::path configFile_; + std::filesystem::path configFile_; public: - boost::filesystem::path configDir; + std::filesystem::path configDir; private: - boost::filesystem::path debugLogfile_; + std::filesystem::path debugLogfile_; void load(); diff --git a/src/xrpld/core/detail/Config.cpp b/src/xrpld/core/detail/Config.cpp index f263fb49ab..efe4ab1cc9 100644 --- a/src/xrpld/core/detail/Config.cpp +++ b/src/xrpld/core/detail/Config.cpp @@ -20,21 +20,20 @@ #include #include #include -#include -#include +#include #include #include #include #include // IWYU pragma: keep #include #include -#include #include #include #include #include #include +#include #include #include #include @@ -44,6 +43,7 @@ #include #include #include +#include #include #include #include @@ -312,13 +312,13 @@ Config::setup(std::string const& strConf, bool bQuiet, bool bSilent, bool bStand // directory, use the current working directory as the // config directory and that with "db" as the data // directory. - boost::filesystem::path dataDir; + std::filesystem::path dataDir; if (!strConf.empty()) { // --conf= : everything is relative that file. configFile_ = strConf; - configDir = boost::filesystem::absolute(configFile_); + configDir = std::filesystem::absolute(configFile_); configDir.remove_filename(); dataDir = configDir / kDatabaseDirName; } @@ -329,13 +329,13 @@ Config::setup(std::string const& strConf, bool bQuiet, bool bSilent, bool bStand // Check if either of the config files exist in the current working // directory, in which case the databases will be stored in a // subdirectory. - configDir = boost::filesystem::current_path(); + configDir = std::filesystem::current_path(); dataDir = configDir / kDatabaseDirName; configFile_ = configDir / kConfigFileName; - if (boost::filesystem::exists(configFile_)) + if (std::filesystem::exists(configFile_)) break; configFile_ = configDir / kConfigLegacyName; - if (boost::filesystem::exists(configFile_)) + if (std::filesystem::exists(configFile_)) break; // Check if the home directory is set, and optionally the XDG config @@ -362,10 +362,10 @@ Config::setup(std::string const& strConf, bool bQuiet, bool bSilent, bool bStand dataDir = strXdgDataHome + "/" + systemName(); configDir = strXdgConfigHome + "/" + systemName(); configFile_ = configDir / kConfigFileName; - if (boost::filesystem::exists(configFile_)) + if (std::filesystem::exists(configFile_)) break; configFile_ = configDir / kConfigLegacyName; - if (boost::filesystem::exists(configFile_)) + if (std::filesystem::exists(configFile_)) break; } @@ -373,7 +373,7 @@ Config::setup(std::string const& strConf, bool bQuiet, bool bSilent, bool bStand dataDir = "/var/lib/" + systemName(); configDir = "/etc/" + systemName(); configFile_ = configDir / kConfigFileName; - if (boost::filesystem::exists(configFile_)) + if (std::filesystem::exists(configFile_)) break; configFile_ = configDir / kConfigLegacyName; } while (false); @@ -386,7 +386,7 @@ Config::setup(std::string const& strConf, bool bQuiet, bool bSilent, bool bStand std::string const dbPath(legacy(Sections::kDatabasePath)); if (!dbPath.empty()) { - dataDir = boost::filesystem::path(dbPath); + dataDir = std::filesystem::path(dbPath); } else if (runStandalone_) { @@ -396,13 +396,13 @@ Config::setup(std::string const& strConf, bool bQuiet, bool bSilent, bool bStand if (!dataDir.empty()) { - boost::system::error_code ec; - boost::filesystem::create_directories(dataDir, ec); + std::error_code ec; + std::filesystem::create_directories(dataDir, ec); if (ec) Throw(boost::str(boost::format("Can not create %s") % dataDir)); - legacy(Sections::kDatabasePath, boost::filesystem::absolute(dataDir).string()); + legacy(Sections::kDatabasePath, std::filesystem::absolute(dataDir).string()); } HTTPClient::initializeSSLContext(this->sslVerifyDir, this->sslVerifyFile, this->sslVerify, j_); @@ -454,7 +454,7 @@ Config::load() if (!quiet_) std::cerr << "Loading: " << configFile_ << "\n"; - boost::system::error_code ec; + std::error_code ec; auto const fileContents = getFileContents(ec, configFile_); if (ec) @@ -507,8 +507,8 @@ Config::loadFromString(std::string const& fileContents) std::string dbPath; if (getSingleSection(secConfig, Sections::kDatabasePath, dbPath, j_)) { - boost::filesystem::path const p(dbPath); - legacy(Sections::kDatabasePath, boost::filesystem::absolute(p).string()); + std::filesystem::path const p(dbPath); + legacy(Sections::kDatabasePath, std::filesystem::absolute(p).string()); } } @@ -1010,7 +1010,7 @@ Config::loadFromString(std::string const& fileContents) // If no path was specified, then look for validators.txt // in the same directory as the config file, but don't complain // if we can't find it. - boost::filesystem::path validatorsFile; + std::filesystem::path validatorsFile; if (getSingleSection(secConfig, Sections::kValidatorsFile, strTemp, j_)) { @@ -1025,7 +1025,7 @@ Config::loadFromString(std::string const& fileContents) if (!validatorsFile.is_absolute() && !configDir.empty()) validatorsFile = configDir / validatorsFile; - if (!boost::filesystem::exists(validatorsFile)) + if (!std::filesystem::exists(validatorsFile)) { Throw( std::string("The file specified in [") + Sections::kValidatorsFile + @@ -1034,8 +1034,8 @@ Config::loadFromString(std::string const& fileContents) validatorsFile.string()); } else if ( - !boost::filesystem::is_regular_file(validatorsFile) && - !boost::filesystem::is_symlink(validatorsFile)) + !std::filesystem::is_regular_file(validatorsFile) && + !std::filesystem::is_symlink(validatorsFile)) { Throw( std::string("Invalid file specified in [") + Sections::kValidatorsFile + @@ -1048,20 +1048,20 @@ Config::loadFromString(std::string const& fileContents) if (!validatorsFile.empty()) { - if (!boost::filesystem::exists(validatorsFile) || - (!boost::filesystem::is_regular_file(validatorsFile) && - !boost::filesystem::is_symlink(validatorsFile))) + if (!std::filesystem::exists(validatorsFile) || + (!std::filesystem::is_regular_file(validatorsFile) && + !std::filesystem::is_symlink(validatorsFile))) { validatorsFile.clear(); } } } - if (!validatorsFile.empty() && boost::filesystem::exists(validatorsFile) && - (boost::filesystem::is_regular_file(validatorsFile) || - boost::filesystem::is_symlink(validatorsFile))) + if (!validatorsFile.empty() && std::filesystem::exists(validatorsFile) && + (std::filesystem::is_regular_file(validatorsFile) || + std::filesystem::is_symlink(validatorsFile))) { - boost::system::error_code ec; + std::error_code ec; auto const data = getFileContents(ec, validatorsFile); if (ec) { @@ -1194,7 +1194,7 @@ Config::loadFromString(std::string const& fileContents) } } -boost::filesystem::path +std::filesystem::path Config::getDebugLogFile() const { auto logFile = debugLogfile_; @@ -1203,17 +1203,17 @@ Config::getDebugLogFile() const { // Unless an absolute path for the log file is specified, the // path is relative to the config file directory. - logFile = boost::filesystem::absolute(logFile, configDir); + logFile = std::filesystem::absolute(configDir / logFile); } if (!logFile.empty()) { auto logDir = logFile.parent_path(); - if (!boost::filesystem::is_directory(logDir)) + if (!std::filesystem::is_directory(logDir)) { - boost::system::error_code ec; - boost::filesystem::create_directories(logDir, ec); + std::error_code ec; + std::filesystem::create_directories(logDir, ec); // If we fail, we warn but continue so that the calling code can // decide how to handle this situation. diff --git a/src/xrpld/perflog/detail/PerfLogImp.cpp b/src/xrpld/perflog/detail/PerfLogImp.cpp index 3aa7e38ea2..2777e0dcdb 100644 --- a/src/xrpld/perflog/detail/PerfLogImp.cpp +++ b/src/xrpld/perflog/detail/PerfLogImp.cpp @@ -17,11 +17,9 @@ #include #include -#include -#include - #include #include +#include #include #include #include @@ -29,6 +27,7 @@ #include #include #include +#include #include #include #include @@ -220,10 +219,10 @@ PerfLogImp::openLog() logFile_.close(); auto logDir = setup_.perfLog.parent_path(); - if (!boost::filesystem::is_directory(logDir)) + if (!std::filesystem::is_directory(logDir)) { - boost::system::error_code ec; - boost::filesystem::create_directories(logDir, ec); + std::error_code ec; + std::filesystem::create_directories(logDir, ec); if (ec) { JLOG(j_.fatal()) << "Unable to create performance log " @@ -478,17 +477,17 @@ PerfLogImp::stop() //----------------------------------------------------------------------------- PerfLog::Setup -setupPerfLog(Section const& section, boost::filesystem::path const& configDir) +setupPerfLog(Section const& section, std::filesystem::path const& configDir) { PerfLog::Setup setup; std::string perfLog; set(perfLog, "perf_log", section); if (!perfLog.empty()) { - setup.perfLog = boost::filesystem::path(perfLog); + setup.perfLog = std::filesystem::path(perfLog); if (setup.perfLog.is_relative()) { - setup.perfLog = boost::filesystem::absolute(setup.perfLog, configDir); + setup.perfLog = std::filesystem::absolute(configDir / setup.perfLog); } } From 1281c7a222f34eeded150323d45061284df76077 Mon Sep 17 00:00:00 2001 From: Timur Yalymov <36795566+tyalymov@users.noreply.github.com> Date: Wed, 12 Aug 2026 13:41:30 +0000 Subject: [PATCH 24/28] refactor: Drop unnecessary associateAsset calls from loan delete paths (#7986) Co-authored-by: Cursor --- src/libxrpl/tx/transactors/lending/LoanBrokerDelete.cpp | 3 --- src/libxrpl/tx/transactors/lending/LoanDelete.cpp | 3 --- 2 files changed, 6 deletions(-) diff --git a/src/libxrpl/tx/transactors/lending/LoanBrokerDelete.cpp b/src/libxrpl/tx/transactors/lending/LoanBrokerDelete.cpp index b36977d225..433d77806a 100644 --- a/src/libxrpl/tx/transactors/lending/LoanBrokerDelete.cpp +++ b/src/libxrpl/tx/transactors/lending/LoanBrokerDelete.cpp @@ -12,7 +12,6 @@ #include #include #include -#include #include #include #include @@ -198,8 +197,6 @@ LoanBrokerDelete::doApply() view().erase(broker); - associateAsset(*broker, vaultAsset); - return tesSUCCESS; } diff --git a/src/libxrpl/tx/transactors/lending/LoanDelete.cpp b/src/libxrpl/tx/transactors/lending/LoanDelete.cpp index 1a77489b4b..bc8e974d10 100644 --- a/src/libxrpl/tx/transactors/lending/LoanDelete.cpp +++ b/src/libxrpl/tx/transactors/lending/LoanDelete.cpp @@ -130,9 +130,6 @@ LoanDelete::doApply() // Decrement the borrower's owner count decreaseOwnerCountForObject(view, borrowerSle, loanSle, 1, j_); - // These associations shouldn't do anything, but do them just to be safe - associateAsset(*loanSle, vaultAsset); - associateAsset(*brokerSle, vaultAsset); associateAsset(*vaultSle, vaultAsset); return tesSUCCESS; From af36890c1113955894dc441721e928ab3072434a Mon Sep 17 00:00:00 2001 From: Timur Yalymov <36795566+tyalymov@users.noreply.github.com> Date: Wed, 12 Aug 2026 13:42:02 +0000 Subject: [PATCH 25/28] test: Verify private-vault DEX permissions survive domain loss (#7937) --- src/test/app/Vault_test.cpp | 188 ++++++++++++++++++++++++++++++++++++ 1 file changed, 188 insertions(+) diff --git a/src/test/app/Vault_test.cpp b/src/test/app/Vault_test.cpp index 70527f570d..6b6c4eb875 100644 --- a/src/test/app/Vault_test.cpp +++ b/src/test/app/Vault_test.cpp @@ -3017,6 +3017,192 @@ class Vault_test : public beast::unit_test::Suite } } + void + testDomainLossAfterAcquisition() + { + using namespace test::jtx; + + testcase("private vault share transfer after depositor loses domain"); + + // The "Private Vault - Access Control Rules" spec requires that a holder who + // loses Layer 2 (Permissioned Domain membership) after acquiring shares be + // blocked from sending them onward, by P2P transfer or DEX offer, the same + // way a brand-new never-authorized holder is blocked. Only withdrawal to + // self is meant to stay open. + // + // For a domain-gated share MPToken, requireAuth()'s escape hatch for + // holders who already have an MPToken (MPTokenHelpers.cpp) only applies to + // the classic explicit-issuer-authorization flag, which + // enforceMPTokenAuthorization documents as "meaningless" for + // domain-authorized holders and never sets. So a stale MPToken does not + // carry authorization forward once the account's domain credential is + // gone, and both actions below are correctly blocked. + + Env env{*this, testableAmendments()}; + Account const issuer{"issuer"}; + Account const owner{"owner"}; + Account const depositor{"depositor"}; + Account const bob{"bob"}; + Account const pdOwner{"pdOwner"}; + Account const credIssuer{"credIssuer"}; + std::string const credType = "credential"; + Vault const vault{env}; + env.fund(XRP(1000), issuer, owner, depositor, bob, pdOwner, credIssuer); + env.close(); + + PrettyAsset const asset = issuer["IOU"]; + env.trust(asset(1000), owner); + env(pay(issuer, owner, asset(500))); + env.trust(asset(1000), depositor); + env(pay(issuer, depositor, asset(500))); + env.trust(asset(1000), bob); + env(pay(issuer, bob, asset(500))); + env.close(); + + // Transferable shares (no tfVaultShareNonTransferable): sections 3.3/3.4 of + // the spec (DEX trading / P2P transfer) only apply to transferable shares. + auto [tx, keylet] = vault.create({.owner = owner, .asset = asset, .flags = tfVaultPrivate}); + env(tx); + env.close(); + + pdomain::Credentials const credentials{{.issuer = credIssuer, .credType = credType}}; + env(pdomain::setTx(pdOwner, credentials)); + auto const domainId = [&]() { + auto tx = env.tx()->getJson(JsonOptions::Values::None); + return pdomain::getNewDomain(env.meta()); + }(); + { + auto domainTx = vault.set({.owner = owner, .id = keylet.key}); + domainTx[sfDomainID] = to_string(domainId); + env(domainTx); + env.close(); + } + + // Both depositor and bob acquire domain membership and deposit, so each + // ends up with an authorized share MPToken. + env(credentials::create(depositor, credIssuer, credType)); + env(credentials::accept(depositor, credIssuer, credType)); + env(credentials::create(bob, credIssuer, credType)); + env(credentials::accept(bob, credIssuer, credType)); + env.close(); + + env(vault.deposit({.depositor = depositor, .id = keylet.key, .amount = asset(100)})); + env(vault.deposit({.depositor = bob, .id = keylet.key, .amount = asset(100)})); + env.close(); + + auto const shares = [&env, keylet = keylet, this]() -> PrettyAsset { + auto const sle = env.le(keylet); + BEAST_EXPECT(sle != nullptr); + return MPTIssue(sle->at(sfShareMPTID)); + }(); + + // Depositor loses Layer 2: their Permissioned Domain credential is revoked. + auto const credKeylet = credentials::keylet(depositor, credIssuer, credType); + env(credentials::deleteCred(credIssuer, depositor, credIssuer, credType)); + env.close(); + BEAST_EXPECT(env.le(credKeylet) == nullptr); + + // Sanity check, mirrors testWithDomainCheck's "not authorized yet" case: a + // brand-new depositor with no MPToken yet is still correctly blocked. The + // gap below is specific to holders who already hold shares. + { + Account const charlie{"charlie"}; + env.fund(XRP(1000), charlie); + env.close(); + auto depTx = + vault.deposit({.depositor = charlie, .id = keylet.key, .amount = asset(1)}); + env(depTx, Ter{tecNO_AUTH}); + } + + // P2P transfer: spec section 3.4 requires this blocked once Layer 2 is + // lost, and it is. + env(pay(depositor, bob, shares(1)), Ter{tecNO_AUTH}); + env.close(); + + // DEX/CLOB: spec section 3.3 requires the seller leg blocked the same way. + // The offer can't even be created: preclaim treats the seller as + // unfunded once their share balance reads as zero for auth purposes. + env(offer(depositor, XRP(1), shares(1)), Ter{tecUNFUNDED_OFFER}); + env.close(); + BEAST_EXPECT(expectOffers(env, depositor, 0)); + } + + void + testDomainCheckBuyerSideOffer() + { + using namespace test::jtx; + + testcase("private vault share purchase via DEX requires buyer domain membership"); + + // The "Private Vault - Access Control Rules" spec requires the buyer leg + // of a DEX trade in private-vault shares to hold Layer 1 and Layer 2 as + // well, not just the seller. + + Env env{*this, testableAmendments()}; + Account const issuer{"issuer"}; + Account const owner{"owner"}; + Account const bob{"bob"}; + Account const charlie{"charlie"}; + Account const pdOwner{"pdOwner"}; + Account const credIssuer{"credIssuer"}; + std::string const credType = "credential"; + Vault const vault{env}; + env.fund(XRP(1000), issuer, owner, bob, charlie, pdOwner, credIssuer); + env.close(); + + PrettyAsset const asset = issuer["IOU"]; + env.trust(asset(1000), owner); + env(pay(issuer, owner, asset(500))); + env.trust(asset(1000), bob); + env(pay(issuer, bob, asset(500))); + env.close(); + + auto [tx, keylet] = vault.create({.owner = owner, .asset = asset, .flags = tfVaultPrivate}); + env(tx); + env.close(); + + pdomain::Credentials const credentials{{.issuer = credIssuer, .credType = credType}}; + env(pdomain::setTx(pdOwner, credentials)); + auto const domainId = [&]() { + auto tx = env.tx()->getJson(JsonOptions::Values::None); + return pdomain::getNewDomain(env.meta()); + }(); + { + auto domainTx = vault.set({.owner = owner, .id = keylet.key}); + domainTx[sfDomainID] = to_string(domainId); + env(domainTx); + env.close(); + } + + // Only bob joins the domain and deposits; charlie never does. + env(credentials::create(bob, credIssuer, credType)); + env(credentials::accept(bob, credIssuer, credType)); + env.close(); + env(vault.deposit({.depositor = bob, .id = keylet.key, .amount = asset(100)})); + env.close(); + + auto const shares = [&env, keylet = keylet, this]() -> PrettyAsset { + auto const sle = env.le(keylet); + BEAST_EXPECT(sle != nullptr); + return MPTIssue(sle->at(sfShareMPTID)); + }(); + + // Bob (domain member, holds shares) rests a sell offer. + env(offer(bob, XRP(1), shares(1))); + env.close(); + BEAST_EXPECT(expectOffers(env, bob, 1)); + + // Charlie never held the domain credential. Buying shares via a + // crossing offer must be blocked the same way a direct MPTokenAuthorize + // + pay attempt already is (see testWithDomainChecXRP's "cannot pay + // shares to 3rd party"): checkAcceptAsset() rejects the offer outright + // in preclaim, before any funding check is even reached. + env(offer(charlie, shares(1), XRP(1)), Ter{tecNO_AUTH}); + env.close(); + BEAST_EXPECT(expectOffers(env, bob, 1)); + BEAST_EXPECT(expectOffers(env, charlie, 0)); + } + void testWithDomainChecXRP() { @@ -8396,6 +8582,8 @@ public: testWithMPT(); testWithIOU(); testWithDomainCheck(); + testDomainLossAfterAcquisition(); + testDomainCheckBuyerSideOffer(); testWithDomainChecXRP(); testNonTransferableShares(); testFailedPseudoAccount(); From 91360c5126ef4456dbca860d7a69f9e75b546c0e Mon Sep 17 00:00:00 2001 From: Vito Tumas <5780819+Tapanito@users.noreply.github.com> Date: Wed, 12 Aug 2026 13:42:55 +0000 Subject: [PATCH 26/28] test: Fix LoanBatch broker cover rates and schedule overflow (#7967) --- src/test/app/lending/LoanMisc_test.cpp | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/test/app/lending/LoanMisc_test.cpp b/src/test/app/lending/LoanMisc_test.cpp index 2cb4f38ecf..c5a7d54311 100644 --- a/src/test/app/lending/LoanMisc_test.cpp +++ b/src/test/app/lending/LoanMisc_test.cpp @@ -473,14 +473,21 @@ protected: TenthBips16 const managementFeeRate{managementFeeRateDist_(engine_)}; auto const serviceFee = serviceFeeDist_(engine_); TenthBips32 interest{interestRateDist_(engine_)}; - auto const payTotal = paymentTotalDist_(engine_); + auto payTotal = paymentTotalDist_(engine_); auto const payInterval = paymentIntervalDist_(engine_); + // The end of the last payment's grace period must fit in a 32-bit + // ripple-epoch timestamp, or LoanSet fails with tecKILLED. Cap the + // schedule well below that horizon (2e9 seconds is roughly 63 years, + // leaving ample headroom over the ledger start date). + constexpr std::uint32_t kMaxScheduleSeconds = 2'000'000'000; + payTotal = std::min(payTotal, static_cast(kMaxScheduleSeconds / payInterval)); BrokerParameters const brokerParams{ .vaultDeposit = principalRequest * 10, .debtMax = 0, .coverRateMin = TenthBips32{0}, - .managementFeeRate = managementFeeRate}; + .managementFeeRate = managementFeeRate, + .coverRateLiquidation = TenthBips32{0}}; LoanParameters const loanParams{ .account = lender, .counter = borrower, From 946827b9bd554eab36645c0bccf65bc46a22a986 Mon Sep 17 00:00:00 2001 From: Ayaz Salikhov Date: Wed, 12 Aug 2026 14:03:37 +0000 Subject: [PATCH 27/28] build: Respect lld linker if it gets auto-selected (#8011) --- cmake/XrplCompiler.cmake | 66 ++++++++++++++++++++++++---------------- 1 file changed, 40 insertions(+), 26 deletions(-) diff --git a/cmake/XrplCompiler.cmake b/cmake/XrplCompiler.cmake index 21566add01..2b46739d97 100644 --- a/cmake/XrplCompiler.cmake +++ b/cmake/XrplCompiler.cmake @@ -188,32 +188,6 @@ else() endif() endif() -# Linker warnings are errors where we control the toolchain and the dependencies: CI and the Nix dev shell. -# On non-Nix macOS we suppress the deployment target warning: an old Conan profile may not pin os.version. -if(is_macos OR is_linux) - if(is_ci OR is_nix_compiler) - if(is_macos) - set(fatal_warnings_flag "-Wl,-fatal_warnings") - else() - set(fatal_warnings_flag "-Wl,--fatal-warnings") - endif() - message( - STATUS - "Treating all linker warnings as errors (${fatal_warnings_flag})" - ) - target_link_options(common INTERFACE "${fatal_warnings_flag}") - unset(fatal_warnings_flag) - elseif(is_macos) - set(silence_flag "-Wl,-deployment_target_mismatches,suppress") - message( - STATUS - "Silencing macOS deployment target mismatch warnings (${silence_flag})" - ) - target_link_options(common INTERFACE "${silence_flag}") - unset(silence_flag) - endif() -endif() - # Antithesis instrumentation will only be built and deployed using machines running Linux. if(voidstar) if(NOT CMAKE_BUILD_TYPE STREQUAL "Debug") @@ -292,10 +266,50 @@ elseif(use_lld) ) if("${LD_VERSION}" MATCHES "LLD") target_link_libraries(common INTERFACE -fuse-ld=lld) + # remembered for the linker flag probe below + set(fuse_ld_flag "-fuse-ld=lld") endif() unset(LD_VERSION) endif() +# Linker warnings are errors where we control the toolchain and the dependencies: CI and the Nix dev shell. +# On non-Nix macOS we suppress the deployment target warning: an old Conan profile may not pin os.version. +# Only the new Apple linker understands the flag, so probe the actual linker (lld may be selected above). +if(is_macos OR is_linux) + if(is_ci OR is_nix_compiler) + if(is_macos) + set(fatal_warnings_flag "-Wl,-fatal_warnings") + else() + set(fatal_warnings_flag "-Wl,--fatal-warnings") + endif() + message( + STATUS + "Treating all linker warnings as errors (${fatal_warnings_flag})" + ) + target_link_options(common INTERFACE "${fatal_warnings_flag}") + unset(fatal_warnings_flag) + elseif(is_macos) + set(silence_flag "-Wl,-deployment_target_mismatches,suppress") + set(probe_flags ${fuse_ld_flag} "${silence_flag}") + include(CheckLinkerFlag) + check_linker_flag( + CXX + "${probe_flags}" + have_deployment_target_mismatches + ) + if(have_deployment_target_mismatches) + message( + STATUS + "Silencing macOS deployment target mismatch warnings (${silence_flag})" + ) + target_link_options(common INTERFACE "${silence_flag}") + endif() + unset(probe_flags) + unset(silence_flag) + endif() +endif() +unset(fuse_ld_flag) + if(assert) foreach(var_ CMAKE_C_FLAGS_RELEASE CMAKE_CXX_FLAGS_RELEASE) string(REGEX REPLACE "[-/]DNDEBUG" "" ${var_} "${${var_}}") From 8e9b1791c5eed272e28232b953fda6ac9500a2b3 Mon Sep 17 00:00:00 2001 From: Jingchen Date: Wed, 12 Aug 2026 17:07:43 +0000 Subject: [PATCH 28/28] feat: Add a new closed ended vault to extend SAV (#7921) Co-authored-by: Vito Tumas <5780819+Tapanito@users.noreply.github.com> --- include/xrpl/ledger/View.h | 12 +- include/xrpl/ledger/helpers/VaultHelpers.h | 81 ++ include/xrpl/protocol/Protocol.h | 31 + .../xrpl/protocol/detail/ledger_entries.macro | 3 + include/xrpl/protocol/detail/sfields.macro | 3 + .../xrpl/protocol/detail/transactions.macro | 3 + .../protocol_autogen/ledger_entries/Vault.h | 105 ++ .../transactions/VaultCreate.h | 111 ++ include/xrpl/tx/invariants/LoanInvariant.h | 2 + include/xrpl/tx/invariants/VaultInvariant.h | 24 + src/libxrpl/ledger/View.cpp | 12 +- src/libxrpl/ledger/helpers/VaultHelpers.cpp | 72 ++ src/libxrpl/tx/invariants/InvariantCheck.cpp | 60 +- src/libxrpl/tx/invariants/LoanInvariant.cpp | 36 +- src/libxrpl/tx/invariants/VaultInvariant.cpp | 104 ++ .../tx/transactors/lending/LoanSet.cpp | 32 +- .../tx/transactors/vault/VaultCreate.cpp | 42 + .../tx/transactors/vault/VaultDeposit.cpp | 12 + .../tx/transactors/vault/VaultWithdraw.cpp | 10 + src/test/app/Invariants_test.cpp | 358 +++++- src/test/app/Vault_test.cpp | 1112 +++++++++++++++++ src/test/app/lending/LoanSet_test.cpp | 126 ++ src/test/app/lending/LoanTestBase.h | 56 +- src/test/app/lending/LoanValidation_test.cpp | 13 +- src/test/jtx/impl/vault.cpp | 6 + src/test/jtx/vault.h | 6 + .../ledger_entries/VaultTests.cpp | 81 ++ .../transactions/VaultCreateTests.cpp | 63 + 28 files changed, 2521 insertions(+), 55 deletions(-) diff --git a/include/xrpl/ledger/View.h b/include/xrpl/ledger/View.h index 768e518008..e8b4a932d0 100644 --- a/include/xrpl/ledger/View.h +++ b/include/xrpl/ledger/View.h @@ -35,6 +35,11 @@ enum class SkipEntry : bool { No = false, Yes }; // //------------------------------------------------------------------------------ +/** + * Whether an expiration check should be inclusive or exclusive. + */ +enum class ExpiryComparison { Inclusive, Exclusive }; + /** * Determines whether the given expiration time has passed. * @@ -54,11 +59,16 @@ enum class SkipEntry : bool { No = false, Yes }; * * @param view The ledger whose parent time is used as the clock. * @param exp The optional expiration time we want to check. + * @param comparison Whether the boundary is inclusive (`now >= exp`, the + * default) or exclusive (`now > exp`). * * @return `true` if `exp` is in the past; `false` otherwise. */ [[nodiscard]] bool -hasExpired(ReadView const& view, std::optional const& exp); +hasExpired( + ReadView const& view, + std::optional const& exp, + ExpiryComparison comparison = ExpiryComparison::Inclusive); // Note, depth parameter is used to limit the recursion depth [[nodiscard]] bool diff --git a/include/xrpl/ledger/helpers/VaultHelpers.h b/include/xrpl/ledger/helpers/VaultHelpers.h index 5681cc57e8..acbf2c3ac0 100644 --- a/include/xrpl/ledger/helpers/VaultHelpers.h +++ b/include/xrpl/ledger/helpers/VaultHelpers.h @@ -6,10 +6,13 @@ #include #include +#include #include namespace xrpl { +class STTx; + /** * From the perspective of a vault, return the number of shares to give * depositor when they offer a fixed amount of assets. Note, since shares are @@ -123,4 +126,82 @@ isSoleShareholder(ReadView const& view, AccountID const& account, SLE::const_ref [[nodiscard]] VaultVersion getVaultVersion(SLE::const_ref vault); +/** + * Resolves the VaultKind of a vault SLE. Returns VaultKind::ClosedEnded when + * sfVaultKind is present and equal to that value; anything else (including an + * absent field or an unrecognised value) is treated as VaultKind::OpenEnded. + * + * @param vault The vault SLE. + */ +[[nodiscard]] VaultKind +getVaultKind(SLE::const_ref vault); + +/** + * Reads sfVaultKind from a transaction. An absent field resolves to + * VaultKind::OpenEnded (matching the on-ledger default); any unrecognised + * value is also treated as VaultKind::OpenEnded, mirroring the SLE overload. + * Callers that need to reject out-of-range values (e.g. preflight) should + * gate on isValidVaultKind() first. + * + * @param tx The transaction. + */ +[[nodiscard]] VaultKind +getVaultKind(STTx const& tx); + +/** + * Returns true iff sfVaultKind is either absent from @p tx or is present and + * equal to a recognised VaultKind enumerator. Intended for use in preflight + * to reject malformed transactions before decoding with getVaultKind(). + * + * @param tx The transaction. + */ +[[nodiscard]] bool +isValidVaultKind(STTx const& tx); + +/** + * Returns true iff the (SubscriptionDate, RedemptionDate) gap of a + * closed-ended vault satisfies + * kMinInvestmentPeriod <= (red - sub) < kMaxInvestmentPeriod. The arithmetic + * is performed in std::int64_t so that @p sub near UINT32_MAX does not + * overflow. Shared by VaultCreate::preflight and the ValidVault invariant. + * + * @param sub The value of sfSubscriptionDate. + * @param red The value of sfRedemptionDate. + */ +[[nodiscard]] bool +isValidClosedEndedGap(std::uint32_t sub, std::uint32_t red); + +/** + * Returns the current lifecycle phase of a vault. Open-ended + * vaults are always NoPhase. For closed-ended vaults the phase is derived + * from the parent ledger close time and the vault's immutable + * SubscriptionDate and RedemptionDate. + * + * @param view The ledger view whose parent close time is used as the clock. + * @param vault The vault SLE. + */ +[[nodiscard]] VaultPhase +getVaultPhase(ReadView const& view, SLE::const_ref vault); + +/** + * Raw-fields overload of getVaultPhase. Derives the phase from an already + * decomposed vault snapshot: an absent or non-ClosedEnded @p vaultKind + * resolves to VaultPhase::NoPhase; otherwise the phase is computed from + * @p subscriptionDate and @p redemptionDate against the view's parent + * close time using the same boundary semantics as the SLE overload + * (Subscription is inclusive of now == SubscriptionDate; Investment starts + * strictly after). + * + * @param view The ledger view whose parent close time is used as the clock. + * @param vaultKind The value of sfVaultKind, or nullopt if absent. + * @param subscriptionDate The value of sfSubscriptionDate, or nullopt if absent. + * @param redemptionDate The value of sfRedemptionDate, or nullopt if absent. + */ +[[nodiscard]] VaultPhase +getVaultPhase( + ReadView const& view, + std::optional vaultKind, + std::optional subscriptionDate, + std::optional redemptionDate); + } // namespace xrpl diff --git a/include/xrpl/protocol/Protocol.h b/include/xrpl/protocol/Protocol.h index 567f66d339..345baef853 100644 --- a/include/xrpl/protocol/Protocol.h +++ b/include/xrpl/protocol/Protocol.h @@ -9,6 +9,7 @@ #include #include +#include #include #include @@ -327,6 +328,36 @@ enum class VaultVersion : uint8_t { CashBasis, }; +/** + * Vault kind. Distinguishes closed-ended vaults from the default open-ended + * kind. Persisted as sfVaultKind (UINT8); absent means OpenEnded. + */ +enum class VaultKind : std::uint8_t { + OpenEnded = 0, + ClosedEnded = 1, +}; + +/** + * Lifecycle phase of a vault. Open-ended vaults are always NoPhase; the other + * three values are the phases of a closed-ended vault. + */ +enum class VaultPhase : std::uint8_t { + NoPhase = 0, + Subscription, + Investment, + Redemption, +}; + +/** + * Bounds on the length of a closed-ended vault's Investment phase + * (RedemptionDate - SubscriptionDate). At vault creation the gap must satisfy + * kMinInvestmentPeriod <= gap < kMaxInvestmentPeriod. + */ +constexpr std::uint32_t kMinInvestmentPeriod = + std::chrono::seconds{std::chrono::minutes{1}}.count(); +// This is 946708560 seconds which 30 x 365.2425 days (the average length of a Gregorian year). +constexpr std::uint32_t kMaxInvestmentPeriod = std::chrono::seconds{std::chrono::years{30}}.count(); + /** * Maximum recursion depth for vault shares being put as an asset inside * another vault; counted from 0 diff --git a/include/xrpl/protocol/detail/ledger_entries.macro b/include/xrpl/protocol/detail/ledger_entries.macro index ffcd025f01..f166473d7f 100644 --- a/include/xrpl/protocol/detail/ledger_entries.macro +++ b/include/xrpl/protocol/detail/ledger_entries.macro @@ -506,6 +506,9 @@ LEDGER_ENTRY(ltVAULT, 0x0084, Vault, vault, ({ {sfWithdrawalPolicy, SoeRequired}, {sfScale, SoeDefault}, {sfLEVersion, SoeDefault}, + {sfVaultKind, SoeDefault}, + {sfSubscriptionDate, SoeOptional}, + {sfRedemptionDate, SoeOptional}, // no SharesTotal ever (use MPTIssuance.sfOutstandingAmount) // no PermissionedDomainID ever (use MPTIssuance.sfDomainID) })) diff --git a/include/xrpl/protocol/detail/sfields.macro b/include/xrpl/protocol/detail/sfields.macro index c323e3a496..ec05804253 100644 --- a/include/xrpl/protocol/detail/sfields.macro +++ b/include/xrpl/protocol/detail/sfields.macro @@ -27,6 +27,7 @@ TYPED_SFIELD(sfUNLModifyDisabling, UINT8, 17) TYPED_SFIELD(sfWasLockingChainSend, UINT8, 19) TYPED_SFIELD(sfWithdrawalPolicy, UINT8, 20) TYPED_SFIELD(sfContractResult, UINT8, 21) +TYPED_SFIELD(sfVaultKind, UINT8, 22) // 16-bit integers (common) TYPED_SFIELD(sfLedgerEntryType, UINT16, 1, SField::kSmdNever) @@ -116,6 +117,8 @@ TYPED_SFIELD(sfSponsoringOwnerCount, UINT32, 71) TYPED_SFIELD(sfSponsoringAccountCount, UINT32, 72) TYPED_SFIELD(sfRemainingOwnerCount, UINT32, 73) TYPED_SFIELD(sfSponsorFlags, UINT32, 74) +TYPED_SFIELD(sfSubscriptionDate, UINT32, 75) +TYPED_SFIELD(sfRedemptionDate, UINT32, 76) // 64-bit integers (common) TYPED_SFIELD(sfIndexNext, UINT64, 1) diff --git a/include/xrpl/protocol/detail/transactions.macro b/include/xrpl/protocol/detail/transactions.macro index 1f9603dbae..f8676d3b63 100644 --- a/include/xrpl/protocol/detail/transactions.macro +++ b/include/xrpl/protocol/detail/transactions.macro @@ -862,6 +862,9 @@ TRANSACTION(ttVAULT_CREATE, 65, VaultCreate, {sfWithdrawalPolicy, SoeOptional}, {sfData, SoeOptional}, {sfScale, SoeOptional}, + {sfVaultKind, SoeOptional}, + {sfSubscriptionDate, SoeOptional}, + {sfRedemptionDate, SoeOptional}, })) /** This transaction updates a single asset vault. */ diff --git a/include/xrpl/protocol_autogen/ledger_entries/Vault.h b/include/xrpl/protocol_autogen/ledger_entries/Vault.h index a6ab54cb0a..389ffb4c46 100644 --- a/include/xrpl/protocol_autogen/ledger_entries/Vault.h +++ b/include/xrpl/protocol_autogen/ledger_entries/Vault.h @@ -311,6 +311,78 @@ public: { return this->sle_->isFieldPresent(sfLEVersion); } + + /** + * @brief Get sfVaultKind (SoeDefault) + * @return The field value, or std::nullopt if not present. + */ + [[nodiscard]] + protocol_autogen::Optional + getVaultKind() const + { + if (hasVaultKind()) + return this->sle_->at(sfVaultKind); + return std::nullopt; + } + + /** + * @brief Check if sfVaultKind is present. + * @return True if the field is present, false otherwise. + */ + [[nodiscard]] + bool + hasVaultKind() const + { + return this->sle_->isFieldPresent(sfVaultKind); + } + + /** + * @brief Get sfSubscriptionDate (SoeOptional) + * @return The field value, or std::nullopt if not present. + */ + [[nodiscard]] + protocol_autogen::Optional + getSubscriptionDate() const + { + if (hasSubscriptionDate()) + return this->sle_->at(sfSubscriptionDate); + return std::nullopt; + } + + /** + * @brief Check if sfSubscriptionDate is present. + * @return True if the field is present, false otherwise. + */ + [[nodiscard]] + bool + hasSubscriptionDate() const + { + return this->sle_->isFieldPresent(sfSubscriptionDate); + } + + /** + * @brief Get sfRedemptionDate (SoeOptional) + * @return The field value, or std::nullopt if not present. + */ + [[nodiscard]] + protocol_autogen::Optional + getRedemptionDate() const + { + if (hasRedemptionDate()) + return this->sle_->at(sfRedemptionDate); + return std::nullopt; + } + + /** + * @brief Check if sfRedemptionDate is present. + * @return True if the field is present, false otherwise. + */ + [[nodiscard]] + bool + hasRedemptionDate() const + { + return this->sle_->isFieldPresent(sfRedemptionDate); + } }; /** @@ -543,6 +615,39 @@ public: return *this; } + /** + * @brief Set sfVaultKind (SoeDefault) + * @return Reference to this builder for method chaining. + */ + VaultBuilder& + setVaultKind(std::decay_t const& value) + { + object_[sfVaultKind] = value; + return *this; + } + + /** + * @brief Set sfSubscriptionDate (SoeOptional) + * @return Reference to this builder for method chaining. + */ + VaultBuilder& + setSubscriptionDate(std::decay_t const& value) + { + object_[sfSubscriptionDate] = value; + return *this; + } + + /** + * @brief Set sfRedemptionDate (SoeOptional) + * @return Reference to this builder for method chaining. + */ + VaultBuilder& + setRedemptionDate(std::decay_t const& value) + { + object_[sfRedemptionDate] = value; + return *this; + } + /** * @brief Build and return the completed Vault wrapper. * @param index The ledger entry index. diff --git a/include/xrpl/protocol_autogen/transactions/VaultCreate.h b/include/xrpl/protocol_autogen/transactions/VaultCreate.h index b7e1527754..e206925e02 100644 --- a/include/xrpl/protocol_autogen/transactions/VaultCreate.h +++ b/include/xrpl/protocol_autogen/transactions/VaultCreate.h @@ -214,6 +214,84 @@ public: { return this->tx_->isFieldPresent(sfScale); } + + /** + * @brief Get sfVaultKind (SoeOptional) + * @return The field value, or std::nullopt if not present. + */ + [[nodiscard]] + protocol_autogen::Optional + getVaultKind() const + { + if (hasVaultKind()) + { + return this->tx_->at(sfVaultKind); + } + return std::nullopt; + } + + /** + * @brief Check if sfVaultKind is present. + * @return True if the field is present, false otherwise. + */ + [[nodiscard]] + bool + hasVaultKind() const + { + return this->tx_->isFieldPresent(sfVaultKind); + } + + /** + * @brief Get sfSubscriptionDate (SoeOptional) + * @return The field value, or std::nullopt if not present. + */ + [[nodiscard]] + protocol_autogen::Optional + getSubscriptionDate() const + { + if (hasSubscriptionDate()) + { + return this->tx_->at(sfSubscriptionDate); + } + return std::nullopt; + } + + /** + * @brief Check if sfSubscriptionDate is present. + * @return True if the field is present, false otherwise. + */ + [[nodiscard]] + bool + hasSubscriptionDate() const + { + return this->tx_->isFieldPresent(sfSubscriptionDate); + } + + /** + * @brief Get sfRedemptionDate (SoeOptional) + * @return The field value, or std::nullopt if not present. + */ + [[nodiscard]] + protocol_autogen::Optional + getRedemptionDate() const + { + if (hasRedemptionDate()) + { + return this->tx_->at(sfRedemptionDate); + } + return std::nullopt; + } + + /** + * @brief Check if sfRedemptionDate is present. + * @return True if the field is present, false otherwise. + */ + [[nodiscard]] + bool + hasRedemptionDate() const + { + return this->tx_->isFieldPresent(sfRedemptionDate); + } }; /** @@ -338,6 +416,39 @@ public: return *this; } + /** + * @brief Set sfVaultKind (SoeOptional) + * @return Reference to this builder for method chaining. + */ + VaultCreateBuilder& + setVaultKind(std::decay_t const& value) + { + object_[sfVaultKind] = value; + return *this; + } + + /** + * @brief Set sfSubscriptionDate (SoeOptional) + * @return Reference to this builder for method chaining. + */ + VaultCreateBuilder& + setSubscriptionDate(std::decay_t const& value) + { + object_[sfSubscriptionDate] = value; + return *this; + } + + /** + * @brief Set sfRedemptionDate (SoeOptional) + * @return Reference to this builder for method chaining. + */ + VaultCreateBuilder& + setRedemptionDate(std::decay_t const& value) + { + object_[sfRedemptionDate] = value; + return *this; + } + /** * @brief Build and return the VaultCreate wrapper. * @param publicKey The public key for signing. diff --git a/include/xrpl/tx/invariants/LoanInvariant.h b/include/xrpl/tx/invariants/LoanInvariant.h index 0648881423..fc72b8d420 100644 --- a/include/xrpl/tx/invariants/LoanInvariant.h +++ b/include/xrpl/tx/invariants/LoanInvariant.h @@ -16,6 +16,8 @@ namespace xrpl { * @brief Invariants: Loans are internally consistent * * 1. If `Loan.PaymentRemaining = 0` then `Loan.PrincipalOutstanding = 0` + * 2. A newly-created Loan against a closed-ended vault must satisfy + * `StartDate + PaymentInterval * PaymentRemaining < Vault.RedemptionDate`. * */ class ValidLoan diff --git a/include/xrpl/tx/invariants/VaultInvariant.h b/include/xrpl/tx/invariants/VaultInvariant.h index 136c6c4a25..2ba42f0ab4 100644 --- a/include/xrpl/tx/invariants/VaultInvariant.h +++ b/include/xrpl/tx/invariants/VaultInvariant.h @@ -38,7 +38,17 @@ namespace xrpl { * - vault set must not alter the vault assets or shares balance * - no vault transaction can change loss unrealized (it's updated by loan * transactions) + * - a created closed-ended vault must satisfy + * MIN_INVESTMENT_PERIOD <= RedemptionDate - SubscriptionDate < + * MAX_INVESTMENT_PERIOD + * - vault deposit may only succeed when the vault phase is NoPhase or + * Subscription + * - vault withdrawal may not succeed when the vault phase is Investment + * - closed-ended loan origination (ttLOAN_SET) may only succeed when the + * vault phase is Investment * + * Immutability of VaultKind, SubscriptionDate and RedemptionDate is enforced + * by NoModifiedUnmodifiableFields (see InvariantCheck.cpp). */ class ValidVault { @@ -55,6 +65,9 @@ class ValidVault Number assetsAvailable = 0; Number assetsMaximum = 0; Number lossUnrealized = 0; + std::optional vaultKind; + std::optional subscriptionDate; + std::optional redemptionDate; Vault static make(SLE const&); }; @@ -153,6 +166,17 @@ private: [[nodiscard]] static bool isVaultEmpty(Vault const& vault); + /** + * @brief Invariant check for @c ttLOAN_SET. + * + * For a closed-ended vault, a loan may only be originated while the vault is in the Investment + * phase (strictly past @c SubscriptionDate and before @c RedemptionDate). Open-ended vaults (@c + * NoPhase) are unaffected. The complementary maturity bound (final payment strictly precedes @c + * RedemptionDate) is enforced by @c ValidLoan. + */ + [[nodiscard]] bool + finalizeLoanSet(ReadView const& view, beast::Journal const& j) const; + public: // Compute the coarsest scale required to represent all numbers [[nodiscard]] static std::int32_t diff --git a/src/libxrpl/ledger/View.cpp b/src/libxrpl/ledger/View.cpp index 8116f4f641..2dd70e2950 100644 --- a/src/libxrpl/ledger/View.cpp +++ b/src/libxrpl/ledger/View.cpp @@ -45,12 +45,20 @@ namespace xrpl { //------------------------------------------------------------------------------ bool -hasExpired(ReadView const& view, std::optional const& exp) +hasExpired( + ReadView const& view, + std::optional const& exp, + ExpiryComparison comparison) { using d = NetClock::duration; using tp = NetClock::time_point; - return exp && (view.parentCloseTime() >= tp{d{*exp}}); + if (!exp) + return false; + auto const boundary = tp{d{*exp}}; + return comparison == ExpiryComparison::Inclusive // + ? view.parentCloseTime() >= boundary + : view.parentCloseTime() > boundary; } bool diff --git a/src/libxrpl/ledger/helpers/VaultHelpers.cpp b/src/libxrpl/ledger/helpers/VaultHelpers.cpp index 78f64d2077..67e0262e14 100644 --- a/src/libxrpl/ledger/helpers/VaultHelpers.cpp +++ b/src/libxrpl/ledger/helpers/VaultHelpers.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #include #include #include // IWYU pragma: keep @@ -11,6 +12,7 @@ #include #include #include // IWYU pragma: keep +#include #include #include @@ -157,4 +159,74 @@ getVaultVersion(SLE::const_ref vault) return static_cast(version); } +namespace { + +[[nodiscard]] VaultKind +decodeVaultKind(std::optional vaultKind) +{ + if (vaultKind && *vaultKind == std::to_underlying(VaultKind::ClosedEnded)) + return VaultKind::ClosedEnded; + return VaultKind::OpenEnded; +} + +} // namespace + +[[nodiscard]] VaultKind +getVaultKind(SLE::const_ref vault) +{ + XRPL_ASSERT(vault && vault->getType() == ltVAULT, "xrpl::getVaultKind : valid Vault sle"); + return decodeVaultKind(vault->at(~sfVaultKind)); +} + +[[nodiscard]] VaultKind +getVaultKind(STTx const& tx) +{ + return decodeVaultKind(tx[~sfVaultKind]); +} + +[[nodiscard]] bool +isValidVaultKind(STTx const& tx) +{ + auto const kindField = tx[~sfVaultKind]; + if (!kindField) + return true; + return *kindField == std::to_underlying(VaultKind::OpenEnded) || + *kindField == std::to_underlying(VaultKind::ClosedEnded); +} + +[[nodiscard]] bool +isValidClosedEndedGap(std::uint32_t sub, std::uint32_t red) +{ + auto const s = static_cast(sub); + auto const r = static_cast(red); + return r >= s + kMinInvestmentPeriod && r < s + kMaxInvestmentPeriod; +} + +[[nodiscard]] VaultPhase +getVaultPhase(ReadView const& view, SLE::const_ref vault) +{ + XRPL_ASSERT(vault && vault->getType() == ltVAULT, "xrpl::getVaultPhase : valid Vault sle"); + return getVaultPhase( + view, (*vault)[~sfVaultKind], (*vault)[~sfSubscriptionDate], (*vault)[~sfRedemptionDate]); +} + +[[nodiscard]] VaultPhase +getVaultPhase( + ReadView const& view, + std::optional vaultKind, + std::optional subscriptionDate, + std::optional redemptionDate) +{ + if (!vaultKind || *vaultKind != std::to_underlying(VaultKind::ClosedEnded)) + return VaultPhase::NoPhase; + + // Subscription includes now == SubscriptionDate; Investment starts + // strictly after SubscriptionDate. + if (!hasExpired(view, subscriptionDate, ExpiryComparison::Exclusive)) + return VaultPhase::Subscription; + if (!hasExpired(view, redemptionDate)) + return VaultPhase::Investment; + return VaultPhase::Redemption; +} + } // namespace xrpl diff --git a/src/libxrpl/tx/invariants/InvariantCheck.cpp b/src/libxrpl/tx/invariants/InvariantCheck.cpp index 9b997e06dd..369206d9e6 100644 --- a/src/libxrpl/tx/invariants/InvariantCheck.cpp +++ b/src/libxrpl/tx/invariants/InvariantCheck.cpp @@ -1126,20 +1126,17 @@ NoModifiedUnmodifiableFields::finalize( auto const& before = slePair.first; auto const& after = slePair.second; auto const type = after->getType(); - bool bad = false; - [[maybe_unused]] bool enforce = false; + // featureLendingProtocol gates enforcement, not detection: changes are + // always logged, but the transaction is only failed once the amendment + // is enabled. Type-specific field lists may add their own gates (see + // ltVAULT). + bool const enforce = view.rules().enabled(featureLendingProtocol); + bool bad = kFieldChanged(before, after, sfLedgerEntryType) || + kFieldChanged(before, after, sfLedgerIndex); switch (type) { case ltLOAN_BROKER: - /* - * We check this invariant regardless of lending protocol - * amendment status, allowing for detection and logging of - * potential issues even when the amendment is disabled. - */ - enforce = view.rules().enabled(featureLendingProtocol); - bad = kFieldChanged(before, after, sfLedgerEntryType) || - kFieldChanged(before, after, sfLedgerIndex) || - kFieldChanged(before, after, sfSequence) || + bad = bad || kFieldChanged(before, after, sfSequence) || kFieldChanged(before, after, sfOwnerNode) || kFieldChanged(before, after, sfVaultNode) || kFieldChanged(before, after, sfVaultID) || @@ -1150,15 +1147,7 @@ NoModifiedUnmodifiableFields::finalize( kFieldChanged(before, after, sfCoverRateLiquidation); break; case ltLOAN: - /* - * We check this invariant regardless of lending protocol - * amendment status, allowing for detection and logging of - * potential issues even when the amendment is disabled. - */ - enforce = view.rules().enabled(featureLendingProtocol); - bad = kFieldChanged(before, after, sfLedgerEntryType) || - kFieldChanged(before, after, sfLedgerIndex) || - kFieldChanged(before, after, sfSequence) || + bad = bad || kFieldChanged(before, after, sfSequence) || kFieldChanged(before, after, sfOwnerNode) || kFieldChanged(before, after, sfLoanBrokerNode) || kFieldChanged(before, after, sfLoanBrokerID) || @@ -1177,19 +1166,28 @@ NoModifiedUnmodifiableFields::finalize( kFieldChanged(before, after, sfGracePeriod) || kFieldChanged(before, after, sfLoanScale); break; - default: + case ltVAULT: /* - * We check this invariant regardless of lending protocol - * amendment status, allowing for detection and logging of - * potential issues even when the amendment is disabled. - * - * We use the lending protocol as a gate, even though - * all transactions are affected because that's when it - * was added. + * sfAccount, sfAsset and sfShareMPTID are already + * captured by VaultInvariant. The additional fields + * below are introduced by featureLendingProtocolV1_1 + * and only exist on V1_1 vaults. */ - enforce = view.rules().enabled(featureLendingProtocol); - bad = kFieldChanged(before, after, sfLedgerEntryType) || - kFieldChanged(before, after, sfLedgerIndex); + if (view.rules().enabled(featureLendingProtocolV1_1)) + { + bad = bad || kFieldChanged(before, after, sfVaultKind) || + kFieldChanged(before, after, sfSubscriptionDate) || + kFieldChanged(before, after, sfRedemptionDate) || + kFieldChanged(before, after, sfSequence) || + kFieldChanged(before, after, sfOwnerNode) || + kFieldChanged(before, after, sfOwner) || + kFieldChanged(before, after, sfWithdrawalPolicy) || + kFieldChanged(before, after, sfScale) || + kFieldChanged(before, after, sfLEVersion); + } + break; + default: + break; } XRPL_ASSERT( !bad || enforce, diff --git a/src/libxrpl/tx/invariants/LoanInvariant.cpp b/src/libxrpl/tx/invariants/LoanInvariant.cpp index ce9a7c6e03..7b96790570 100644 --- a/src/libxrpl/tx/invariants/LoanInvariant.cpp +++ b/src/libxrpl/tx/invariants/LoanInvariant.cpp @@ -4,7 +4,10 @@ #include #include #include +#include +#include #include +#include #include #include #include // IWYU pragma: keep @@ -12,6 +15,8 @@ #include #include +#include + namespace xrpl { void @@ -26,7 +31,7 @@ ValidLoan::visitEntry(bool isDelete, SLE::const_ref before, SLE::const_ref after bool ValidLoan::finalize( STTx const& tx, - TER const, + TER const result, XRPAmount const, ReadView const& view, beast::Journal const& j) @@ -36,6 +41,35 @@ ValidLoan::finalize( for (auto const& [before, after] : loans_) { + // A closed-ended vault must not accept a loan whose final scheduled payment falls on or + // after the vault's RedemptionDate. This mirrors the LoanSet::preclaim gate and only fires + // on loan creation; once the loan exists, its StartDate / PaymentInterval are immutable and + // PaymentRemaining only decreases, so the bound is preserved. + if (!before && isTesSuccess(result)) + { + auto const broker = view.read(keylet::loanBroker(after->at(sfLoanBrokerID))); + if (broker) + { + auto const vault = view.read(keylet::vault(broker->at(sfVaultID))); + // We don't check for LendingProtocolV1_1 amendment because a ClosedEnded Vault will + // not exist without the amendment enabled + if (vault && getVaultKind(vault) == VaultKind::ClosedEnded) + { + std::uint32_t const startDate = after->at(sfStartDate); + std::uint32_t const interval = after->at(sfPaymentInterval); + std::uint32_t const remaining = after->at(sfPaymentRemaining); + std::uint32_t const redemption = vault->at(sfRedemptionDate); + if (std::uint64_t{startDate} + (std::uint64_t{interval} * remaining) >= + redemption) + { + JLOG(j.fatal()) << "Invariant failed: closed-ended loan final payment " + "must precede RedemptionDate"; + return false; + } + } + } + } + // https://github.com/Tapanito/XRPL-Standards/blob/xls-66-lending-protocol/XLS-0066d-lending-protocol/README.md#3223-invariants // If `Loan.PaymentRemaining = 0` then the loan MUST be fully paid off if (after->at(sfPaymentRemaining) == 0 && diff --git a/src/libxrpl/tx/invariants/VaultInvariant.cpp b/src/libxrpl/tx/invariants/VaultInvariant.cpp index c577fdf356..dc6021beb5 100644 --- a/src/libxrpl/tx/invariants/VaultInvariant.cpp +++ b/src/libxrpl/tx/invariants/VaultInvariant.cpp @@ -6,6 +6,7 @@ #include #include #include +#include #include #include #include @@ -24,11 +25,27 @@ #include #include #include +#include #include #include namespace xrpl { +namespace { + +/* + * True iff the recorded sfVaultKind identifies a closed-ended vault. + * Centralizes the presence + enum-value check used by the phase-gate + * invariants below. + */ +[[nodiscard]] bool +isClosedEnded(std::optional const& vaultKind) +{ + return vaultKind && *vaultKind == std::to_underlying(VaultKind::ClosedEnded); +} + +} // namespace + ValidVault::Vault ValidVault::Vault::make(SLE const& from) { @@ -44,6 +61,9 @@ ValidVault::Vault::make(SLE const& from) self.assetsAvailable = from.at(sfAssetsAvailable); self.assetsMaximum = from.at(sfAssetsMaximum); self.lossUnrealized = from.at(sfLossUnrealized); + self.vaultKind = from[~sfVaultKind]; + self.subscriptionDate = from[~sfSubscriptionDate]; + self.redemptionDate = from[~sfRedemptionDate]; return self; } @@ -254,6 +274,37 @@ ValidVault::isVaultEmpty(Vault const& vault) return vault.assetsAvailable == 0 && vault.assetsTotal == 0; } +bool +ValidVault::finalizeLoanSet(ReadView const& view, beast::Journal const& j) const +{ + if (afterVault_.empty()) + { + // LCOV_EXCL_START + UNREACHABLE("xrpl::ValidVault::finalizeLoanSet : vault exists"); + return false; + // LCOV_EXCL_STOP + } + + auto const& afterVault = afterVault_[0]; + + // Loan origination against a closed-ended vault is only permitted while the vault is in the + // Investment phase - strictly past SubscriptionDate and before RedemptionDate. Open-ended + // vaults have NoPhase and are unaffected. + auto const phase = getVaultPhase( + view, afterVault.vaultKind, afterVault.subscriptionDate, afterVault.redemptionDate); + if (phase == VaultPhase::NoPhase) + return true; + + if (phase != VaultPhase::Investment) + { + JLOG(j.fatal()) << // + "Invariant failed: loan origination only allowed in Investment phase"; + return false; + } + + return true; +} + std::int32_t ValidVault::computeVaultMinScale(DeltaInfo const& vaultDelta, Rules const& rules) const { @@ -520,6 +571,9 @@ ValidVault::finalize( result = false; } + // Immutability of VaultKind, SubscriptionDate and RedemptionDate is enforced by + // NoModifiedUnmodifiableFields in InvariantCheck.cpp. + auto const beforeShares = [&]() -> std::optional { if (beforeVault_.empty()) return std::nullopt; @@ -606,6 +660,26 @@ ValidVault::finalize( result = false; } + if (isClosedEnded(afterVault.vaultKind)) + { + if (!afterVault.subscriptionDate || !afterVault.redemptionDate) + { + JLOG(j.fatal()) // + << "Invariant failed: closed-ended vault must have SubscriptionDate " + "and RedemptionDate"; + result = false; + } + else if (!isValidClosedEndedGap( + *afterVault.subscriptionDate, *afterVault.redemptionDate)) + { + JLOG(j.fatal()) // + << "Invariant failed: closed-ended vault RedemptionDate - " + "SubscriptionDate must be within [MIN_INVESTMENT_PERIOD, " + "MAX_INVESTMENT_PERIOD)"; + result = false; + } + } + return result; } case ttVAULT_SET: { @@ -666,6 +740,21 @@ ValidVault::finalize( !beforeVault_.empty(), "xrpl::ValidVault::finalize : deposit updated a vault"); auto const& beforeVault = beforeVault_[0]; + // Deposit is only allowed while the vault is in NoPhase or + // Subscription. + auto const depositPhase = getVaultPhase( + view, + afterVault.vaultKind, + afterVault.subscriptionDate, + afterVault.redemptionDate); + if (depositPhase != VaultPhase::NoPhase && depositPhase != VaultPhase::Subscription) + { + JLOG(j.fatal()) << // + "Invariant failed: deposit only allowed in " + "Subscription or NoPhase"; + result = false; + } + auto const maybeVaultDeltaAssets = deltaAssets(afterVault.pseudoId); if (!maybeVaultDeltaAssets) { @@ -804,6 +893,20 @@ ValidVault::finalize( "xrpl::ValidVault::finalize : withdrawal updated a vault"); auto const& beforeVault = beforeVault_[0]; + // Withdrawal from a closed-ended vault is not allowed during the Investment phase + // (strictly past SubscriptionDate, before RedemptionDate). + if (getVaultPhase( + view, + afterVault.vaultKind, + afterVault.subscriptionDate, + afterVault.redemptionDate) == VaultPhase::Investment) + { + JLOG(j.fatal()) << // + "Invariant failed: withdrawal not allowed during " + "Investment phase"; + result = false; + } + auto const maybeVaultDeltaAssets = deltaAssets(afterVault.pseudoId); if (!maybeVaultDeltaAssets) { @@ -1052,6 +1155,7 @@ ValidVault::finalize( } case ttLOAN_SET: + return finalizeLoanSet(view, j); case ttLOAN_MANAGE: case ttLOAN_PAY: return true; diff --git a/src/libxrpl/tx/transactors/lending/LoanSet.cpp b/src/libxrpl/tx/transactors/lending/LoanSet.cpp index 6533a47916..2def3d2eb2 100644 --- a/src/libxrpl/tx/transactors/lending/LoanSet.cpp +++ b/src/libxrpl/tx/transactors/lending/LoanSet.cpp @@ -10,6 +10,7 @@ #include #include #include +#include #include #include #include @@ -225,6 +226,8 @@ TER LoanSet::preclaim(PreclaimContext const& ctx) { auto const& tx = ctx.tx; + auto const interval = ctx.tx.at(~sfPaymentInterval).value_or(kDefaultPaymentInterval); + auto const total = ctx.tx.at(~sfPaymentTotal).value_or(kDefaultPaymentTotal); { // Check for numeric overflow of the schedule before we load any @@ -238,9 +241,6 @@ LoanSet::preclaim(PreclaimContext const& ctx) static_assert(kMaxTime == 4'294'967'295); auto const timeAvailable = kMaxTime - getStartDate(ctx.view); - - auto const interval = ctx.tx.at(~sfPaymentInterval).value_or(kDefaultPaymentInterval); - auto const total = ctx.tx.at(~sfPaymentTotal).value_or(kDefaultPaymentTotal); auto const grace = ctx.tx.at(~sfGracePeriod).value_or(kDefaultGracePeriod); // The grace period can't be larger than the interval. Check it first, @@ -310,6 +310,32 @@ LoanSet::preclaim(PreclaimContext const& ctx) return tefBAD_LEDGER; // LCOV_EXCL_LINE } + if (ctx.view.rules().enabled(featureLendingProtocolV1_1)) + { + auto const phase = getVaultPhase(ctx.view, vault); + if (phase == VaultPhase::Subscription) + { + JLOG(ctx.j.warn()) << "Vault is still in the subscription phase."; + return tecTOO_SOON; + } + if (phase == VaultPhase::Redemption) + { + JLOG(ctx.j.warn()) << "Vault has entered the redemption phase."; + return tecEXPIRED; + } + if (phase == VaultPhase::Investment) + { + auto const finalPayment = + std::uint64_t{getStartDate(ctx.view)} + (std::uint64_t{interval} * total); + if (finalPayment >= vault->at(sfRedemptionDate)) + { + JLOG(ctx.j.warn()) << "Final loan payment date is on or after " + "the vault's redemption date."; + return tecNO_PERMISSION; + } + } + } + if (vault->at(sfAssetsMaximum) != 0 && vault->at(sfAssetsTotal) >= vault->at(sfAssetsMaximum)) { JLOG(ctx.j.warn()) << "Vault at maximum assets limit. Can't add another loan."; diff --git a/src/libxrpl/tx/transactors/vault/VaultCreate.cpp b/src/libxrpl/tx/transactors/vault/VaultCreate.cpp index f74a27c39b..7ade4ed5ab 100644 --- a/src/libxrpl/tx/transactors/vault/VaultCreate.cpp +++ b/src/libxrpl/tx/transactors/vault/VaultCreate.cpp @@ -8,6 +8,7 @@ #include #include #include +#include #include #include #include @@ -43,6 +44,11 @@ VaultCreate::checkExtraFeatures(PreflightContext const& ctx) if (ctx.tx.isFieldPresent(sfDomainID) && !ctx.rules.enabled(featurePermissionedDomains)) return false; + if (!ctx.rules.enabled(featureLendingProtocolV1_1) && + (ctx.tx.isFieldPresent(sfVaultKind) || ctx.tx.isFieldPresent(sfSubscriptionDate) || + ctx.tx.isFieldPresent(sfRedemptionDate))) + return false; + return true; } @@ -99,6 +105,22 @@ VaultCreate::preflight(PreflightContext const& ctx) return temMALFORMED; } + if (!isValidVaultKind(ctx.tx)) + return temMALFORMED; + auto const kind = getVaultKind(ctx.tx); + auto const hasSubscription = ctx.tx.isFieldPresent(sfSubscriptionDate); + auto const hasRedemption = ctx.tx.isFieldPresent(sfRedemptionDate); + auto const isClosedEnded = kind == VaultKind::ClosedEnded; + if (!isClosedEnded && (hasSubscription || hasRedemption)) + return temMALFORMED; + if (isClosedEnded) + { + if (!hasSubscription || !hasRedemption) + return temMALFORMED; + if (!isValidClosedEndedGap(ctx.tx[sfSubscriptionDate], ctx.tx[sfRedemptionDate])) + return temMALFORMED; + } + return tesSUCCESS; } @@ -136,6 +158,16 @@ VaultCreate::preclaim(PreclaimContext const& ctx) accountId == beast::kZero) return terADDRESS_COLLISION; + // preflight enforces red >= sub + kMinInvestmentPeriod for closed-ended + // vaults, so a past RedemptionDate always implies a strictly-earlier, + // equally-past SubscriptionDate. The RedemptionDate arm below is therefore + // defensive: it cannot be the sole cause of tecEXPIRED. It is kept to + // preserve the invariant locally in case the preflight gap check is ever + // weakened. + if (hasExpired(ctx.view, ctx.tx[~sfSubscriptionDate]) || + hasExpired(ctx.view, ctx.tx[~sfRedemptionDate])) + return tecEXPIRED; + return tesSUCCESS; } @@ -242,7 +274,17 @@ VaultCreate::doApply() if (scale != 0u) vault->at(sfScale) = scale; if (view().rules().enabled(featureLendingProtocolV1_1)) + { vault->at(sfLEVersion) = std::to_underlying(VaultVersion::CashBasis); + + auto const kind = getVaultKind(tx); + vault->at(sfVaultKind) = std::to_underlying(kind); + if (kind == VaultKind::ClosedEnded) + { + vault->at(sfSubscriptionDate) = tx[sfSubscriptionDate]; + vault->at(sfRedemptionDate) = tx[sfRedemptionDate]; + } + } view().insert(vault); // Explicitly create MPToken for the vault owner diff --git a/src/libxrpl/tx/transactors/vault/VaultDeposit.cpp b/src/libxrpl/tx/transactors/vault/VaultDeposit.cpp index aa9cfc8537..a3c0a94eb5 100644 --- a/src/libxrpl/tx/transactors/vault/VaultDeposit.cpp +++ b/src/libxrpl/tx/transactors/vault/VaultDeposit.cpp @@ -13,6 +13,7 @@ #include #include #include +#include #include #include #include @@ -71,6 +72,17 @@ VaultDeposit::preclaim(PreclaimContext const& ctx) if (!vault) return tecNO_ENTRY; + if (ctx.view.rules().enabled(featureLendingProtocolV1_1)) + { + auto const phase = getVaultPhase(ctx.view, vault); + if (phase == VaultPhase::Investment || phase == VaultPhase::Redemption) + { + JLOG(ctx.j.debug()) << "VaultDeposit: vault deposit is not allowed in the investment " + "or redemption phase."; + return tecEXPIRED; + } + } + auto const& account = ctx.tx[sfAccount]; auto const amount = ctx.tx[sfAmount]; auto const vaultAsset = vault->at(sfAsset); diff --git a/src/libxrpl/tx/transactors/vault/VaultWithdraw.cpp b/src/libxrpl/tx/transactors/vault/VaultWithdraw.cpp index 353b72c30d..7b5bb1ea94 100644 --- a/src/libxrpl/tx/transactors/vault/VaultWithdraw.cpp +++ b/src/libxrpl/tx/transactors/vault/VaultWithdraw.cpp @@ -73,6 +73,16 @@ VaultWithdraw::preclaim(PreclaimContext const& ctx) if (!vault) return tecNO_ENTRY; + if (ctx.view.rules().enabled(featureLendingProtocolV1_1)) + { + if (getVaultPhase(ctx.view, vault) == VaultPhase::Investment) + { + JLOG(ctx.j.debug()) + << "VaultWithdraw: vault withdrawal is not allowed in the investment phase."; + return tecTOO_SOON; + } + } + auto const amount = ctx.tx[sfAmount]; auto const vaultAsset = vault->at(sfAsset); auto const vaultShare = vault->at(sfShareMPTID); diff --git a/src/test/app/Invariants_test.cpp b/src/test/app/Invariants_test.cpp index ed09b7b660..6878b2b5d0 100644 --- a/src/test/app/Invariants_test.cpp +++ b/src/test/app/Invariants_test.cpp @@ -16,6 +16,7 @@ #include #include +#include #include #include #include @@ -65,6 +66,7 @@ #include #include #include +#include #include #include #include @@ -135,7 +137,8 @@ class Invariants_test : public beast::unit_test::Suite STTx tx = STTx{ttACCOUNT_SET, [](STObject&) {}}, std::initializer_list ters = {tecINVARIANT_FAILED, tefINVARIANT_FAILED}, Preclose const& preclose = {}, - TxAccount setTxAccount = TxAccount::None) + TxAccount setTxAccount = TxAccount::None, + std::source_location const& loc = std::source_location::current()) { doInvariantCheck( makeEnv(defaultAmendments()), @@ -145,7 +148,8 @@ class Invariants_test : public beast::unit_test::Suite tx, ters, preclose, - setTxAccount); + setTxAccount, + loc); } void @@ -157,7 +161,8 @@ class Invariants_test : public beast::unit_test::Suite STTx tx = STTx{ttACCOUNT_SET, [](STObject&) {}}, std::initializer_list ters = {tecINVARIANT_FAILED, tefINVARIANT_FAILED}, Preclose const& preclose = {}, - TxAccount setTxAccount = TxAccount::None) + TxAccount setTxAccount = TxAccount::None, + std::source_location const& loc = std::source_location::current()) { using namespace test::jtx; @@ -171,7 +176,7 @@ class Invariants_test : public beast::unit_test::Suite if (setTxAccount != TxAccount::None) tx.setAccountID(sfAccount, setTxAccount == TxAccount::A1 ? a1.id() : a2.id()); - doInvariantCheck(std::move(env), a1, a2, expectLogs, precheck, fee, tx, ters); + doInvariantCheck(std::move(env), a1, a2, expectLogs, precheck, fee, tx, ters, loc); } void @@ -184,7 +189,8 @@ class Invariants_test : public beast::unit_test::Suite Precheck const& precheck, XRPAmount fee = XRPAmount{}, STTx tx = STTx{ttACCOUNT_SET, [](STObject&) {}}, - std::initializer_list ters = {tecINVARIANT_FAILED, tefINVARIANT_FAILED}) + std::initializer_list ters = {tecINVARIANT_FAILED, tefINVARIANT_FAILED}, + std::source_location const& loc = std::source_location::current()) { using namespace test::jtx; @@ -211,23 +217,27 @@ class Invariants_test : public beast::unit_test::Suite for (TER const& terExpect : ters) { terActual = transactor->checkInvariants(terActual, fee); - BEAST_EXPECTS( + expect( terExpect == terActual, - "expected: " + transToken(terExpect) + " got: " + transToken(terActual)); + "expected: " + transToken(terExpect) + " got: " + transToken(terActual), + loc.file_name(), + loc.line()); auto const messages = sink.messages().str(); if (!isTesSuccess(terActual)) { - BEAST_EXPECTS( + expect( messages.starts_with("Invariant failed:") || messages.starts_with("Transaction caused an exception"), - messages); + messages, + loc.file_name(), + loc.line()); } // std::cerr << messages << '\n'; for (auto const& m : expectLogs) { - BEAST_EXPECTS(messages.contains(m), m); + expect(messages.contains(m), m, loc.file_name(), loc.line()); } } } @@ -2475,6 +2485,54 @@ class Invariants_test : public beast::unit_test::Suite // TODO: Loan Object + // VaultKind, SubscriptionDate and RedemptionDate are immutable once set at creation. + // Enforced by NoModifiedUnmodifiableFields on ltVAULT via kFieldChanged. + Keylet closedEndedVaultKeylet = keylet::amendments(); + Preclose const createClosedEndedVault = [&, this]( + Account const& a, Account const&, Env& env) { + auto const sub = env.now().time_since_epoch().count() + 60; + auto const red = sub + kMinInvestmentPeriod + 1'000'000; + Vault const vault{env}; + auto [tx, keylet] = vault.create( + {.owner = a, + .asset = xrpIssue(), + .vaultKind = std::to_underlying(VaultKind::ClosedEnded), + .subscriptionDate = sub, + .redemptionDate = red}); + env(tx); + closedEndedVaultKeylet = keylet; + return BEAST_EXPECT(env.le(closedEndedVaultKeylet)); + }; + + { + // Each mutation must keep the vault otherwise valid so that only the immutability check + // fires. Shifting both dates by the same offset preserves the gap; bumping sfVaultKind + // stays within the recognised range. + auto const mods = std::to_array>({ + [](SLE::pointer& sle) { sle->at(sfVaultKind) += 1; }, + [](SLE::pointer& sle) { sle->at(sfSubscriptionDate) += 1; }, + [](SLE::pointer& sle) { sle->at(sfRedemptionDate) += 1; }, + }); + + for (auto const& mod : mods) + { + doInvariantCheck( + {{"changed an unchangeable field"}}, + [&](Account const&, Account const&, ApplyContext& ac) { + auto sle = ac.view().peek(closedEndedVaultKeylet); + if (!sle) + return false; + mod(sle); + ac.view().update(sle); + return true; + }, + XRPAmount{}, + STTx{ttACCOUNT_SET, [](STObject&) {}}, + {tecINVARIANT_FAILED, tefINVARIANT_FAILED}, + createClosedEndedVault); + } + } + { auto const mods = std::to_array>({ [](SLE::pointer& sle) { sle->at(sfLedgerEntryType) += 1; }, @@ -4367,6 +4425,286 @@ class Invariants_test : public beast::unit_test::Suite }}, {tecINVARIANT_FAILED, tecINVARIANT_FAILED}, precloseMpt); + + // ───────────────────────────────────────────────────────────── + // Closed-ended vault invariants added in ValidVault::finalize (create must supply both + // dates and satisfy the redemption-buffer gap), deposit only in Subscription / NoPhase, + // withdraw not in Investment, loan origination only in Investment. + + using d = NetClock::duration; + using tp = NetClock::time_point; + + auto const closedEnded = std::to_underlying(VaultKind::ClosedEnded); + + // Vault keylet captured by precloseClosedEnded so precheck does not have to rederive it + // from ac.view().seq(), which depends on how many env.close() calls preclose issued. + Keylet closedEndedKeylet = keylet::amendments(); + + // Preclose that creates a closed-ended vault (in Subscription), optionally seeds it with + // three deposits (so a1/a2/a3 hold a share MPToken that kAdjust can then adjust), and + // optionally advances parent close time past SubscriptionDate. A negative @p advanceBySub + // leaves the vault in Subscription. + auto const precloseClosedEnded = [&](std::int32_t advanceBySub, bool doDeposit) { + return [&, advanceBySub, doDeposit]( + Account const& a1, Account const& a2, Env& env) -> bool { + env.fund(XRP(1000), a3, a4); + auto const sub = env.now().time_since_epoch().count() + 60; + auto const red = sub + kMinInvestmentPeriod + 1'000'000; + Vault const vault{env}; + auto [tx, keylet] = vault.create( + {.owner = a1, + .asset = xrpIssue(), + .vaultKind = closedEnded, + .subscriptionDate = sub, + .redemptionDate = red}); + env(tx); + closedEndedKeylet = keylet; + if (doDeposit) + { + env(vault.deposit({.depositor = a1, .id = keylet.key, .amount = XRP(10)})); + env(vault.deposit({.depositor = a2, .id = keylet.key, .amount = XRP(10)})); + env(vault.deposit({.depositor = a3, .id = keylet.key, .amount = XRP(10)})); + } + if (advanceBySub >= 0) + env.close(tp{d{sub + advanceBySub}}); + return true; + }; + }; + + // Manually insert a bare closed-ended vault (+ pseudo-account + share MPTokenIssuance) + // directly into the view, bypassing the transactor path. Used to synthesize ttVAULT_CREATE + // states no legitimate transactor would produce. + auto const insertBareClosedEndedVault = + [closedEnded]( + ApplyContext& ac, + Account const& owner, + std::optional subscriptionDate, + std::optional redemptionDate) -> bool { + auto const sequence = ac.view().seq(); + auto const vaultKeylet = keylet::vault(owner.id(), SeqProxy::rawSequence(sequence)); + auto sleVault = std::make_shared(vaultKeylet); + auto const vaultPage = ac.view().dirInsert( + keylet::ownerDir(owner.id()), sleVault->key(), describeOwnerDir(owner.id())); + if (!vaultPage) + return false; + sleVault->setFieldU64(sfOwnerNode, *vaultPage); + + auto const pseudoId = pseudoAccountAddress(ac.view(), vaultKeylet.key); + auto sleAccount = std::make_shared(keylet::account(pseudoId)); + sleAccount->setAccountID(sfAccount, pseudoId); + sleAccount->setFieldAmount(sfBalance, STAmount{}); + sleAccount->setFieldU32(sfSequence, 0); + sleAccount->setFieldU32(sfFlags, lsfDisableMaster | lsfDefaultRipple | lsfDepositAuth); + sleAccount->setFieldH256(sfVaultID, vaultKeylet.key); + ac.view().insert(sleAccount); + + auto const sharesMptId = makeMptID(sequence, pseudoId); + auto const sharesKeylet = keylet::mptokenIssuance(sharesMptId); + auto sleShares = std::make_shared(sharesKeylet); + auto const sharesPage = ac.view().dirInsert( + keylet::ownerDir(pseudoId), sharesKeylet, describeOwnerDir(pseudoId)); + if (!sharesPage) + return false; + sleShares->setFieldU64(sfOwnerNode, *sharesPage); + sleShares->at(sfFlags) = 0; + sleShares->at(sfIssuer) = pseudoId; + sleShares->at(sfOutstandingAmount) = 0; + sleShares->at(sfSequence) = sequence; + + sleVault->at(sfAccount) = pseudoId; + sleVault->at(sfFlags) = 0; + sleVault->at(sfSequence) = sequence; + sleVault->at(sfOwner) = owner.id(); + sleVault->setFieldIssue(sfAsset, STIssue{sfAsset, Asset{xrpIssue()}}); + sleVault->at(sfAssetsTotal) = Number(0); + sleVault->at(sfAssetsAvailable) = Number(0); + sleVault->at(sfLossUnrealized) = Number(0); + sleVault->at(sfShareMPTID) = sharesMptId; + sleVault->at(sfWithdrawalPolicy) = kVaultStrategyFirstComeFirstServe; + sleVault->at(sfVaultKind) = closedEnded; + if (subscriptionDate) + sleVault->at(sfSubscriptionDate) = *subscriptionDate; + if (redemptionDate) + sleVault->at(sfRedemptionDate) = *redemptionDate; + + ac.view().insert(sleVault); + ac.view().insert(sleShares); + return true; + }; + + testcase << "Vault create closed-ended"; + + // A fresh closed-ended vault must carry both SubscriptionDate and RedemptionDate. + doInvariantCheck( + {"closed-ended vault must have SubscriptionDate and RedemptionDate"}, + [&](Account const& a1, Account const&, ApplyContext& ac) { + return insertBareClosedEndedVault(ac, a1, std::nullopt, std::nullopt); + }, + XRPAmount{}, + STTx{ttVAULT_CREATE, [](STObject&) {}}, + {tecINVARIANT_FAILED, tefINVARIANT_FAILED}); + + // Gap smaller than MIN_INVESTMENT_PERIOD but with RedemptionDate > SubscriptionDate; + // exercises the sub-minimum branch of the gap check. + doInvariantCheck( + {"closed-ended vault RedemptionDate - SubscriptionDate must be " + "within [MIN_INVESTMENT_PERIOD, MAX_INVESTMENT_PERIOD)"}, + [&](Account const& a1, Account const&, ApplyContext& ac) { + std::uint32_t const sub = 1'000'000'000; + std::uint32_t const red = sub + kMinInvestmentPeriod - 1; + return insertBareClosedEndedVault(ac, a1, sub, red); + }, + XRPAmount{}, + STTx{ttVAULT_CREATE, [](STObject&) {}}, + {tecINVARIANT_FAILED, tefINVARIANT_FAILED}); + + // RedemptionDate strictly before SubscriptionDate; the signed int64 gap is negative and + // is caught by the sub-minimum branch of the gap check. + doInvariantCheck( + {"closed-ended vault RedemptionDate - SubscriptionDate must be " + "within [MIN_INVESTMENT_PERIOD, MAX_INVESTMENT_PERIOD)"}, + [&](Account const& a1, Account const&, ApplyContext& ac) { + std::uint32_t const sub = 1'000'000'000; + std::uint32_t const red = sub - 1; + return insertBareClosedEndedVault(ac, a1, sub, red); + }, + XRPAmount{}, + STTx{ttVAULT_CREATE, [](STObject&) {}}, + {tecINVARIANT_FAILED, tefINVARIANT_FAILED}); + + // Gap exactly MAX_INVESTMENT_PERIOD is out of range (bound is half-open on the right). + doInvariantCheck( + {"closed-ended vault RedemptionDate - SubscriptionDate must be " + "within [MIN_INVESTMENT_PERIOD, MAX_INVESTMENT_PERIOD)"}, + [&](Account const& a1, Account const&, ApplyContext& ac) { + std::uint32_t const sub = 1'000'000'000; + std::uint32_t const red = sub + kMaxInvestmentPeriod; + return insertBareClosedEndedVault(ac, a1, sub, red); + }, + XRPAmount{}, + STTx{ttVAULT_CREATE, [](STObject&) {}}, + {tecINVARIANT_FAILED, tefINVARIANT_FAILED}); + + testcase << "Vault deposit closed-ended"; + + // A deposit into a closed-ended vault that has advanced past SubscriptionDate. kArgs + // simulates an otherwise valid deposit shape so only the phase invariant fires. + doInvariantCheck( + {"deposit only allowed in Subscription or NoPhase"}, + [&](Account const&, Account const& a2, ApplyContext& ac) { + return kAdjust( + ac.view(), closedEndedKeylet, kArgs(a2.id(), 10, [](Adjustments&) {})); + }, + XRPAmount{}, + STTx{ttVAULT_DEPOSIT, [](STObject& tx) { tx[sfAmount] = XRPAmount(10); }}, + {tecINVARIANT_FAILED, tecINVARIANT_FAILED}, + precloseClosedEnded(/*advanceBySub=*/1, /*doDeposit=*/true), + TxAccount::A2); + + testcase << "Vault withdrawal closed-ended"; + + // A withdrawal from a closed-ended vault in the Investment phase. + doInvariantCheck( + {"withdrawal not allowed during Investment phase"}, + [&](Account const&, Account const& a2, ApplyContext& ac) { + return kAdjust( + ac.view(), closedEndedKeylet, kArgs(a2.id(), -10, [](Adjustments&) {})); + }, + XRPAmount{}, + STTx{ttVAULT_WITHDRAW, [](STObject&) {}}, + {tecINVARIANT_FAILED, tecINVARIANT_FAILED}, + precloseClosedEnded(/*advanceBySub=*/1, /*doDeposit=*/true), + TxAccount::A2); + + testcase << "Vault loan set"; + + // ttLOAN_SET against a closed-ended vault that is not in Investment. finalizeLoanSet fires + // on any vault mutation; touching the vault SLE with no field change is sufficient. + doInvariantCheck( + {"loan origination only allowed in Investment phase"}, + [&](Account const&, Account const&, ApplyContext& ac) { + auto sleVault = ac.view().peek(closedEndedKeylet); + if (!sleVault) + return false; + ac.view().update(sleVault); + return true; + }, + XRPAmount{}, + STTx{ttLOAN_SET, [](STObject&) {}}, + {tecINVARIANT_FAILED, tecINVARIANT_FAILED}, + precloseClosedEnded(/*advanceBySub=*/-1, /*doDeposit=*/false)); + + testcase << "Vault loan set - closed-ended final payment past " + "RedemptionDate"; + + // A newly-created loan against a closed-ended vault must satisfy StartDate + + // PaymentInterval * PaymentRemaining < RedemptionDate. LoanSet::preclaim enforces the same + // bound; this test synthesises an invalid loan directly in the ApplyView so the invariant + // catches it even when preclaim is bypassed. + Keylet closedEndedBrokerKeylet = keylet::amendments(); + std::uint32_t closedEndedRed = 0; + doInvariantCheck( + {"closed-ended loan final payment must precede RedemptionDate"}, + [&](Account const& a1, Account const&, ApplyContext& ac) { + // Touch the vault so ValidVault::finalizeLoanSet sees an + // entry in afterVault_; the vault is in Investment, so + // finalizeLoanSet itself passes. + auto sleVault = ac.view().peek(closedEndedKeylet); + if (!sleVault) + return false; + ac.view().update(sleVault); + + // Read the broker's next loan sequence to build the loan + // keylet the same way LoanSet::doApply would. + auto sleBroker = ac.view().peek(closedEndedBrokerKeylet); + if (!sleBroker) + return false; + std::uint32_t const loanSeq = sleBroker->at(sfLoanSequence); + + // Synthesize a Loan whose final scheduled payment lands + // exactly at RedemptionDate: StartDate = red, interval = 60, + // remaining = 1 => red + 60 >= red. + auto sleLoan = std::make_shared( + keylet::loan(closedEndedBrokerKeylet.key, SeqProxy::rawSequence(loanSeq))); + sleLoan->at(sfLoanBrokerID) = closedEndedBrokerKeylet.key; + sleLoan->at(sfLoanSequence) = loanSeq; + sleLoan->at(sfBorrower) = a1.id(); + sleLoan->at(sfStartDate) = closedEndedRed; + sleLoan->at(sfPaymentInterval) = 60; + sleLoan->at(sfPaymentRemaining) = 1; + sleLoan->at(sfTotalValueOutstanding) = Number(100); + sleLoan->at(sfPeriodicPayment) = Number(1); + ac.view().insert(sleLoan); + return true; + }, + XRPAmount{}, + STTx{ttLOAN_SET, [](STObject&) {}}, + {tecINVARIANT_FAILED, tecINVARIANT_FAILED}, + [&](Account const& a1, Account const&, Env& env) -> bool { + auto const sub = env.now().time_since_epoch().count() + 60; + auto const red = sub + kMinInvestmentPeriod + 1'000'000; + closedEndedRed = red; + + Vault const vault{env}; + auto [tx, keylet] = vault.create( + {.owner = a1, + .asset = xrpIssue(), + .vaultKind = closedEnded, + .subscriptionDate = sub, + .redemptionDate = red}); + env(tx); + closedEndedKeylet = keylet; + + // Create the loan broker; LoanBrokerSet has no phase gate. + closedEndedBrokerKeylet = + keylet::loanBroker(a1.id(), SeqProxy::rawSequence(env.seq(a1))); + env(loan_broker::set(a1, keylet.key)); + + // Advance parent close time into Investment so + // ValidVault::finalizeLoanSet is satisfied. + env.close(tp{d{sub + 1}}); + return true; + }); } void diff --git a/src/test/app/Vault_test.cpp b/src/test/app/Vault_test.cpp index 6b6c4eb875..34ac40fb54 100644 --- a/src/test/app/Vault_test.cpp +++ b/src/test/app/Vault_test.cpp @@ -28,6 +28,7 @@ #include #include +#include #include #include #include @@ -41,11 +42,13 @@ #include #include #include +#include #include #include #include #include #include +#include #include #include #include @@ -63,10 +66,12 @@ #include #include #include +#include #include #include #include #include +#include #include #include #include @@ -82,6 +87,75 @@ class Vault_test : public beast::unit_test::Suite return {STAmount{asset.raw(), 1ul, 0, true, STAmount::Unchecked{}}, ""}; }; + /** + * Get the current ledger's close time resolution. + * @param env The test environment. + */ + static NetClock::duration + getLedgerTimeResolution(test::jtx::Env& env) + { + return env.current()->header().closeTimeResolution; + } + + void + closeToTime( + test::jtx::Env& env, + NetClock::time_point time, + std::source_location const& loc = std::source_location::current()) + { + using namespace std::chrono_literals; + env.close(time - env.closed()->header().closeTimeResolution + 1s); + expect( + env.closed()->header().closeTime == time, + std::format( + "current ledger time {} is not equal to the target ledger time {}", + env.closed()->header().closeTime.time_since_epoch(), + time.time_since_epoch()), + loc.file_name(), + loc.line()); + } + + using d = NetClock::duration; + using tp = NetClock::time_point; + + // Vault holds an Env& so no default initializer is possible; the + // struct is always aggregate-initialized by makeClosedEndedVault. + // NOLINTBEGIN(cppcoreguidelines-pro-type-member-init) + struct ClosedEndedSetup + { + test::jtx::Vault vault; + Keylet keylet; + std::uint32_t sub = 0; + std::uint32_t red = 0; + }; + // NOLINTEND(cppcoreguidelines-pro-type-member-init) + + // Submit a VaultCreate for a closed-ended vault with SubscriptionDate at + // env.now() + subOffset and RedemptionDate at SubscriptionDate + gap, then + // close the ledger. Returns the Vault helper, the vault's keylet and the + // resolved sub/red timestamps. + static ClosedEndedSetup + makeClosedEndedVault( + test::jtx::Env& env, + test::jtx::Account const& owner, + Asset const& asset, + std::uint32_t subOffset, + std::uint32_t gap) + { + auto const sub = env.now().time_since_epoch().count() + subOffset; + auto const red = sub + gap; + test::jtx::Vault const vault{env}; + auto [tx, keylet] = vault.create( + {.owner = owner, + .asset = asset, + .vaultKind = std::to_underlying(VaultKind::ClosedEnded), + .subscriptionDate = sub, + .redemptionDate = red}); + env(tx); + env.close(); + return {.vault = vault, .keylet = keylet, .sub = sub, .red = red}; + } + void testSequences() { @@ -1107,6 +1181,949 @@ class Vault_test : public beast::unit_test::Suite }); } + // VaultCreate malformation and happy paths for closed-ended vaults, plus the + // featureLendingProtocolV1_1 gate. + void + testVaultCreateClosedEnded() + { + testcase("closed-ended VaultCreate"); + using namespace test::jtx; + + auto const withEnv = [this](FeatureBitset features, auto&& body) { + Env env{*this, features}; + Account const owner{"owner"}; + env.fund(XRP(1000), owner); + env.close(); + Vault vault{env}; + body(env, owner, vault); + }; + + Asset const asset = xrpIssue(); + auto const minPeriod = kMinInvestmentPeriod; + auto const maxPeriod = kMaxInvestmentPeriod; + auto const closedEnded = std::to_underlying(VaultKind::ClosedEnded); + + // Gate: the three new fields require featureLendingProtocolV1_1. + withEnv( + testableAmendments() - featureLendingProtocolV1_1, + [&](Env& env, Account const& owner, Vault& vault) { + auto const sub = env.now().time_since_epoch().count() + 60; + auto [tx, keylet] = vault.create( + {.owner = owner, + .asset = asset, + .vaultKind = closedEnded, + .subscriptionDate = sub, + .redemptionDate = sub + minPeriod}); + env(tx, Ter{temDISABLED}); + }); + + /* + * Valid closed-ended creation with a comfortably interior gap (well above + * MIN_INVESTMENT_PERIOD and well below MAX_INVESTMENT_PERIOD). + */ + withEnv(testableAmendments(), [&](Env& env, Account const& owner, Vault& vault) { + auto const sub = env.now().time_since_epoch().count() + 60; + auto const red = sub + 86400; + auto [tx, keylet] = vault.create( + {.owner = owner, + .asset = asset, + .vaultKind = closedEnded, + .subscriptionDate = sub, + .redemptionDate = red}); + env(tx); + env.close(); + auto const sle = env.le(keylet); + if (BEAST_EXPECT(sle)) + { + BEAST_EXPECT(sle->at(sfVaultKind) == closedEnded); + BEAST_EXPECT(sle->at(sfSubscriptionDate) == sub); + BEAST_EXPECT(sle->at(sfRedemptionDate) == red); + } + }); + + // ClosedEnded missing one of SubscriptionDate / RedemptionDate => temMALFORMED. + withEnv(testableAmendments(), [&](Env& env, Account const& owner, Vault& vault) { + auto const sub = env.now().time_since_epoch().count() + 60; + auto [tx, keylet] = vault.create( + {.owner = owner, + .asset = asset, + .vaultKind = closedEnded, + .redemptionDate = sub + minPeriod}); + env(tx, Ter{temMALFORMED}); + }); + withEnv(testableAmendments(), [&](Env& env, Account const& owner, Vault& vault) { + auto const sub = env.now().time_since_epoch().count() + 60; + auto [tx, keylet] = vault.create( + {.owner = owner, + .asset = asset, + .vaultKind = closedEnded, + .subscriptionDate = sub}); + env(tx, Ter{temMALFORMED}); + }); + + /* + * SubscriptionDate not strictly after parent close time (preclaim, state-dependent - + * returns tecEXPIRED). This is the only reachable path to tecEXPIRED in VaultCreate; see + * the note below the next case. Note: there is no separate "expired RedemptionDate" test + * case here. preflight enforces red >= sub + kMinInvestmentPeriod, so any past + * RedemptionDate implies a strictly-earlier, equally-past SubscriptionDate; the + * SubscriptionDate check above short-circuits first. The RedemptionDate arm of the + * hasExpired check in VaultCreate::preclaim is defensive and unreachable as the sole cause + * of tecEXPIRED. + */ + withEnv(testableAmendments(), [&](Env& env, Account const& owner, Vault& vault) { + auto const nowSec = env.now().time_since_epoch().count(); + auto [tx, keylet] = vault.create( + {.owner = owner, + .asset = asset, + .vaultKind = closedEnded, + .subscriptionDate = nowSec, + .redemptionDate = nowSec + minPeriod}); + env(tx, Ter{tecEXPIRED}); + }); + + /* + * Gap smaller than MIN_INVESTMENT_PERIOD => temMALFORMED. Includes the SubscriptionDate >= + * RedemptionDate degenerate cases: the red == sub boundary and the strictly-reversed red < + * sub case, the latter yielding a negative signed int64 gap that is caught by the + * sub-minimum branch of the gap check. + */ + withEnv(testableAmendments(), [&](Env& env, Account const& owner, Vault& vault) { + auto const sub = env.now().time_since_epoch().count() + 60; + auto [tx, keylet] = vault.create( + {.owner = owner, + .asset = asset, + .vaultKind = closedEnded, + .subscriptionDate = sub, + .redemptionDate = sub + minPeriod - 1}); + env(tx, Ter{temMALFORMED}); + }); + withEnv(testableAmendments(), [&](Env& env, Account const& owner, Vault& vault) { + auto const sub = env.now().time_since_epoch().count() + 60; + auto [tx, keylet] = vault.create( + {.owner = owner, + .asset = asset, + .vaultKind = closedEnded, + .subscriptionDate = sub, + .redemptionDate = sub}); + env(tx, Ter{temMALFORMED}); + }); + withEnv(testableAmendments(), [&](Env& env, Account const& owner, Vault& vault) { + auto const sub = env.now().time_since_epoch().count() + 60; + auto [tx, keylet] = vault.create( + {.owner = owner, + .asset = asset, + .vaultKind = closedEnded, + .subscriptionDate = sub, + .redemptionDate = sub - 1}); + env(tx, Ter{temMALFORMED}); + }); + + // Gap equal to MAX_INVESTMENT_PERIOD => temMALFORMED (bound is half-open on the right). + withEnv(testableAmendments(), [&](Env& env, Account const& owner, Vault& vault) { + auto const sub = env.now().time_since_epoch().count() + 60; + auto [tx, keylet] = vault.create( + {.owner = owner, + .asset = asset, + .vaultKind = closedEnded, + .subscriptionDate = sub, + .redemptionDate = sub + maxPeriod}); + env(tx, Ter{temMALFORMED}); + }); + + // Gap strictly greater than MAX_INVESTMENT_PERIOD => temMALFORMED. Same code path as + // gap == MAX_INVESTMENT_PERIOD above, but covers the "gap >= MAX" bullet fully. + withEnv(testableAmendments(), [&](Env& env, Account const& owner, Vault& vault) { + auto const sub = env.now().time_since_epoch().count() + 60; + auto [tx, keylet] = vault.create( + {.owner = owner, + .asset = asset, + .vaultKind = closedEnded, + .subscriptionDate = sub, + .redemptionDate = sub + maxPeriod + 1}); + env(tx, Ter{temMALFORMED}); + }); + + // Happy path: gap exactly equal to MIN_INVESTMENT_PERIOD is accepted (lower bound is + // inclusive). + withEnv(testableAmendments(), [&](Env& env, Account const& owner, Vault& vault) { + auto const sub = env.now().time_since_epoch().count() + 60; + auto const red = sub + minPeriod; + auto [tx, keylet] = vault.create( + {.owner = owner, + .asset = asset, + .vaultKind = closedEnded, + .subscriptionDate = sub, + .redemptionDate = red}); + env(tx); + env.close(); + auto const sle = env.le(keylet); + if (BEAST_EXPECT(sle)) + { + BEAST_EXPECT(sle->at(sfRedemptionDate) == red); + } + }); + + // Happy path: gap one second less than MAX_INVESTMENT_PERIOD is + // accepted (upper bound is exclusive). + withEnv(testableAmendments(), [&](Env& env, Account const& owner, Vault& vault) { + auto const sub = env.now().time_since_epoch().count() + 60; + auto const red = sub + maxPeriod - 1; + auto [tx, keylet] = vault.create( + {.owner = owner, + .asset = asset, + .vaultKind = closedEnded, + .subscriptionDate = sub, + .redemptionDate = red}); + env(tx); + env.close(); + auto const sle = env.le(keylet); + if (BEAST_EXPECT(sle)) + { + BEAST_EXPECT(sle->at(sfRedemptionDate) == red); + } + }); + + // OpenEnded (absent/0) with SubscriptionDate or RedemptionDate present + // => temMALFORMED. + withEnv(testableAmendments(), [&](Env& env, Account const& owner, Vault& vault) { + auto const sub = env.now().time_since_epoch().count() + 60; + auto [tx, keylet] = + vault.create({.owner = owner, .asset = asset, .subscriptionDate = sub}); + env(tx, Ter{temMALFORMED}); + }); + withEnv(testableAmendments(), [&](Env& env, Account const& owner, Vault& vault) { + auto const sub = env.now().time_since_epoch().count() + 60; + auto [tx, keylet] = + vault.create({.owner = owner, .asset = asset, .redemptionDate = sub + minPeriod}); + env(tx, Ter{temMALFORMED}); + }); + + // Unrecognised VaultKind => temMALFORMED. + withEnv(testableAmendments(), [&](Env& env, Account const& owner, Vault& vault) { + auto [tx, keylet] = vault.create( + {.owner = owner, + .asset = asset, + .vaultKind = static_cast(closedEnded + 1)}); + env(tx, Ter{temMALFORMED}); + }); + + // Happy path: open-ended vault (no new fields present) is unaffected. + withEnv(testableAmendments(), [&](Env& env, Account const& owner, Vault& vault) { + auto [tx, keylet] = vault.create({.owner = owner, .asset = asset}); + env(tx); + env.close(); + auto const sle = env.le(keylet); + if (BEAST_EXPECT(sle)) + { + BEAST_EXPECT(!sle->isFieldPresent(sfVaultKind)); + BEAST_EXPECT(!sle->isFieldPresent(sfSubscriptionDate)); + BEAST_EXPECT(!sle->isFieldPresent(sfRedemptionDate)); + } + }); + + // Happy path: explicit `VaultKind = 0` (OpenEnded) behaves the same + // as absent. Per spec, absent and OpenEnded are equivalent. + withEnv(testableAmendments(), [&](Env& env, Account const& owner, Vault& vault) { + auto [tx, keylet] = vault.create( + {.owner = owner, + .asset = asset, + .vaultKind = std::to_underlying(VaultKind::OpenEnded)}); + env(tx); + env.close(); + auto const sle = env.le(keylet); + if (BEAST_EXPECT(sle)) + { + // OpenEnded is sfVaultKind's default; SoeDefault fields + // aren't serialized when they hold the default value. + BEAST_EXPECT(!sle->isFieldPresent(sfVaultKind)); + BEAST_EXPECT(!sle->isFieldPresent(sfSubscriptionDate)); + BEAST_EXPECT(!sle->isFieldPresent(sfRedemptionDate)); + } + }); + } + + // Phase derivation across the SubscriptionDate / RedemptionDate boundaries, including the now + // == SubscriptionDate case (which must still resolve to Subscription). + void + testVaultPhaseDerivation() + { + testcase("closed-ended phase derivation"); + using namespace test::jtx; + + Env env{*this, testableAmendments()}; + Account const owner{"owner"}; + Account const depositor{"depositor"}; + env.fund(XRP(1000), owner, depositor); + env.close(); + + Asset const asset = xrpIssue(); + auto const [vault, keylet, sub, red] = + makeClosedEndedVault(env, owner, asset, 60u, kMinInvestmentPeriod); + + // Pre-seed shares during Subscription so the depositor has capital to + // withdraw at the Redemption boundary below. + env(vault.deposit({.depositor = depositor, .id = keylet.key, .amount = XRP(10).value()})); + env.close(); + + auto const deposit = + [&](TER expected, std::source_location const& loc = std::source_location::current()) { + env( + WithSourceLocation{ + vault.deposit( + {.depositor = depositor, .id = keylet.key, .amount = XRP(1).value()}), + loc}, + Ter{expected}); + }; + auto const withdraw = + [&](TER expected, std::source_location const& loc = std::source_location::current()) { + env( + WithSourceLocation{ + vault.withdraw( + {.depositor = depositor, .id = keylet.key, .amount = XRP(1).value()}), + loc}, + Ter{expected}); + }; + + auto const runTest = [&](TER expectedDeposit, + TER expectedWithdraw, + std::source_location const& loc = + std::source_location::current()) { + deposit(expectedDeposit, loc); + withdraw(expectedWithdraw, loc); + }; + + // Assert both deposit and withdraw return codes at each point so the + // active phase is uniquely identified: + // Subscription: deposit tesSUCCESS, withdraw tesSUCCESS + // Investment: deposit tecEXPIRED, withdraw tecTOO_SOON + // Redemption: deposit tecEXPIRED, withdraw tesSUCCESS + + // Ledger time comfortably before SubscriptionDate: Subscription. + runTest(tesSUCCESS, tesSUCCESS); + + // Boundary: parent close time exactly at SubscriptionDate must still + // be Subscription. + closeToTime(env, tp{d{sub}}); + runTest(tesSUCCESS, tesSUCCESS); + + // One second past SubscriptionDate: Investment. + closeToTime(env, tp{d{sub}} + getLedgerTimeResolution(env)); + runTest(tecEXPIRED, tecTOO_SOON); + + // Any point strictly before RedemptionDate remains Investment. + closeToTime(env, tp{d{red}} - getLedgerTimeResolution(env)); + runTest(tecEXPIRED, tecTOO_SOON); + + // Boundary: parent close time == RedemptionDate is Redemption (per + // spec table: now >= RedemptionDate). Deposits are rejected but + // withdrawals succeed. + closeToTime(env, tp{d{red}}); + runTest(tecEXPIRED, tesSUCCESS); + env.close(); + } + + // Open-ended vaults are always in VaultPhase::NoPhase, regardless of the ledger clock or any + // dates present on the vault. + void + testVaultPhaseDerivationOpenEnded() + { + testcase("open-ended phase derivation"); + using namespace test::jtx; + + Env env{*this, testableAmendments()}; + Account const owner{"owner"}; + env.fund(XRP(1000), owner); + env.close(); + + Asset const asset = xrpIssue(); + Vault const vault{env}; + auto [tx, keylet] = vault.create({.owner = owner, .asset = asset}); + env(tx); + env.close(); + + auto const checkPhaseAt = [&](NetClock::time_point at) { + closeToTime(env, at); + auto const sle = env.le(keylet); + if (!BEAST_EXPECT(sle)) + return; + BEAST_EXPECT(getVaultPhase(*env.current(), sle) == VaultPhase::NoPhase); + }; + + // Advance the clock through a wide range of ledger times: an open-ended vault's phase + // must be NoPhase at every one of them, because the derivation short-circuits on + // VaultKind::OpenEnded before it looks at any dates. + auto const ledgerTime = tp{d{30}} + env.closed()->header().closeTimeResolution; + checkPhaseAt(ledgerTime); + checkPhaseAt(ledgerTime + std::chrono::seconds{kMinInvestmentPeriod}); + checkPhaseAt( + ledgerTime + std::chrono::seconds{kMaxInvestmentPeriod} - + env.closed()->header().closeTimeResolution); + } + + // VaultDeposit is allowed only during Subscription (or NoPhase). Rejected during Investment and + // Redemption. + void + testVaultDepositClosedEnded() + { + testcase("closed-ended VaultDeposit phase gating"); + using namespace test::jtx; + + Env env{*this, testableAmendments()}; + Account const owner{"owner"}; + Account const depositor{"depositor"}; + env.fund(XRP(1000), owner, depositor); + env.close(); + + Asset const asset = xrpIssue(); + auto const [vault, keylet, sub, red] = + makeClosedEndedVault(env, owner, asset, 60u, kMinInvestmentPeriod); + + auto const deposit = + [&](TER expected, std::source_location const& loc = std::source_location::current()) { + env( + WithSourceLocation{ + vault.deposit( + {.depositor = depositor, .id = keylet.key, .amount = XRP(1).value()}), + loc}, + Ter{expected}); + env.close(); + }; + + // Subscription: allowed. + deposit(tesSUCCESS); + + // Investment: rejected. + env.close(tp{d{sub + 1}}); + deposit(tecEXPIRED); + + // Redemption: rejected. + env.close(tp{d{red}}); + deposit(tecEXPIRED); + } + + // VaultWithdraw is allowed in Subscription and Redemption; rejected in Investment. The + // AssetsAvailable cap continues to apply and is exercised in Redemption against a vault with + // capital deployed as an outstanding loan. + void + testVaultWithdrawClosedEnded() + { + testcase("closed-ended VaultWithdraw phase gating"); + using namespace test::jtx; + using namespace loan_broker; + using namespace loan; + + Env env{*this, testableAmendments()}; + Account const owner{"owner"}; + Account const depositor{"depositor"}; + Account const borrower{"borrower"}; + env.fund(XRP(10'000), owner, depositor, borrower); + env.close(); + + Asset const asset = xrpIssue(); + // Widen the Investment window so a single-payment loan (min payment + // interval kMinPaymentInterval = 60s) fits before RedemptionDate. + auto const [vault, keylet, sub, red] = + makeClosedEndedVault(env, owner, asset, 60u, kMinInvestmentPeriod + 3600u); + + // Deposit XRP(100) in Subscription so the depositor's shares are + // worth XRP(100). The vault holds XRP(100) with + // AssetsAvailable == AssetsTotal. + env(vault.deposit({.depositor = depositor, .id = keylet.key, .amount = XRP(100).value()})); + env.close(); + + // Create a loan broker backed by this vault. LoanBrokerSet has no + // phase gate, so this is fine to do in Subscription. + auto const brokerKeylet = + keylet::loanBroker(owner.id(), SeqProxy::rawSequence(env.seq(owner))); + env(loan_broker::set(owner, keylet.key)); + env.close(); + + auto const withdraw = [&](STAmount const& amount, + TER expected, + std::source_location const& loc = + std::source_location::current()) { + env( + WithSourceLocation{ + vault.withdraw({.depositor = depositor, .id = keylet.key, .amount = amount}), + loc}, + Ter{expected}); + env.close(); + }; + + // Subscription: allowed (LP cancel). + withdraw(XRP(1).value(), tesSUCCESS); + + // Investment: rejected. + closeToTime(env, tp{d{sub}} + getLedgerTimeResolution(env)); + withdraw(XRP(1).value(), tecTOO_SOON); + + // Deploy capital: borrower takes a loan of XRP(60) against the + // vault, dropping AssetsAvailable to ~XRP(39) while AssetsTotal + // remains ~XRP(99). + env(loan::set(borrower, brokerKeylet.key, XRP(60).value()), + loan::kInterestRate(TenthBips32(0)), + kGracePeriod(60), + kPaymentInterval(60), + kPaymentTotal(1), + Sig(sfCounterpartySignature, owner), + Fee(env.current()->fees().base * 2)); + env.close(); + + // Redemption: withdrawals are allowed but subject to the AssetsAvailable cap. A small + // withdrawal within AssetsAvailable succeeds. A withdrawal within the depositor's share + // value but exceeding the vault's liquid balance fails with tecINSUFFICIENT_FUNDS from the + // vault-shortage guard (not the insufficient-shares guard). + closeToTime(env, tp{d{red}}); + withdraw(XRP(10).value(), tesSUCCESS); + withdraw(XRP(80).value(), tecINSUFFICIENT_FUNDS); + } + + // End-to-end lifecycle of a closed-ended vault (Subscription → Investment → Redemption) with + // multiple depositors and a real loan originated through the Investment leg. Exercises every + // phase transition and verifies the expected deposit, withdrawal, and lending behaviour in each + // phase. + void + testVaultClosedEndedLifecycle() + { + testcase("closed-ended vault lifecycle (subscribe → invest → redeem)"); + using namespace test::jtx; + using namespace loan_broker; + using namespace loan; + + Env env{*this, testableAmendments()}; + Account const owner{"owner"}; + Account const alice{"alice"}; + Account const bob{"bob"}; + Account const borrower{"borrower"}; + env.fund(XRP(10'000), owner, alice, bob, borrower); + env.close(); + + auto const closedEnded = std::to_underlying(VaultKind::ClosedEnded); + Asset const asset = xrpIssue(); + // Widen the Investment window so a single-payment loan (min payment interval + // kMinPaymentInterval = 60s) fits before RedemptionDate with headroom. + auto const [vault, keylet, sub, red] = + makeClosedEndedVault(env, owner, asset, 300u, kMinInvestmentPeriod + 3600u); + + auto const sleCreate = env.le(keylet); + BEAST_EXPECT(sleCreate); + MPTIssue const shares{sleCreate->at(sfShareMPTID)}; + + auto const balancesEq = [&](STAmount const& available, STAmount const& total) { + auto const sle = env.le(keylet); + BEAST_EXPECT(sle->at(sfAssetsAvailable) == available); + BEAST_EXPECT(sle->at(sfAssetsTotal) == total); + }; + auto const availableEq = [&](STAmount const& expected) { balancesEq(expected, expected); }; + + // env.balance(account, mptIssue) name-resolves the issuer via Env::lookup, but the share + // issuer is the vault's pseudo-account and is never registered with the jtx Env. Read the + // MPToken SLE directly to avoid the lookup. + auto const sharesEq = [&](Account const& holder, std::uint64_t expected) { + auto const sle = env.le(keylet::mptoken(shares.getMptID(), holder.id())); + std::uint64_t const actual = sle ? sle->getFieldU64(sfMPTAmount) : 0u; + BEAST_EXPECT(actual == expected); + }; + + // ---- Subscription phase ---- + // A legitimate VaultSet succeeds (positive control for 3.7). + { + auto tx = vault.set({.owner = owner, .id = keylet.key}); + tx[sfData] = "AA"; + env(tx); + env.close(); + } + + // alice deposits 100 XRP. + env(vault.deposit({.depositor = alice, .id = keylet.key, .amount = XRP(100).value()})); + env.close(); + sharesEq(alice, 100'000'000); + availableEq(XRP(100).value()); + + // bob deposits 200 XRP. + env(vault.deposit({.depositor = bob, .id = keylet.key, .amount = XRP(200).value()})); + env.close(); + sharesEq(bob, 200'000'000); + availableEq(XRP(300).value()); + + // alice cancels 25 XRP (LP cancel is permitted in Subscription). + env(vault.withdraw({.depositor = alice, .id = keylet.key, .amount = XRP(25).value()})); + env.close(); + sharesEq(alice, 75'000'000); + availableEq(XRP(275).value()); + + // Create a loan broker backed by this vault. LoanBrokerSet has no phase gate, so it is + // fine to do in Subscription. + auto const brokerKeylet = + keylet::loanBroker(owner.id(), SeqProxy::rawSequence(env.seq(owner))); + env(loan_broker::set(owner, keylet.key)); + env.close(); + + // ---- Investment phase (now == sub + 1) ---- + env.close(tp{d{sub + 1}}); + + // Deposits into a closed-ended vault past SubscriptionDate return tecEXPIRED. + env(vault.deposit({.depositor = alice, .id = keylet.key, .amount = XRP(10).value()}), + Ter{tecEXPIRED}); + env.close(); + // Withdrawals from a closed-ended vault during the Investment phase return tecTOO_SOON. + env(vault.withdraw({.depositor = alice, .id = keylet.key, .amount = XRP(10).value()}), + Ter{tecTOO_SOON}); + env.close(); + + // A real loan is originated during Investment (permitted only in this phase). Zero-interest + // one-payment schedule keeps AssetsTotal unchanged (both accrual and cash-basis + // accounting recognise no interest at origination); AssetsAvailable drops by the loan + // principal. + env(loan::set(borrower, brokerKeylet.key, XRP(60).value()), + loan::kInterestRate(TenthBips32(0)), + kGracePeriod(60), + kPaymentInterval(60), + kPaymentTotal(1), + Sig(sfCounterpartySignature, owner), + Fee(env.current()->fees().base * 2)); + env.close(); + auto const sleBroker = env.le(keylet::loanBroker(brokerKeylet.key)); + BEAST_EXPECT(sleBroker); + auto const loanKeylet = keylet::loan(brokerKeylet.key, SeqProxy::rawSequence(1u)); + BEAST_EXPECT(env.le(loanKeylet)); + balancesEq(XRP(215).value(), XRP(275).value()); + + // Non-immutable VaultSet still works in Investment (positive control). + { + auto tx = vault.set({.owner = owner, .id = keylet.key}); + tx[sfData] = "BB"; + env(tx); + env.close(); + } + + // Depositor share balances unchanged by the loan origination; only AssetsAvailable moved. + sharesEq(alice, 75'000'000); + sharesEq(bob, 200'000'000); + + // ---- Redemption phase (now == red) ---- + env.close(tp{d{red}}); + + // Deposits into a closed-ended vault past SubscriptionDate return tecEXPIRED, in both + // Investment and Redemption. + env(vault.deposit({.depositor = alice, .id = keylet.key, .amount = XRP(10).value()}), + Ter{tecEXPIRED}); + env.close(); + + // alice redeems her remaining 75 XRP (fits within AssetsAvailable = 215). + env(vault.withdraw({.depositor = alice, .id = keylet.key, .amount = XRP(75).value()})); + env.close(); + sharesEq(alice, 0); + balancesEq(XRP(140).value(), XRP(200).value()); + + // bob has 200 XRP-worth of shares but only 140 XRP is available (the remaining 60 XRP + // sits in the outstanding loan). A full 200 XRP withdrawal fails against the + // AssetsAvailable cap; bob redeems 140 XRP instead and is left holding 60M shares backed + // by the loan receivable — the realistic outcome when capital is still deployed at + // Redemption. + env(vault.withdraw({.depositor = bob, .id = keylet.key, .amount = XRP(200).value()}), + Ter{tecINSUFFICIENT_FUNDS}); + env.close(); + env(vault.withdraw({.depositor = bob, .id = keylet.key, .amount = XRP(140).value()})); + env.close(); + sharesEq(bob, 60'000'000); + balancesEq(XRP(0).value(), XRP(60).value()); + + // Defensive spot-check that the three immutable fields have not changed across the entire + // lifecycle. Direct immutability coverage lives with the invariant tests. + auto const sleFinal = env.le(keylet); + if (BEAST_EXPECT(sleFinal)) + { + BEAST_EXPECT(sleFinal->at(sfVaultKind) == closedEnded); + BEAST_EXPECT(sleFinal->at(sfSubscriptionDate) == sub); + BEAST_EXPECT(sleFinal->at(sfRedemptionDate) == red); + } + } + + // SubscriptionDate boundary cases at the top of the UINT32 range. + // (1) The largest legal sub picks red = UINT32_MAX exactly, which hits + // the inclusive lower bound of the kMinInvestmentPeriod gap check. + // (2) sub = UINT32_MAX must be rejected: sub + kMinInvestmentPeriod is + // unrepresentable as the tx's UINT32 sfRedemptionDate, so no red value + // can satisfy the gap check. + void + testVaultCreateSubscriptionDateBoundary() + { + testcase("closed-ended VaultCreate SubscriptionDate near UINT32_MAX"); + using namespace test::jtx; + + auto const closedEnded = std::to_underlying(VaultKind::ClosedEnded); + Asset const asset = xrpIssue(); + + { + Env env{*this, testableAmendments()}; + Account const owner{"owner"}; + env.fund(XRP(1000), owner); + env.close(); + + Vault const vault{env}; + auto const sub = std::numeric_limits::max() - kMinInvestmentPeriod; + auto const red = std::numeric_limits::max(); + auto [tx, keylet] = vault.create( + {.owner = owner, + .asset = asset, + .vaultKind = closedEnded, + .subscriptionDate = sub, + .redemptionDate = red}); + env(tx); + env.close(); + auto const sle = env.le(keylet); + if (BEAST_EXPECT(sle)) + { + BEAST_EXPECT(sle->at(sfSubscriptionDate) == sub); + BEAST_EXPECT(sle->at(sfRedemptionDate) == red); + } + } + + // sub = UINT32_MAX: no legal red exists because sub + kMinInvestmentPeriod + // wraps in a UINT32. Every candidate red must fall to temMALFORMED via + // the gap check in preflight. + auto const rejectAtMax = [&, this](std::uint32_t red) { + Env env{*this, testableAmendments()}; + Account const owner{"owner"}; + env.fund(XRP(1000), owner); + env.close(); + + Vault const vault{env}; + auto [tx, keylet] = vault.create( + {.owner = owner, + .asset = asset, + .vaultKind = closedEnded, + .subscriptionDate = std::numeric_limits::max(), + .redemptionDate = red}); + env(tx, Ter{temMALFORMED}); + }; + rejectAtMax(std::numeric_limits::max()); + rejectAtMax(0u); + rejectAtMax(kMinInvestmentPeriod - 1u); + } + + // A loan whose payment is made after the Investment phase has ended + // (well past its next-due-date and grace period, into Redemption) must + // still be repayable. The vault phase must not gate LoanPay. + void + testVaultLoanLatePaymentAfterInvestment() + { + testcase("closed-ended vault: late loan payment during Redemption succeeds"); + using namespace test::jtx; + using namespace loan_broker; + using namespace loan; + + Env env{*this, testableAmendments()}; + Account const owner{"owner"}; + Account const alice{"alice"}; + Account const borrower{"borrower"}; + env.fund(XRP(10'000), owner, alice, borrower); + env.close(); + + Asset const asset = xrpIssue(); + auto const [vault, keylet, sub, red] = + makeClosedEndedVault(env, owner, asset, 300u, kMinInvestmentPeriod + 3600u); + + env(vault.deposit({.depositor = alice, .id = keylet.key, .amount = XRP(100).value()})); + env.close(); + + auto const brokerKeylet = + keylet::loanBroker(owner.id(), SeqProxy::rawSequence(env.seq(owner))); + env(loan_broker::set(owner, keylet.key)); + env.close(); + + // Investment phase: originate a zero-interest, single-payment loan + // with a 300s payment interval and 60s grace. The payment is due + // shortly after origination and well before RedemptionDate. + env.close(tp{d{sub + 1}}); + env(loan::set(borrower, brokerKeylet.key, XRP(60).value()), + loan::kInterestRate(TenthBips32(0)), + kGracePeriod(60), + kPaymentInterval(300), + kPaymentTotal(1), + Sig(sfCounterpartySignature, owner), + Fee(env.current()->fees().base * 2)); + env.close(); + auto const loanKeylet = keylet::loan(brokerKeylet.key, SeqProxy::rawSequence(1u)); + BEAST_EXPECT(env.le(loanKeylet)); + + // Advance to Redemption. The payment is now past its due date and + // grace, and the vault is no longer in Investment. + closeToTime(env, tp{d{red}}); + + env(loan::pay(borrower, loanKeylet.key, XRP(60).value(), tfLoanLatePayment)); + env.close(); + + // Loan principal returned to the vault; assetsAvailable == assetsTotal. + auto const sleAfter = env.le(keylet); + if (BEAST_EXPECT(sleAfter)) + { + BEAST_EXPECT(sleAfter->at(sfAssetsAvailable) == sleAfter->at(sfAssetsTotal)); + BEAST_EXPECT(sleAfter->at(sfAssetsAvailable) == XRP(100).value()); + } + + env(vault.withdraw({.depositor = alice, .id = keylet.key, .amount = XRP(100).value()})); + env.close(); + } + + // Two concurrent loans against the same closed-ended vault in Investment + // must coexist: both loan SLEs are created, AssetsAvailable reflects the + // sum of the two outstanding principals, and each can be repaid + // independently. + void + testVaultClosedEndedMultipleLoans() + { + testcase("closed-ended vault: multiple concurrent loans in Investment"); + using namespace test::jtx; + using namespace loan_broker; + using namespace loan; + + Env env{*this, testableAmendments()}; + Account const owner{"owner"}; + Account const alice{"alice"}; + Account const bob{"bob"}; + Account const borrower1{"borrower1"}; + Account const borrower2{"borrower2"}; + env.fund(XRP(10'000), owner, alice, bob, borrower1, borrower2); + env.close(); + + Asset const asset = xrpIssue(); + auto const [vault, keylet, sub, red] = + makeClosedEndedVault(env, owner, asset, 300u, kMinInvestmentPeriod + 3600u); + + env(vault.deposit({.depositor = alice, .id = keylet.key, .amount = XRP(100).value()})); + env.close(); + env(vault.deposit({.depositor = bob, .id = keylet.key, .amount = XRP(100).value()})); + env.close(); + + auto const brokerKeylet = + keylet::loanBroker(owner.id(), SeqProxy::rawSequence(env.seq(owner))); + env(loan_broker::set(owner, keylet.key)); + env.close(); + + env.close(tp{d{sub + 1}}); + + auto const originate = [&](Account const& b, STAmount const& principal) { + env(loan::set(b, brokerKeylet.key, principal), + loan::kInterestRate(TenthBips32(0)), + kGracePeriod(60), + kPaymentInterval(300), + kPaymentTotal(1), + Sig(sfCounterpartySignature, owner), + Fee(env.current()->fees().base * 2)); + env.close(); + }; + originate(borrower1, XRP(50).value()); + originate(borrower2, XRP(70).value()); + + auto const loan1 = keylet::loan(brokerKeylet.key, SeqProxy::rawSequence(1u)); + auto const loan2 = keylet::loan(brokerKeylet.key, SeqProxy::rawSequence(2u)); + BEAST_EXPECT(env.le(loan1)); + BEAST_EXPECT(env.le(loan2)); + + // Zero-interest at origination: AssetsTotal unchanged, AssetsAvailable + // drops by the sum of the two loan principals. + { + auto const sle = env.le(keylet); + if (BEAST_EXPECT(sle)) + { + BEAST_EXPECT(sle->at(sfAssetsTotal) == XRP(200).value()); + BEAST_EXPECT(sle->at(sfAssetsAvailable) == XRP(80).value()); + } + } + + // Repay the first loan; the second remains outstanding. + env(loan::pay(borrower1, loan1.key, XRP(50).value())); + env.close(); + { + auto const sle = env.le(keylet); + if (BEAST_EXPECT(sle)) + { + BEAST_EXPECT(sle->at(sfAssetsTotal) == XRP(200).value()); + BEAST_EXPECT(sle->at(sfAssetsAvailable) == XRP(130).value()); + } + } + + // Repay the second loan; vault is fully liquid again. + env(loan::pay(borrower2, loan2.key, XRP(70).value())); + env.close(); + { + auto const sle = env.le(keylet); + if (BEAST_EXPECT(sle)) + { + BEAST_EXPECT(sle->at(sfAssetsAvailable) == sle->at(sfAssetsTotal)); + BEAST_EXPECT(sle->at(sfAssetsAvailable) == XRP(200).value()); + } + } + + // Redemption: both depositors withdraw in full. + env.close(tp{d{red}}); + env(vault.withdraw({.depositor = alice, .id = keylet.key, .amount = XRP(100).value()})); + env.close(); + env(vault.withdraw({.depositor = bob, .id = keylet.key, .amount = XRP(100).value()})); + env.close(); + } + + // VaultClawback has no phase gate: an issuer must be able to reclaim + // asset from a depositor in Subscription, Investment and Redemption + // alike. Uses an IOU with asfAllowTrustLineClawback so the issuer path + // is exercised (XRP clawback with an explicit amount is temMALFORMED). + void + testVaultClawbackClosedEndedPhases() + { + testcase("closed-ended vault: VaultClawback succeeds in each phase"); + using namespace test::jtx; + + Env env{*this, testableAmendments()}; + Account const issuer{"issuer"}; + Account const owner{"owner"}; + Account const alice{"alice"}; + env.fund(XRP(10'000), issuer, owner, alice); + env.close(); + + env(fset(issuer, asfAllowTrustLineClawback)); + env.close(); + + PrettyAsset const iou = issuer["IOU"]; + env.trust(iou(10'000), alice); + env(pay(issuer, alice, iou(1'000))); + env.close(); + + auto const [vault, keylet, sub, red] = + makeClosedEndedVault(env, owner, iou, 300u, kMinInvestmentPeriod + 3600u); + + env(vault.deposit({.depositor = alice, .id = keylet.key, .amount = iou(300).value()})); + env.close(); + + auto const totalsEq = [&](STAmount const& expected) { + auto const sle = env.le(keylet); + if (BEAST_EXPECT(sle)) + BEAST_EXPECT(sle->at(sfAssetsTotal) == expected); + }; + + // Subscription phase clawback. + env(vault.clawback( + {.issuer = issuer, .id = keylet.key, .holder = alice, .amount = iou(10).value()})); + env.close(); + totalsEq(iou(290).value()); + + // Investment phase clawback. + env.close(tp{d{sub + 1}}); + env(vault.clawback( + {.issuer = issuer, .id = keylet.key, .holder = alice, .amount = iou(10).value()})); + env.close(); + totalsEq(iou(280).value()); + + // Redemption phase clawback. + env.close(tp{d{red}}); + env(vault.clawback( + {.issuer = issuer, .id = keylet.key, .holder = alice, .amount = iou(10).value()})); + env.close(); + totalsEq(iou(270).value()); + } + // Test for non-asset specific behaviors. void testCreateFailXRP() @@ -4591,6 +5608,90 @@ class Vault_test : public beast::unit_test::Suite } } + // RPC coverage: closed-ended vaults must return VaultKind, SubscriptionDate and RedemptionDate + // in both vault_info and ledger_entry responses. Open-ended vaults must not. + void + testRPCClosedEnded() + { + using namespace test::jtx; + + testcase("RPC closed-ended vault fields"); + Env env{*this, testableAmendments()}; + Account const owner{"owner"}; + Account const owner2{"owner2"}; + env.fund(XRP(1000), owner, owner2); + env.close(); + + auto const closedEnded = std::to_underlying(VaultKind::ClosedEnded); + Asset const asset = xrpIssue(); + auto const sub = env.now().time_since_epoch().count() + 60; + auto const red = sub + kMinInvestmentPeriod; + + Vault const vault{env}; + auto [tx, keylet] = vault.create( + {.owner = owner, + .asset = asset, + .vaultKind = closedEnded, + .subscriptionDate = sub, + .redemptionDate = red}); + env(tx); + env.close(); + + auto [tx2, keylet2] = vault.create({.owner = owner2, .asset = asset}); + env(tx2); + env.close(); + + auto const asUInt = [](json::Value const& jv) -> json::UInt { + return jv.isUInt() ? jv.asUInt() : json::UInt(jv.asInt()); + }; + auto const checkClosedEnded = [&](json::Value const& v) { + BEAST_EXPECT(v.isObject()); + BEAST_EXPECT(v.isMember(sfVaultKind.fieldName)); + BEAST_EXPECT(asUInt(v[sfVaultKind.fieldName]) == json::UInt(closedEnded)); + BEAST_EXPECT(v.isMember(sfSubscriptionDate.fieldName)); + BEAST_EXPECT(asUInt(v[sfSubscriptionDate.fieldName]) == json::UInt(sub)); + BEAST_EXPECT(v.isMember(sfRedemptionDate.fieldName)); + BEAST_EXPECT(asUInt(v[sfRedemptionDate.fieldName]) == json::UInt(red)); + }; + auto const checkOpenEnded = [&](json::Value const& v) { + BEAST_EXPECT(v.isObject()); + BEAST_EXPECT(!v.isMember(sfVaultKind.fieldName)); + BEAST_EXPECT(!v.isMember(sfSubscriptionDate.fieldName)); + BEAST_EXPECT(!v.isMember(sfRedemptionDate.fieldName)); + }; + + { + json::Value jvParams; + jvParams[jss::vault_id] = strHex(keylet.key); + auto jv = env.rpc("json", "vault_info", to_string(jvParams)); + BEAST_EXPECT(!jv[jss::result].isMember(jss::error)); + checkClosedEnded(jv[jss::result][jss::vault]); + } + { + json::Value jvParams; + jvParams[jss::ledger_index] = jss::validated; + jvParams[jss::vault] = strHex(keylet.key); + auto jv = env.rpc("json", "ledger_entry", to_string(jvParams)); + BEAST_EXPECT(!jv[jss::result].isMember(jss::error)); + checkClosedEnded(jv[jss::result][jss::node]); + } + { + json::Value jvParams; + jvParams[jss::vault_id] = strHex(keylet2.key); + auto jv = env.rpc("json", "vault_info", to_string(jvParams)); + BEAST_EXPECT(!jv[jss::result].isMember(jss::error)); + checkOpenEnded(jv[jss::result][jss::vault]); + } + { + json::Value jvParams; + jvParams[jss::ledger_index] = jss::validated; + jvParams[jss::vault] = strHex(keylet2.key); + auto jv = env.rpc("json", "ledger_entry", to_string(jvParams)); + BEAST_EXPECT(!jv[jss::result].isMember(jss::error)); + checkOpenEnded(jv[jss::result][jss::node]); + } + } + void testVaultClawbackBurnShares() { @@ -8579,6 +9680,16 @@ public: testCreateFailXRP(); testCreateFailIOU(); testCreateFailMPT(); + testVaultCreateClosedEnded(); + testVaultCreateSubscriptionDateBoundary(); + testVaultPhaseDerivation(); + testVaultPhaseDerivationOpenEnded(); + testVaultDepositClosedEnded(); + testVaultWithdrawClosedEnded(); + testVaultClosedEndedLifecycle(); + testVaultLoanLatePaymentAfterInvestment(); + testVaultClosedEndedMultipleLoans(); + testVaultClawbackClosedEndedPhases(); testWithMPT(); testWithIOU(); testWithDomainCheck(); @@ -8589,6 +9700,7 @@ public: testFailedPseudoAccount(); testScaleIOU(); testRPC(); + testRPCClosedEnded(); testVaultClawbackBurnShares(); testVaultClawbackAssets(); testVaultEscrowedMPT(); diff --git a/src/test/app/lending/LoanSet_test.cpp b/src/test/app/lending/LoanSet_test.cpp index 85528ee9a0..3571853b47 100644 --- a/src/test/app/lending/LoanSet_test.cpp +++ b/src/test/app/lending/LoanSet_test.cpp @@ -13,12 +13,14 @@ #include #include +#include #include #include #include #include #include #include +#include #include #include #include @@ -26,6 +28,7 @@ #include #include +#include #include #include #include @@ -592,6 +595,127 @@ private: nullptr); } + // LoanSet in a closed-ended vault — phase gating and maturity bound. + void + testLoanSetClosedEnded() + { + testcase("LoanSet closed-ended: phase and maturity bound"); + using namespace jtx; + using namespace loan; + + Account const issuer{"issuer"}; + Account const lender{"lender"}; + Account const borrower{"borrower"}; + + // Common loan schedule used by the phase-rejection cases below. + constexpr std::uint32_t kInterval = 3600u * 24u; // 1 day + constexpr std::uint32_t kTotal = 2u; + + // featureLendingProtocolV1_1 is excluded from `all_` by convention (see the comment on + // `all_`), so callers must opt in. Closed-ended vaults are gated on this amendment; without + // it VaultCreate returns temDISABLED and every follow-on txn sees tecNO_ENTRY. + auto const withEnv = [&, this](auto&& body) { + Env env(*this, testableAmendments() | featureLendingProtocolV1_1); + env.fund(XRP(1'000'000'000), issuer, lender, borrower); + env.close(); + PrettyAsset const asset{xrpIssue(), 1'000'000}; + body(env, asset); + }; + + auto const setLoan = [&](Env& env, BrokerInfo const& broker, TER expected) { + env(set(lender, broker.brokerID, broker.asset(100).value()), + kCounterparty(borrower), + Sig(sfCounterpartySignature, borrower), + Fee(env.current()->fees().base * 5), + kPaymentTotal(kTotal), + kPaymentInterval(kInterval), + Ter(expected)); + env.close(); + }; + + // 1. Rejected during Subscription: the broker is created in Subscription (skipPhaseAdvance + // = true), then LoanSet is attempted before advancing past SubscriptionDate. + withEnv([&](Env& env, PrettyAsset const& asset) { + auto const broker = createVaultAndBroker( + env, + asset, + lender, + BrokerParameters{.vaultKind = VaultKind::ClosedEnded, .skipPhaseAdvance = true}); + setLoan(env, broker, tecTOO_SOON); + }); + + // 2. Rejected during Redemption: broker is set up normally (which lands the vault in + // Investment), then advance the clock past RedemptionDate before attempting LoanSet. + withEnv([&](Env& env, PrettyAsset const& asset) { + auto const broker = createVaultAndBroker( + env, asset, lender, BrokerParameters{.vaultKind = VaultKind::ClosedEnded}); + BEAST_EXPECT(broker.redemptionDate.has_value()); + using d = NetClock::duration; + using tp = NetClock::time_point; + env.close(tp{d{*broker.redemptionDate + 1}}); + setLoan(env, broker, tecEXPIRED); + }); + + // 3. Accepted during Investment when the schedule comfortably fits before RedemptionDate. + withEnv([&](Env& env, PrettyAsset const& asset) { + auto const broker = createVaultAndBroker( + env, asset, lender, BrokerParameters{.vaultKind = VaultKind::ClosedEnded}); + setLoan(env, broker, tesSUCCESS); + }); + + // 4. Rejected during Investment when the loan's final payment would land on or after + // RedemptionDate. Use a tight redemptionOffset and a schedule whose final payment is well + // past that boundary. + withEnv([&](Env& env, PrettyAsset const& asset) { + constexpr std::uint32_t kRedemptionOffset = 3u * 24u * 3600u; + auto const broker = createVaultAndBroker( + env, + asset, + lender, + BrokerParameters{ + .vaultKind = VaultKind::ClosedEnded, .redemptionOffset = kRedemptionOffset}); + env(set(lender, broker.brokerID, broker.asset(100).value()), + kCounterparty(borrower), + Sig(sfCounterpartySignature, borrower), + Fee(env.current()->fees().base * 5), + kPaymentTotal(10u), + kPaymentInterval(kInterval), + Ter(tecNO_PERMISSION)); + env.close(); + }); + + // 5. Boundary: schedule whose finalPayment lands exactly (RedemptionDate - 1) is accepted, + // and one second later (== RedemptionDate) is rejected. Uses payTotal = 1 so the arithmetic + // is simple: finalPayment = startDate + interval. + withEnv([&](Env& env, PrettyAsset const& asset) { + auto const broker = createVaultAndBroker( + env, asset, lender, BrokerParameters{.vaultKind = VaultKind::ClosedEnded}); + BEAST_EXPECT(broker.redemptionDate.has_value()); + + auto const startDate = env.now().time_since_epoch().count(); + auto const acceptInterval = *broker.redemptionDate - 1 - startDate; + env(set(lender, broker.brokerID, broker.asset(100).value()), + kCounterparty(borrower), + Sig(sfCounterpartySignature, borrower), + Fee(env.current()->fees().base * 5), + kPaymentTotal(1u), + kPaymentInterval(acceptInterval), + Ter(tesSUCCESS)); + env.close(); + + auto const rejectInterval = + *broker.redemptionDate - env.now().time_since_epoch().count(); + env(set(lender, broker.brokerID, broker.asset(100).value()), + kCounterparty(borrower), + Sig(sfCounterpartySignature, borrower), + Fee(env.current()->fees().base * 5), + kPaymentTotal(1u), + kPaymentInterval(rejectInterval), + Ter(tecNO_PERMISSION)); + env.close(); + }); + } + public: void run() override @@ -599,6 +723,8 @@ public: for (auto const& features : jtx::amendmentCombinations( {fixCleanup3_1_3, fixCleanup3_2_0, featureMPTokensV2}, all_)) testLoanSet(features); + + testLoanSetClosedEnded(); } }; diff --git a/src/test/app/lending/LoanTestBase.h b/src/test/app/lending/LoanTestBase.h index dabdfc9bed..950b196043 100644 --- a/src/test/app/lending/LoanTestBase.h +++ b/src/test/app/lending/LoanTestBase.h @@ -95,6 +95,23 @@ protected: // tests that need finer loanScale to exercise rounding edge cases. std::optional vaultScale = std::nullopt; // NOLINT(readability-redundant-member-init) + // Vault kind axis. When ClosedEnded, createVaultAndBroker sets sfSubscriptionDate / + // sfRedemptionDate from env.now() using the offsets below and advances the ledger clock + // past SubscriptionDate so the vault is in the Investment phase by the time the broker is + // set up. Requires featureLendingProtocolV1_1. + VaultKind vaultKind = VaultKind::OpenEnded; + // Seconds past env.now() at which SubscriptionDate lands. Must be strictly positive + // (VaultCreate::preclaim rejects SubscriptionDate <= parentCloseTime). + std::uint32_t subscriptionOffset = 60; + // Seconds between SubscriptionDate and RedemptionDate. Must be >= kMinInvestmentPeriod, < + // kMaxInvestmentPeriod, and generous enough to fit any loan schedule the test runs + // (finalPayment must be strictly before RedemptionDate). Default sized to comfortably + // exceed any schedule realistic tests are likely to configure. + std::uint32_t redemptionOffset = 10u * 365u * 24u * 60u * 60u; + // When true, createVaultAndBroker skips its automatic clock advance past SubscriptionDate. + // Useful for tests that need to observe the vault while it is still in the Subscription + // phase. Ignored for open-ended vaults. + bool skipPhaseAdvance = false; [[nodiscard]] Number maxCoveredLoanValue(Number const& currentDebt) const @@ -122,15 +139,23 @@ protected: uint256 brokerID; uint256 vaultID; BrokerParameters params; + // Absolute dates resolved by createVaultAndBroker when params.vaultKind + // is ClosedEnded; std::nullopt for open-ended vaults. + std::optional subscriptionDate; + std::optional redemptionDate; BrokerInfo( jtx::PrettyAsset const& asset, Keylet const& brokerKeylet, Keylet const& vaultKeylet, - BrokerParameters p) + BrokerParameters p, + std::optional subscriptionDate = std::nullopt, + std::optional redemptionDate = std::nullopt) : asset(asset) , brokerID(brokerKeylet.key) , vaultID(vaultKeylet.key) , params(std::move(p)) + , subscriptionDate(subscriptionDate) + , redemptionDate(redemptionDate) { } @@ -461,7 +486,23 @@ protected: auto const coverRateMinValue = params.coverRateMin; - auto [tx, vaultKeylet] = vault.create({.owner = lender, .asset = asset}); + std::optional subscriptionDate; + std::optional redemptionDate; + if (params.vaultKind == VaultKind::ClosedEnded) + { + auto const nowSec = env.now().time_since_epoch().count(); + subscriptionDate = nowSec + params.subscriptionOffset; + redemptionDate = *subscriptionDate + params.redemptionOffset; + } + + auto [tx, vaultKeylet] = vault.create( + {.owner = lender, + .asset = asset, + .vaultKind = params.vaultKind == VaultKind::OpenEnded + ? std::optional{} + : std::optional{std::to_underlying(params.vaultKind)}, + .subscriptionDate = subscriptionDate, + .redemptionDate = redemptionDate}); if (params.vaultScale) tx[sfScale] = *params.vaultScale; env(tx); @@ -475,6 +516,15 @@ protected: BEAST_EXPECT(vault->at(sfAssetsAvailable) == deposit.value()); } + // For closed-ended vaults, advance past SubscriptionDate so subsequent LoanSet operations + // run in the Investment phase (unless the caller explicitly asked to stay in Subscription). + if (subscriptionDate && !params.skipPhaseAdvance) + { + using d = NetClock::duration; + using tp = NetClock::time_point; + env.close(tp{d{*subscriptionDate + 1}}); + } + auto const keylet = keylet::loanBroker(lender.id(), SeqProxy::rawSequence(env.seq(lender))); using namespace loan_broker; @@ -490,7 +540,7 @@ protected: env.close(); - return {asset, keylet, vaultKeylet, params}; + return {asset, keylet, vaultKeylet, params, subscriptionDate, redemptionDate}; } /** diff --git a/src/test/app/lending/LoanValidation_test.cpp b/src/test/app/lending/LoanValidation_test.cpp index 884384db55..c6ff22bbb3 100644 --- a/src/test/app/lending/LoanValidation_test.cpp +++ b/src/test/app/lending/LoanValidation_test.cpp @@ -26,6 +26,7 @@ #include #include #include +#include #include #include #include @@ -90,9 +91,11 @@ private: } void - testInvalidLoanSet() + testInvalidLoanSet(VaultKind vaultKind) { - testcase("Invalid LoanSet"); + testcase( + std::string("Invalid LoanSet (") + + (vaultKind == VaultKind::OpenEnded ? "open-ended" : "closed-ended") + " vault)"); using namespace jtx; using namespace loan; Account const lender{"lender"}; @@ -106,7 +109,8 @@ private: env.fund(XRP(1'000), lender, issuer, borrower, sponsor); env(trust(lender, iou(10'000'000))); env(pay(issuer, lender, iou(5'000'000))); - BrokerInfo const brokerInfo{createVaultAndBroker(env, issuer["IOU"], lender)}; + BrokerInfo const brokerInfo{ + createVaultAndBroker(env, issuer["IOU"], lender, {.vaultKind = vaultKind})}; auto const loanSetFee = Fee(env.current()->fees().base * 2); Number const debtMaximumRequest = brokerInfo.asset(1'000).value(); @@ -530,7 +534,8 @@ private: runAmendmentIndependent() { testDisabled(); - testInvalidLoanSet(); + for (auto const kind : {VaultKind::OpenEnded, VaultKind::ClosedEnded}) + testInvalidLoanSet(kind); testInvalidLoanDelete(); testInvalidLoanManage(); testInvalidLoanPay(); diff --git a/src/test/jtx/impl/vault.cpp b/src/test/jtx/impl/vault.cpp index baff576243..978c3864d6 100644 --- a/src/test/jtx/impl/vault.cpp +++ b/src/test/jtx/impl/vault.cpp @@ -28,6 +28,12 @@ Vault::create(CreateArgs const& args) const jv[jss::Asset] = toJson(args.asset); if (args.flags) jv[jss::Flags] = *args.flags; + if (args.vaultKind) + jv[sfVaultKind] = *args.vaultKind; + if (args.subscriptionDate) + jv[sfSubscriptionDate] = *args.subscriptionDate; + if (args.redemptionDate) + jv[sfRedemptionDate] = *args.redemptionDate; return {jv, keylet}; } diff --git a/src/test/jtx/vault.h b/src/test/jtx/vault.h index e72eae89b7..992051b61f 100644 --- a/src/test/jtx/vault.h +++ b/src/test/jtx/vault.h @@ -25,6 +25,12 @@ struct Vault Asset asset; std::optional flags = std::nullopt; // NOLINT(readability-redundant-member-init) + std::optional vaultKind = + std::nullopt; // NOLINT(readability-redundant-member-init) + std::optional subscriptionDate = + std::nullopt; // NOLINT(readability-redundant-member-init) + std::optional redemptionDate = + std::nullopt; // NOLINT(readability-redundant-member-init) }; /** diff --git a/src/tests/libxrpl/protocol_autogen/ledger_entries/VaultTests.cpp b/src/tests/libxrpl/protocol_autogen/ledger_entries/VaultTests.cpp index f55d01f606..26dde55563 100644 --- a/src/tests/libxrpl/protocol_autogen/ledger_entries/VaultTests.cpp +++ b/src/tests/libxrpl/protocol_autogen/ledger_entries/VaultTests.cpp @@ -36,6 +36,9 @@ TEST(VaultTests, BuilderSettersRoundTrip) auto const withdrawalPolicyValue = canonical_UINT8(); auto const scaleValue = canonical_UINT8(); auto const lEVersionValue = canonical_UINT8(); + auto const vaultKindValue = canonical_UINT8(); + auto const subscriptionDateValue = canonical_UINT32(); + auto const redemptionDateValue = canonical_UINT32(); VaultBuilder builder{ previousTxnIDValue, @@ -56,6 +59,9 @@ TEST(VaultTests, BuilderSettersRoundTrip) builder.setLossUnrealized(lossUnrealizedValue); builder.setScale(scaleValue); builder.setLEVersion(lEVersionValue); + builder.setVaultKind(vaultKindValue); + builder.setSubscriptionDate(subscriptionDateValue); + builder.setRedemptionDate(redemptionDateValue); builder.setLedgerIndex(index); builder.setFlags(0x1u); @@ -176,6 +182,30 @@ TEST(VaultTests, BuilderSettersRoundTrip) EXPECT_TRUE(entry.hasLEVersion()); } + { + auto const& expected = vaultKindValue; + auto const actualOpt = entry.getVaultKind(); + ASSERT_TRUE(actualOpt.has_value()); + expectEqualField(expected, *actualOpt, "sfVaultKind"); + EXPECT_TRUE(entry.hasVaultKind()); + } + + { + auto const& expected = subscriptionDateValue; + auto const actualOpt = entry.getSubscriptionDate(); + ASSERT_TRUE(actualOpt.has_value()); + expectEqualField(expected, *actualOpt, "sfSubscriptionDate"); + EXPECT_TRUE(entry.hasSubscriptionDate()); + } + + { + auto const& expected = redemptionDateValue; + auto const actualOpt = entry.getRedemptionDate(); + ASSERT_TRUE(actualOpt.has_value()); + expectEqualField(expected, *actualOpt, "sfRedemptionDate"); + EXPECT_TRUE(entry.hasRedemptionDate()); + } + EXPECT_TRUE(entry.hasLedgerIndex()); auto const ledgerIndex = entry.getLedgerIndex(); ASSERT_TRUE(ledgerIndex.has_value()); @@ -205,6 +235,9 @@ TEST(VaultTests, BuilderFromSleRoundTrip) auto const withdrawalPolicyValue = canonical_UINT8(); auto const scaleValue = canonical_UINT8(); auto const lEVersionValue = canonical_UINT8(); + auto const vaultKindValue = canonical_UINT8(); + auto const subscriptionDateValue = canonical_UINT32(); + auto const redemptionDateValue = canonical_UINT32(); auto sle = std::make_shared(Vault::entryType, index); @@ -224,6 +257,9 @@ TEST(VaultTests, BuilderFromSleRoundTrip) sle->at(sfWithdrawalPolicy) = withdrawalPolicyValue; sle->at(sfScale) = scaleValue; sle->at(sfLEVersion) = lEVersionValue; + sle->at(sfVaultKind) = vaultKindValue; + sle->at(sfSubscriptionDate) = subscriptionDateValue; + sle->at(sfRedemptionDate) = redemptionDateValue; VaultBuilder builderFromSle{sle}; EXPECT_TRUE(builderFromSle.validate()); @@ -415,6 +451,45 @@ TEST(VaultTests, BuilderFromSleRoundTrip) expectEqualField(expected, *fromBuilderOpt, "sfLEVersion"); } + { + auto const& expected = vaultKindValue; + + auto const fromSleOpt = entryFromSle.getVaultKind(); + auto const fromBuilderOpt = entryFromBuilder.getVaultKind(); + + ASSERT_TRUE(fromSleOpt.has_value()); + ASSERT_TRUE(fromBuilderOpt.has_value()); + + expectEqualField(expected, *fromSleOpt, "sfVaultKind"); + expectEqualField(expected, *fromBuilderOpt, "sfVaultKind"); + } + + { + auto const& expected = subscriptionDateValue; + + auto const fromSleOpt = entryFromSle.getSubscriptionDate(); + auto const fromBuilderOpt = entryFromBuilder.getSubscriptionDate(); + + ASSERT_TRUE(fromSleOpt.has_value()); + ASSERT_TRUE(fromBuilderOpt.has_value()); + + expectEqualField(expected, *fromSleOpt, "sfSubscriptionDate"); + expectEqualField(expected, *fromBuilderOpt, "sfSubscriptionDate"); + } + + { + auto const& expected = redemptionDateValue; + + auto const fromSleOpt = entryFromSle.getRedemptionDate(); + auto const fromBuilderOpt = entryFromBuilder.getRedemptionDate(); + + ASSERT_TRUE(fromSleOpt.has_value()); + ASSERT_TRUE(fromBuilderOpt.has_value()); + + expectEqualField(expected, *fromSleOpt, "sfRedemptionDate"); + expectEqualField(expected, *fromBuilderOpt, "sfRedemptionDate"); + } + EXPECT_EQ(entryFromSle.getKey(), index); EXPECT_EQ(entryFromBuilder.getKey(), index); } @@ -499,5 +574,11 @@ TEST(VaultTests, OptionalFieldsReturnNullopt) EXPECT_FALSE(entry.getScale().has_value()); EXPECT_FALSE(entry.hasLEVersion()); EXPECT_FALSE(entry.getLEVersion().has_value()); + EXPECT_FALSE(entry.hasVaultKind()); + EXPECT_FALSE(entry.getVaultKind().has_value()); + EXPECT_FALSE(entry.hasSubscriptionDate()); + EXPECT_FALSE(entry.getSubscriptionDate().has_value()); + EXPECT_FALSE(entry.hasRedemptionDate()); + EXPECT_FALSE(entry.getRedemptionDate().has_value()); } } diff --git a/src/tests/libxrpl/protocol_autogen/transactions/VaultCreateTests.cpp b/src/tests/libxrpl/protocol_autogen/transactions/VaultCreateTests.cpp index 9c1e14f6f4..592d40a6f6 100644 --- a/src/tests/libxrpl/protocol_autogen/transactions/VaultCreateTests.cpp +++ b/src/tests/libxrpl/protocol_autogen/transactions/VaultCreateTests.cpp @@ -36,6 +36,9 @@ TEST(TransactionsVaultCreateTests, BuilderSettersRoundTrip) auto const withdrawalPolicyValue = canonical_UINT8(); auto const dataValue = canonical_VL(); auto const scaleValue = canonical_UINT8(); + auto const vaultKindValue = canonical_UINT8(); + auto const subscriptionDateValue = canonical_UINT32(); + auto const redemptionDateValue = canonical_UINT32(); VaultCreateBuilder builder{ accountValue, @@ -51,6 +54,9 @@ TEST(TransactionsVaultCreateTests, BuilderSettersRoundTrip) builder.setWithdrawalPolicy(withdrawalPolicyValue); builder.setData(dataValue); builder.setScale(scaleValue); + builder.setVaultKind(vaultKindValue); + builder.setSubscriptionDate(subscriptionDateValue); + builder.setRedemptionDate(redemptionDateValue); auto tx = builder.build(publicKey, secretKey); @@ -122,6 +128,30 @@ TEST(TransactionsVaultCreateTests, BuilderSettersRoundTrip) EXPECT_TRUE(tx.hasScale()); } + { + auto const& expected = vaultKindValue; + auto const actualOpt = tx.getVaultKind(); + ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfVaultKind should be present"; + expectEqualField(expected, *actualOpt, "sfVaultKind"); + EXPECT_TRUE(tx.hasVaultKind()); + } + + { + auto const& expected = subscriptionDateValue; + auto const actualOpt = tx.getSubscriptionDate(); + ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfSubscriptionDate should be present"; + expectEqualField(expected, *actualOpt, "sfSubscriptionDate"); + EXPECT_TRUE(tx.hasSubscriptionDate()); + } + + { + auto const& expected = redemptionDateValue; + auto const actualOpt = tx.getRedemptionDate(); + ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfRedemptionDate should be present"; + expectEqualField(expected, *actualOpt, "sfRedemptionDate"); + EXPECT_TRUE(tx.hasRedemptionDate()); + } + } // 2 & 4) Start from an STTx, construct a builder from it, build a new wrapper, @@ -145,6 +175,9 @@ TEST(TransactionsVaultCreateTests, BuilderFromStTxRoundTrip) auto const withdrawalPolicyValue = canonical_UINT8(); auto const dataValue = canonical_VL(); auto const scaleValue = canonical_UINT8(); + auto const vaultKindValue = canonical_UINT8(); + auto const subscriptionDateValue = canonical_UINT32(); + auto const redemptionDateValue = canonical_UINT32(); // Build an initial transaction VaultCreateBuilder initialBuilder{ @@ -160,6 +193,9 @@ TEST(TransactionsVaultCreateTests, BuilderFromStTxRoundTrip) initialBuilder.setWithdrawalPolicy(withdrawalPolicyValue); initialBuilder.setData(dataValue); initialBuilder.setScale(scaleValue); + initialBuilder.setVaultKind(vaultKindValue); + initialBuilder.setSubscriptionDate(subscriptionDateValue); + initialBuilder.setRedemptionDate(redemptionDateValue); auto initialTx = initialBuilder.build(publicKey, secretKey); @@ -226,6 +262,27 @@ TEST(TransactionsVaultCreateTests, BuilderFromStTxRoundTrip) expectEqualField(expected, *actualOpt, "sfScale"); } + { + auto const& expected = vaultKindValue; + auto const actualOpt = rebuiltTx.getVaultKind(); + ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfVaultKind should be present"; + expectEqualField(expected, *actualOpt, "sfVaultKind"); + } + + { + auto const& expected = subscriptionDateValue; + auto const actualOpt = rebuiltTx.getSubscriptionDate(); + ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfSubscriptionDate should be present"; + expectEqualField(expected, *actualOpt, "sfSubscriptionDate"); + } + + { + auto const& expected = redemptionDateValue; + auto const actualOpt = rebuiltTx.getRedemptionDate(); + ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfRedemptionDate should be present"; + expectEqualField(expected, *actualOpt, "sfRedemptionDate"); + } + } // 3) Verify wrapper throws when constructed from wrong transaction type. @@ -295,6 +352,12 @@ TEST(TransactionsVaultCreateTests, OptionalFieldsReturnNullopt) EXPECT_FALSE(tx.getData().has_value()); EXPECT_FALSE(tx.hasScale()); EXPECT_FALSE(tx.getScale().has_value()); + EXPECT_FALSE(tx.hasVaultKind()); + EXPECT_FALSE(tx.getVaultKind().has_value()); + EXPECT_FALSE(tx.hasSubscriptionDate()); + EXPECT_FALSE(tx.getSubscriptionDate().has_value()); + EXPECT_FALSE(tx.hasRedemptionDate()); + EXPECT_FALSE(tx.getRedemptionDate().has_value()); } }

    AaLi9S$CuuB?2~`qvfQM&JShVuX|H+j{%OMA}w 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